lib/roda.rb in roda-3.16.0 vs lib/roda.rb in roda-3.17.0

- old
+ new

@@ -58,11 +58,13 @@ @app = nil @inherit_middleware = true @middleware = [] @opts = {} + @raw_route_block = nil @route_block = nil + @rack_app_route_block = nil # Module in which all Roda plugins should be stored. Also contains logic for # registering and loading plugins. module RodaPlugins OPTS = {}.freeze @@ -159,10 +161,19 @@ @opts.freeze @middleware.freeze super end + # Rebuild the _roda_before and _roda_after methods whenever a plugin might + # have added a _roda_before_* or _roda_after_* method. + def include(*a) + res = super + def_roda_before + def_roda_after + res + end + # When inheriting Roda, copy the shared data into the subclass, # and setup the request and response subclasses. def inherited(subclass) raise RodaError, "Cannot subclass a frozen Roda class" if frozen? super @@ -172,12 +183,13 @@ subclass.opts.to_a.each do |k,v| if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen? subclass.opts[k] = v.dup end end - subclass.instance_variable_set(:@route_block, @route_block) - subclass.send(:build_rack_app) + if block = @raw_route_block + subclass.route(&block) + end request_class = Class.new(self::RodaRequest) request_class.roda_class = subclass request_class.match_pattern_cache = RodaCache.new subclass.const_set(:RodaRequest, request_class) @@ -219,11 +231,13 @@ # end # # This should only be called once per class, and if called multiple # times will overwrite the previous routing. def route(&block) - @route_block = block + @raw_route_block = block + @route_block = block = convert_route_block(block) + @rack_app_route_block = rack_app_route_block(block) build_rack_app end # Add a middleware to use for the rack application. Must be # called before calling #route to have an effect. Example: @@ -236,27 +250,69 @@ private # Build the rack app to use def build_rack_app - if block = @route_block - block = rack_app_route_block(block) + if block = @rack_app_route_block app = lambda{|env| new(env).call(&block)} @middleware.reverse_each do |args, bl| mid, *args = args app = mid.new(app, *args, &bl) app.freeze if opts[:freeze_middleware] end @app = app end end - # The route block to use when building the rack app. + # Modify the route block to use for any route block provided as input, + # which can include route blocks that are delegated to by the main route block. # Can be modified by plugins. - def rack_app_route_block(block) + def convert_route_block(block) block end + + # Build a _roda_before method that calls each _roda_before_* method + # in order, if any _roda_before_* methods are defined. Also, rebuild + # the route block if a _roda_before method is defined. + def def_roda_before + meths = private_instance_methods.grep(/\A_roda_before_\d\d/).sort.join(';') + unless meths.empty? + class_eval("def _roda_before; #{meths} end", __FILE__, __LINE__) + private :_roda_before + if @raw_route_block + route(&@raw_route_block) + end + end + end + + # Build a _roda_after method that calls each _roda_after_* method + # in order, if any _roda_after_* methods are defined. Also, use + # the internal after hook plugin if the _roda_after method is defined. + def def_roda_after + meths = private_instance_methods.grep(/\A_roda_after_\d\d/).sort.map{|s| "#{s}(res)"}.join(';') + unless meths.empty? + plugin :_after_hook unless private_method_defined?(:_roda_after) + class_eval("def _roda_after(res); #{meths} end", __FILE__, __LINE__) + private :_roda_after + end + end + + # The route block to use when building the rack app (or other initial + # entry point to the route block). + # By default, modifies the rack app route block to support before hooks + # if any before hooks are defined. + # Can be modified by plugins. + def rack_app_route_block(block) + if private_method_defined?(:_roda_before) + lambda do |r| + _roda_before + instance_exec(r, &block) + end + else + block + end + end end # Instance methods for the Roda class. # # In addition to the listed methods, the following two methods are available: @@ -934,11 +990,10 @@ # code for non-empty responses and a 404 code for empty responses. attr_accessor :status # Set the default headers when creating a response. def initialize - @status = nil @headers = {} @body = [] @length = 0 end @@ -984,17 +1039,21 @@ # # => [200, # # {'Content-Type'=>'text/html', 'Content-Length'=>'0'}, # # []] def finish b = @body - empty = b.empty? - s = (@status ||= empty ? 404 : default_status) set_default_headers h = @headers - if empty && (s == 304 || s == 204 || s == 205 || (s >= 100 && s <= 199)) - h.delete("Content-Type") + if b.empty? + s = @status || 404 + if (s == 304 || s == 204 || s == 205 || (s >= 100 && s <= 199)) + h.delete("Content-Type") + else + h["Content-Length"] ||= '0' + end else + s = @status || default_status h["Content-Length"] ||= @length.to_s end [s, h, b] end