Class: Scorched::Controller

Inherits:
Object
  • Object
show all
Extended by:
Forwardable
Defined in:
lib/scorched/controller.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(env) ⇒ Controller

Returns a new instance of Controller.



267
268
269
270
271
272
273
274
# File 'lib/scorched/controller.rb', line 267

def initialize(env)
  define_singleton_method :env do
    env
  end
  env['scorched.root_path'] ||= env['SCRIPT_NAME']
  @request = Request.new(env)
  @response = Response.new
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(method_name, *args, &block) ⇒ Object



259
260
261
# File 'lib/scorched/controller.rb', line 259

def method_missing(method_name, *args, &block)
  (self.class.respond_to? method_name) ? self.class.__send__(method_name, *args, &block) : super
end

Instance Attribute Details

#requestObject (readonly)

Returns the value of attribute request.



16
17
18
# File 'lib/scorched/controller.rb', line 16

def request
  @request
end

#responseObject (readonly)

Returns the value of attribute response.



16
17
18
# File 'lib/scorched/controller.rb', line 16

def response
  @response
end

Class Method Details

.after(force: false, **conditions, &block) ⇒ Object

Syntactic sugar for defining an after filter. If force is true, the filter is run even if another filter halts the request.



212
213
214
# File 'lib/scorched/controller.rb', line 212

def after(force: false, **conditions, &block)
  filter(:after, force: force, conditions: conditions, &block)
end

.before(force: false, **conditions, &block) ⇒ Object

Syntactic sugar for defining a before filter. If force is true, the filter is run even if another filter halts the request.



206
207
208
# File 'lib/scorched/controller.rb', line 206

def before(force: false, **conditions, &block)
  filter(:before, force: force, conditions: conditions, &block)
end

.call(env) ⇒ Object



114
115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/scorched/controller.rb', line 114

def call(env)
  @instance_cache ||= {}
  loaded = env['scorched.middleware'] ||= Set.new
  to_load = middleware.reject{ |v| loaded.include? v }
  key = [loaded, to_load].map { |x| x.map &:object_id }
  unless @instance_cache[key]
    builder = Rack::Builder.new
    to_load.each { |proc| builder.instance_exec(self, &proc) }
    builder.run(lambda { |env| self.new(env).respond })
    @instance_cache[key] = builder.to_app
  end
  loaded.merge(to_load)
  @instance_cache[key].call(env)
end

.controller(pattern = '/', klass = self, **mapping, &block) ⇒ Object

Maps a new ad-hoc or predefined controller.

If a block is given, creates a new controller as a sub-class of klass (self by default), otherwise maps klass itself. Returns the new anonymous controller class if a block is given, or klass otherwise.



152
153
154
155
156
157
158
159
160
161
# File 'lib/scorched/controller.rb', line 152

def controller(pattern = '/', klass = self, **mapping, &block)
  if block_given?
    controller = Class.new(klass, &block)
    controller.config[:auto_pass] = true if klass < Scorched::Controller
  else
    controller = klass
  end
  self.map **{pattern: pattern, target: controller}.merge(mapping)
  controller
end

.error(*classes, **conditions, &block) ⇒ Object

Syntactic sugar for defining an error filter. Takes one or more optional exception classes for which this error filter should handle. Handles all exceptions by default.



219
220
221
# File 'lib/scorched/controller.rb', line 219

def error(*classes, **conditions, &block)
  filter(:error, args: classes, conditions: conditions, &block)
end

.filter(type, args: nil, force: nil, conditions: nil, **more_conditions, &block) ⇒ Object

Defines a filter of type. args is used internally by Scorched for passing additional arguments to some filters, such as the exception in the case of error filters.



199
200
201
202
# File 'lib/scorched/controller.rb', line 199

def filter(type, args: nil, force: nil, conditions: nil, **more_conditions, &block)
  more_conditions.merge!(conditions || {})
  filters[type.to_sym] << {args: args, force: force, conditions: more_conditions, proc: block}
end

.filtersObject



110
111
112
# File 'lib/scorched/controller.rb', line 110

def filters
  @filters ||= {before: before_filters, after: after_filters, error: error_filters}
end

.map(pattern: nil, priority: nil, conditions: {}, target: nil) ⇒ Object Also known as: <<

Generates and assigns mapping hash from the given arguments.

Accepts the following keyword arguments:

:pattern - The url pattern to match on. Required.
:target - A proc to execute, or some other object that responds to #call. Required.
:priority - Negative or positive integer for giving a priority to the mapped item.
:conditions - A hash of condition:value pairs

