module Roda::RodaPlugins::Base::ClassMethods

  1. lib/roda.rb

Class methods for the Roda class.

Attributes

inherit_middleware [RW]

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.

opts [R]

The settings/options hash for the current class.

route_block [R]

The route block that this class uses.

Public Instance methods

app()

The rack application that this class uses.

[show source]
   # File lib/roda.rb
33 def app
34   @app || build_rack_app
35 end
call(env)

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.

[show source]
   # File lib/roda.rb
52 def call(env)
53   app.call(env)
54 end
clear_middleware!()

Clear the middleware stack

[show source]
   # File lib/roda.rb
57 def clear_middleware!
58   @middleware.clear
59   @app = nil
60 end
define_roda_method(meth, expected_arity, &block)

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 that string. The expected arity should be either 0 (no arguments), 1 (single argument), or :any (any number of arguments).

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

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

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

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

[show source]
    # File lib/roda.rb
 85 def define_roda_method(meth, expected_arity, &block)
 86   if meth.is_a?(String)
 87     meth = roda_method_name(meth)
 88   end
 89   call_meth = meth
 90 
 91   # RODA4: Switch to false # :warn in last Roda 3 version
 92   if (check_arity = opts.fetch(:check_arity, true)) && !block.lambda?
 93     required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block)
 94 
 95     if keyword == :required && (expected_arity == 0 || expected_arity == 1)
 96       raise RodaError, "cannot use block with required keyword arguments when calling define_roda_method with expected arity #{expected_arity}"
 97     end
 98 
 99     case expected_arity
100     when 0
101       unless required_args == 0
102         if check_arity == :warn
103           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 0, but arguments required for #{block.inspect}"
104         end
105         b = block
106         block = lambda{instance_exec(&b)} # Fallback
107       end
108     when 1
109       if required_args == 0 && optional_args == 0 && !rest
110         if check_arity == :warn
111           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but no arguments accepted for #{block.inspect}"
112         end
113         temp_method = roda_method_name("temp")
114         class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__)
115         alias_method meth, temp_method
116         undef_method temp_method
117         private meth
118         alias_method meth, meth
119         meth = :"#{meth}_arity"
120       elsif required_args > 1
121         if check_arity == :warn
122           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but multiple arguments required for #{block.inspect}"
123         end
124         b = block
125         block = lambda{|r| instance_exec(r, &b)} # Fallback
126       end
127     when :any
128       if check_dynamic_arity = opts.fetch(:check_dynamic_arity, check_arity)
129         if keyword
130           # Complexity of handling keyword arguments using define_method is too high,
131           # Fallback to instance_exec in this case.
132           b = block
133           block = if RUBY_VERSION >= '2.7'
134             eval('lambda{|*a, **kw| instance_exec(*a, **kw, &b)}', nil, __FILE__, __LINE__) # Keyword arguments fallback
135           else
136             # :nocov:
137             lambda{|*a| instance_exec(*a, &b)} # Keyword arguments fallback
138             # :nocov:
139           end
140         else
141           arity_meth = meth
142           meth = :"#{meth}_arity"
143         end
144       end
145     else
146       raise RodaError, "unexpected arity passed to define_roda_method: #{expected_arity.inspect}"
147     end
148   end
149 
150   define_method(meth, &block)
151   private meth
152   alias_method meth, meth
153 
154   if arity_meth
155     required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(instance_method(meth))
156     max_args = required_args + optional_args
157     define_method(arity_meth) do |*a|
158       arity = a.length
159       if arity > required_args
160         if arity > max_args && !rest
161           if check_dynamic_arity == :warn
162             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}"
163           end
164           a = a.slice(0, max_args)
165         end
166       elsif arity < required_args
167         if check_dynamic_arity == :warn
168           RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. #{required_args} args required, but #{arity} arguments given for #{block.inspect}"
169         end
170         a.concat([nil] * (required_args - arity))
171       end
172 
173       send(meth, *a)
174     end
175     private arity_meth
176     alias_method arity_meth, arity_meth
177   end
178 
179   call_meth
180 end
expand_path(path, root=opts[:root])

Expand the given path, using the root argument as the base directory.

[show source]
    # File lib/roda.rb
183 def expand_path(path, root=opts[:root])
184   ::File.expand_path(path, root)
185 end
freeze()

