# frozen-string-literal: true # class Roda module RodaPlugins # The _optimized_matching plugin is automatically used internally to speed # up matching when a single argument String instance, String class, Integer # class, or Regexp matcher is passed to +r.on+, +r.is_+, or a verb method # such as +r.get+ or +r.post+. # # The optimization works by avoiding the +if_match+ method if possible. # Instead of clearing the captures array on every call, and having the # matching append to the captures, it checks directly for the match, # and on succesful match, it yields directly to the block without using # the captures array. module OptimizedMatching TERM = Base::RequestMethods::TERM module RequestMethods # Optimize the r.is method handling of a single string, String, Integer, # regexp, or true, argument. def is(*args, &block) case args.length when 1 _is1(args, &block) when 0 always(&block) if @remaining_path.empty? else if_match(args << TERM, &block) end end # Optimize the r.on method handling of a single string, String, Integer, # or regexp argument. Inline the related matching code to avoid the # need to modify @captures. def on(*args, &block) case args.length when 1 case matcher = args[0] when String always{yield} if _match_string(matcher) when Class if matcher == String rp = @remaining_path if rp.getbyte(0) == 47 if last = rp.index('/', 1) @remaining_path = rp[last, rp.length] always{yield rp[1, last-1]} elsif (len = rp.length) > 1 @remaining_path = "" always{yield rp[1, len]} end end elsif matcher == Integer if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1])) @remaining_path = matchdata.post_match always{yield(value)} end else path = @remaining_path captures = @captures.clear meth = :"_match_class_#{matcher}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private if send(meth, &block) block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end else unsupported_matcher(matcher) end end when Regexp if matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path) @remaining_path = matchdata.post_match always{yield(*matchdata.captures)} end when true always(&block) when false, nil # nothing else path = @remaining_path captures = @captures.clear matched = case matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Symbol _match_symbol(matcher) when Proc matcher.call else unsupported_matcher(matcher) end if matched block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end when 0 always(&block) else if_match(args, &block) end end private # Optimize the r.get/r.post method handling of a single string, String, Integer, # regexp, or true, argument. def _verb(args, &block) case args.length when 0 always(&block) when 1 _is1(args, &block) else if_match(args << TERM, &block) end end # Internals of r.is/r.get/r.post optimization. Inline the related matching # code to avoid the need to modify @captures. def _is1(args, &block) case matcher = args[0] when String rp = @remaining_path if _match_string(matcher) if @remaining_path.empty? always{yield} else @remaining_path = rp nil end end when Class if matcher == String rp = @remaining_path if rp.getbyte(0) == 47 && !rp.index('/', 1) && (len = rp.length) > 1 @remaining_path = '' always{yield rp[1, len]} end elsif matcher == Integer if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1])) @remaining_path = '' always{yield(value)} end else path = @remaining_path captures = @captures.clear meth = :"_match_class_#{matcher}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private if send(meth, &block) && @remaining_path.empty? block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end else unsupported_matcher(matcher) end end when Regexp if (matchdata = self.class.cached_matcher(matcher){matcher}.match(@remaining_path)) && matchdata.post_match.empty? @remaining_path = '' always{yield(*matchdata.captures)} end when true always(&block) if @remaining_path.empty? when false, nil # nothing else path = @remaining_path captures = @captures.clear matched = case matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Symbol _match_symbol(matcher) when Proc matcher.call else unsupported_matcher(matcher) end if matched && @remaining_path.empty? block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end end end end register_plugin(:_optimized_matching, OptimizedMatching) end end