Raises ArgumentError if required key values are not provided.

Raises:

  • (ArgumentError)


137
138
139
140
141
142
143
144
145
# File 'lib/scorched/controller.rb', line 137

def map(pattern: nil, priority: nil, conditions: {}, target: nil)
  raise ArgumentError, "Mapping must specify url pattern and target" unless pattern && target
  mappings << {
    pattern: compile(pattern),
    priority: priority.to_i,
    conditions: conditions,
    target: target
  }
end

.mappingsObject



106
107
108
# File 'lib/scorched/controller.rb', line 106

def mappings
  @mappings ||= []
end

.route(pattern = nil, priority = nil, **conds, &block) ⇒ Object

Generates and returns a new route proc from the given block, and optionally maps said proc using the given args. Helper methods are provided for each HTTP method which automatically define the appropriate :method condition.

:call-seq:

route(pattern = nil, priority = nil, **conds, &block)
get(pattern = nil, priority = nil, **conds, &block)
post(pattern = nil, priority = nil, **conds, &block)
put(pattern = nil, priority = nil, **conds, &block)
delete(pattern = nil, priority = nil, **conds, &block)
head(pattern = nil, priority = nil, **conds, &block)
options(pattern = nil, priority = nil, **conds, &block)
patch(pattern = nil, priority = nil, **conds, &block)


176
177
178
179
180
181
182
183
184
185
186
# File 'lib/scorched/controller.rb', line 176

def route(pattern = nil, priority = nil, **conds, &block)
  target = lambda do
    args = captures.respond_to?(:values) ? captures.values : captures
    response.body = instance_exec(*args, &block)
    response
  end
  [*pattern].compact.each do |pattern|
    self.map pattern: compile(pattern, true), priority: priority, conditions: conds, target: target
  end
  target
end

Instance Method Details

#absolute(path = nil) ⇒ Object

