scorched 0.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7468f10cbee0373d243fd4d66dd081ea4b1cecc9
4
+ data.tar.gz: 6225d5221e70a5ba5f3b68d8c6db0144f34d9dfc
5
+ SHA512:
6
+ metadata.gz: 91385d8f1501c564b9ba510ab3d4faef763ea4d6cee6d79b2a23c74cf8511352c8b0c7443360031e8b4ae3149c33c216c4b4b7311eac03d3bee9ce348d075b81
7
+ data.tar.gz: e0d15fc7205e937a8249599833bf9b2a74c9e3abaa3db4decef548eb6292185b53d3da0eede09439efdf5b4deb205b9563d256ed4a93365c3b31270c8db6f5e1
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2013 Tom Wardrop
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,65 @@
1
+ Milestones
2
+ ==========
3
+
4
+ Changelog
5
+ ---------
6
+
7
+ ### v0.5
8
+ * Implemented view rendering using Tilt.
9
+ * Added session method for convenience, and implemented helper for flash session data.
10
+ * Added cookie helper for conveniently setting, retrieving and deleting cookies.
11
+ * Static file serving actually works now
12
+ * Custom middleware Scorched::Static serves as a thin layer on top of Rack::File.
13
+ * Added specs for each configuration option.
14
+ * Using Ruby 2.0 features where applicable. No excuse not to be able to deploy on 2.0 by the time Scorched is ready for production.
15
+ * Keyword arguments instead of ``*args`` combined with ``Hash === args.last``.
16
+ * Replaced instances of __FILE__ with __dir__.
17
+ * Added expected Rack middleware, Rack::MethodOverride and Rack::Head.
18
+
19
+ ### v0.4
20
+ * Make filters behave like middleware. Inheritable, but are only executed once.
21
+ * Improved implementation of Options and Collection classes
22
+
23
+ ### v0.3 and earlier
24
+ * Basic request handling and routing
25
+ * String and Regex URL matching, with capture support
26
+ * Implemented route conditions
27
+ * Added HTTP method condition which the route helpers depend on.
28
+ * Added route helpers
29
+ * Implemented support for sub-controllers
30
+ * Implement before and after filters with proper execution order.
31
+ * Configuration inheritance between controllers. This has been implemented as the Options class.
32
+ * Mechanism for including Rack middleware.
33
+ * Added more route conditions e.g. content-type, language, user-agent, etc.
34
+ * Provide means to `halt` request.
35
+ * Added redirect helping for halting and redirecting request
36
+ * Mechanism for handling exceptions in routes and before/after filters.
37
+ * Added static resource serving. E.g. public folder.
38
+
39
+
40
+
41
+ Remaining
42
+ ---------
43
+ Some of these remaining features may be broken out into a separate contributor library to keep the core lean and focused.
44
+
45
+ * Make specs for Collection and Options classes more thorough, e.g. test all non-reading modifiers such as clear, delete, etc.
46
+ * Add view helpers
47
+ * Add helper to easily read and build HTTP query strings. Takes care of "?" and "&" logic, escaping, etc. This is
48
+ intended to make link building easier.
49
+ * Form populator
50
+ * Provide a default error page somewhat similar to what Sinatra has.
51
+ * Add debug logging to show each routing hop and the current environment (variables, mode, etc)
52
+ * Environment optimised defaults
53
+ * Production
54
+ * Rack::Protection
55
+ * Disable static file serving
56
+ * Development
57
+ * Verbose logging to STDOUT
58
+ * use Rack::ShowExceptions
59
+
60
+ Unlikely
61
+ --------
62
+ * Mutex locking option? I'm of the opinion that the web server should be configured for the concurrency model of the application, rather than the framework.
63
+
64
+
65
+ More things will be added to these lists as they're thought of and considered.
@@ -0,0 +1,78 @@
1
+ Scorched
2
+ ========
3
+
4
+ *Light-weight, DRY as a desert, web framework for Ruby. Inspired by Sinatra, this framework is my vision of the next evolutionary step in light-weight ruby web frameworks.*
5
+
6
+ Scorched honours the patrons of the past. It's not a complete reinvention of the lightweight web framework, but rather what I hope is an evolutionary enhancement. Most of the concepts are carried forward from predecessors. Scorched merely enhances those concepts in an attempt to extract their full potential, as well as offering up to scrutiny some entirely new idioms. All with the intention to make developing lightweight web apps in Ruby even more enjoyable.
7
+
8
+ The name 'Scorched' is inspired by the main goal of the project, which is to DRY-up what the likes of Sinatra and Padrino left moist.
9
+
10
+
11
+ The Errors of Our Past (aka. Areas of Moisty-ness)
12
+ --------------------------------------------------
13
+ I think the biggest mistake made by the predecessors of Scorched such as Sinatra/Padrino, was to not leverage the power
14
+ of the class. The consequences of this made for some awkwardness. Helpers are a classical reinvention of what
15
+ classes and modules were already made to solve. Scorched implements Controllers as Classes, which in addition to having their own DSL, allow defining and calling traditional class methods. Allowing developers to implement helpers and other common functionality as proper methods not only makes them more predictable and familiar, but of course allow such helpers to be inheritable via plain-old Class inheritance.
16
+
17
+ Perhaps another error (or area of sogginess, if you will) has been a lack of consideration for the hierarchical nature of websites, and the fact that sub-directories are often expected to inherit attributes of their parents. Scorched supports sub-controllers to any arbitrary depth, with each controllers filters and route conditions applied along the way. This can assist many areas of web development, including security, restful interfaces, and interchangeable output formats.
18
+
19
+
20
+ Design Philosophy
21
+ -----------------
22
+ Scorched has a relatively simple design philosophy. The main objective is to keep Scorched lean and generic. Scorched refrains from expressing too much opinion. The general idea behind Scorched is to give developers all the tools to quickly put together small, medium and perhaps even large websites and applications.
23
+
24
+ There's little need for a framework to be opinionated if the opinions of the developer can be quickly and easily built into it on a per-application basis. To do this effectively, developers really need to understand Scorched, and the best way to lower facilitate that is to lower the learning curve by keeping the core design, logical, predictable, and concise.
25
+
26
+
27
+ First Impressions
28
+ -----------------
29
+ Below I present a sample of the API as it currently stands:
30
+
31
+ class MyApp < Scorched::Controller
32
+
33
+ # From the most simple route possible...
34
+ get '/' do
35
+ "Hello World"
36
+ end
37
+
38
+ # To something that gets the muscle's flexing a little
39
+ route '/articles/:title/::opts', 2, methods: ['GET', 'POST'], content_type: :json do
40
+ # Do what you want in here. Note, the second argument is the optional route priority.
41
+ end
42
+
43
+ # Anonymous controllers allow for convenient route grouping to which filters and conditions can be applied
44
+ controller conditions: {content_type: :json} do
45
+ get '/articles/*' do |page|
46
+ {title: 'Scorched Rocks', body: '...', created_at: '27/08/2012', created_by: 'Bob'}
47
+ end
48
+
49
+ after do
50
+ response.to_json
51
+ end
52
+ end
53
+
54
+ # The things you get for free by using Classes for Controllers (...that's directed at you Padrino)
55
+ def my_little_helper
56
+ # Do some crazy awesome stuff that no route can resist using.
57
+ end
58
+
59
+ # You can always avoid the routing helpers and add mappings manually. Anything that responds to #call is a valid target, with the only minor exception being that proc's are instance_exec'd, not #call'd.
60
+ self << {url: '/admin', priority: 10, target: My3rdPartyAdminApp}
61
+ self << {url: '**', conditions: {maintenance_mode: true}, target: proc { |env|
62
+ @request.body << 'Maintenance underway, please be patient.'
63
+ }}
64
+ end
65
+
66
+ This API shouldn't look too foreign to anyone familiar with frameworks like Sinatra, and the potential power at hand should be obvious. The `route` method demonstrates a few minor features of Scorched:
67
+
68
+ * Multi-method routes - Because sometimes the difference between a GET and POST can be a single line of code. If no methods are provided, the route receives all HTTP methods.
69
+ * Named Wildcards - Not an original idea, but you may note the named wildcard with the double colon. This maps to the '**' glob directive, which will span forward-slashes while matching. The single asterisk (or colon) behaves like the single asterisk glob directive, and will not match forward-slashes.
70
+ * Route priorities - Routes (referred to as mappings internally) can be assigned priorities. A priority can be any arbitrary number by which the routes are ordered. The higher the number, the higher the priority.
71
+ * Conditions - Conditions are merely procs defined on the controller which are inherited (and can be overriden) by child controllers. When a request comes in, mappings that match the requested URL, first have their conditions evaluated in the context of the controller instance, before control is handed off to the target associated with that mapping. It's a very simple implementation that comes with a lot of potential.
72
+
73
+ That should hopefully give you an example of how the core of Scorched is shaping up. This demonstrates only a subset of what Scorched will offer.
74
+
75
+
76
+ Development Progress
77
+ --------------------
78
+ Please refer to [Milestones.md](Milestones.md) for a breakdown of development progress.
@@ -0,0 +1,32 @@
1
+ Effortless REST
2
+ ---------------
3
+ An easy way to serve multiple content-types:
4
+
5
+ class App < Scorched::Controller
6
+ def view(view = nil)
7
+ view ? env['app.view'] = view : env['app.view']
8
+ end
9
+
10
+ after do
11
+ if check_condition?(:media_type, 'text/html')
12
+ response.body = [render(view)]
13
+ if check_condition?(:media_type, 'application/json')
14
+ response['Content-type'] = 'application/json'
15
+ response.body = [response.body.to_json]
16
+ elsif check_condition?(:media_type, 'application/pdf')
17
+ response['Content-type'] = 'application/pdf'
18
+ # response.body = [render_pdf(view)]
19
+ else
20
+ response.body = [render(view)]
21
+ end
22
+ end
23
+
24
+ get '/' do
25
+ view :index
26
+ [
27
+ {title: 'Sweet Purple Unicorns', date: '08/03/2013'},
28
+ {title: 'Mellow Grass Men', date: '21/03/2013'}
29
+ ]
30
+ end
31
+ end
32
+
@@ -0,0 +1,8 @@
1
+
2
+ After Filters
3
+ -------------
4
+
5
+
6
+ Error Filters
7
+ -------------
8
+ Error filters are processed like regular filters, except they're called whenever an exception occurs. If an error filter returns false, it's assumed to be unhandled, and the next error filter is called. This continues until one of the following is true, 1) an error filter returns true, in which case the exception is assumed to be handled, 2) an error filter raises an exception itself, or 3) there are no more error handlers defined on the current controller, in which case the exception is re-raised.
@@ -0,0 +1,29 @@
1
+ Patterns
2
+ ========
3
+ All patterns attempt to match the remaining unmatched portion of the request path (the request path being rack's
4
+ `path_info` variable). The unmatched path will always begin with a forward slash if the previously matched portion of the
5
+ path ended at a forward slash, regardless of whether it actually included the forward slash in the match, or if the
6
+ forward slash was the next character. As an example, if the request was to "/article/21", then both "/article/" => "/21"
7
+ and "/article" => "/21" would match.
8
+
9
+ All patterns match from the beginning of the path. Matches that occur beyond the beginning of the path won't count as a
10
+ match. So even though the pattern "article" would match "/article/21", it wouldn't count as a match because the match
11
+ didn't start at a non-zero offset.
12
+
13
+ If a pattern contains named captures, unnamed captures will be lost (this is how named regex captures work in Ruby). So
14
+ if you name one capture, make sure you name any other captures you may want to access.
15
+
16
+ String Patterns
17
+ ---------------
18
+ * `*` - Matches all characters excluding the forward slash.
19
+ * `**` - Matches all characters including the forward slash.
20
+ * `:param` - Same as `*` except the capture is named to whatever the string following the single-colon is.
21
+ * `::param` - Same as `**` except the capture is named to whatever the string following the double-colon is.
22
+ * `$` - If placed at the end of a pattern, the pattern only matches if it matches the entire path. For routes, this is
23
+ implied, so it should only be explicitly added if trying to match a dollar sign character. If added anywhere other
24
+ than the end of the pattern, it will match the dollar sign character.
25
+
26
+
27
+ Regex Patterns
28
+ --------------
29
+ Regex patterns offer more power and flexibility than string patterns (naturally). To be continued... (captures, named captures, etc).
@@ -0,0 +1,5 @@
1
+ Managing Request State
2
+ ----------------------
3
+ Because Scorched allows sub-controllers to an arbitrary depth and treats all controllers equal (i.e has no concept of a _root_ controller), it means instance variables cannot be used to track, maintain or share request state between controllers. I mention this because instance variables are the most common way to manage the request state in the likes of Sinatra.
4
+
5
+ As an example, consider the Scorched implementation of flash session data. This data must be shared between controllers throughout the life of the request. The solution here, which you can find by looking at the Scorched source code, is to use the Rack environment hash. It's the one object that's accessible throughout the entire life-cycle of a request, and it's what you should use to maintain request state between controllers and even other embedded Rack applications.
@@ -0,0 +1,18 @@
1
+ require 'json'
2
+ require_relative '../lib/scorched.rb'
3
+
4
+ class MediaTypesExample < Scorched::Controller
5
+
6
+ get '/', media_type: 'text/html' do
7
+ <<-HTML
8
+ <div><strong>Name: </strong> John Falkon</div>
9
+ <div><strong>Age: </strong> 39</div>
10
+ <div><strong>Occupation: </strong> Carpet Cleaner</div>
11
+ HTML
12
+ end
13
+
14
+ get '/,', media_type: 'application/json' do
15
+ {name: 'John Falkon', age: 39, occupation: 'Carpet Cleaner'}.to_json
16
+ end
17
+
18
+ end
@@ -0,0 +1,2 @@
1
+ require './media_types.rb'
2
+ run MediaTypesExample
@@ -0,0 +1,19 @@
1
+ # Gems
2
+ require 'rack'
3
+ require 'rack/accept'
4
+ require 'tilt'
5
+
6
+ # Stdlib
7
+ require 'set'
8
+ require 'logger'
9
+
10
+ require_relative 'scorched/static'
11
+ require_relative 'scorched/dynamic_delegate'
12
+ require_relative 'scorched/options'
13
+ require_relative 'scorched/collection'
14
+ require_relative 'scorched/view_helpers'
15
+ require_relative 'scorched/controller'
16
+ require_relative 'scorched/error'
17
+ require_relative 'scorched/request'
18
+ require_relative 'scorched/response'
19
+ require_relative 'scorched/version'
@@ -0,0 +1,60 @@
1
+ module Scorched
2
+ class Collection < Set
3
+ # Redefine all methods as delegates of the underlying local set.
4
+ extend DynamicDelegate
5
+ alias_each(Set.instance_methods(false)) { |m| "_#{m}" }
6
+ delegate 'to_set', *Set.instance_methods(false).reject { |m|
7
+ [:<<, :add, :add?, :clear, :delete, :delete?, :delete_if, :merge, :replace, :subtract].include? m
8
+ }
9
+
10
+ # sets parent Collection object and returns self
11
+ def parent!(parent)
12
+ @parent = parent
13
+ self
14
+ end
15
+
16
+ def to_set(inherit = true)
17
+ if inherit && (Set === @parent || Array === @parent)
18
+ # An important attribute of a Scorched::Collection is that the merged set is ordered from inner to outer.
19
+ Set.new.merge(self._to_a).merge(@parent.to_set)
20
+ else
21
+ Set.new.merge(self._to_a)
22
+ end
23
+ end
24
+
25
+ def to_a(inherit = true)
26
+ to_set(inherit).to_a
27
+ end
28
+
29
+ def inspect
30
+ "#<#{self.class}: #{_inspect}, #{to_set.inspect}>"
31
+ end
32
+ end
33
+
34
+ class << self
35
+ def Collection(accessor_name)
36
+ m = Module.new
37
+ m.class_eval <<-MOD
38
+ class << self
39
+ def included(klass)
40
+ klass.extend(ClassMethods)
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+ def #{accessor_name}
46
+ @#{accessor_name} || begin
47
+ parent = superclass.#{accessor_name} if superclass.respond_to?(:#{accessor_name}) && Scorched::Collection === superclass.#{accessor_name}
48
+ @#{accessor_name} = Collection.new.parent!(parent)
49
+ end
50
+ end
51
+ end
52
+
53
+ def #{accessor_name}(*args)
54
+ self.class.#{accessor_name}(*args)
55
+ end
56
+ MOD
57
+ m
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,343 @@
1
+ module Scorched
2
+ class Controller
3
+ include ViewHelpers
4
+ include Scorched::Options('config')
5
+ include Scorched::Options('view_config')
6
+ include Scorched::Options('conditions')
7
+ include Scorched::Collection('middleware')
8
+ include Scorched::Collection('before_filters')
9
+ include Scorched::Collection('after_filters')
10
+ include Scorched::Collection('error_filters')
11
+
12
+ config << {
13
+ :strip_trailing_slash => :redirect, # :redirect => Strips and redirects URL ending in forward slash, :ignore => internally ignores trailing slash, false => does nothing.
14
+ :static_dir => 'public', # The directory Scorched should serve static files from. Set to false if web server or anything else is serving static files.
15
+ :logger => Logger.new(STDOUT)
16
+ }
17
+
18
+ view_config << {
19
+ :dir => 'views', # The directory containing all the view templates, relative to the application root.
20
+ :layout => false, # The default layout template to use, relative to the view directory. Set to false for no default layout.
21
+ :engine => :erb
22
+ }
23
+
24
+ conditions << {
25
+ charset: proc { |charsets|
26
+ [*charsets].any? { |charset| request.env['rack-accept.request'].charset? charset }
27
+ },
28
+ encoding: proc { |encodings|
29
+ [*encodings].any? { |encoding| request.env['rack-accept.request'].encoding? encoding }
30
+ },
31
+ host: proc { |host|
32
+ (Regexp === host) ? host =~ request.host : host == request.host
33
+ },
34
+ language: proc { |languages|
35
+ [*languages].any? { |language| request.env['rack-accept.request'].language? language }
36
+ },
37
+ media_type: proc { |types|
38
+ [*types].any? { |type| request.env['rack-accept.request'].media_type? type }
39
+ },
40
+ methods: proc { |accepts|
41
+ [*accepts].include?(request.request_method)
42
+ },
43
+ user_agent: proc { |user_agent|
44
+ (Regexp === user_agent) ? user_agent =~ request.user_agent : user_agent == request.user_agent
45
+ },
46
+ status: proc { |statuses|
47
+ [*statuses].include?(response.status)
48
+ },
49
+ }
50
+
51
+ middleware << proc { |this|
52
+ use Rack::Head
53
+ use Rack::MethodOverride
54
+ use Rack::Accept
55
+ use Scorched::Static, :dir => this.config[:static_dir] if this.config[:static_dir]
56
+ use Rack::Logger, this.config[:logger] if this.config[:logger]
57
+ }
58
+
59
+ class << self
60
+
61
+ def mappings
62
+ @mappings ||= []
63
+ end
64
+
65
+ def filters
66
+ @filters ||= {before: before_filters, after: after_filters, error: error_filters}
67
+ end
68
+
69
+ def call(env)
70
+ loaded = env['scorched.middleware'] ||= Set.new
71
+ app = lambda do |env|
72
+ instance = self.new(env)
73
+ instance.action
74
+ end
75
+
76
+ builder = Rack::Builder.new
77
+ middleware.reject{ |v| loaded.include? v }.each do |proc|
78
+ builder.instance_exec(self, &proc)
79
+ loaded << proc
80
+ end
81
+ builder.run(app)
82
+ builder.call(env)
83
+ end
84
+
85
+ # Generates and assigns mapping hash from the given arguments.
86
+ #
87
+ # Accepts the following keyword arguments:
88
+ # :url - The url pattern to match on. Required.
89
+ # :target - A proc to execute, or some other object that responds to #call. Required.
90
+ # :priority - Negative or positive integer for giving a priority to the mapped item.
91
+ # :conditions - A hash of condition:value pairs
92
+ # Raises ArgumentError if required key values are not provided.
93
+ def map(url: nil, priority: nil, conditions: {}, target: nil)
94
+ raise ArgumentError, "Mapping must specify url pattern and target" unless url && target
95
+ priority = priority.to_i
96
+ insert_pos = mappings.take_while { |v| priority <= v[:priority] }.length
97
+ mappings.insert(insert_pos, {
98
+ url: compile(url),
99
+ priority: priority,
100
+ conditions: conditions,
101
+ target: target
102
+ })
103
+ end
104
+ alias :<< :map
105
+
106
+ # Creates a new controller as a sub-class of self (by default), mapping it to self using the provided mapping
107
+ # hash if one is provided. Returns the new anonymous controller class.
108
+ #
109
+ # Takes two optional arguments and a block: a parent class from which the generated controller class inherits
110
+ # from, a mapping hash to automatically map the new controller, and of course a block which defines the
111
+ # controller class.
112
+ #
113
+ # It's worth noting, however obvious, that the resulting class will only be a controller if the parent class is
114
+ # (or inherits from) a Scorched::Controller.
115
+ def controller(parent_class = self, **mapping, &block)
116
+ c = Class.new(parent_class, &block)
117
+ self << {url: '/', target: c}.merge(mapping)
118
+ c
119
+ end
120
+
121
+ # Generates and returns a new route proc from the given block, and optionally maps said proc using the given args.
122
+ def route(url = nil, priority = nil, **conditions, &block)
123
+ target = lambda do |env|
124
+ env['rack.response'].body << instance_exec(*env['rack.request'].captures, &block)
125
+ env['rack.response']
126
+ end
127
+ self << {url: compile(url, true), priority: priority, conditions: conditions, target: target} if url
128
+ target
129
+ end
130
+
131
+ ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'].each do |method|
132
+ methods = (method == 'get') ? ['GET', 'HEAD'] : [method.upcase]
133
+ define_method(method) do |*args, **conditions, &block|
134
+ conditions.merge!(methods: methods)
135
+ route(*args, **conditions, &block)
136
+ end
137
+ end
138
+
139
+ def filter(type, *args, **conditions, &block)
140
+ filters[type.to_sym] << {args: args, conditions: conditions, proc: block}
141
+ end
142
+
143
+ # A bit of syntactic sugar for #filter.
144
+ ['before', 'after', 'error'].each do |type|
145
+ define_method(type) do |*args, &block|
146
+ filter(type, *args, &block)
147
+ end
148
+ end
149
+
150
+ private
151
+
152
+ # Parses and compiles the given URL string pattern into a regex if not already, returning the resulting regexp
153
+ # object. Accepts an optional _match_to_end_ argument which will ensure the generated pattern matches to the end
154
+ # of the string.
155
+ def compile(url, match_to_end = false)
156
+ return url if Regexp === url
157
+ raise Error, "Can't compile URL of type #{url.class}. Must be String or Regexp." unless String === url
158
+ match_to_end = !!url.sub!(/\$$/, '') || match_to_end
159
+ pattern = url.split(%r{(\*{1,2}|(?<!\\):{1,2}[^/*$]+)}).each_slice(2).map { |unmatched, match|
160
+ Regexp.escape(unmatched) << begin
161
+ if %w{* **}.include? match
162
+ match == '*' ? "([^/]+)" : "(.+)"
163
+ elsif match
164
+ match[0..1] == '::' ? "(?<#{match[2..-1]}>.+)" : "(?<#{match[1..-1]}>[^/]+)"
165
+ else
166
+ ''
167
+ end
168
+ end
169
+ }.join
170
+ pattern << '$' if match_to_end
171
+ Regexp.new(pattern)
172
+ end
173
+ end
174
+
175
+ def method_missing(method, *args, &block)
176
+ (self.class.respond_to? method) ? self.class.__send__(method, *args, &block) : super
177
+ end
178
+
179
+ def initialize(env)
180
+ define_singleton_method :env do
181
+ env
182
+ end
183
+ env['rack.request'] ||= Request.new(env)
184
+ env['rack.response'] ||= Response.new
185
+ end
186
+
187
+ def action
188
+ inner_error = nil
189
+ rescue_block = proc do |e|
190
+ raise unless filters[:error].any? do |f|
191
+ (f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }) && check_conditions?(f[:conditions]) && instance_exec(e, &f[:proc])
192
+ end
193
+ end
194
+
195
+ match = matches(true).first
196
+ begin
197
+ catch(:halt) do
198
+ if config[:strip_trailing_slash] == :redirect && request.path =~ %r{./$}
199
+ redirect(request.path.chomp('/'))
200
+ end
201
+
202
+ run_filters(:before)
203
+ if match
204
+ request.breadcrumb << match
205
+ # Proc's are executed in the context of this controller instance.
206
+ target = match[:mapping][:target]
207
+ begin
208
+ catch(:halt) do
209
+ response.merge! (Proc === target) ? instance_exec(request.env, &target) : target.call(request.env)
210
+ end
211
+ rescue => inner_error
212
+ rescue_block.call(inner_error)
213
+ end
214
+ else
215
+ response.status = 404
216
+ end
217
+ run_filters(:after)
218
+ end
219
+ rescue => outer_error
220
+ outer_error == inner_error ? raise : rescue_block.call(outer_error)
221
+ end
222
+ response
223
+ end
224
+
225
+ def match?
226
+ !matches(true).empty?
227
+ end
228
+
229
+ # Finds mappings that match the currently unmatched portion of the request path, returning an array of all matches.
230
+ # If _short_circuit_ is set to true, it stops matching at the first positive match, returning only a single match.
231
+ def matches(short_circuit = false)
232
+ to_match = request.unmatched_path
233
+ to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
234
+ matches = []
235
+ mappings.each do |m|
236
+ m[:url].match(to_match) do |match_data|
237
+ if match_data.pre_match == ''
238
+ if check_conditions?(m[:conditions])
239
+ if match_data.names.empty?
240
+ captures = match_data.captures
241
+ else
242
+ captures = Hash[match_data.names.map{|v| v.to_sym}.zip match_data.captures]
243
+ end
244
+ matches << {mapping: m, captures: captures, url: match_data.to_s}
245
+ break if short_circuit
246
+ end
247
+ end
248
+ end
249
+ end
250
+ matches
251
+ end
252
+
253
+ def check_conditions?(conds)
254
+ if !conds
255
+ true
256
+ else
257
+ conds.all? { |c,v| check_condition?(c, v) }
258
+ end
259
+ end
260
+
261
+ def check_condition?(c, v)
262
+ raise Error, "The condition `#{c}` either does not exist, or is not a Proc object" unless Proc === self.conditions[c]
263
+ instance_exec(v, &self.conditions[c])
264
+ end
265
+
266
+ def redirect(url, status = 307)
267
+ response['Location'] = url
268
+ halt(status)
269
+ end
270
+
271
+ def halt(status = 200)
272
+ response.status = status
273
+ throw :halt
274
+ end
275
+
276
+ # Convenience method for accessing Rack request.
277
+ def request
278
+ env['rack.request']
279
+ end
280
+
281
+ # Convenience method for accessing Rack response.
282
+ def response
283
+ env['rack.response']
284
+ end
285
+
286
+ # Convenience method for accessing Rack session.
287
+ def session
288
+ env['rack.session']
289
+ end
290
+
291
+ # Flash session storage helper.
292
+ # Stores session data until the next time this method is called with the same arguments, at which point it's reset.
293
+ # The typical use case is to provide feedback to the user on the previous action they performed.
294
+ def flash(key = :flash)
295
+ raise Error, "Flash session data cannot be used without a valid Rack session" unless session
296
+ flash_hash = env['scorched.flash'] ||= {}
297
+ flash_hash[key] ||= {}
298
+ session[key] ||= {}
299
+ unless session[key].methods(false).include? :[]=
300
+ session[key].define_singleton_method(:[]=) do |k, v|
301
+ flash_hash[key][k] = v
302
+ end
303
+ end
304
+ session[key]
305
+ end
306
+
307
+ after do
308
+ env['scorched.flash'].each { |k,v| session[k] = v } if session && env['scorched.flash']
309
+ end
310
+
311
+ # Serves a thin layer of convenience to Rack's built-in methods: Request#cookies, Response#set_cookie, and
312
+ # Response#delete_cookie.
313
+ # If only one argument is given, the specified cookie is retreived and returned.
314
+ # If both arguments are supplied, the cookie is either set or deleted, depending on whether the second argument is
315
+ # nil, or otherwise is a hash containing the key/value pair ``:value => nil``.
316
+ # If you wish to set a cookie to an empty value without deleting it, you pass an empty string as the value
317
+ def cookie(name, *value)
318
+ name = name.to_s
319
+ if value.empty?
320
+ request.cookies[name]
321
+ else
322
+ value = Hash === value[0] ? value[0] : {value: value}
323
+ if value[:value].nil?
324
+ response.delete_cookie(name, value)
325
+ else
326
+ response.set_cookie(name, value)
327
+ end
328
+ end
329
+ end
330
+
331
+ private
332
+
333
+ def run_filters(type)
334
+ tracker = env['scorched.filters'] ||= {before: Set.new, after: Set.new}
335
+ filters[type].reject{ |f| tracker[type].include? f }.each do |f|
336
+ if check_conditions?(f[:conditions])
337
+ tracker[type] << f
338
+ instance_exec(&f[:proc])
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end