class Scorched::Controller
Attributes
Public Class Methods
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
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
# 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
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
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
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
# File lib/scorched/controller.rb, line 110 def filters @filters ||= {before: before_filters, after: after_filters, error: error_filters} end
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
# File lib/scorched/controller.rb, line 106 def mappings @mappings ||= [] end
# 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
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
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
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
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
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
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
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 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
# 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
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
# 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
# File lib/scorched/controller.rb, line 425 def pass throw :pass end
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
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
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
# File lib/scorched/controller.rb, line 263 def respond_to_missing?(method_name, include_private = false) self.class.respond_to? method_name end
Convenience method for accessing Rack session.
# File lib/scorched/controller.rb, line 430 def session env['rack.session'] end
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
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
# 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
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
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