lib/roda.rb in roda-cj-0.9.6 vs lib/roda.rb in roda-cj-1.0.0
- old
+ new
@@ -49,13 +49,14 @@
# methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
class RodaResponse < ::Rack::Response;
@roda_class = ::Roda
end
- @builder = ::Rack::Builder.new
+ @app = nil
@middleware = []
@opts = {}
+ @route_block = nil
# Module in which all Roda plugins should be stored. Also contains logic for
# registering and loading plugins.
module RodaPlugins
# Stores registered plugins
@@ -73,11 +74,13 @@
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)
@plugins[name] = mod
end
# The base plugin for Roda, implementing all default functionality.
@@ -90,10 +93,13 @@
attr_reader :app
# The settings/options hash for the current class.
attr_reader :opts
+ # The route block that this class uses.
+ attr_reader :route_block
+
# Call the internal rack application with the given environment.
# This allows the class itself to be used as a rack application.
# However, for performance, it's better to use #app to get direct
# access to the underlying rack app.
def call(env)
@@ -102,25 +108,34 @@
# 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,
- # but using plugins in child classes does not affect the parent.
+ # When inheriting Roda, copy the shared data into the subclass,
+ # and setup the request and response subclasses.
def inherited(subclass)
super
- subclass.instance_variable_set(:@builder, ::Rack::Builder.new)
subclass.instance_variable_set(:@middleware, @middleware.dup)
subclass.instance_variable_set(:@opts, opts.dup)
+ subclass.instance_variable_set(:@route_block, @route_block)
+ subclass.send(:build_rack_app)
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)
@@ -131,10 +146,13 @@
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
@@ -166,44 +184,91 @@
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
+ @route_block = block
+ build_rack_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]
+ build_rack_app
end
private
+ # Build the rack app to use
+ def build_rack_app
+ if block = @route_block
+ builder = Rack::Builder.new
+ @middleware.each{|a, b| builder.use(*a, &b)}
+ builder.run lambda{|env| new.call(env, &block)}
+ @app = builder.to_app
+ end
+ end
+
# Backbone of the request_module and response_module support.
def module_include(type, mod)
if type == :response
klass = self::RodaResponse
iv = :@response_module
@@ -232,29 +297,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.
@@ -361,25 +435,37 @@
@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]}"
end
# 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
- # Whether this request is a get request. Similar to the default
- # Rack::Request get? method, but can be overridden without changing
- # rack's behavior.
+ # 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 match block return values. By default, if a string is given
@@ -391,16 +477,60 @@
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}>"
end
- # Does a terminal match on the input, matching only if the arguments
- # have fully matched the patch.
+ # 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)
if args.empty?
if @env[PATH_INFO] == EMPTY_STRING
always(&block)
end
@@ -408,43 +538,143 @@
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)
if args.empty?
always(&block)
else
if_match(args, &block)
end
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.
+ # 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)
throw :halt, response.finish
end
- # If this is a GET request for the root ("/"), yield to the match block.
+ # 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 immediately return
- # the response as the response for this request.
+ # 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)
throw :halt, app.call(@env)
end
private
@@ -597,11 +827,11 @@
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([:extension, ext]){"([^\\/]+?)\.#{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)
@@ -662,16 +892,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
@@ -685,42 +919,61 @@
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