# frozen-string-literal: true
#
class Roda
module RodaPlugins
# The class_matchers plugin allows you do define custom regexps and
# conversion procs to use for specific classes. For example, if you
# have multiple routes similar to:
#
# r.on /(\d\d\d\d)-(\d\d)-(\d\d)/ do |y, m, d|
# date = Date.new(y.to_i, m.to_i, d.to_i)
# # ...
# end
#
# You can register a Date class matcher for that regexp:
#
# class_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
#
# And then use the Date class as a matcher, and it will yield a Date object:
#
# r.on Date do |date|
# # ...
# end
#
# This is useful to DRY up code if you are using the same type of pattern and
# type conversion in multiple places in your application. You can have the
# block return an array to yield multiple captures.
#
# 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:
#
# class_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
#
# The second argument to class_matcher can be a class already registered
# as a class matcher. This can DRY up code that wants a conversion
# performed by an existing class matcher:
#
# class_matcher Employee, Integer do |id|
# Employee[id]
# end
#
# With the above example, the Integer matcher performs the conversion to
# integer, so +id+ is yielded as an integer. The block then looks up the
# employee with that id. If there is no employee with that id, then
# the Employee matcher will not match.
#
# If using the symbol_matchers plugin, you can provide a recognized symbol
# matcher as the second argument to class_matcher, and it will work in
# a similar manner:
#
# symbol_matcher(:employee_id, /E-(\d{6})/) do |employee_id|
# employee_id.to_i
# end
# class_matcher Employee, :employee_id do |id|
# Employee[id]
# end
#
# Blocks passed to the class_matchers plugin are evaluated in route
# block context.
#
# This plugin does not work with the params_capturing plugin, as it does not
# offer the ability to associate block arguments with named keys.
module ClassMatchers
def self.load_dependencies(app)
app.plugin :_symbol_class_matchers
end
def self.configure(app)
app.opts[:class_matchers] ||= {
Integer=>[/(\d{1,100})/, /\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer].freeze,
String=>[/([^\/]+)/, nil, nil].freeze
}
end
module ClassMethods
# Set the matcher and block to use for the given class.
# The matcher can be a regexp, registered class matcher, or registered symbol
# matcher (if using the symbol_matchers plugin).
#
# If providing a regexp, the block given will be called with all regexp captures.
# If providing a registered class or symbol, the block will be called with the
# captures returned by the block for the registered class or symbol, or the regexp
# captures if no block was registered with the class or symbol. In either case,
# if a block is given, it should return an array with the captures to yield to
# the match block.
def class_matcher(klass, matcher, &block)
_symbol_class_matcher(Class, klass, matcher, block) do |meth, (_, regexp, convert_meth)|
if regexp
define_method(meth){consume(regexp, convert_meth)}
else
define_method(meth){_consume_segment(convert_meth)}
end
end
end
# Freeze the class_matchers hash when freezing the app.
def freeze
opts[:class_matchers].freeze
super
end
end
module RequestMethods
# Use faster approach for segment matching. This is used for
# matchers based on the String class matcher, and avoids the
# use of regular expressions for scanning.
def _consume_segment(convert_meth)
rp = @remaining_path
if _match_class_String
if convert_meth
if captures = scope.send(convert_meth, @captures.pop)
if captures.is_a?(Array)
@captures.concat(captures)
else
@captures << captures
end
else
@remaining_path = rp
nil
end
else
true
end
end
end
end
end
register_plugin(:class_matchers, ClassMatchers)
end
end