lib/roda.rb in roda-1.0.0 vs lib/roda.rb in roda-1.1.0

- old
+ new

@@ -8,10 +8,11 @@ class Roda # Error class raised by Roda class RodaError < StandardError; end if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby' + # :nocov: # 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. @@ -28,10 +29,11 @@ # Make setting value in underlying hash thread safe. def []=(key, value) @mutex.synchronize{@hash[key] = value} end end + # :nocov: else # Hashes are already thread-safe in MRI, due to the GVL, so they # can safely be used as a cache. RodaCache = Hash end @@ -49,13 +51,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 @@ -84,18 +87,23 @@ # 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. module Base + SESSION_KEY = 'rack.session'.freeze + # Class methods for the Roda class. module ClassMethods # The rack application that this class uses. 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) @@ -120,21 +128,18 @@ # 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) @@ -233,13 +238,12 @@ # 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 @@ -250,14 +254,25 @@ # 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| allocate.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 @@ -268,11 +283,13 @@ if mod raise RodaError, "can't provide both argument and block to response_module" if block_given? klass.send(:include, mod) else - unless mod = instance_variable_get(iv) + if instance_variable_defined?(iv) + mod = instance_variable_get(iv) + else mod = instance_variable_set(iv, Module.new) klass.send(:include, mod) end mod.module_eval(&Proc.new) if block_given? @@ -282,12 +299,10 @@ end end # Instance methods for the Roda class. 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. This is not usually # called directly. def call(env, &block) @@ -928,19 +943,37 @@ def empty? @body.empty? end # Return the rack response array of status, headers, and body - # for the current response. Example: + # for the current response. If the status has not been set, + # uses a 200 status if the body has been written to, otherwise + # uses a 404 status. Adds the Content-Length header to the + # size of the response body. # - # response.finish # => [200, {'Content-Type'=>'text/html'}, []] + # Example: + # + # response.finish + # # => [200, + # # {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, + # # []] def finish b = @body s = (@status ||= b.empty? ? 404 : 200) - [s, @headers, b] + h = @headers + h[CONTENT_LENGTH] = @length.to_s + [s, h, b] end + # Return the rack response array using a given body. Assumes a + # 200 response status unless status has been explicitly set, + # and doesn't add the Content-Length header or use the existing + # body. + def finish_with_body(body) + [@status || 200, @headers, body] + end + # Set the Location header to the given path, and the status # to the given status. Example: # # response.redirect('foo', 301) # response.redirect('bar') @@ -955,19 +988,15 @@ # 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. Example: + # Write to the response body. Returns nil. # # response.write('foo') - # response['Content-Length'] # =>'3' def write(str) s = str.to_s - @length += s.bytesize - @headers[CONTENT_LENGTH] = @length.to_s @body << s nil end end end