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.

plugins [R]

The plugins loaded into 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
34 def app
35   @app || build_rack_app
36 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
56 def call(env)
57   app.call(env)
58 end
clear_middleware!()

Clear the middleware stack

[show source]
   # File lib/roda.rb
61 def clear_middleware!
62   @middleware.clear
63   @app = nil
64 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. The :check_arity default app option setting is :warn, in which case 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. The default value of the :check_dynamic_arity app option is to use the same value as the :check_arity app option.

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.

Support for the :check_arity and :check_dynamic_arity options will be removed in Roda 4.

[show source]
    # File lib/roda.rb
 94 def define_roda_method(meth, expected_arity, &block)
 95   if meth.is_a?(String)
 96     meth = roda_method_name(meth)
 97   end
 98   call_meth = meth
 99 
100   # RODA4: Remove support for :check_arity, default to false behavior
101   if (check_arity = opts.fetch(:check_arity, :warn)) && !block.lambda?
102     required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(block)
103 
104     if keyword == :required && (expected_arity == 0 || expected_arity == 1)
105       raise RodaError, "cannot use block with required keyword arguments when calling define_roda_method with expected arity #{expected_arity}"
106     end
107 
108     case expected_arity
109     when 0
110       unless required_args == 0
111         if check_arity == :warn
112           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 0, but arguments required for #{block.inspect}"
113         end
114         b = block
115         block = lambda{instance_exec(&b)} # Fallback
116       end
117     when 1
118       if required_args == 0 && optional_args == 0 && !rest
119         if check_arity == :warn
120           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but no arguments accepted for #{block.inspect}"
121         end
122         temp_method = roda_method_name("temp")
123         class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__)
124         alias_method meth, temp_method
125         undef_method temp_method
126         private meth
127         alias_method meth, meth
128         meth = :"#{meth}_arity"
129       elsif required_args > 1
130         if check_arity == :warn
131           RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but multiple arguments required for #{block.inspect}"
132         end
133         b = block
134         block = lambda{|r| instance_exec(r, &b)} # Fallback
135       end
136     when :any
137       if check_dynamic_arity = opts.fetch(:check_dynamic_arity, check_arity)
138         if keyword
139           # Complexity of handling keyword arguments using define_method is too high,
140           # Fallback to instance_exec in this case.
141           b = block
142           block = if RUBY_VERSION >= '2.7'
143             eval('lambda{|*a, **kw| instance_exec(*a, **kw, &b)}', nil, __FILE__, __LINE__) # Keyword arguments fallback
144           else
145             # :nocov:
146             lambda{|*a| instance_exec(*a, &b)} # Keyword arguments fallback
147             # :nocov:
148           end
149         else
150           arity_meth = meth
151           meth = :"#{meth}_arity"
152         end
153       end
154     else
155       raise RodaError, "unexpected arity passed to define_roda_method: #{expected_arity.inspect}"
156     end
157   end
158 
159   define_method(meth, &block)
160   private meth
161   alias_method meth, meth
162 
163   if arity_meth
164     required_args, optional_args, rest, keyword = _define_roda_method_arg_numbers(instance_method(meth))
165     max_args = required_args + optional_args
166     define_method(arity_meth) do |*a|
167       arity = a.length
168       if arity > required_args
169         if arity > max_args && !rest
170           if check_dynamic_arity == :warn
171             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}"
172           end
173           a = a.slice(0, max_args)
174         end
175       elsif arity < required_args
176         if check_dynamic_arity == :warn
177           RodaPlugins.warn "Dynamic arity mismatch in block passed to define_roda_method. #{required_args} args required, but #{arity} arguments given for #{block.inspect}"
178         end
179         a.concat([nil] * (required_args - arity))
180       end
181 
182       send(meth, *a)
183     end
184     private arity_meth
185     alias_method arity_meth, arity_meth
186   end
187 
188   call_meth
189 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
192 def expand_path(path, root=opts[:root])
193   ::File.expand_path(path, root)
194 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
203 def freeze
204   return self if frozen?
205 
206   unless opts[:subclassed]
207     # If the _roda_run_main_route instance method has not been overridden,
208     # make it an alias to _roda_main_route for performance
209     if instance_method(:_roda_run_main_route).owner == InstanceMethods
210       class_eval("alias _roda_run_main_route _roda_main_route")
211     end
212     self::RodaResponse.class_eval do
213       if instance_method(:set_default_headers).owner == ResponseMethods &&
214          instance_method(:default_headers).owner == ResponseMethods
215 
216         private
217 
218         alias set_default_headers set_default_headers
219         def set_default_headers
220           @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
221         end
222       end
223     end
224 
225     if @middleware.empty? && use_new_dispatch_api?
226       plugin :direct_call
227     end
228 
229     if RUBY_VERSION > "2.4" && ([: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}
230       plugin :_optimized_matching
231     end
232   end
233 
234   build_rack_app
235   @opts.freeze
236   @plugins.freeze
237   @middleware.freeze
238 
239   super
240 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
244 def include(*a)
245   res = super
246   def_roda_before
247   def_roda_after
248   res
249 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
253 def inherited(subclass)
254   raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
255 
256   # Mark current class as having been subclassed, as some optimizations
257   # depend on the class not being subclassed
258   opts[:subclassed] = true
259 
260   super
261   subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
262   subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
263   subclass.instance_variable_set(:@plugins, @plugins.dup)
264   subclass.instance_variable_set(:@opts, opts.dup)
265   subclass.opts.delete(:subclassed)
266   subclass.opts.to_a.each do |k,v|
267     if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
268       subclass.opts[k] = v.dup
269     end
270   end
271   if block = @raw_route_block
272     subclass.route(&block)
273   end
274   
275   request_class = Class.new(self::RodaRequest)
276   request_class.roda_class = subclass
277   request_class.match_pattern_cache = RodaCache.new
278   subclass.const_set(:RodaRequest, request_class)
279 
280   response_class = Class.new(self::RodaResponse)
281   response_class.roda_class = subclass
282   subclass.const_set(:RodaResponse, response_class)
283 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
294 def plugin(plugin, *args, &block)
295   raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
296   plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
297   raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module)
298 
299   if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block)
300     # RODA4: switch from warning to error
301     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.")
302   end
303 
304   plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
305   @plugins << plugin unless @plugins.include?(plugin)
306   include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
307   extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
308   self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods)
309   self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods)
310   self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods)
311   self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods)
312   plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
313   @app = nil
314 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
332 def route(&block)
333   unless block
334     RodaPlugins.warn "no block passed to Roda.route"
335     return
336   end
337 
338   @raw_route_block = block
339   @route_block = block = convert_route_block(block)
340   @rack_app_route_block = block = rack_app_route_block(block)
341   public define_roda_method(:_roda_main_route, 1, &block)
342   @app = nil
343 end
set_default_headers()
[show source]
    # File lib/roda.rb
219 def set_default_headers
220   @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
221 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
349 def use(*args, &block)
350   @middleware << [args, block].freeze
351   @app = nil
352 end