# 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