lib/roda/plugins/class_matchers.rb in roda-3.84.0 vs lib/roda/plugins/class_matchers.rb in roda-3.85.0
- old
+ new
@@ -10,50 +10,127 @@
# 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 (note that
- # the block must return an array):
+ # 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)]
+ # 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.
+ # 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)
+ # 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 regexp to use for the given class. The block given will be
- # called with all matched values from the regexp, and should return an
- # array with the captures to yield to the match block.
- def class_matcher(klass, re, &block)
- meth = :"_match_class_#{klass}"
- self::RodaRequest.class_eval do
- consume_re = consume_pattern(re)
- define_method(meth){consume(consume_re, &block)}
- private meth
+ # 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