Class: Scorched::Controller
- Inherits:
-
Object
- Object
- Scorched::Controller
- Extended by:
- Forwardable
- Defined in:
- lib/scorched/controller.rb
Instance Attribute Summary collapse
-
#request ⇒ Object
readonly
Returns the value of attribute request.
-
#response ⇒ Object
readonly
Returns the value of attribute response.
Class Method Summary collapse
-
.after(force: false, **conditions, &block) ⇒ Object
Syntactic sugar for defining an after filter.
-
.around(**conditions, &block) ⇒ Object
Syntactic sugar for defining an around filter.
-
.before(force: false, **conditions, &block) ⇒ Object
Syntactic sugar for defining a before filter.
- .call(env) ⇒ Object
-
.controller(pattern = '/', klass = self, **mapping, &block) ⇒ Object
Maps a new ad-hoc or predefined controller.
-
.error(*classes, **conditions, &block) ⇒ Object
Syntactic sugar for defining an error filter.
-
.filter(type, args: nil, force: nil, conditions: nil, **more_conditions, &block) ⇒ Object
Defines a filter of
type. - .filters ⇒ Object
-
.map(pattern: nil, priority: nil, conditions: {}, target: nil) ⇒ Object
(also: <<)
Generates and assigns mapping hash from the given arguments.
- .mappings ⇒ Object
-
.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.
Instance Method Summary collapse
-
#absolute(path = nil) ⇒ Object
Takes an optional path, relative to the applications root URL, and returns an absolute path.
-
#check_condition?(c, v) ⇒ Boolean
Test the given condition, returning true if the condition passes, or false otherwise.
-
#check_for_failed_condition(conds) ⇒ Object
Tests the given conditions, returning the name of the first failed condition, or nil otherwise.
-
#cookie(name, *value) ⇒ Object
Serves as a thin layer of convenience to Rack's built-in method: Request#cookies, Response#set_cookie, and Response#delete_cookie.
-
#dispatch(match) ⇒ Object
Dispatches the request to the matched target.
-
#eligable_matches ⇒ Object
Returns an ordered list of eligable matches.
-
#flash(key = :flash) ⇒ Object
Flash session storage helper.
-
#halt(status = nil, body = nil) ⇒ Object
call-seq: halt(status=nil, body=nil) halt(body).
-
#initialize(env) ⇒ Controller
constructor
A new instance of Controller.
-
#matches ⇒ Object
Finds mappings that match the unmatched portion of the request path, returning an array of
Matchobjects, or an empty array if no matches were found. - #method_missing(method_name, *args, &block) ⇒ Object
- #pass ⇒ Object
-
#redirect(url, status: (env['HTTP_VERSION'] == 'HTTP/1.1') ? 303 : 302, halt: true) ⇒ Object
Redirects to the specified path or URL.
-
#render(string_or_file, dir: , layout: , engine: , locals: , tilt: , **options, &block) ⇒ Object
Renders the given string or file path using the Tilt templating library.
-
#respond ⇒ Object
This is where the magic happens.
- #respond_to_missing?(method_name, include_private = false) ⇒ Boolean
-
#session ⇒ Object
Convenience method for accessing Rack session.
-
#try_matches ⇒ Object
Tries to dispatch to each eligable match.
-
#url(path = nil, scheme: nil) ⇒ Object
Takes an optional URL, relative to the applications root, and returns a fully qualified URL.
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
#request ⇒ Object (readonly)
Returns the value of attribute request.
17 18 19 |
# File 'lib/scorched/controller.rb', line 17 def request @request end |
#response ⇒ Object (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 |
.filters ⇒ Object
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.
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 |
.mappings ⇒ Object
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.
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 |
#cookie(name, *value) ⇒ Object
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 (name, *value) name = name.to_s if value.empty? request.[name] else value = (Hash === value[0]) ? value[0] : {value: value[0]} if value[:value].nil? response.(name, value) else response.(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_matches ⇒ Object
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.
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 |
#matches ⇒ Object
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 |
#pass ⇒ Object
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.
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], **, &block ) template_cache = config[:cache_templates] ? TemplateCache : Tilt::Cache.new = .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.(file, dir) if dir template_cache.fetch(:file, tilt_engine, file, ) do tilt_engine.new(file, nil, ) end else template_cache.fetch(:string, tilt_engine, string_or_file, ) do tilt_engine.new(nil, nil, ) { 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, **) { output } else output end end |
#respond ⇒ Object
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
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 |
#session ⇒ Object
Convenience method for accessing Rack session.
447 448 449 |
# File 'lib/scorched/controller.rb', line 447 def session env['rack.session'] end |
#try_matches ⇒ Object
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. << match catch(:pass) { dispatch(match) return true } request..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 |