lib/roda.rb in roda-0.9.0 vs lib/roda.rb in roda-1.0.0
- old
+ new
@@ -1,86 +1,87 @@
require "rack"
require "thread"
+require "roda/version"
# The main class for Roda. Roda is built completely out of plugins, with the
# default plugin being Roda::RodaPlugins::Base, so this class is mostly empty
# except for some constants.
class Roda
- # Roda's version, always specified by a string in \d+\.\d+\.\d+ format.
- RodaVersion = '0.9.0'.freeze
-
# 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
+ 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
- class << self
- # Reference to the Roda class related to this request class.
- attr_accessor :roda_class
+ # Make getting value from underlying hash thread safe.
+ def [](key)
+ @mutex.synchronize{@hash[key]}
+ 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"
+ # 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
+ # 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.
+ # with a symbol. Should be used by plugin files. Example:
+ #
+ # Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule)
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.
@@ -99,10 +100,30 @@
# 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.
+ #
+ # class App < Roda
+ # hash_matcher(:foo) do |v|
+ # self['foo'] == v
+ # end
+ #
+ # route do
+ # r.on :foo=>'bar' do
+ # # matches when param foo has value bar
+ # end
+ # end
+ # end
+ 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,
@@ -113,20 +134,24 @@
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)
end
# Load a new plugin into the current class. A plugin can be a module
# which is used directly, or a symbol represented a registered plugin
# which will be required and then used.
+ #
+ # Roda.plugin PluginModule
+ # Roda.plugin :csrf
def plugin(mixin, *args, &block)
if mixin.is_a?(Symbol)
mixin = RodaPlugins.load_plugin(mixin)
end
@@ -141,43 +166,92 @@
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
# Include the given module in the request class. If a block
# is provided instead of a module, create a module using the
- # the block.
+ # the block. Example:
+ #
+ # Roda.request_module SomeModule
+ #
+ # Roda.request_module do
+ # def description
+ # "#{request_method} #{path_info}"
+ # end
+ # end
+ #
+ # Roda.route do |r|
+ # r.description
+ # end
def request_module(mod = nil, &block)
module_include(:request, mod, &block)
end
# Include the given module in the response class. If a block
# is provided instead of a module, create a module using the
- # the block.
+ # the block. Example:
+ #
+ # Roda.response_module SomeModule
+ #
+ # Roda.response_module do
+ # def error!
+ # self.status = 500
+ # end
+ # end
+ #
+ # Roda.route do |r|
+ # response.error!
+ # end
def response_module(mod = nil, &block)
module_include(:response, mod, &block)
end
- # Setup route definitions for the current class, and build the
- # rack application using the stored middleware.
+ # Setup routing tree for the current Roda application, and build the
+ # underlying rack application using the stored middleware. Requires
+ # a block, which is yielded the request. By convention, the block
+ # argument should be named +r+. Example:
+ #
+ # Roda.route do |r|
+ # r.root do
+ # "Root"
+ # end
+ # end
+ #
+ # This should only be called once per class, and if called multiple
+ # times will overwrite the previous routing.
def route(&block)
@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.
+ # called before calling #route to have an effect. Example:
+ #
+ # Roda.use Rack::Session::Cookie, :secret=>ENV['secret']
def use(*args, &block)
@middleware << [args, block]
end
private
@@ -212,29 +286,38 @@
module InstanceMethods
SESSION_KEY = 'rack.session'.freeze
# Create a request and response of the appopriate
# class, the instance_exec the route block with
- # the request, handling any halts.
+ # the request, handling any halts. This is not usually
+ # called directly.
def call(env, &block)
@_request = self.class::RodaRequest.new(self, env)
@_response = self.class::RodaResponse.new
_route(&block)
end
- # The environment for the current request.
+ # The environment hash for the current request. Example:
+ #
+ # env['REQUEST_METHOD'] # => 'GET'
def env
request.env
end
# The class-level options hash. This should probably not be
- # modified at the instance level.
+ # modified at the instance level. Example:
+ #
+ # Roda.plugin :render
+ # Roda.route do |r|
+ # opts[:render_opts].inspect
+ # end
def opts
self.class.opts
end
# The instance of the request class related to this request.
+ # This is the same object yielded by Roda.route.
def request
@_request
end
# The instance of the response class related to this request.
@@ -252,26 +335,83 @@
# 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
+
+ # Define a verb method in the given that will yield to the match block
+ # if the request method matches and there are either no arguments or
+ # there is a successful terminal match on the arguments.
+ def def_verb_method(mod, verb)
+ mod.class_eval(<<-END, __FILE__, __LINE__+1)
+ def #{verb}(*args, &block)
+ _verb(args, &block) if #{verb == :get ? :is_get : verb}?
+ end
+ END
+ 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
- TERM = {:term=>true}.freeze
+ SLASH = "/".freeze
SEGMENT = "([^\\/]+)".freeze
+ TERM_INSPECT = "TERM".freeze
+ GET_REQUEST_METHOD = 'GET'.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
@@ -284,185 +424,412 @@
@captures = []
super(env)
end
# As request routing modifies SCRIPT_NAME and PATH_INFO, this exists
- # as a helper method to get the full request of the path info.
+ # as a helper method to get the full path of the request.
+ #
+ # r.env['SCRIPT_NAME'] = '/foo'
+ # r.env['PATH_INFO'] = '/bar'
+ # r.full_path_info
+ # # => '/foo/bar'
def full_path_info
- "#{env[SCRIPT_NAME]}#{env[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.
- def get(*args, &block)
- is_or_on(*args, &block) if get?
+ # Immediately stop execution of the route block and return the given
+ # rack response array of status, headers, and body. If no argument
+ # is given, uses the current response.
+ #
+ # r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
+ #
+ # response.status = 200
+ # response['Content-Type'] = 'text/html'
+ # response.write 'Hello World!'
+ # r.halt
+ def halt(res=response.finish)
+ throw :halt, res
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)
+ # Optimized method for whether this request is a +GET+ request.
+ # Similar to the default Rack::Request get? method, but can be
+ # overridden without changing rack's behavior.
+ def is_get?
+ @env[REQUEST_METHOD] == GET_REQUEST_METHOD
end
- # Handle #on block return values. By default, if a string is given
+ # Handle match 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.
+ #
+ # r.inspect
+ # # => '#<Roda::RodaRequest GET /foo/bar>'
def inspect
- "#<#{self.class.inspect} #{env[REQUEST_METHOD]} #{full_path_info}>"
+ "#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{full_path_info}>"
end
- # Adds TERM as the final argument and passes to #on, ensuring that
- # there is only a match if #on has fully matched the path.
+ # Does a terminal match on the current path, matching only if the arguments
+ # have fully matched the path. If it matches, the match block is
+ # executed, and when the match block returns, the rack response is
+ # returned.
+ #
+ # r.path_info
+ # # => "/foo/bar"
+ #
+ # r.is 'foo' do
+ # # does not match, as path isn't fully matched (/bar remaining)
+ # end
+ #
+ # r.is 'foo/bar' do
+ # # matches as path is empty after matching
+ # end
+ #
+ # If no arguments are given, matches if the path is already fully matched.
+ #
+ # r.on 'foo/bar' do
+ # r.is do
+ # # matches as path is already empty
+ # end
+ # end
+ #
+ # Note that this matches only if the path after matching the arguments
+ # is empty, not if it still contains a trailing slash:
+ #
+ # r.path_info
+ # # => "/foo/bar/"
+ #
+ # r.is 'foo/bar' do
+ # # does not match, as path isn't fully matched (/ remaining)
+ # end
+ #
+ # r.is 'foo/bar/' do
+ # # matches as path is empty after matching
+ # end
+ #
+ # r.on 'foo/bar' do
+ # r.is "" do
+ # # matches as path is empty after matching
+ # end
+ # end
def is(*args, &block)
- args << TERM
- on(*args, &block)
+ if args.empty?
+ if @env[PATH_INFO] == EMPTY_STRING
+ always(&block)
+ end
+ else
+ args << TERM
+ if_match(args, &block)
+ end
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.
+ # Does a match on the path, matching only if the arguments
+ # have matched the path. Because this doesn't fully match the
+ # path, this is usually used to setup branches of the routing tree,
+ # not for final handling of the request.
+ #
+ # r.path_info
+ # # => "/foo/bar"
+ #
+ # r.on 'foo' do
+ # # matches, path is /bar after matching
+ # end
+ #
+ # r.on 'bar' do
+ # # does not match
+ # end
+ #
+ # Like other routing methods, If it matches, the match block is
+ # executed, and when the match block returns, the rack response is
+ # returned. However, in general you will call another routing method
+ # inside the match block that fully matches the path and does the
+ # final handling for the request:
+ #
+ # r.on 'foo' do
+ # r.is 'bar' do
+ # # handle /foo/bar request
+ # end
+ # end
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
+ if args.empty?
+ always(&block)
+ else
+ if_match(args, &block)
end
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.
- def post(*args, &block)
- is_or_on(*args, &block) if post?
- end
-
- # The response related to the current request.
+ # The response related to the current request. See ResponseMethods for
+ # instance methods for the response, but in general the most common usage
+ # is to override the response status and headers:
+ #
+ # response.status = 200
+ # response['Header-Name'] = 'Header value'
def response
scope.response
end
- # Immediately redirect to the given path.
- def redirect(path, status=302)
+ # Immediately redirect to the path using the status code. This ends
+ # the processing of the request:
+ #
+ # r.redirect '/page1', 301 if r['param'] == 'value1'
+ # r.redirect '/page2' # uses 302 status code
+ # response.status = 404 # not reached
+ #
+ # If you do not provide a path, by default it will redirect to the same
+ # path if the request is not a +GET+ request. This is designed to make
+ # it easy to use where a +POST+ request to a URL changes state, +GET+
+ # returns the current state, and you want to show the current state
+ # after changing:
+ #
+ # r.is "foo" do
+ # r.get do
+ # # show state
+ # end
+ #
+ # r.post do
+ # # change state
+ # r.redirect
+ # end
+ # end
+ def redirect(path=default_redirect_path, status=302)
response.redirect(path, status)
- _halt response.finish
+ throw :halt, response.finish
end
- # Call the given rack app with the environment and immediately return
- # the response as the response for this request.
+ # Routing matches that only matches +GET+ requests where the current
+ # path is +/+. If it matches, the match block is executed, and when
+ # the match block returns, the rack response is returned.
+ #
+ # [r.request_method, r.path_info]
+ # # => ['GET', '/']
+ #
+ # r.root do
+ # # matches
+ # end
+ #
+ # This is usuable inside other match blocks:
+ #
+ # [r.request_method, r.path_info]
+ # # => ['GET', '/foo/']
+ #
+ # r.on 'foo' do
+ # r.root do
+ # # matches
+ # end
+ # end
+ #
+ # Note that this does not match non-+GET+ requests:
+ #
+ # [r.request_method, r.path_info]
+ # # => ['POST', '/']
+ #
+ # r.root do
+ # # does not match
+ # end
+ #
+ # Use <tt>r.post ""</tt> for +POST+ requests where the current path
+ # is +/+.
+ #
+ # Nor does it match empty paths:
+ #
+ # [r.request_method, r.path_info]
+ # # => ['GET', '/foo']
+ #
+ # r.on 'foo' do
+ # r.root do
+ # # does not match
+ # end
+ # end
+ #
+ # Use <tt>r.get true</tt> to handle +GET+ requests where the current
+ # path is empty.
+ def root(&block)
+ if @env[PATH_INFO] == SLASH && is_get?
+ always(&block)
+ end
+ end
+
+ # Call the given rack app with the environment and return the response
+ # from the rack app as the response for this request. This ends
+ # the processing of the request:
+ #
+ # r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
+ # r.run(proc{[404, {}, []]})
+ # response.status = 404 # not reached
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
+ # 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+)/){|m| _match_symbol_regexp($1)}})
+ end
+
+ # Match the given symbol if any segment matches.
+ def _match_symbol(sym)
+ 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
+
+ # 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)
+ if args.empty?
+ always(&block)
+ else
+ args << TERM
+ if_match(args, &block)
+ end
+ end
+
+ # Yield to the match block and return rack response after the block returns.
+ def always
+ block_result(yield)
+ throw :halt, response.finish
+ 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(/\A(\/(?:#{pattern}))(\/|\z)/)
+ env = @env
+ 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}"
+ env[PATH_INFO] = 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
+ # The default path to use for redirects when a path is not given.
+ # For non-GET requests, redirects to the current path, which will
+ # trigger a GET request. This is to make the common case where
+ # a POST request will redirect to a GET request at the same location
+ # will work fine.
+ #
+ # If the current request is a GET request, raise an error, as otherwise
+ # it is easy to create an infinite redirect.
+ def default_redirect_path
+ raise RodaError, "must provide path argument to redirect for get requests" if is_get?
+ full_path_info
end
+ # If all of the arguments match, yields to the match block and
+ # returns the rack response when the block returns. If any of
+ # the match arguments doesn't match, does nothing.
+ def if_match(args)
+ env = @env
+ 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
+
# 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
+ # 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("([^\\/]+?)\.#{ext}\\z")
+ consume(self.class.cached_matcher([:extension, ext]){/([^\\\/]+)\.#{ext}/})
end
# Match by request method. This can be an array if you want
# to match on multiple methods.
def match_method(type)
if type.is_a?(Array)
type.any?{|t| match_method(t)}
else
- type.to_s.upcase == env[REQUEST_METHOD]
+ type.to_s.upcase == @env[REQUEST_METHOD]
end
end
# Match the given parameter if present, even if the parameter is empty.
# Adds any match to the captures.
@@ -477,42 +844,23 @@
def match_param!(key)
if (v = self[key]) && !v.empty?
captures << v
end
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
+ # Class methods for RodaResponse
+ module ResponseClassMethods
+ # Reference to the Roda class related to this response class.
+ attr_accessor :roda_class
- # Only match if the request path is empty, which usually indicates it
- # has already been fully matched.
- def match_term(term)
- !(term ^ (env[PATH_INFO] == EMPTY_STRING))
+ # 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
-
- # 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]
-
- # 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
- end
end
# Instance methods for RodaResponse
module ResponseMethods
CONTENT_LENGTH = "Content-Length".freeze
@@ -533,16 +881,20 @@
@headers = default_headers
@body = []
@length = 0
end
- # Return the response header with the given key.
+ # Return the response header with the given key. Example:
+ #
+ # response['Content-Type'] # => 'text/html'
def [](key)
@headers[key]
end
# Set the response header with the given key to the given value.
+ #
+ # response['Content-Type'] = 'application/json'
def []=(key, value)
@headers[key] = value
end
# Show response class, status code, response headers, and response body
@@ -556,43 +908,62 @@
end
# Modify the headers to include a Set-Cookie value that
# deletes the cookie. A value hash can be provided to
# override the default one used to delete the cookie.
+ # Example:
+ #
+ # response.delete_cookie('foo')
+ # response.delete_cookie('foo', :domain=>'example.org')
def delete_cookie(key, value = {})
::Rack::Utils.delete_cookie_header!(@headers, key, value)
end
# Whether the response body has been written to yet. Note
# that writing an empty string to the response body marks
- # the response as not empty.
+ # the response as not empty. Example:
+ #
+ # response.empty? # => true
+ # response.write('a')
+ # response.empty? # => false
def empty?
@body.empty?
end
# Return the rack response array of status, headers, and body
- # for the current response.
+ # for the current response. Example:
+ #
+ # response.finish # => [200, {'Content-Type'=>'text/html'}, []]
def finish
b = @body
s = (@status ||= b.empty? ? 404 : 200)
[s, @headers, b]
end
# Set the Location header to the given path, and the status
- # to the given status.
+ # to the given status. Example:
+ #
+ # response.redirect('foo', 301)
+ # response.redirect('bar')
def redirect(path, status = 302)
@headers[LOCATION] = path
@status = status
end
# Set the cookie with the given key in the headers.
+ #
+ # response.set_cookie('foo', 'bar')
+ # response.set_cookie('foo', :value=>'bar', :domain=>'example.org')
def set_cookie(key, value)
::Rack::Utils.set_cookie_header!(@headers, key, value)
end
# Write to the response body. Updates Content-Length header
- # with the size of the string written. Returns nil.
+ # with the size of the string written. Returns nil. Example:
+ #
+ # response.write('foo')
+ # response['Content-Length'] # =>'3'
def write(str)
s = str.to_s
@length += s.bytesize
@headers[CONTENT_LENGTH] = @length.to_s
@@ -603,6 +974,8 @@
end
end
extend RodaPlugins::Base::ClassMethods
plugin RodaPlugins::Base
+ RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :get)
+ RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :post)
end