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