module Roda::RodaPlugins::Base::ClassMethods

def _define_roda_method_arg_numbers(callable)

or :required).
and whether the callable accepts keyword arguments (true, false
whether the callable accepts any additional arguments,
Return the number of required argument, optional arguments,
def _define_roda_method_arg_numbers(callable)
  optional_args = 0
  rest = false
  keyword = false
  callable.parameters.map(&:first).each do |arg_type, _|
    case arg_type
    when :opt
      optional_args += 1
    when :rest
      rest = true
    when :keyreq
      keyword = :required
    when :key, :keyrest
      keyword ||= true
    end
  end
  arity = callable.arity
  if arity < 0
    arity = arity.abs - 1
  end
  required_args = arity
  arity -= 1 if keyword == :required
  if callable.is_a?(Proc) && !callable.lambda?
    optional_args -= arity
  end
  [required_args, optional_args, rest, keyword]
end

def app

The rack application that this class uses.
def app
  @app || build_rack_app
end

def base_rack_app_callable(new_api=true)

The base rack app to use, before middleware is added.
def base_rack_app_callable(new_api=true)
  if new_api
    lambda{|env| new(env)._roda_handle_main_route}
  else
    block = @rack_app_route_block
    lambda{|env| new(env).call(&block)}
  end
end

def build_rack_app

Build the rack app to use
def build_rack_app
  app = base_rack_app_callable(use_new_dispatch_api?)
  @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

def call(env)

access to the underlying rack app.
However, for performance, it's better to use #app to get direct
This allows the class itself to be used as a rack application.
Call the internal rack application with the given environment.
def call(env)
  app.call(env)
end

def clear_middleware!

Clear the middleware stack
def clear_middleware!
  @middleware.clear
  @app = nil
end

def convert_route_block(block)

Can be modified by plugins.
which can include route blocks that are delegated to by the main route block.
Modify the route block to use for any route block provided as input,
def convert_route_block(block)
  block
end

def def_roda_after

the internal after hook plugin if the _roda_after method is defined.
in order, if any _roda_after_* methods are defined. Also, use
Build a _roda_after method that calls each _roda_after_* method
def def_roda_after
  meths = private_instance_methods.grep(/\A_roda_after_\d\d/).sort
  unless meths.empty?
    plugin :error_handler unless private_method_defined?(:_roda_after)
    if meths.length == 1
      class_eval("alias _roda_after #{meths.first}", __FILE__, __LINE__)
    else
      class_eval("def _roda_after(res); #{meths.map{|s| "#{s}(res)"}.join(';')} end", __FILE__, __LINE__)
    end
    private :_roda_after
    alias_method :_roda_after, :_roda_after
  end
end

def def_roda_before

the route block if a _roda_before method is defined.
in order, if any _roda_before_* methods are defined. Also, rebuild
Build a _roda_before method that calls each _roda_before_* method
def def_roda_before
  meths = private_instance_methods.grep(/\A_roda_before_\d\d/).sort
  unless meths.empty?
    plugin :_before_hook unless private_method_defined?(:_roda_before)
    if meths.length == 1
      class_eval("alias _roda_before #{meths.first}", __FILE__, __LINE__)
    else
      class_eval("def _roda_before; #{meths.join(';')} end", __FILE__, __LINE__)
    end
    private :_roda_before
    alias_method :_roda_before, :_roda_before
  end
end

def define_roda_method(meth, expected_arity, &block)

expected arity is 0 or 1.
Roda does not support blocks with required keyword arguments if the

fixes Roda uses for regular blocks would not work for lambda blocks.
Roda only checks arity for regular blocks, not lambda blocks, as the

to adjust arity dynamically.
:check_dynamic_arity app option can be to :warn to warn if Roda needs
set to false to turn off the dynamic arity checks. The
where the arity matches. The :check_dynamic_arity app option can be
when the method is called, which can hurt performance even in the case
If the expected arity is :any, Roda must perform a dynamic arity check

cases where the arity does not match what is expected.
cases where it does not. If it is set to :warn, Roda will warn in the
the arity of the block matches the expected arity, and compensate for
If the :check_arity app option is not set to false, Roda will check that

1 (single argument), or :any (any number of arguments).
that string. The expected arity should be either 0 (no arguments),
If the name is given as a String, a unique name will be generated using
expected arity. If the name is given as a Symbol, it is used directly.
Define an instance method using the block with the provided name and
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)
      raise RodaError, "cannot use block with required keyword arguments when calling define_roda_method with expected arity #{expected_arity}"
    end
    case expected_arity
    when 0
      unless required_args == 0
        if check_arity == :warn
          RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 0, but arguments required for #{block.inspect}"
        end
        b = block
        block = lambda{instance_exec(&b)} # Fallback
      end
    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
        alias_method meth, meth
        meth = :"#{meth}_arity"
      elsif required_args > 1
        b = block
        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,
          # Fallback to instance_exec in this case.
          b = block
          block = if RUBY_VERSION >= '2.7'
            eval('lambda{|*a, **kw| instance_exec(*a, **kw, &b)}', nil, __FILE__, __LINE__) # Keyword arguments fallback
          else
            # :nocov:
            lambda{|*a| instance_exec(*a, &b)} # Keyword arguments fallback
            # :nocov:
          end
        else
          arity_meth = meth
          meth = :"#{meth}_arity"
        end
      end
    else
      raise RodaError, "unexpected arity passed to define_roda_method: #{expected_arity.inspect}"
    end
  end
  define_method(meth, &block)
  private meth
  alias_method meth, meth
  if arity_meth
    required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(instance_method(meth))
    max_args = required_args + optional_args
    define_method(arity_meth) do |*a|
      arity = a.length
      if arity > required_args
        if arity > max_args && !rest
          if check_dynamic_arity == :warn
            RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. At most #{max_args} arguments accepted, but #{arity} arguments given for #{block.inspect}"
          end
          a = a.slice(0, max_args)
        end
      elsif arity < required_args
        if check_dynamic_arity == :warn
          RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. #{required_args} args required, but #{arity} arguments given for #{block.inspect}"
        end
        a.concat([nil] * (required_args - arity))
      end
      send(meth, *a)
    end
    private arity_meth
    alias_method arity_meth, arity_meth
  end
  call_meth
end

def expand_path(path, root=opts[:root])

Expand the given path, using the root argument as the base directory.
def expand_path(path, root=opts[:root])
  ::File.expand_path(path, root)
end

def freeze

it would cause some plugins to break.
Note that freezing the class prevents you from subclassing it, mostly because

be raised if you try to modify the internal state after calling this.
internal state at runtime anyway, but this makes sure an exception will
It's optional to call this method, as nothing should be modifying the
Freeze the internal state of the class, to avoid thread safety issues at runtime.
def 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
      class_eval("alias _roda_run_main_route _roda_main_route")
    end
    self::RodaResponse.class_eval do
      if instance_method(:set_default_headers).owner == ResponseMethods &&
         instance_method(:default_headers).owner == ResponseMethods
        private
        alias set_default_headers set_default_headers
        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

def include(*a)

have added a _roda_before_* or _roda_after_* method.
Rebuild the _roda_before and _roda_after methods whenever a plugin might
def include(*a)
  res = super
  def_roda_before
  def_roda_after
  res
end

def inherited(subclass)

and setup the request and response subclasses.
When inheriting Roda, copy the shared data into the subclass,
def inherited(subclass)
  raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
  # Mark current class as having been subclassed, as some optimizations
  # depend on the class not being subclassed
  opts[:subclassed] = true
  super
  subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
  subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
  subclass.instance_variable_set(:@opts, opts.dup)
  subclass.opts.delete(:subclassed)
  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
  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)
  response_class = Class.new(self::RodaResponse)
  response_class.roda_class = subclass
  subclass.const_set(:RodaResponse, response_class)
