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