lib/roda.rb in roda-cj-0.9.2 vs lib/roda.rb in roda-cj-0.9.3
- old
+ new
@@ -7,124 +7,79 @@
# except for some constants.
class Roda
# Error class raised by Roda
class RodaError < StandardError; end
- # 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
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby'
+ # A thread safe cache class, offering only #[] and #[]= methods,
+ # each protected by a mutex. Used on non-MRI where Hash is not
+ # thread safe.
+ class RodaCache
+ # Create a new thread safe cache.
+ def initialize
+ @mutex = Mutex.new
+ @hash = {}
end
- def self.inherited(subclass)
- super
- subclass.instance_variable_set(:@match_pattern_cache, {})
- subclass.instance_variable_set(:@match_pattern_mutex, Mutex.new)
+ # Make getting value from underlying hash thread safe.
+ def [](key)
+ @mutex.synchronize{@hash[key]}
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, {})
+ # Make setting value in underlying hash thread safe.
+ def []=(key, value)
+ @mutex.synchronize{@hash[key] = value}
end
end
+ else
+ # Hashes are already thread-safe in MRI, due to the GVL, so they
+ # can safely be used as a cache.
+ RodaCache = Hash
+ 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
+ # Base class used for Roda requests. The instance methods for this
+ # class are added by Roda::RodaPlugins::Base::RequestMethods, the
+ # class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
+ class RodaRequest < ::Rack::Request;
+ @roda_class = ::Roda
+ @match_pattern_cache = ::Roda::RodaCache.new
end
# Base class used for Roda responses. The instance methods for this
- # class are added by Roda::RodaPlugins::Base::ResponseMethods, so this
- # only contains the class methods.
+ # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
+ # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
class RodaResponse < ::Rack::Response;
@roda_class = ::Roda
-
- class << self
- # Reference to the Roda class related to this response class.
- attr_accessor :roda_class
-
- # Since RodaResponse 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}::RodaResponse"
- end
- end
end
@builder = ::Rack::Builder.new
@middleware = []
@opts = {}
# Module in which all Roda plugins should be stored. Also contains logic for
# registering and loading plugins.
module RodaPlugins
- # Mutex protecting the plugins hash
- @mutex = ::Mutex.new
-
# Stores registered plugins
- @plugins = {}
+ @plugins = RodaCache.new
# If the registered plugin already exists, use it. Otherwise,
# require it and return it. This raises a LoadError if such a
# plugin doesn't exist, or a RodaError if it exists but it does
# not register itself correctly.
def self.load_plugin(name)
h = @plugins
- unless plugin = @mutex.synchronize{h[name]}
+ unless plugin = h[name]
require "roda/plugins/#{name}"
- raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = @mutex.synchronize{h[name]}
+ raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name]
end
plugin
end
# Register the given plugin with Roda, so that it can be loaded using #plugin
# with a symbol. Should be used by plugin files.
def self.register_plugin(name, mod)
- @mutex.synchronize{@plugins[name] = mod}
+ @plugins[name] = mod
end
# The base plugin for Roda, implementing all default functionality.
# Methods are put into a plugin so future plugins can easily override
# them and call super to get the default behavior.
@@ -143,10 +98,18 @@
# access to the underlying rack app.
def call(env)
app.call(env)
end
+ # Create a match_#{key} method in the request class using the given
+ # block, so that using a hash key in a request match method will
+ # call the block. The block should return nil or false to not
+ # match, and anything else to match.
+ def hash_matcher(key, &block)
+ request_module{define_method(:"match_#{key}", &block)}
+ end
+
# When inheriting Roda, setup a new rack app builder, copy the
# default middleware and opts into the subclass, and set the
# request and response classes in the subclasses to be subclasses
# of the request and responses classes in the parent class. This
# makes it so child classes inherit plugins from their parent,
@@ -157,10 +120,11 @@
subclass.instance_variable_set(:@middleware, @middleware.dup)
subclass.instance_variable_set(:@opts, opts.dup)
request_class = Class.new(self::RodaRequest)
request_class.roda_class = subclass
+ request_class.match_pattern_cache = thread_safe_cache
subclass.const_set(:RodaRequest, request_class)
response_class = Class.new(self::RodaResponse)
response_class.roda_class = subclass
subclass.const_set(:RodaResponse, response_class)
@@ -185,13 +149,19 @@
extend mixin::ClassMethods
end
if defined?(mixin::RequestMethods)
self::RodaRequest.send(:include, mixin::RequestMethods)
end
+ if defined?(mixin::RequestClassMethods)
+ self::RodaRequest.extend mixin::RequestClassMethods
+ end
if defined?(mixin::ResponseMethods)
self::RodaResponse.send(:include, mixin::ResponseMethods)
end
+ if defined?(mixin::ResponseClassMethods)
+ self::RodaResponse.extend mixin::ResponseClassMethods
+ end
if mixin.respond_to?(:configure)
mixin.configure(self, *args, &block)
end
end
@@ -216,10 +186,16 @@
@middleware.each{|a, b| @builder.use(*a, &b)}
@builder.run lambda{|env| new.call(env, &block)}
@app = @builder.to_app
end
+ # A new thread safe cache instance. This is a method so it can be
+ # easily overridden for alternative implementations.
+ def thread_safe_cache
+ RodaCache.new
+ end
+
# Add a middleware to use for the rack application. Must be
# called before calling #route.
def use(*args, &block)
@middleware << [args, block]
end
@@ -296,27 +272,72 @@
# Internals of #call, extracted so that plugins can override
# behavior after the request and response have been setup.
def _route(&block)
catch(:halt) do
- request.handle_on_result(instance_exec(@_request, &block))
+ request.block_result(instance_exec(@_request, &block))
response.finish
end
end
end
+ # Class methods for RodaRequest
+ module RequestClassMethods
+ # Reference to the Roda class related to this request class.
+ attr_accessor :roda_class
+
+ # The cache to use for match patterns for this request class.
+ attr_accessor :match_pattern_cache
+
+ # 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 cached_matcher(obj)
+ cache = @match_pattern_cache
+
+ unless pattern = cache[obj]
+ pattern = cache[obj] = consume_pattern(yield)
+ end
+
+ pattern
+ end
+
+ # 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
+
# Instance methods for RodaRequest, mostly related to handling routing
# for the request.
module RequestMethods
PATH_INFO = "PATH_INFO".freeze
SCRIPT_NAME = "SCRIPT_NAME".freeze
REQUEST_METHOD = "REQUEST_METHOD".freeze
EMPTY_STRING = "".freeze
SLASH = "/".freeze
- TERM = Object.new.freeze
SEGMENT = "([^\\/]+)".freeze
+ EMPTY_ARRAY = [].freeze
+ TERM_INSPECT = "TERM".freeze
+ TERM = Object.new
+ def TERM.inspect
+ TERM_INSPECT
+ end
+ TERM.freeze
+
# The current captures for the request. This gets modified as routing
# occurs.
attr_reader :captures
# The Roda instance related to this request object. Useful if routing
@@ -334,28 +355,30 @@
# as a helper method to get the full request of the path info.
def full_path_info
"#{env[SCRIPT_NAME]}#{env[PATH_INFO]}"
end
- # If this is not a GET method, returns immediately. Otherwise, calls
- # #is if there are any arguments, or #on if there are no arguments.
+ # If this is not a GET method, returns immediately. Otherwise, if there
+ # are arguments, do a terminal match on the arguments, otherwise do a
+ # regular match.
def get(*args, &block)
- is_or_on(*args, &block) if get?
+ _verb(args, &block) if get?
end
# Immediately stop execution of the route block and return the given
- # rack response array of status, headers, and body.
- def halt(response)
- _halt(response)
+ # rack response array of status, headers, and body. If no argument
+ # is given, uses the current response.
+ def halt(res=response.finish)
+ throw :halt, res
end
# Handle #on block return values. By default, if a string is given
# and the response is empty, use the string as the response body.
- def handle_on_result(result)
+ def block_result(result)
res = response
- if result.is_a?(String) && res.empty?
- res.write(result)
+ if res.empty? && (body = block_result_body(result))
+ res.write(body)
end
end
# Show information about current request, including request class,
# request method and full path.
@@ -365,79 +388,57 @@
# Adds TERM as the final argument and passes to #on, ensuring that
# there is only a match if #on has fully matched the path.
def is(*args, &block)
args << TERM
- on(*args, &block)
+ _on(args, &block)
end
# Attempts to match on all of the arguments. If all of the
# arguments match, control is yielded to the block, and after
# the block returns, the rack response will be returned.
# If any of the arguments fails, ensures the request state is
# returned to that before matches were attempted.
def on(*args, &block)
- try do
- # We stop evaluation of this entire matcher unless
- # each and every `arg` defined for this matcher evaluates
- # to a non-false value.
- #
- # Short circuit examples:
- # on true, false do
- #
- # # PATH_INFO=/user
- # on true, "signup"
- return unless args.all?{|arg| match(arg)}
-
- # The captures we yield here were generated and assembled
- # by evaluating each of the `arg`s above. Most of these
- # are carried out by #consume.
- handle_on_result(yield(*captures))
-
- _halt response.finish
- end
+ _on(args, &block)
end
- # If this is not a POST method, returns immediately. Otherwise, calls
- # #is if there are any arguments, or #on if there are no arguments.
+ # If this is not a GET method, returns immediately. Otherwise, if there
+ # are arguments, do a terminal match on the arguments, otherwise do a
+ # regular match.
def post(*args, &block)
- is_or_on(*args, &block) if post?
+ _verb(args, &block) if post?
end
# The response related to the current request.
def response
scope.response
end
# Immediately redirect to the given path.
def redirect(path, status=302)
response.redirect(path, status)
- _halt response.finish
+ throw :halt, response.finish
end
- #
+ # If the current path is the root ("/"), match on the block. If a request
+ # method is given, return immediately if the request does not use the given
+ # method.
def root(request_method=nil, &block)
if env[PATH_INFO] == SLASH && (!request_method || send(:"#{request_method}?"))
- on(&block)
+ _on(EMPTY_ARRAY, &block)
end
end
# Call the given rack app with the environment and immediately return
# the response as the response for this request.
def run(app)
- _halt app.call(env)
+ throw :halt, app.call(env)
end
private
- # Internal halt method, used so that halt can be overridden to handle
- # non-rack response arrays, but internal code that always generates
- # 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|
@@ -463,46 +464,76 @@
# 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)})
+ consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
end
# Match the given symbol if any segment matches.
def _match_symbol(sym)
- consume(self.class.cached_matcher(sym){SEGMENT})
+ consume(self.class.cached_matcher(sym){_match_symbol_regexp(sym)})
end
+ # The regular expression to use for matching symbols. By default, any non-empty
+ # segment matches.
+ def _match_symbol_regexp(s)
+ SEGMENT
+ end
+
+ # Internal match method taking array of matchers instead of multiple
+ # arguments.
+ def _on(args)
+ script = env[SCRIPT_NAME]
+ path = env[PATH_INFO]
+
+ # For every block, we make sure to reset captures so that
+ # nesting matchers won't mess with each other's captures.
+ captures.clear
+
+ return unless match_all(args)
+ block_result(yield(*captures))
+ throw :halt, response.finish
+ ensure
+ env[SCRIPT_NAME] = script
+ env[PATH_INFO] = path
+ end
+
+ # Backbone of the verb method support, using a terminal match if
+ # args is not empty, or a regular match if it is empty.
+ def _verb(args, &block)
+ unless args.empty?
+ args << TERM
+ end
+ _on(args, &block)
+ end
+
+ # The body to use for the response if the response does not return
+ # a body. By default, a String is returned directly, and nil is
+ # returned otherwise.
+ def block_result_body(result)
+ if result.is_a?(String)
+ result
+ end
+ 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(pattern)
+ return unless matchdata = env[PATH_INFO].match(pattern)
- return false unless matchdata
-
vars = matchdata.captures
# Don't mutate SCRIPT_NAME, breaks try
env[SCRIPT_NAME] += vars.shift
env[PATH_INFO] = "#{vars.pop}#{matchdata.post_match}"
captures.concat(vars)
end
- # Backbone of the verb method support, calling #is if there are any
- # arguments, or #on if there are none.
- def is_or_on(*args, &block)
- if args.empty?
- on(*args, &block)
- else
- is(*args, &block)
- end
- end
-
# Attempt to match the argument to the given request, handling
# common ruby types.
def match(matcher)
case matcher
when String
@@ -522,10 +553,15 @@
else
matcher
end
end
+ # Match only if all of the arguments in the given array match.
+ def match_all(args)
+ args.all?{|arg| match(arg)}
+ end
+
# Match files with the given extension. Requires that the
# request path end with the extension.
def match_extension(ext)
consume(self.class.cached_matcher(ext){"([^\\/]+?)\.#{ext}\\z"})
end
@@ -553,25 +589,21 @@
def match_param!(key)
if (v = self[key]) && !v.empty?
captures << v
end
end
+ end
- # Yield to the given block, clearing any captures before
- # yielding and restoring the SCRIPT_NAME and PATH_INFO on exit.
- def try
- script = env[SCRIPT_NAME]
- path = env[PATH_INFO]
+ # Class methods for RodaResponse
+ module ResponseClassMethods
+ # Reference to the Roda class related to this response class.
+ attr_accessor :roda_class
- # For every block, we make sure to reset captures so that
- # nesting matchers won't mess with each other's captures.
- captures.clear
-
- yield
-
- ensure
- env[SCRIPT_NAME] = script
- env[PATH_INFO] = path
+ # Since RodaResponse 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}::RodaResponse"
end
end
# Instance methods for RodaResponse
module ResponseMethods