module Roda::RodaPlugins::Base::RequestMethods
def _match_array(matcher)
first match without evaluating future matches. Returns false
Match any of the elements in the given array. Return at the
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
def _match_class(klass)
Integer :: Match an integer segment, yielding result to block as an integer
are supported by default:
Match the given class. Currently, the following classes
def _match_class(klass) meth = :"_match_class_#{klass}" if respond_to?(meth, true) # Allow calling private methods, as match methods are generally private send(meth) else unsupported_matcher(klass) end end
def _match_class_Integer
Match integer segment of up to 100 decimal characters, and yield resulting value as an
def _match_class_Integer consume(/\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer) end
def _match_hash(hash)
def _match_hash(hash) # Allow calling private methods, as match methods are generally private hash.all?{|k,v| send("match_#{k}", v)} end
def _match_regexp(re)
Match only if all of the arguments in the given array match.
def _match_regexp(re) consume(self.class.cached_matcher(re){re}) end
def _match_string(str)
request path ends with the string or if the next character in the
Match the given string to the request path. Matches only if the
def _match_string(str) rp = @remaining_path length = str.length match = case rp.rindex(str, length) when nil # segment does not match, most common case return when 1 # segment matches, check first character is / rp.getbyte(0) == 47 else # must be 0 # segment matches at first character, only a match if # empty string given and first character is / length == 0 && rp.getbyte(0) == 47 end if match length += 1 case rp.getbyte(length) when 47 # next character is /, update remaining path to rest of string @remaining_path = rp[length, 100000000] when nil # end of string, so remaining path is empty @remaining_path = "" # else # Any other value means this was partial segment match, # so we return nil in that case without updating the # remaining_path. No need for explicit else clause. end end end
def _match_symbol(sym=nil)
def _match_symbol(sym=nil) rp = @remaining_path if rp.getbyte(0) == 47 if last = rp.index('/', 1) @captures << rp[1, last-1] @remaining_path = rp[last, rp.length] elsif (len = rp.length) > 1 @captures << rp[1, len] @remaining_path = "" end end end
def _remaining_path(env)
def _remaining_path(env) env["PATH_INFO"] end
def _verb(args, &block)
Backbone of the verb method support, using a terminal match if
def _verb(args, &block) if args.empty? always(&block) else args << TERM if_match(args, &block) end end
def always
def always block_result(yield) throw :halt, response.finish end
def block_result(result)
Handle match block return values. By default, if a string is given
def block_result(result) res = response if res.empty? && (body = block_result_body(result)) res.write(body) end end
def block_result_body(result)
a body. By default, a String is returned directly, and nil is
The body to use for the response if the response does not already have
def block_result_body(result) case result when String result when nil, false # nothing else unsupported_block_result(result) end end
def consume(pattern, meth=nil)
SCRIPT_NAME to include the matched path, removes the matched
match, returns false without changes. Otherwise, modifies
Attempts to match the pattern to the current path. If there is no
def consume(pattern, meth=nil) if matchdata = pattern.match(@remaining_path) captures = matchdata.captures if meth return unless captures = scope.send(meth, *captures) # :nocov: elsif defined?(yield) # RODA4: Remove return unless captures = yield(*captures) # :nocov: end @remaining_path = matchdata.post_match if captures.is_a?(Array) @captures.concat(captures) else @captures << captures end end end
def default_redirect_path
If the current request is a GET request, raise an error, as otherwise
will work fine.
a POST request will redirect to a GET request at the same location
trigger a GET request. This is to make the common case where
For non-GET requests, redirects to the current path, which will
The default path to use for redirects when a path is not given.
def default_redirect_path raise RodaError, "must provide path argument to redirect for get requests" if is_get? path end
def default_redirect_status
The default status to use for redirects if a status is not provided,
def default_redirect_status 302 end
def empty_path?
def empty_path? @remaining_path.empty? end
def get(*args, &block)
requests, otherwise, matches only GET requests where the arguments
Match GET requests. If no arguments are provided, matches all GET
def get(*args, &block) _verb(args, &block) if is_get? end
def halt(res=response.finish)
response.write 'Hello World!'
response['Content-Type'] = 'text/html'
response.status = 200
r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
is given, uses the current response.
rack response array of status, headers, and body. If no argument
Immediately stop execution of the route block and return the given
def halt(res=response.finish) throw :halt, res end
def http_version
def http_version # Prefer SERVER_PROTOCOL as it is required in Rack 3. # Still fall back to HTTP_VERSION if SERVER_PROTOCOL # is not set, in case the server in use is not Rack 3 # compliant. @env['SERVER_PROTOCOL'] || @env['HTTP_VERSION'] end
def http_version
def http_version # Prefer HTTP_VERSION as it is backwards compatible # with previous Roda versions. Fallback to # SERVER_PROTOCOL for servers that do not set # HTTP_VERSION. @env['HTTP_VERSION'] || @env['SERVER_PROTOCOL'] end
def if_match(args)
returns the rack response when the block returns. If any of
If all of the arguments match, yields to the match block and
def if_match(args) path = @remaining_path # For every block, we make sure to reset captures so that # nesting matchers won't mess with each other's captures. captures = @captures.clear if match_all(args) block_result(yield(*captures)) throw :halt, response.finish else @remaining_path = path false end end
def initialize(scope, env)
def initialize(scope, env) @scope = scope @captures = [] @remaining_path = _remaining_path(env) @env = env end
def inspect
r.inspect
request method and full path.
Show information about current request, including request class,
def inspect "#<#{self.class.inspect} #{@env["REQUEST_METHOD"]} #{path}>" end
def is(*args, &block)
end
# matches as path is empty after matching
r.is "" do
r.on 'foo/bar' do
end
# matches as path is empty after matching
r.is 'foo/bar/' do
end
# does not match, as path isn't fully matched (/ remaining)
r.is 'foo/bar' do
# => "/foo/bar/"
r.remaining_path
is empty, not if it still contains a trailing slash:
Note that this matches only if the path after matching the arguments
end
end
# matches as path is already empty
r.is do
r.on 'foo/bar' do
If no arguments are given, matches if the path is already fully matched.
end
# matches as path is empty after matching
r.is 'foo/bar' do
end
# does not match, as path isn't fully matched (/bar remaining)
r.is 'foo' do
# => "/foo/bar"
r.remaining_path
returned.
executed, and when the match block returns, the rack response is
have fully matched the path. If it matches, the match block is
Does a terminal match on the current path, matching only if the arguments
def is(*args, &block) if args.empty? if empty_path? always(&block) end else args << TERM if_match(args, &block) end end
def is_get?
Similar to the default Rack::Request get? method, but can be
Optimized method for whether this request is a +GET+ request.
def is_get? @env["REQUEST_METHOD"] == 'GET' end
def match(matcher)
Attempt to match the argument to the given request, handling
def match(matcher) case matcher when String _match_string(matcher) when Class _match_class(matcher) when TERM empty_path? when Regexp _match_regexp(matcher) when true matcher when Array _match_array(matcher) when Hash _match_hash(matcher) when Symbol _match_symbol(matcher) when false, nil matcher when Proc matcher.call else unsupported_matcher(matcher) end end
def match_all(args)
def match_all(args) args.all?{|arg| match(arg)} end
def match_method(type)
Match by request method. This can be an array if you want
def match_method(type) if type.is_a?(Array) type.any?{|t| match_method(t)} else type.to_s.upcase == @env["REQUEST_METHOD"] end end
def matched_path
def matched_path e = @env e["SCRIPT_NAME"] + e["PATH_INFO"].chomp(@remaining_path) end
def on(*args, &block)
end
# handle /foo/bar request
r.is 'bar' do
r.on 'foo' do
final handling for the request:
inside the match block that fully matches the path and does the
returned. However, in general you will call another routing method
executed, and when the match block returns, the rack response is
Like other routing methods, If it matches, the match block is
end
# does not match
r.on 'bar' do
end
# matches, path is /bar after matching
r.on 'foo' do
# => "/foo/bar"
r.remaining_path
not for final handling of the request.
path, this is usually used to setup branches of the routing tree,
have matched the path. Because this doesn't fully match the
Does a match on the path, matching only if the arguments
def on(*args, &block) if args.empty? always(&block) else if_match(args, &block) end end
def path
r.path
r.env['PATH_INFO'] = '/bar'
r.env['SCRIPT_NAME'] = '/foo'
This an an optimized version of Rack::Request#path.
def path e = @env "#{e["SCRIPT_NAME"]}#{e["PATH_INFO"]}" end
def post(*args, &block)
requests, otherwise, matches only POST requests where the arguments
Match POST requests. If no arguments are provided, matches all POST
def post(*args, &block) _verb(args, &block) if post? end
def redirect(path=default_redirect_path, status=default_redirect_status)
end
r.redirect
# change state
r.post do
end
# show state
r.get do
r.is "foo" do
after changing:
returns the current state, and you want to show the current state
it easy to use where a +POST+ request to a URL changes state, +GET+
path if the request is not a +GET+ request. This is designed to make
If you do not provide a path, by default it will redirect to the same
response.status = 404 # not reached
r.redirect '/page2' # uses 302 status code
r.redirect '/page1', 301 if r['param'] == 'value1'
the processing of the request:
Immediately redirect to the path using the status code. This ends
def redirect(path=default_redirect_path, status=default_redirect_status) response.redirect(path, status) throw :halt, response.finish end
def response
response.status = 200
is to override the response status and headers:
instance methods for the response, but in general the most common usage
The response related to the current request. See ResponseMethods for
def response @scope.response end
def roda_class
def roda_class self.class.roda_class end
def root(&block)
Use r.get true to handle +GET+ requests where the current
end
end
# does not match
r.root do
r.on 'foo' do
# => ['GET', '/foo']
[r.request_method, r.remaining_path]
Nor does it match empty paths:
is +/+.
Use r.post "" for +POST+ requests where the current path
end
# does not match
r.root do
# => ['POST', '/']
[r.request_method, r.remaining_path]
Note that this does not match non-+GET+ requests:
end
end
# matches
r.root do
r.on 'foo' do
# => ['GET', '/foo/']
[r.request_method, r.remaining_path]
This is usuable inside other match blocks:
end
# matches
r.root do
# => ['GET', '/']
[r.request_method, r.remaining_path]
the match block returns, the rack response is returned.
path is +/+. If it matches, the match block is executed, and when
Match method that only matches +GET+ requests where the current
def root(&block) if @remaining_path == "/" && is_get? always(&block) end end
def run(app)
before dispatching to another rack app, so the app still works as
This updates SCRIPT_NAME/PATH_INFO based on the current remaining_path
response.status = 404 # not reached
r.run(proc{[404, {}, []]})
r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
the processing of the request:
from the rack app as the response for this request. This ends
Call the given rack app with the environment and return the response
def run(app) e = @env path = real_remaining_path sn = "SCRIPT_NAME" pi = "PATH_INFO" script_name = e[sn] path_info = e[pi] begin e[sn] += path_info.chomp(path) e[pi] = path throw :halt, app.call(e) ensure e[sn] = script_name e[pi] = path_info end end
def session
The session for the current request. Raises a RodaError if
def session @env['rack.session'] || raise(RodaError, "You're missing a session handler, try using the sessions plugin.") end
def unsupported_block_result(result)
How to handle block results that are not nil, false, or a String.
def unsupported_block_result(result) raise RodaError, "unsupported block result: #{result.inspect}" end
def unsupported_matcher(matcher)
def unsupported_matcher(matcher) raise RodaError, "unsupported matcher: #{matcher.inspect}" end