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
38 def app
39   @app || build_rack_app
40 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
60 def call(env)
61   app.call(env)
62 end
clear_middleware!()

Clear the middleware stack

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

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

[show source]
    # File lib/roda.rb
196 def expand_path(path, root=app_root)
197   ::File.expand_path(path, root)
198 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
207 def freeze
208   return self if frozen?
209 
210   unless opts[:subclassed]
211     # If the _roda_run_main_route instance method has not been overridden,
212     # make it an alias to _roda_main_route for performance
213     if instance_method(:_roda_run_main_route).owner == InstanceMethods
214       alias_method :_roda_run_main_route, :_roda_main_route
215     end
216     self::RodaResponse.class_exec do
217       if instance_method(:set_default_headers).owner == ResponseMethods &&
218          instance_method(:default_headers).owner == ResponseMethods
219 
220         private
221 
222         alias set_default_headers set_default_headers
223         def set_default_headers
224           @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
225         end
226       end
227     end
228 
229     if @middleware.empty? && use_new_dispatch_api?
230       plugin :direct_call
231     end
232 
233     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, :always]).all?{|m| self::RodaRequest.instance_method(m).owner == RequestMethods}
234       plugin :_optimized_matching
235     end
236   end
237 
238   build_rack_app
239   @opts.freeze
240   @plugins.freeze
241   @middleware.freeze
242 
243   super
244 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
248 def include(*a)
249   res = super
250   def_roda_before
251   def_roda_after
252   res
253 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
257 def inherited(subclass)
258   raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
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 
265   opts = RodaOpts.new
266   opts.roda_class = subclass
267   @opts.each do |k,v|
268     opts[k] = if k == :subclassed
269       false
270     elsif (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
271       v.dup
272     else
273       v
274     end
275   end
276   subclass.instance_variable_set(:@opts, opts)
277 
278   # Mark current class as having been subclassed, as some optimizations
279   # depend on the class not being subclassed
280   @opts[:subclassed] = true
281 
282   if block = @raw_route_block
283     subclass.route(&block)
284   end
285   
286   request_class = Class.new(self::RodaRequest)
287   request_class.roda_class = subclass
288   request_class.match_pattern_cache = RodaCache.new
289   subclass.const_set(:RodaRequest, request_class)
290 
291   response_class = Class.new(self::RodaResponse)
292   response_class.roda_class = subclass
293   subclass.const_set(:RodaResponse, response_class)
294 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
305 def plugin(plugin, *args, &block)
306   raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
307   plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
308   raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module)
309 
310   if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block)
311     # RODA4: switch from warning to error
312     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.")
313   end
314 
315   plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
316   @plugins << plugin unless @plugins.include?(plugin)
317   include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
318   extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
319   self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods)
320   self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods)
321   self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods)
322   self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods)
323   plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
324   @app = nil
325 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
343 def route(&block)
344   unless block
345     RodaPlugins.warn "no block passed to Roda.route"
346     return
347   end
348 
349   @raw_route_block = block
350   @route_block = block = convert_route_block(block)
351   @rack_app_route_block = block = rack_app_route_block(block)
352   public define_roda_method(:_roda_main_route, 1, &block)
353   @app = nil
354 end
set_default_headers()
[show source]
    # File lib/roda.rb
223 def set_default_headers
224   @headers[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
225 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
360 def use(*args, &block)
361   @middleware << [args, block].freeze
362   @app = nil
363 end