class Scorched::Controller

Attributes

request[R]
response[R]

Public Class Methods

<<(pattern: nil, priority: nil, conditions: {}, target: nil)
Alias for: map
after(force: false, **conditions, &block) click to toggle source

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

# 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) click to toggle source

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

# File lib/scorched/controller.rb, line 206
def before(force: false, **conditions, &block)
  filter(:before, force: force, conditions: conditions, &block)
end
call(env) click to toggle source
# 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) click to toggle source

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.

# 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) click to toggle source

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.

# 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) click to toggle source

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.

# 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
filters() click to toggle source
# 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) click to toggle source

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.

# 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
Also aliased as: <<
mappings() click to toggle source
# File lib/scorched/controller.rb, line 106
def mappings
  @mappings ||= []
end
new(env) click to toggle source
# 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
route(pattern = nil, priority = nil, **conds, &block) click to toggle source
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)

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.

# 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

Private Class Methods

compile(pattern, match_to_end = false) click to toggle source

Parses and compiles the given URL string pattern into a regex if not already, returning the resulting regexp object. Accepts an optional match_to_end argument which will ensure the generated pattern matches to the end of the string.

# File lib/scorched/controller.rb, line 228
def compile(pattern, match_to_end = false)
  return pattern if Regexp === pattern
  raise Error, "Can't compile URL of type #{pattern.class}. Must be String or Regexp." unless String === pattern
  match_to_end = !!pattern.sub!(/\$$/, '') || match_to_end
  compiled = pattern.split(%r{(\*{1,2}\??|(?<!\\):{1,2}[^/*$]+\??)}).each_slice(2).map { |unmatched, match|
    Regexp.escape(unmatched) << begin
      op = (match && match[-1] == '?' && match.chomp!('?')) ? '*' : '+'
      if %w{* **}.include? match
        match == '*' ? "([^/]#{op})" : "(.#{op})"
      elsif match
        if match[0..1] == '::'
          "(?<#{match[2..-1]}>.#{op})"
        else
          name = match[1..-1].to_sym
          regexp = symbol_matchers[name] ? [*symbol_matchers[name]][0] : "[^/]"
          "(?<#{name}>#{regexp}#{op})"
        end
      else
        ''
      end
    end
  }.join
  compiled << '$' if match_to_end
  Regexp.new(compiled)
end

Public Instance Methods

absolute(path = nil) click to toggle source

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

# 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) click to toggle source

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

# 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) click to toggle source

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

# 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
dispatch(match) click to toggle source

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.

# 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_matches() click to toggle source

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.

# 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) click to toggle source

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.

# 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) click to toggle source
halt(body)
# 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
matches() click to toggle source

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.

# 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
method_missing(method_name, *args, &block) click to toggle source
Calls superclass method
# 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
pass() click to toggle source
# 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) click to toggle source

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

# 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: 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 ) click to toggle source

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.

# 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
respond() click to toggle source

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

# 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) click to toggle source
# File lib/scorched/controller.rb, line 263
def respond_to_missing?(method_name, include_private = false)
  self.class.respond_to? method_name
end
session() click to toggle source

Convenience method for accessing Rack session.

# File lib/scorched/controller.rb, line 430
def session
  env['rack.session']
end
try_matches() click to toggle source

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.

# 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) click to toggle source

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

# 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

Private Instance Methods

log(type = nil, message = nil) click to toggle source
# File lib/scorched/controller.rb, line 620
def log(type = nil, message = nil)
  config[:logger].progname ||= 'Scorched'
  if(type)
    type = Logger.const_get(type.to_s.upcase)
    config[:logger].add(type, message)
  end
  config[:logger]
end
run_filter(f) click to toggle source

Returns false if the filter halted. True otherwise.

# File lib/scorched/controller.rb, line 612
def run_filter(f)
  catch(:halt) do
    instance_exec(&f[:proc])
    return true
  end
  return false
end
run_filters(type) click to toggle source

Returns false if any of the filters halted the request. True otherwise.

# File lib/scorched/controller.rb, line 599
def run_filters(type)
  halted = false
  tracker = env['scorched.executed_filters'] ||= {before: Set.new, after: Set.new}
  filters[type].reject { |f| tracker[type].include?(f) }.each do |f|
    unless check_for_failed_condition(f[:conditions]) || (halted && !f[:force])
      tracker[type] << f
      halted = true unless run_filter(f)
    end
  end
  !halted
end