Takes an optional path, relative to the applications root URL, and returns an absolute path. If relative path given (i.e. anything not starting with ‘/`), returns it as-is. Example: absolute(’/style.css’) #=> /myapp/style.css



552
553
554
555
556
557
558
559
560
561
# File 'lib/scorched/controller.rb', line 552

def absolute(path = nil)
  return path if path && path[0] != '/'
  abs = if path
    [env['scorched.root_path'], path].join('/').gsub(%r{/+}, '/')
  else
    env['scorched.root_path']
  end
  abs.insert(0, '/') unless abs[0] == '/'
  abs
end

#check_condition?(c, v) ⇒ Boolean

Test the given condition, returning true if the condition passes, or false otherwise.

Returns:

  • (Boolean)

Raises:



398
399
400
401
402
403
# File 'lib/scorched/controller.rb', line 398

def check_condition?(c, v)
  c = c[0..-2].to_sym if invert = (c[-1] == '!')
  raise Error, "The condition `#{c}` either does not exist, or is not an instance of Proc" unless Proc === self.conditions[c]
  retval = instance_exec(v, &self.conditions[c])
  invert ? !retval : !!retval
end

#check_for_failed_condition(conds) ⇒ Object

Tests the given conditions, returning the name of the first failed condition, or nil otherwise.



389
390
391
392
393
394
395
# File 'lib/scorched/controller.rb', line 389

def check_for_failed_condition(conds)
  failed = (conds || []).find { |c, v| !check_condition?(c, v) }
  if failed
    failed[0] = failed[0][0..-2].to_sym if failed[0][-1] == '!'
  end
  failed
end

Serves as a thin layer of convenience to Rack’s built-in method: Request#cookies, Response#set_cookie, and Response#delete_cookie.

If only one argument is given, the specified cookie is retreived and returned. If both arguments are supplied, the cookie is either set or deleted, depending on whether the second argument is nil, or otherwise is a hash containing the key/value pair “:value => nil“. If you wish to set a cookie to an empty value without deleting it, you pass an empty string as the value



465
466
467
468
469
470
471
472
473
474
475
476
477
# File 'lib/scorched/controller.rb', line 465

def cookie(name, *value)
  name = name.to_s
  if value.empty?
    request.cookies[name]
  else
    value = (Hash === value[0]) ? value[0] : {value: value[0]}
    if value[:value].nil?
      response.delete_cookie(name, value)
    else
      response.set_cookie(name, value)
    end
  end
end

#dispatch(match) ⇒ Object

Dispatches the request to the matched target. Overriding this method provides the opportunity for one to have more control over how mapping targets are invoked.



329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/scorched/controller.rb', line 329

def dispatch(match)
  @_dispatched = true
  target = match.mapping[:target]
  response.merge! begin
    if Proc === target
      instance_exec(&target)
    else
      target.call(env.merge(
        'SCRIPT_NAME' => request.matched_path.chomp('/'),
        'PATH_INFO' => request.unmatched_path[match.path.chomp('/').length..-1]
      ))
    end
  end
end

#eligable_matchesObject

Returns an ordered list of eligable matches. Orders matches based on media_type, ensuring priority and definition order are respected appropriately. Sorts by mapping priority first, media type appropriateness second, and definition order third.



374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/scorched/controller.rb', line 374

def eligable_matches
  @_eligable_matches ||= begin
    matches.select { |m| m.failed_condition.nil? }.each_with_index.sort_by do |m,idx|
      priority = m.mapping[:priority] || 0
      media_type_rank = [*m.mapping[:conditions][:media_type]].map { |type|
        env['scorched.accept'][:accept].rank(type, true)
      }.max
      media_type_rank ||= env['scorched.accept'][:accept].rank('*/*', true) || 0 # Default to "*/*" if no media type condition specified.
      order = -idx
      [priority, media_type_rank, order]
    end.reverse
  end
end

#flash(key = :flash) ⇒ Object

Flash session storage helper. Stores session data until the next time this method is called with the same arguments, at which point it’s reset. The typical use case is to provide feedback to the user on the previous action they performed.

Raises:



441
442
443
444
445
446
447
448
449
450
451
452
# File 'lib/scorched/controller.rb', line 441

def flash(key = :flash)
  raise Error, "Flash session data cannot be used without a valid Rack session" unless session
  flash_hash = env['scorched.flash'] ||= {}
  flash_hash[key] ||= {}
  session[key] ||= {}
  unless session[key].methods(false).include? :[]=
    session[key].define_singleton_method(:[]=) do |k, v|
      flash_hash[key][k] = v
    end
  end
  session[key]
end

#halt(status = nil, body = nil) ⇒ Object

call-seq:

halt(status=nil, body=nil)
halt(body)


415
416
417
418
419
420
421
422
423
# File 'lib/scorched/controller.rb', line 415

def halt(status=nil, body=nil)
  unless status.nil? || Integer === status
    body = status
    status = nil
  end
  response.status = status if status
  response.body = body if body
  throw :halt
end

#matchesObject

Finds mappings that match the unmatched portion of the request path, returning an array of ‘Match` objects, or an empty array if no matches were found.

The ‘:eligable` attribute of the `Match` object indicates whether the conditions for that mapping passed. The result is cached for the life time of the controller instance, for the sake of effecient recalling.



349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/scorched/controller.rb', line 349

def matches
  @_matches ||= begin
    to_match = request.unmatched_path
    to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
    mappings.map { |mapping|
      mapping[:pattern].match(to_match) do |match_data|
        if match_data.pre_match == ''
          if match_data.names.empty?
            captures = match_data.captures
          else
            captures = Hash[match_data.names.map {|v| v.to_sym}.zip(match_data.captures)]
            captures.each do |k,v|
              captures[k] = symbol_matchers[k][1].call(v) if Array === symbol_matchers[k]
            end
          end
          Match.new(mapping, captures, match_data.to_s, check_for_failed_condition(mapping[:conditions]))
        end
      end
    }.compact
  end
end

#passObject



425
426
427
# File 'lib/scorched/controller.rb', line 425

def pass
  throw :pass
end

#redirect(url, status: (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302, halt: true) ⇒ Object

Redirects to the specified path or URL. An optional HTTP status is also accepted.



406
407
408
409
410
# File 'lib/scorched/controller.rb', line 406

def redirect(url, status: (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302, halt: true)
  response['Location'] = absolute(url)
  response.status = status
  self.halt if halt
end

#render(string_or_file, dir: , layout: , engine: , locals: , tilt: , **options, &block) ⇒ Object

Renders the given string or file path using the Tilt templating library. Each option defaults to the corresponding value defined in render_defaults attribute. Unrecognised options are passed through to Tilt, but a ‘:tilt` option is also provided for passing options directly to Tilt. The template engine is derived from the file name, or otherwise as specified by the :engine option. If a string is given, the :engine option must be set.

Refer to Tilt documentation for a list of valid template engines and Tilt options.

Raises:



486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
# File 'lib/scorched/controller.rb', line 486

def render(
  string_or_file,
  dir: render_defaults[:dir],
  layout: @_no_default_layout ? nil : render_defaults[:layout],
  engine: render_defaults[:engine],
  locals: render_defaults[:locals],
  tilt: render_defaults[:tilt],
  **options,
  &block
)
  template_cache = config[:cache_templates] ? TemplateCache : Tilt::Cache.new
  tilt_options = options.merge(tilt || {})
  tilt_engine = (derived_engine = Tilt[string_or_file.to_s]) || Tilt[engine]
  raise Error, "Invalid or undefined template engine: #{engine.inspect}" unless tilt_engine

  template = if Symbol === string_or_file
    file = string_or_file.to_s
    file = file << ".#{engine}" unless derived_engine
    file = File.expand_path(file, dir) if dir

    template_cache.fetch(:file, tilt_engine, file, tilt_options) do
      tilt_engine.new(file, nil, tilt_options)
    end
  else
    template_cache.fetch(:string, tilt_engine, string_or_file, tilt_options) do
      tilt_engine.new(nil, nil, tilt_options) { string_or_file }
    end
  end

  # The following is responsible for preventing the rendering of layouts within views.
  begin
    original_no_default_layout = @_no_default_layout
    @_no_default_layout = true
    output = template.render(self, locals, &block)
  ensure
    @_no_default_layout = original_no_default_layout
  end

  if layout
    render(layout, dir: dir, layout: false, engine: engine, locals: locals, tilt: tilt, **options) { output }
  else
    output
  end
end

#respondObject

This is where the magic happens. Applies filters, matches mappings, applies error handlers, catches :halt and :pass, etc. Returns a rack-compatible tuple



279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# File 'lib/scorched/controller.rb', line 279

def respond
  inner_error = nil
  rescue_block = proc do |e|
    (env['rack.exception'] = e && raise) unless filters[:error].any? do |f|
      if !f[:args] || f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }
        instance_exec(e, &f[:proc]) unless check_for_failed_condition(f[:conditions])
      end
    end
  end

  begin
    if config[:strip_trailing_slash] == :redirect && request.path =~ %r{[^/]/+$}
      query_string = request.query_string.empty? ? '' : '?' << request.query_string
      redirect(request.path.chomp('/') + query_string, status: 307, halt: false)
      return response.finish
    end
    pass if config[:auto_pass] && eligable_matches.empty?

    if run_filters(:before)
      catch(:halt) {
        begin
          try_matches
        rescue => inner_error
          rescue_block.call(inner_error)
        end
      }
    end
    run_filters(:after)
  rescue => outer_error
    outer_error == inner_error ? raise : catch(:halt) { rescue_block.call(outer_error) }
  end
  response.finish
end

#respond_to_missing?(method_name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


263
264
265
# File 'lib/scorched/controller.rb', line 263

def respond_to_missing?(method_name, include_private = false)
  self.class.respond_to? method_name
end

#sessionObject

Convenience method for accessing Rack session.



430
431
432
# File 'lib/scorched/controller.rb', line 430

def session
  env['rack.session']
end

#try_matchesObject

Tries to dispatch to each eligable match. If the first match passes, tries the second match and so on. If there are no eligable matches, or all eligable matches pass, an appropriate 4xx response status is set.



315
316
317
318
319
320
321
322
323
324
325
# File 'lib/scorched/controller.rb', line 315

def try_matches
  eligable_matches.each do |match,idx|
    request.breadcrumb << match
    catch(:pass) {
      dispatch(match)
      return true
    }
    request.breadcrumb.pop # Current match passed, so pop the breadcrumb before the next iteration.
  end
  response.status = (!matches.empty? && eligable_matches.empty?) ? 403 : 404
end

#url(path = nil, scheme: nil) ⇒ Object

Takes an optional URL, relative to the applications root, and returns a fully qualified URL. Example: url(‘/example?show=30’) #=> localhost:9292/myapp/example?show=30



533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
# File 'lib/scorched/controller.rb', line 533

def url(path = nil, scheme: nil)
  return path if path && URI.parse(path).scheme
  uri = URI::Generic.build(
    scheme: scheme || env['rack.url_scheme'],
    host: env['SERVER_NAME'],
    port: env['SERVER_PORT'].to_i,
    path: env['scorched.root_path'],
  )
  if path
    path[0,0] = '/' unless path[0] == '/'
    uri.to_s.chomp('/') << path
  else
    uri.to_s
  end
end