lib/roda.rb in roda-cj-0.9.1 vs lib/roda.rb in roda-cj-0.9.2
- old
+ new
@@ -12,21 +12,67 @@
# Base class used for Roda requests. The instance methods for this
# class are added by Roda::RodaPlugins::Base::RequestMethods, so this
# only contains the class methods.
class RodaRequest < ::Rack::Request;
@roda_class = ::Roda
+ @match_pattern_cache = {}
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby'
+ # :nocov:
+ @match_pattern_mutex = Mutex.new
+
+ def self.cached_matcher(obj)
+ unless pattern = @match_pattern_mutex.synchronize{@match_pattern_cache[obj]}
+ pattern = consume_pattern(yield)
+ @match_pattern_mutex.synchronize{@match_pattern_cache[obj] = pattern}
+ end
+ pattern
+ end
+
+ def self.inherited(subclass)
+ super
+ subclass.instance_variable_set(:@match_pattern_cache, {})
+ subclass.instance_variable_set(:@match_pattern_mutex, Mutex.new)
+ end
+ # :nocov:
+ else
+ # Return the cached pattern for the given object. If the object is
+ # not already cached, yield to get the basic pattern, and convert the
+ # basic pattern to a pattern that does not partial segments.
+ def self.cached_matcher(obj)
+ unless pattern = @match_pattern_cache[obj]
+ pattern = @match_pattern_cache[obj] = consume_pattern(yield)
+ end
+ pattern
+ end
+
+ # Initialize the match_pattern cache in the subclass.
+ def self.inherited(subclass)
+ super
+ subclass.instance_variable_set(:@match_pattern_cache, {})
+ end
+ end
+
class << self
# Reference to the Roda class related to this request class.
attr_accessor :roda_class
# Since RodaRequest is anonymously subclassed when Roda is subclassed,
# and then assigned to a constant of the Roda subclass, make inspect
# reflect the likely name for the class.
def inspect
"#{roda_class.inspect}::RodaRequest"
end
+
+ private
+
+ # The pattern to use for consuming, based on the given argument. The returned
+ # pattern requires the path starts with a string and does not match partial
+ # segments.
+ def consume_pattern(pattern)
+ /\A(\/(?:#{pattern}))(\/|\z)/
+ end
end
end
# Base class used for Roda responses. The instance methods for this
# class are added by Roda::RodaPlugins::Base::ResponseMethods, so this
@@ -388,16 +434,53 @@
# rack response arrays can use this for performance.
def _halt(response)
throw :halt, response
end
+ # Match any of the elements in the given array. Return at the
+ # first match without evaluating future matches. Returns false
+ # if no elements in the array match.
+ def _match_array(matcher)
+ matcher.any? do |m|
+ if matched = match(m)
+ if m.is_a?(String)
+ captures.push(m)
+ end
+ end
+
+ matched
+ end
+ end
+
+ # Match the given regexp exactly if it matches a full segment.
+ def _match_regexp(re)
+ consume(self.class.cached_matcher(re){re})
+ end
+
+ # Match the given hash if all hash matchers match.
+ def _match_hash(hash)
+ hash.all?{|k,v| send("match_#{k}", v)}
+ end
+
+ # Match the given string to the request path. Regexp escapes the
+ # string so that regexp metacharacters are not matched, and recognizes
+ # colon tokens for placeholders.
+ def _match_string(str)
+ consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:\w+/, SEGMENT)})
+ end
+
+ # Match the given symbol if any segment matches.
+ def _match_symbol(sym)
+ consume(self.class.cached_matcher(sym){SEGMENT})
+ end
+
# Attempts to match the pattern to the current path. If there is no
# match, returns false without changes. Otherwise, modifies
# SCRIPT_NAME to include the matched path, removes the matched
# path from PATH_INFO, and updates captures with any regex captures.
def consume(pattern)
- matchdata = env[PATH_INFO].match(/\A(\/(?:#{pattern}))(\/|\z)/)
+ matchdata = env[PATH_INFO].match(pattern)
return false unless matchdata
vars = matchdata.captures
@@ -421,47 +504,32 @@
# Attempt to match the argument to the given request, handling
# common ruby types.
def match(matcher)
case matcher
when String
- match_string(matcher)
+ _match_string(matcher)
when Regexp
- consume(matcher)
+ _match_regexp(matcher)
when Symbol
- consume(SEGMENT)
+ _match_symbol(matcher)
when TERM
env[PATH_INFO] == EMPTY_STRING
when Hash
- matcher.all?{|k,v| send("match_#{k}", v)}
+ _match_hash(matcher)
when Array
- match_array(matcher)
+ _match_array(matcher)
when Proc
matcher.call
else
matcher
end
end
- # Match any of the elements in the given array. Return at the
- # first match without evaluating future matches. Returns false
- # if no elements in the array match.
- def match_array(matcher)
- matcher.any? do |m|
- if matched = match(m)
- if m.is_a?(String)
- captures.push(m)
- end
- end
-
- matched
- end
- end
-
# Match files with the given extension. Requires that the
# request path end with the extension.
def match_extension(ext)
- consume("([^\\/]+?)\.#{ext}\\z")
+ consume(self.class.cached_matcher(ext){"([^\\/]+?)\.#{ext}\\z"})
end
# Match by request method. This can be an array if you want
# to match on multiple methods.
def match_method(type)
@@ -484,18 +552,9 @@
# Adds any match to the captures.
def match_param!(key)
if (v = self[key]) && !v.empty?
captures << v
end
- end
-
- # Match the given string to the request path. Regexp escapes the
- # string so that regexp metacharacters are not matched, and recognizes
- # colon tokens for placeholders.
- def match_string(str)
- str = Regexp.escape(str)
- str.gsub!(/:\w+/, SEGMENT)
- consume(str)
end
# Yield to the given block, clearing any captures before
# yielding and restoring the SCRIPT_NAME and PATH_INFO on exit.
def try