Freeze the internal state of the class, to avoid thread safety issues at runtime. It’s optional to call this method, as nothing should be modifying the internal state at runtime anyway, but this makes sure an exception will 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.

[show source]
    # File lib/roda.rb
194 def freeze
195   return self if frozen?
196 
197   unless opts[:subclassed]
198     # If the _roda_run_main_route instance method has not been overridden,
199     # make it an alias to _roda_main_route for performance
200     if instance_method(:_roda_run_main_route).owner == InstanceMethods
201       class_eval("alias _roda_run_main_route _roda_main_route")
202     end
203     self::RodaResponse.class_eval do
204       if instance_method(:set_default_headers).owner == ResponseMethods &&
205          instance_method(:default_headers).owner == ResponseMethods
206 
207         private
208 
209         alias set_default_headers set_default_headers
210         def set_default_headers
211           @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
212         end
213       end
214     end
215 
216     if @middleware.empty? && use_new_dispatch_api?
217       plugin :direct_call
218     end
219 
220     if ([:on, :is, :_verb, :_match_class_String, :_match_class_Integer, :_match_string, :_match_regexp, :empty_path?, :if_match, :match, :_match_class]).all?{|m| self::RodaRequest.instance_method(m).owner == RequestMethods}
221       plugin :_optimized_matching
222     end
223   end
224 
225   build_rack_app
226   @opts.freeze
227   @middleware.freeze
228 
229   super
230 end
include(*a)

Rebuild the _roda_before and _roda_after methods whenever a plugin might have added a roda_before* or roda_after* method.

[show source]
    # File lib/roda.rb
234 def include(*a)
235   res = super
236   def_roda_before
237   def_roda_after
238   res
239 end
inherited(subclass)

When inheriting Roda, copy the shared data into the subclass, and setup the request and response subclasses.

[show source]
    # File lib/roda.rb
243 def inherited(subclass)
244   raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
245 
246   # Mark current class as having been subclassed, as some optimizations
247   # depend on the class not being subclassed
248   opts[:subclassed] = true
249 
250   super
251   subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
252   subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
253   subclass.instance_variable_set(:@opts, opts.dup)
254   subclass.opts.delete(:subclassed)
255   subclass.opts.to_a.each do |k,v|
256     if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
257       subclass.opts[k] = v.dup
258     end
259   end
260   if block = @raw_route_block
261     subclass.route(&block)
262   end
263   
264   request_class = Class.new(self::RodaRequest)
265   request_class.roda_class = subclass
266   request_class.match_pattern_cache = RodaCache.new
267   subclass.const_set(:RodaRequest, request_class)
268 
269   response_class = Class.new(self::RodaResponse)
270   response_class.roda_class = subclass
271   subclass.const_set(:RodaResponse, response_class)
272 end
plugin(plugin, *args, &block)

Load a new plugin into the current class. A plugin can be a module which is used directly, or a symbol representing a registered plugin which will be required and then used. Returns nil.

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

Roda.plugin PluginModule
Roda.plugin :csrf
[show source]
    # File lib/roda.rb
283 def plugin(plugin, *args, &block)
284   raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
285   plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
286   raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module)
287 
288   if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block)
289     # RODA4: switch from warning to error
290     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.")
291   end
292 
293   plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
294   include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
295   extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
296   self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods)
297   self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods)
298   self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods)
299   self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods)
300   plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
301   @app = nil
302 end
route(&block)

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.

[show source]
    # File lib/roda.rb
320 def route(&block)
321   unless block
322     RodaPlugins.warn "no block passed to Roda.route"
323     return
324   end
325 
326   @raw_route_block = block
327   @route_block = block = convert_route_block(block)
328   @rack_app_route_block = block = rack_app_route_block(block)
329   public define_roda_method(:_roda_main_route, 1, &block)
330   @app = nil
331 end
set_default_headers()
[show source]
    # File lib/roda.rb
210 def set_default_headers
211   @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
212 end
use(*args, &block)

Add a middleware to use for the rack application. Must be called before calling route to have an effect. Example:

Roda.use Rack::ShowExceptions
[show source]
    # File lib/roda.rb
337 def use(*args, &block)
338   @middleware << [args, block].freeze
339   @app = nil
340 end