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.
-
.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 ‘Match` objects, 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.
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
#request ⇒ Object (readonly)
Returns the value of attribute request.
16 17 18 |
# File 'lib/scorched/controller.rb', line 16 def request @request end |
#response ⇒ Object (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 |
.filters ⇒ Object
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.
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 |
.mappings ⇒ Object
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)
(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.
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 |
#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
465 466 467 468 469 470 471 472 473 474 475 476 477 |
# File 'lib/scorched/controller.rb', line 465 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.
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_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.
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.
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 |
#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.
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 |
#pass ⇒ Object
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.
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], **, &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
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
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 |
#session ⇒ Object
Convenience method for accessing Rack session.
430 431 432 |
# File 'lib/scorched/controller.rb', line 430 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.
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. << 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’) #=> 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 |