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