# frozen-string-literal: true
#
class Roda
module RodaPlugins
# The symbol_matchers plugin allows you do define custom regexps to use
# for specific symbols. For example, if you have a route such as:
#
# r.on :username do |username|
# # ...
# end
#
# By default this will match all nonempty segments. However, if your usernames
# must be 6-20 characters, and can only contain +a-z+ and +0-9+, you can do:
#
# plugin :symbol_matchers
# symbol_matcher :username, /([a-z0-9]{6,20})/
#
# Then the route will only if the path is +/foobar123+, but not if it is
# +/foo+, +/FooBar123+, or +/foobar_123+.
#
# By default, this plugin sets up the following symbol matchers:
#
# :d :: /(\d+)/, a decimal segment
# :rest :: /(.*)/, all remaining characters, if any
# :w :: /(\w+)/, an alphanumeric segment
#
# If the placeholder_string_matchers plugin is loaded, this feature also applies to
# placeholders in strings, so the following:
#
# r.on "users/:username" do |username|
# # ...
# end
#
# Would match +/users/foobar123+, but not +/users/foo+, +/users/FooBar123+,
# or +/users/foobar_123+.
#
# If using this plugin with the params_capturing plugin, this plugin should
# be loaded first.
#
# You can provide a block when calling +symbol_matcher+, and it will be called
# for all matches to allow for type conversion:
#
# symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
# Date.new(y.to_i, m.to_i, d.to_i)
# end
#
# route do |r|
# r.on :date do |date|
# # date is an instance of Date
# end
# end
#
# If you have a segment match the passed regexp, but decide during block
# processing that you do not want to treat it as a match, you can have the
# block return nil or false. This is useful if you want to make sure you
# are using valid data:
#
# symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
# y = y.to_i
# m = m.to_i
# d = d.to_i
# Date.new(y, m, d) if Date.valid_date?(y, m, d)
# end
#
# You can have the block return an array to yield multiple captures.
#
# The second argument to symbol_matcher can be a symbol already registered
# as a symbol matcher. This can DRY up code that wants a conversion
# performed by an existing class matcher or to use the same regexp:
#
# symbol_matcher :employee_id, :d do |id|
# id.to_i
# end
# symbol_matcher :employee, :employee_id do |id|
# Employee[id]
# end
#
# With the above example, the :d matcher matches only decimal strings, but
# yields them as string. The registered :employee_id matcher converts the
# decimal string to an integer. The registered :employee matcher builds
# on that and uses the integer to lookup the related employee. If there is
# no employee with that id, then the :employee matcher will not match.
#
# If using the class_matchers plugin, you can provide a recognized class
# matcher as the second argument to symbol_matcher, and it will work in
# a similar manner:
#
# symbol_matcher :employee, Integer do |id|
# Employee[id]
# end
#
# Blocks passed to the symbol matchers plugin are evaluated in route
# block context.
#
# If providing a block to the symbol_matchers plugin, the symbol may
# not work with the params_capturing plugin. Note that the use of
# symbol matchers inside strings when using the placeholder_string_matchers
# plugin only uses the regexp, it does not respect the conversion blocks
# registered with the symbols.
module SymbolMatchers
def self.load_dependencies(app)
app.plugin :_symbol_regexp_matchers
app.plugin :_symbol_class_matchers
end
def self.configure(app)
app.opts[:symbol_matchers] ||= {}
app.symbol_matcher(:d, /(\d+)/)
app.symbol_matcher(:w, /(\w+)/)
app.symbol_matcher(:rest, /(.*)/)
end
module ClassMethods
# Set the matcher and block to use for the given class.
# The matcher can be a regexp, registered symbol matcher, or registered class
# matcher (if using the class_matchers plugin).
#
# If providing a regexp, the block given will be called with all regexp captures.
# If providing a registered symbol or class, the block will be called with the
# captures returned by the block for the registered symbol or class, or the regexp
# captures if no block was registered with the symbol or class. In either case,
# if a block is given, it should return an array with the captures to yield to
# the match block.
def symbol_matcher(s, matcher, &block)
_symbol_class_matcher(Symbol, s, matcher, block) do |meth, array|
define_method(meth){array}
end
nil
end
# Freeze the class_matchers hash when freezing the app.
def freeze
opts[:symbol_matchers].freeze
super
end
end
module RequestMethods
private
# Use regular expressions to the symbol-specific regular expression
# if the symbol is registered. Otherwise, call super for the default
# behavior.
def _match_symbol(s)
meth = :"match_symbol_#{s}"
if respond_to?(meth, true)
# Allow calling private match methods
_, re, convert_meth = send(meth)
if re
consume(re, convert_meth)
else
# defined in class_matchers plugin
_consume_segment(convert_meth)
end
else
super
end
end
# Return the symbol-specific regular expression if one is registered.
# Otherwise, call super for the default behavior.
def _match_symbol_regexp(s)
meth = :"match_symbol_#{s}"
if respond_to?(meth, true)
# Allow calling private match methods
re, = send(meth)
re
else
super
end
end
end
end
register_plugin(:symbol_matchers, SymbolMatchers)
end
end