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