Module: Roda::RodaPlugins::Base::ClassMethods

Included in:
Roda
Defined in:
lib/roda.rb

Overview

Class methods for the Roda class.

Instance Attribute Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#inherit_middlewareObject

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.



40
41
42
# File 'lib/roda.rb', line 40

def inherit_middleware
  @inherit_middleware
end

#optsObject (readonly)

The settings/options hash for the current class.



43
44
45
# File 'lib/roda.rb', line 43

def opts
  @opts
end

#route_blockObject (readonly)

The route block that this class uses.



46
47
48
# File 'lib/roda.rb', line 46

def route_block
  @route_block
end

Instance Method Details

#appObject

The rack application that this class uses.



33
34
35
# File 'lib/roda.rb', line 33

def app
  @app || build_rack_app
end

#call(env) ⇒ Object

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.



52
53
54
# File 'lib/roda.rb', line 52

def call(env)
  app.call(env)
end

#clear_middleware!Object

Clear the middleware stack



57
58
59
60
# File 'lib/roda.rb', line 57

def clear_middleware!
  @middleware.clear
  @app = nil
end

#define_roda_method(meth, expected_arity, &block) ⇒ Object

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.



85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/roda.rb', line 85

def define_roda_method(meth, expected_arity, &block)
  if meth.is_a?(String)
    meth = roda_method_name(meth)
  end
  call_meth = meth

  # RODA4: Switch to false # :warn in last Roda 3 version
  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
        if check_arity == :warn
          RodaPlugins.warn "Arity mismatch in block passed to define_roda_method. Expected Arity 1, but multiple arguments required for #{block.inspect}"
        end
        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

#expand_path(path, root = ) ⇒ Object

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



183
184
185
# File 'lib/roda.rb', line 183

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

#freezeObject

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.



194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/roda.rb', line 194

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[RodaResponseHeaders::CONTENT_TYPE] ||= 'text/html'
        end
      end
    end

    if @middleware.empty? && use_new_dispatch_api?
      plugin :direct_call
    end

    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}
      plugin :_optimized_matching
    end
  end

  build_rack_app
  @opts.freeze
  @middleware.freeze

  super
end

#include(*a) ⇒ Object

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



234
235
236
237
238
239
# File 'lib/roda.rb', line 234

def include(*a)
  res = super
  def_roda_before
  def_roda_after
  res
end

#inherited(subclass) ⇒ Object

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

Raises:



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/roda.rb', line 243

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

#plugin(plugin, *args, &block) ⇒ Object

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

Raises:



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# File 'lib/roda.rb', line 283

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

#route(&block) ⇒ Object

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.



320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/roda.rb', line 320

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

#use(*args, &block) ⇒ Object

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

Roda.use Rack::ShowExceptions


337
338
339
340
# File 'lib/roda.rb', line 337

def use(*args, &block)
  @middleware << [args, block].freeze
  @app = nil
end