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.



278
279
280
281
282
283
284
285
# File 'lib/scorched/controller.rb', line 278

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



270
271
272
# File 'lib/scorched/controller.rb', line 270

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.



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

def request
  @request
end

#responseObject (readonly)

Returns the value of attribute response.



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

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.



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

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

.around(**conditions, &block) ⇒ Object

Syntactic sugar for defining an around filter. The block is passed a single argument - a proc representing the rest of the filter chain (including the eventual dispatch to before/matching/after processing). The block must call it (e.g. cont.call) for request handling to proceed; if it doesn't, the wrapped chain simply never runs. Around filters defined earlier (including those defined in superclasses) wrap those defined later, with the innermost proc being the actual before/dispatch/after cycle.



223
224
225
# File 'lib/scorched/controller.rb', line 223

def around(**conditions, &block)
  filter(:around, 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.



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

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

.call(env) ⇒ Object



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

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.



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

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.



230
231
232
# File 'lib/scorched/controller.rb', line 230

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.



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

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



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

def filters
  @filters ||= {before: before_filters, around: around_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)


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

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



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

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)



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

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



569
570
571
572
573
574
575
576
577
578
# File 'lib/scorched/controller.rb', line 569

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 = "/#{abs}" 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:



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

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.



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

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



482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/scorched/controller.rb', line 482

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.



343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
# File 'lib/scorched/controller.rb', line 343

def dispatch(match)
  @_dispatched = true
  if String === match.mapping[:target] || Symbol === match.mapping[:target]
    match.mapping[:target] = Object.const_get(match.mapping[:target]) # Resolve symbol to constant, and replace target to avoid future lookups
  end
  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.



391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/scorched/controller.rb', line 391

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:



458
459
460
461
462
463
464
465
466
467
468
469
# File 'lib/scorched/controller.rb', line 458

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)



432
433
434
435
436
437
438
439
440
# File 'lib/scorched/controller.rb', line 432

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.



366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
# File 'lib/scorched/controller.rb', line 366

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



442
443
444
# File 'lib/scorched/controller.rb', line 442

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.



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

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:



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
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
# File 'lib/scorched/controller.rb', line 503

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



290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# File 'lib/scorched/controller.rb', line 290

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

  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

  run_around_filters do
    begin
      pass if config[:auto_pass] && eligable_matches.empty?

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

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

Returns:

  • (Boolean)


274
275
276
# File 'lib/scorched/controller.rb', line 274

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

#sessionObject

Convenience method for accessing Rack session.



447
448
449
# File 'lib/scorched/controller.rb', line 447

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.



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

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') #=> https://localhost:9292/myapp/example?show=30



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

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