lib/roda.rb in roda-3.18.0 vs lib/roda.rb in roda-3.19.0

- old
+ new

@@ -116,11 +116,13 @@ # them and call super to get the default behavior. module Base # Class methods for the Roda class. module ClassMethods # The rack application that this class uses. - attr_reader :app + def app + @app || build_rack_app + end # Whether middleware from the current class should be inherited by subclasses. # True by default, should be set to false when using a design where the parent # class accepts requests and uses run to dispatch the request to a subclass. attr_accessor :inherit_middleware @@ -140,11 +142,11 @@ end # Clear the middleware stack def clear_middleware! @middleware.clear - build_rack_app + @app = nil end # Define an instance method using the block with the provided name and # expected arity. If the name is given as a Symbol, it is used directly. # If the name is given as a String, a unique name will be generated using @@ -170,10 +172,11 @@ # expected arity is 0 or 1. def define_roda_method(meth, expected_arity, &block) if meth.is_a?(String) meth = roda_method_name(meth) end + call_meth = meth if (check_arity = opts.fetch(:check_arity, true)) && !block.lambda? required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block) if keyword == :required && (expected_arity == 0 || expected_arity == 1) @@ -192,12 +195,19 @@ when 1 if required_args == 0 && optional_args == 0 && !rest if check_arity == :warn RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but no arguments accepted for #{block.inspect}" end + temp_method = roda_method_name("temp") + class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__) + alias_method meth, temp_method + undef_method temp_method + private meth + meth = :"#{meth}_arity" + elsif required_args > 1 b = block - block = lambda{|_| instance_exec(&b)} # Fallback + block = lambda{|r| instance_exec(r, &b)} # Fallback end when :any if check_dynamic_arity = opts.fetch(:check_dynamic_arity, check_arity) if keyword # Complexity of handling keyword arguments using define_method is too high, @@ -237,14 +247,13 @@ end send(meth, *a) end private arity_meth - arity_meth - else - meth end + + call_meth end # Expand the given path, using the root argument as the base directory. def expand_path(path, root=opts[:root]) ::File.expand_path(path, root) @@ -256,12 +265,11 @@ # be raised if you try to modify the internal state after calling this. # # Note that freezing the class prevents you from subclassing it, mostly because # it would cause some plugins to break. def freeze - @opts.freeze - @middleware.freeze + return self if frozen? unless opts[:subclassed] # If the _roda_run_main_route instance method has not been overridden, # make it an alias to _roda_main_route for performance if instance_method(:_roda_run_main_route).owner == InstanceMethods @@ -274,12 +282,20 @@ def set_default_headers @headers['Content-Type'] ||= 'text/html' end end end + + if @middleware.empty? && use_new_dispatch_api? + plugin :direct_call + end end + build_rack_app + @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. @@ -338,11 +354,11 @@ self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods) self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods) self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods) self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods) plugin.configure(self, *args, &block) if plugin.respond_to?(:configure) - nil + @app = nil end # 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 @@ -364,20 +380,20 @@ @raw_route_block = block @route_block = block = convert_route_block(block) @rack_app_route_block = block = rack_app_route_block(block) public define_roda_method(:_roda_main_route, 1, &block) - build_rack_app + @app = nil end # Add a middleware to use for the rack application. Must be # called before calling #route to have an effect. Example: # # Roda.use Rack::ShowExceptions def use(*args, &block) @middleware << [args, block].freeze - build_rack_app + @app = nil end private # Return the number of required argument, optional arguments, @@ -424,33 +440,19 @@ end end # Build the rack app to use def build_rack_app - if @rack_app_route_block - # RODA4: Assume optimize is true - optimize = ancestors.each do |mod| - break true if mod == InstanceMethods - meths = mod.instance_methods(false) - if meths.include?(:call) && !(meths.include?(:_roda_handle_main_route) || meths.include?(:_roda_run_main_route)) - RodaPlugins.warn <<WARNING -Falling back to using #call for dispatching for #{self}, due to #call override in #{mod}. -#{mod} should be fixed to adjust to Roda's new dispatch API, and override _roda_handle_main_route or _roda_run_main_route -WARNING - break false - end - end + app = base_rack_app_callable(use_new_dispatch_api?) - app = base_rack_app_callable(optimize) - - @middleware.reverse_each do |args, bl| - mid, *args = args - app = mid.new(app, *args, &bl) - app.freeze if opts[:freeze_middleware] - end - @app = app + @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 # 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. @@ -497,10 +499,28 @@ # Can be modified by plugins. def rack_app_route_block(block) block end + # Whether the new dispatch API should be used. + def use_new_dispatch_api? + # RODA4: remove this method + ancestors.each do |mod| + break if mod == InstanceMethods + meths = mod.instance_methods(false) + if meths.include?(:call) && !(meths.include?(:_roda_handle_main_route) || meths.include?(:_roda_run_main_route)) + RodaPlugins.warn <<WARNING +Falling back to using #call for dispatching for #{self}, due to #call override in #{mod}. +#{mod} should be fixed to adjust to Roda's new dispatch API, and override _roda_handle_main_route or _roda_run_main_route +WARNING + return false + end + end + + true + end + method_num = 0 method_num_mutex = Mutex.new # Return a unique method name symbol for the given suffix. define_method(:roda_method_name) do |suffix| :"_roda_#{suffix}_#{method_num_mutex.synchronize{method_num += 1}}" @@ -1015,25 +1035,46 @@ # Match the given string to the request path. Matches only if the # request path ends with the string or if the next character in the # request path is a slash (indicating a new segment). def _match_string(str) rp = @remaining_path - if rp.start_with?("/#{str}") - last = str.length + 1 - case rp[last] - when "/" - @remaining_path = rp[last, rp.length] + 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 # Match the given symbol if any segment matches. def _match_symbol(sym=nil) rp = @remaining_path - if rp[0, 1] == "/" + if rp.getbyte(0) == 47 if last = rp.index('/', 1) if last > 1 @captures << rp[1, last-1] @remaining_path = rp[last, rp.length] end @@ -1115,11 +1156,11 @@ 302 end # Whether the current path is considered empty. def empty_path? - remaining_path == "" + remaining_path.empty? end # If all of the arguments match, yields to the match block and # returns the rack response when the block returns. If any of # the match arguments doesn't match, does nothing. @@ -1146,21 +1187,23 @@ _match_string(matcher) when Class _match_class(matcher) when TERM empty_path? - when Symbol - _match_symbol(matcher) when Regexp _match_regexp(matcher) - when Hash - _match_hash(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 - when true, false, nil - matcher else unsupported_matcher(matcher) end end