end

def plugin(plugin, *args, &block)

Roda.plugin :csrf
Roda.plugin PluginModule

class has been subclassed, as doing so can break the subclasses.
Note that you should not load plugins into a Roda class after the

which will be required and then used. Returns nil.
which is used directly, or a symbol representing a registered plugin
Load a new plugin into the current class. A plugin can be a module
def plugin(plugin, *args, &block)
  raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
  plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
  raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module)
  if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block)
    # RODA4: switch from warning to error
    RodaPlugins.warn("Plugin #{plugin} does not accept arguments or a block, but arguments or a block was passed when loading this. This will raise an error in Roda 4.")
  end
  plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
  include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
  extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
  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)
  @app = nil
end

def rack_app_route_block(block)

Can be modified by plugins.
if any before hooks are defined.
By default, modifies the rack app route block to support before hooks
entry point to the route block).
The route block to use when building the rack app (or other initial
def rack_app_route_block(block)
  block
end

def route(&block)

times will overwrite the previous routing.
This should only be called once per class, and if called multiple

end
end
"Root"
r.root do
Roda.route do |r|

argument should be named +r+. Example:
a block, which is yielded the request. By convention, the block
underlying rack application using the stored middleware. Requires
Setup routing tree for the current Roda application, and build the
def route(&block)
  unless block
    RodaPlugins.warn "no block passed to Roda.route"
    return
  end
  @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)
  @app = nil
end

def set_default_headers

def set_default_headers
  @headers['Content-Type'] ||= 'text/html'
end

def use(*args, &block)

Roda.use Rack::ShowExceptions

called before calling #route to have an effect. Example:
Add a middleware to use for the rack application. Must be
def use(*args, &block)
  @middleware << [args, block].freeze
  @app = nil
end

def use_new_dispatch_api?

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
back to using #call for dispatching for #{self}, due to #call override in #{mod}.
hould be fixed to adjust to Roda's new dispatch API, and override _roda_handle_main_route or _roda_run_main_route
      return false
    end
  end
  true
end