scorched 0.9 → 0.10

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 295ef9e64b632d7c0d594d7faa770abe1a276306
4
- data.tar.gz: 77d884750ab7dd5946e124963a0f818141074a6c
3
+ metadata.gz: 08889289c8e64bbfd860c495a1e0319c5c1c9310
4
+ data.tar.gz: 7c203bcdc50f2070e157302e6daa8b442c8a6694
5
5
  SHA512:
6
- metadata.gz: a22df1cc5accdf2ef58b0a9135726d5379b74b55ecd5ed6d301c5e7adf55e14f5905c400411d12263f65552cbf32792d0f1400397fc6be260831536033f52f09
7
- data.tar.gz: 426055e514e3ec9a4a7a13c2dccb692a914a38d054630a4f39b9df0a1d973520850571491741d3dc3f2a8aab363d62c283b4fac0a76140e1b9433bd825564eff
6
+ metadata.gz: 5aaa29d4c05e8bc4b4a2e11381c6a6030f91cbe0e3d2d7d312eabceabcae3447d742485852bfc115938526e21d32fa1399777737eaae9e71a40fe1f2879e8f11
7
+ data.tar.gz: 17c71a09fb5cf953a3dd96b880be3d68332622002ca3dfe9c1393b72e3141c63ea1fb4009bb0044f18a923c6f5e95657781a4514e50745f2735895f830fff57f
data/Gemfile CHANGED
File without changes
data/LICENSE CHANGED
File without changes
@@ -3,6 +3,23 @@ Milestones
3
3
 
4
4
  Changelog
5
5
  ---------
6
+ ### v0.10
7
+ * Route matching internals have been refactored.
8
+ * Match information is now stored in the `Match` struct for better formalisation.
9
+ * `matches` method no longer has a short-circuit option, and now returns all mappings that match the URL, regardless of whether their conditions passed. It also now caches the set of matches which are returned on subsequent calls.
10
+ * The first failed condition (if any) is stored in the `Match` struct as `:failed_condition`. This allows one to change the response in an after block depending on what condition failed. For example, proper status codes can be set depending on the failed condition.
11
+ * Response status defaults to 403 if one or more mappings are matched, but their conditions do not pass. The existing behaviour was to always return 404.
12
+ * Added `:proc` condition which takes one or more Proc objects, allowing custom conditions to be added on-the-fly.
13
+ * Added `:matched` condition. When a controller delegates the request to a mapping, it's considered to be matched.
14
+ * Added `:failed_condition` condition. If one or more mappings are matched, but they're conditions do not pass, the first failed condition of the first matched mapping is considered the `failed_condition` for the request.
15
+ * Added `:config` condition which takes a hash, each element of which must match the value of the corresponding config option.
16
+ * Renamed `:methods` condition to `:method` for consistency sake.
17
+ * Added default error message for all empty responses with a HTTP status code between 400 and 599, inclusive.
18
+ * `Scorched::Collection` now merges parent values onto the beginning of self, rather than the end.
19
+ * To compensate for the previous change, an `append_parent` accessor added to `Scorched::Collection` to allow _after_ filters to run in the correct order, executing inner filters before outer filters.
20
+ * Added `:show_http_error_pages` config option. If true, it shows the Scorched HTTP error pages. Defaults to false.
21
+ * Filters have been added to set the appropriate HTTP status code for certain failed conditions, such as returning `405 Method Not Allowed` when for example, a POST is made to a URL that only accepts GET requests.
22
+
6
23
  ### v0.9
7
24
  * Refactored `render` method:
8
25
  * All Scorched options are now keyword arguments, including `:locals` which was added as a proper render option.
@@ -24,13 +41,13 @@ Changelog
24
41
  * Non-Development
25
42
  * `config[:static_dir] = false` * Development
26
43
  * `config[:show_exceptions] = true` * `config[:logger] = Logger.new(STDOUT)` * Add developer-friendly 404 error page. This is implemented as an after filter, and won't have any effect if the response body is set.
27
- * `absolute`ethod now returns forward slash if script name is empty.
44
+ * `absolute` method now returns forward slash if script name is empty.
28
45
 
29
46
  ### v0.6
30
47
  * `view_config` options hash renamed to ` `render_defaults`ch better reflects its function.
31
48
 
32
49
  ### v0.5.2
33
- * Minor modification to routing to make it behave as a documented regarding matching at the directly before or on a path.
50
+ * Minor modification to routing to make it behave as documented regarding matching a forward slash directly after or at the end of the matched path.
34
51
  * Response content-type now defaults to "text/html;charset=utf-8", rather than empty.
35
52
 
36
53
  ### v0.5.1
@@ -80,6 +97,7 @@ Some of these remaining features may be reconsidered and either left out, or put
80
97
  intended to make link building easier.
81
98
  * Form populator implemented with Nokogiri. This would have to be added to a contrib library.
82
99
  * Add Verbose logging, including debug logging to show each routing hop and the current environment (variables, mode, etc)
100
+ * I need feedback on what order _after_ filters should be run. While it's somewhat intuitive for them to run in the order they're defined, it could also be considered intuitive for the first defined _after_ filter to be the last to touch the outgoing response; to have priority.
83
101
 
84
102
 
85
103
  Unlikely
@@ -88,6 +106,7 @@ These features are unlikely to be implemented unless someone provides a good rea
88
106
 
89
107
  * 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.
90
108
  * Using Rack::Protection by default - The problem here is that a good portion of Rack::Protection involves sessions, and given that Scorched doesn't itself load any session middleware, these components of Rack::Protection would have to be excluded. I wouldn't want to invoke a false sense of security
109
+ * Filter priorities - They're technically possible, but I believe it would introduce the potential for _filter hell_; badly written filters and mass confusion. Filter order has to be logical and predictable. Adding prioritisation would undermine that, and make for lazy use of filters. By not having prioritisation, there's incentive to design filters to be order-agnostic.
91
110
 
92
111
 
93
112
  More things will be added as they're thought of and considered.
data/README.md CHANGED
@@ -70,7 +70,7 @@ class MyApp < Scorched::Controller
70
70
  end
71
71
 
72
72
  # To something that gets the muscle's flexing
73
- route '/articles/:title/::opts', 2, methods: ['GET', 'POST'], content_type: :json do
73
+ route '/articles/:title/::opts', 2, method: ['GET', 'POST'], content_type: :json do
74
74
  # Do what you want in here. Note, the second argument is the optional route priority.
75
75
  end
76
76
 
@@ -71,7 +71,7 @@ class ControllerA < Scorched::Controller
71
71
  end
72
72
  end
73
73
 
74
- class ControllerB < Scorched::Controller
74
+ class ControllerB < ControllerA
75
75
  render_defaults[:layout] = :controller_b
76
76
 
77
77
  get '/', user: 'bob' do
@@ -117,7 +117,7 @@ class MyApp < Scorched::Controller
117
117
  end
118
118
  ```
119
119
 
120
- The controller helper takes an optional URL pattern as it's first argument, an optional parent class as it's second, and finally a mapping hash as its third optional argument, where you can define a priority, conditions, or override the URL pattern. Of course, the `controller` helper takes a block as well, which defines the body of the new controller class.
120
+ The `controller` helper takes an optional URL pattern as it's first argument, an optional parent class as it's second, and finally a mapping hash as its third optional argument, where you can define a priority, conditions, or override the URL pattern. Of course, the `controller` helper takes a block as well, which defines the body of the new controller class.
121
121
 
122
122
  The optional URL pattern defaults to `'/'` which means it's essentially a match-all mapping. In addition, the generated controller has `:auto_pass` set to `true` by default (refer to configuration documentation for more information). This is a handy combination for grouping a set of routes in their own scope, with their own methods, filters, configuration, etc.
123
123
 
@@ -158,4 +158,4 @@ The Root Controller
158
158
  -------------------
159
159
  Although you will likely have a main controller to serve as the target for Rack, Scorched does not have the concept of a root controller. It makes no differentiation between a sub-controller and any other controller. All Controllers are made equal.
160
160
 
161
- You can arrange and nest your controllers in any way, shape or form. Scorched has been designed to not make any assumptions about how you structure your controllers, which again, can accommodate creative solutions.
161
+ You can arrange and nest your controllers in any way, shape or form. Scorched has been designed to not make any assumptions about how you structure your controllers, which again, can accommodate creative solutions.
@@ -8,24 +8,26 @@ There are two sets of configurables. Those which apply to views, and everything
8
8
  Options
9
9
  -------
10
10
 
11
- Each configuration is listed below, with the default value of each included. Note, development environment may override the default values below.
11
+ Each configuration is listed below, with the default value of each included. Note, development environment defaults may override the default values below.
12
12
 
13
+ * `config[:auto_pass] = false` - If no routes within the current controller match, automatically _pass_ the request back to the outer controller without running any filters. This makes sub-controllers behave more like a kind-of route group.
14
+ * `config[:cache_templates] = true` - If true, caches compiled templates using Tilt::Cache.
15
+ * `config[:logger] = false` - Is currently only used for Rack::Logger.
16
+ * `config[:show_exceptions] = false` - If true, shows exceptions using Rack::ShowExceptions
17
+ * `config[:show_http_error_pages] = false` - If true, shows the default Scorched HTTP error page.
18
+ * `config[:static_dir] = 'public'`
19
+ The directory Scorched should serve static files from. Should be set to false if the web server or some other middleware is serving static files.
13
20
  * `config[:strip_trailing_slash] = :redirect`
14
21
  Controls how trailing forward slashes in requests are handled.
15
22
  * `:redirect` - Strips and redirects URL's ending in a forward slash
16
23
  * `:ignore` - Internally ignores trailing slash
17
24
  * `false` - Does nothing. Respects the presence of a trailing forward flash.
18
- * `config[:static_dir] = 'public'`
19
- The directory Scorched should serve static files from. Should be set to false if the web server or some other middleware is serving static files.
20
- * `config[:logger] = false` - Is currently only used for Rack::Logger.
21
- * `config[:auto_pass] = false` - If no routes within the current controller match, automatically _pass_ the request back to the outer controller without running any filters. This makes sub-controllers behave more like a kind-of route group.
22
- * `config[:cache_templates] = true` - If true, caches compiled templates using Tilt::Cache.
23
25
 
24
26
  You can also configure the default options when rendering views by setting them on the `render_defaults` hash. The options specified here are merged with those provided when calling the `render` method, with the explicit options obviously taking precedence over the defaults.
25
27
 
26
28
  Refer to the _views_ page for more information.
27
29
 
28
- Here is an example of the configuration options in action. A couple of different ways to set the options are shown. Refer to the API documentation for the `Scorched::Options` for more information.
30
+ Here is an example of the configuration options in action. A couple of different ways to set the options are shown. Refer to the API documentation for `Scorched::Options` for more information.
29
31
 
30
32
  ```ruby
31
33
  class MyApp < Scorched::Controller
@@ -6,7 +6,7 @@ When Scorched receives a request, the first thing it does is iterate over it's i
6
6
  Mappings can be defined manually using the `map` class method, also aliased as `<<`. Besides the required URL pattern and target elements, a mapping can also define a priority, and one or more conditions. The example below demonstrates the use of all of them.
7
7
 
8
8
  ```ruby
9
- map pattern: '/', priority: -99, conditions: {methods: ['POST', 'PUT', 'DELETE']}, target: proc { |env|
9
+ map pattern: '/', priority: -99, conditions: {method: ['POST', 'PUT', 'DELETE']}, target: proc { |env|
10
10
  [200, {}, 'Bugger off']
11
11
  }
12
12
  ```
@@ -26,7 +26,7 @@ route '/' do
26
26
  'Well hello there'
27
27
  end
28
28
 
29
- route '/*', 5, methods: ['POST', 'PUT', 'DELETE'] do |capture|
29
+ route '/*', 5, method: ['POST', 'PUT', 'DELETE'] do |capture|
30
30
  "Hmm trying to change #{capture} I see"
31
31
  end
32
32
  ```
@@ -37,7 +37,7 @@ The first exception is that the pattern must match to the end of the request pat
37
37
 
38
38
  The other more notable exception is in how the given block is treated. The block given to the route helper is wrapped in another proc. The wrapping proc does a couple of things. It first sends all the captures in the url pattern as argument to the given block, this is shown in the example above. The other thing it does is takes care of assigning the return value to the body of the response.
39
39
 
40
- In the latter of the two examples above, a `:methods` condition defines what methods the route is intended to process. The first example has no such condition, so it accepts all HTTP methods. Typically however, a route will handle a single HTTP method, which is why Scorched also provides the convenience helpers: `get`, `post`, `put`, `delete`, `head`, `options`, and `patch`. These methods automatically define the corresponding `:method` condition, with the `get` helper also including `head` as an accepted HTTP method.
40
+ In the latter of the two examples above, a `:method` condition defines what methods the route is intended to process. The first example has no such condition, so it accepts all HTTP methods. Typically however, a route will handle a single HTTP method, which is why Scorched also provides the convenience helpers: `get`, `post`, `put`, `delete`, `head`, `options`, and `patch`. These methods automatically define the corresponding `:method` condition, with the `get` helper also including `head` as an accepted HTTP method.
41
41
 
42
42
  Pattern Matching
43
43
  ----------------
@@ -68,15 +68,19 @@ Conditions
68
68
  Conditions are essentially just pre-requisites that must be met before a mapping is invoked to handle the current request. They're implemented as `Proc` objects which take a single argument, and return true if the condition is satisfied, or false otherwise. Scorched comes with a number of pre-defined conditions included, many of which are provided by _rack-accept_ - one of the few dependancies of Scorched.
69
69
 
70
70
  * `:charset` - Character sets accepted by the client.
71
+ * `:config` - Takes a hash, each element of which must match the value of the corresponding config option.
71
72
  * `:encoding` - Encodings accepted by the client.
73
+ * `:failed_condition` - If one or more mappings are matched, but they're conditions do not pass, the first failed condition of the first matched mapping is considered the `failed_condition` for the request.
72
74
  * `:host` - The host name (i.e. domain name) used in the request.
73
75
  * `:language` - Languages accepted by the client.
74
76
  * `:media_type` - Media types (i.e. content types) accepted by the client.
75
- * `:methods` - The request method used, e.g. GET, POST, PUT, ...
77
+ * `:matched` - Whether a mapping in the controller instance was invoked as the target for the request.
78
+ * `:method` - The request method used, e.g. GET, POST, PUT, ... .
79
+ * `:proc` - An on-the-fly condition to be evaluated in the context of the controller instance. Should return true if the condition was satisfied, or false otherwise.
76
80
  * `:user_agent` - The user agent string provided with the request. Takes a Regexp or String.
77
81
  * `:status` - The response status of the request. Intended for use by _after_ filters.
78
82
 
79
- Like configuration options, conditions are implemented using the `Scorched::Options` class, so they're inherited and be overridable by child classes. You may easily add your own conditions as the example below demonstrates.
83
+ Like configuration options, conditions are implemented using the `Scorched::Options` class, so they're inherited and can be overridden by child classes. You may easily add your own conditions as the example below demonstrates.
80
84
 
81
85
  ```ruby
82
86
  condition[:has_permission] = proc { |v|
@@ -32,4 +32,4 @@ A route may _pass_ a request to the next matching route. _passing_ is very simil
32
32
 
33
33
  Redirections
34
34
  ------------
35
- A common requirement of many applications is to redirect requests to another URL based on some kind of condition. Scorched offers the very simple `redirect` method which takes one argument - the URL to redirect to. Like `halt` it's mostly a convenience method. It sets the _Location_ header of the response before halting the request.
35
+ A common requirement of many applications is to redirect requests to another URL based on some kind of condition. Scorched offers the very simple `redirect` method which takes one required argument - the URL to redirect to - and an optional response status, which defaults to 307 (temporary redirect). `redirect` is mostly a convenience method. It sets the _Location_ header of the response before halting the request.
@@ -13,6 +13,7 @@ require_relative 'scorched/static'
13
13
  require_relative 'scorched/dynamic_delegate'
14
14
  require_relative 'scorched/options'
15
15
  require_relative 'scorched/collection'
16
+ require_relative 'scorched/match'
16
17
  require_relative 'scorched/controller'
17
18
  require_relative 'scorched/error'
18
19
  require_relative 'scorched/request'
@@ -7,7 +7,10 @@ module Scorched
7
7
  [:<<, :add, :add?, :clear, :delete, :delete?, :delete_if, :merge, :replace, :subtract].include? m
8
8
  }
9
9
 
10
- # sets parent Collection object and returns self
10
+ # If true, parent values are appended to self. The default behavior is to append self to the parent values.
11
+ attr_accessor :append_parent
12
+
13
+ # Sets parent Collection object and returns self
11
14
  def parent!(parent)
12
15
  @parent = parent
13
16
  self
@@ -15,8 +18,11 @@ module Scorched
15
18
 
16
19
  def to_set(inherit = true)
17
20
  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)
21
+ if append_parent
22
+ Set.new.merge(self._to_a).merge(@parent.to_set)
23
+ else
24
+ Set.new.merge(@parent.to_set).merge(self._to_a)
25
+ end
20
26
  else
21
27
  Set.new.merge(self._to_a)
22
28
  end
@@ -27,12 +33,12 @@ module Scorched
27
33
  end
28
34
 
29
35
  def inspect
30
- "#<#{self.class}: #{_inspect}, #{to_set.inspect}>"
36
+ "#<#{self.class}(#{super}, #{to_set.inspect})>"
31
37
  end
32
38
  end
33
39
 
34
40
  class << self
35
- def Collection(accessor_name)
41
+ def Collection(accessor_name, append_parent = false)
36
42
  m = Module.new
37
43
  m.class_eval <<-MOD
38
44
  class << self
@@ -46,6 +52,8 @@ module Scorched
46
52
  @#{accessor_name} || begin
47
53
  parent = superclass.#{accessor_name} if superclass.respond_to?(:#{accessor_name}) && Scorched::Collection === superclass.#{accessor_name}
48
54
  @#{accessor_name} = Collection.new.parent!(parent)
55
+ @#{accessor_name}.append_parent = #{append_parent.inspect}
56
+ @#{accessor_name}
49
57
  end
50
58
  end
51
59
  end
@@ -7,16 +7,18 @@ module Scorched
7
7
  include Scorched::Options('conditions')
8
8
  include Scorched::Collection('middleware')
9
9
  include Scorched::Collection('before_filters')
10
- include Scorched::Collection('after_filters')
10
+ include Scorched::Collection('after_filters', true)
11
11
  include Scorched::Collection('error_filters')
12
12
 
13
13
  config << {
14
- :strip_trailing_slash => :redirect, # :redirect => Strips and redirects URL ending in forward slash, :ignore => internally ignores trailing slash, false => does nothing.
15
- :static_dir => false, # The directory Scorched should serve static files from. Set to false if web server or anything else is serving static files.
14
+ :auto_pass => false, # Automatically _pass_ request back to outer controller if no route matches.
15
+ :cache_templates => true,
16
16
  :logger => nil,
17
17
  :show_exceptions => false,
18
- :auto_pass => false, # Automatically _pass_ request back to outer controller if no route matches.
19
- :cache_templates => true
18
+ :show_http_error_pages => false, # If true, shows the default Scorched HTTP error page.
19
+ :static_dir => false, # The directory Scorched should serve static files from. Set to false if web server or anything else is serving static files.
20
+ :strip_trailing_slash => :redirect, # :redirect => Strips and redirects URL ending in forward slash, :ignore => internally ignores trailing slash, false => does nothing.
21
+
20
22
  }
21
23
 
22
24
  render_defaults << {
@@ -32,15 +34,24 @@ module Scorched
32
34
  config[:show_exceptions] = true
33
35
  config[:static_dir] = 'public'
34
36
  config[:cache_templates] = false
37
+ config[:show_http_error_pages] = true
35
38
  end
36
39
 
37
40
  conditions << {
38
41
  charset: proc { |charsets|
39
42
  [*charsets].any? { |charset| request.env['rack-accept.request'].charset? charset }
40
43
  },
44
+ config: proc { |map|
45
+ map.all? { |k,v| config[k] == v }
46
+ },
41
47
  encoding: proc { |encodings|
42
48
  [*encodings].any? { |encoding| request.env['rack-accept.request'].encoding? encoding }
43
49
  },
50
+ failed_condition: proc { |conditions|
51
+ if !matches.empty? && matches.all? { |m| m.failed_condition }
52
+ [*conditions].include? matches.first.failed_condition[0]
53
+ end
54
+ },
44
55
  host: proc { |host|
45
56
  (Regexp === host) ? host =~ request.host : host == request.host
46
57
  },
@@ -50,8 +61,14 @@ module Scorched
50
61
  media_type: proc { |types|
51
62
  [*types].any? { |type| request.env['rack-accept.request'].media_type? type }
52
63
  },
53
- methods: proc { |accepts|
54
- [*accepts].include?(request.request_method)
64
+ method: proc { |methods|
65
+ [*methods].include?(request.request_method)
66
+ },
67
+ matched: proc { |bool|
68
+ @_matched == bool
69
+ },
70
+ proc: proc { |*blocks|
71
+ [*blocks].all? { |b| instance_exec(&b) }
55
72
  },
56
73
  user_agent: proc { |user_agent|
57
74
  (Regexp === user_agent) ? user_agent =~ request.user_agent : user_agent == request.user_agent
@@ -146,7 +163,7 @@ module Scorched
146
163
  ['get', 'post', 'put', 'delete', 'head', 'options', 'patch'].each do |method|
147
164
  methods = (method == 'get') ? ['GET', 'HEAD'] : [method.upcase]
148
165
  define_method(method) do |*args, **conds, &block|
149
- conds.merge!(methods: methods)
166
+ conds.merge!(method: methods)
150
167
  route(*args, **conds, &block)
151
168
  end
152
169
  end
@@ -187,6 +204,10 @@ module Scorched
187
204
  end
188
205
  end
189
206
 
207
+ after(failed_condition: :host) { response.status = 404 }
208
+ after(failed_condition: :method) { response.status = 405 }
209
+ after(failed_condition: %i{charset encoding language media_type}) { response.status = 406 }
210
+
190
211
  def method_missing(method, *args, &block)
191
212
  (self.class.respond_to? method) ? self.class.__send__(method, *args, &block) : super
192
213
  end
@@ -203,7 +224,9 @@ module Scorched
203
224
  inner_error = nil
204
225
  rescue_block = proc do |e|
205
226
  raise unless filters[:error].any? do |f|
206
- (f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }) && check_conditions?(f[:conditions]) && instance_exec(e, &f[:proc])
227
+ (f[:args].empty? || f[:args].any? { |type| e.is_a?(type) }) &&
228
+ !check_for_failed_condition(f[:conditions]) &&
229
+ instance_exec(e, &f[:proc])
207
230
  end
208
231
  end
209
232
 
@@ -213,22 +236,22 @@ module Scorched
213
236
  redirect(request.path.chomp('/'))
214
237
  end
215
238
 
216
- all_matches = matches
217
- if all_matches.empty?
239
+ if matches.all? { |m| m.failed_condition }
218
240
  pass if config[:auto_pass]
219
- response.status = 404
241
+ response.status = matches.empty? ? 404 : 403
220
242
  end
221
243
 
222
244
  run_filters(:before)
223
245
  begin
224
- all_matches.each do |match|
246
+ @_matched = true == matches.each { |match|
247
+ next if match.failed_condition
225
248
  request.breadcrumb << match
226
- processed = catch(:pass) {
227
- target = match[:mapping][:target]
228
- response.merge! (Proc === target) ? instance_exec(request.env, &target) : target.call(request.env)
249
+ break if catch(:pass) {
250
+ target = match.mapping[:target]
251
+ response.merge! (Proc === target) ? instance_exec(env, &target) : target.call(env)
229
252
  }
230
- processed ? break : request.breadcrumb.pop
231
- end
253
+ request.breadcrumb.pop
254
+ }
232
255
  rescue => inner_error
233
256
  rescue_block.call(inner_error)
234
257
  end
@@ -240,44 +263,37 @@ module Scorched
240
263
  response
241
264
  end
242
265
 
243
- def match?
244
- !matches(true).empty?
245
- end
246
-
247
- # Finds mappings that match the currently unmatched portion of the request path, returning an array of all matches.
248
- # If _short_circuit_ is set to true, it stops matching at the first positive match, returning only a single match.
249
- def matches(short_circuit = false)
266
+ # Finds mappings that match the unmatched portion of the request path, returning an array of `Match` objects, or an
267
+ # empty array if no matches were found.
268
+ #
269
+ # The `:eligable` attribute of the `Match` object indicates whether the conditions for that mapping passed.
270
+ # The result is cached for the life time of the controller instance, for the sake of effecient-recalling.
271
+ def matches
272
+ return @matches if @matches
250
273
  to_match = request.unmatched_path
251
274
  to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
252
- matches = []
253
- mappings.each do |m|
254
- m[:pattern].match(to_match) do |match_data|
275
+ @matches = mappings.map { |mapping|
276
+ mapping[:pattern].match(to_match) do |match_data|
255
277
  if match_data.pre_match == ''
256
- if check_conditions?(m[:conditions])
257
- if match_data.names.empty?
258
- captures = match_data.captures
259
- else
260
- captures = Hash[match_data.names.map{|v| v.to_sym}.zip match_data.captures]
261
- end
262
- matches << {mapping: m, captures: captures, path: match_data.to_s}
263
- break if short_circuit
278
+ if match_data.names.empty?
279
+ captures = match_data.captures
280
+ else
281
+ captures = Hash[match_data.names.map{|v| v.to_sym}.zip match_data.captures]
264
282
  end
283
+ Match.new(mapping, captures, match_data.to_s, check_for_failed_condition(mapping[:conditions]))
265
284
  end
266
285
  end
267
- end
268
- matches
286
+ }.compact
269
287
  end
270
288
 
271
- def check_conditions?(conds)
272
- if !conds
273
- true
274
- else
275
- conds.all? { |c,v| check_condition?(c, v) }
276
- end
289
+ # Tests the given conditions, returning the name of the first failed condition, or nil otherwise.
290
+ def check_for_failed_condition(conds)
291
+ (conds || []).find { |c,v| check_condition?(c, v) ? false : c }
277
292
  end
278
293
 
294
+ # Test the given condition, returning true if the condition passes, or false otherwise.
279
295
  def check_condition?(c, v)
280
- raise Error, "The condition `#{c}` either does not exist, or is not a Proc object" unless Proc === self.conditions[c]
296
+ raise Error, "The condition `#{c}` either does not exist, or is not an instance of Proc" unless Proc === self.conditions[c]
281
297
  instance_exec(v, &self.conditions[c])
282
298
  end
283
299
 
@@ -330,7 +346,7 @@ module Scorched
330
346
  env['scorched.flash'].each { |k,v| session[k] = v } if session && env['scorched.flash']
331
347
  end
332
348
 
333
- # Serves as a thin layer of convenience to Rack's built-in methods: Request#cookies, Response#set_cookie, and
349
+ # Serves as a thin layer of convenience to Rack's built-in method: Request#cookies, Response#set_cookie, and
334
350
  # Response#delete_cookie.
335
351
  #
336
352
  # If only one argument is given, the specified cookie is retreived and returned.
@@ -431,35 +447,33 @@ module Scorched
431
447
  return_path[0] == '/' ? return_path : return_path.insert(0, '/')
432
448
  end
433
449
 
434
- if ENV['RACK_ENV'] == 'development' &&
435
- after do
436
- if response.empty?
437
- response.body = <<-HTML
438
- <!DOCTYPE html>
439
- <html>
440
- <head>
441
- <style type="text/css">
442
- @import url(http://fonts.googleapis.com/css?family=Titillium+Web|Open+Sans:300italic,400italic,700italic,400,700,300);
443
- html, body { height: 100%; width: 100%; margin: 0; font-family: 'Open Sans', 'Lucida Sans', 'Arial'; }
444
- body { color: #333; display: table; }
445
- #container { display: table-cell; vertical-align: middle; text-align: center; }
446
- #container > * { display: inline-block; text-align: center; vertical-align: middle; }
447
- #logo {
448
- padding: 12px 24px 12px 120px; color: white; background: rgb(191, 64, 0);
449
- font-family: 'Titillium Web', 'Lucida Sans', 'Arial'; font-size: 36pt; text-decoration: none;
450
- }
451
- h1 { margin-left: 18px; font-weight: 400; }
452
- </style>
453
- </head>
454
- <body>
455
- <div id="container">
456
- <a id="logo" href="http://scorchedrb.com">Scorched</a>
457
- <h1>404 Page Not Found</h1>
458
- </div>
459
- </body>
460
- </html>
461
- HTML
462
- end
450
+ after config: {show_http_error_pages: true}, status: 400..599 do
451
+ if response.empty?
452
+ response.body = <<-HTML
453
+ <!DOCTYPE html>
454
+ <html>
455
+ <head>
456
+ <style type="text/css">
457
+ @import url(http://fonts.googleapis.com/css?family=Titillium+Web|Open+Sans:300italic,400italic,700italic,400,700,300);
458
+ html, body { height: 100%; width: 100%; margin: 0; font-family: 'Open Sans', 'Lucida Sans', 'Arial'; }
459
+ body { color: #333; display: table; }
460
+ #container { display: table-cell; vertical-align: middle; text-align: center; }
461
+ #container > * { display: inline-block; text-align: center; vertical-align: middle; }
462
+ #logo {
463
+ padding: 12px 24px 12px 120px; color: white; background: rgb(191, 64, 0);
464
+ font-family: 'Titillium Web', 'Lucida Sans', 'Arial'; font-size: 36pt; text-decoration: none;
465
+ }
466
+ h1 { margin-left: 18px; font-weight: 400; }
467
+ </style>
468
+ </head>
469
+ <body>
470
+ <div id="container">
471
+ <a id="logo" href="http://scorchedrb.com">Scorched</a>
472
+ <h1>#{response.status} #{Rack::Utils::HTTP_STATUS_CODES[response.status]}</h1>
473
+ </div>
474
+ </body>
475
+ </html>
476
+ HTML
463
477
  end
464
478
  end
465
479
 
@@ -467,9 +481,9 @@ module Scorched
467
481
  private
468
482
 
469
483
  def run_filters(type)
470
- tracker = env['scorched.filters'] ||= {before: Set.new, after: Set.new}
484
+ tracker = env['scorched.executed_filters'] ||= {before: Set.new, after: Set.new}
471
485
  filters[type].reject{ |f| tracker[type].include? f }.each do |f|
472
- if check_conditions?(f[:conditions])
486
+ unless check_for_failed_condition(f[:conditions])
473
487
  tracker[type] << f
474
488
  instance_exec(&f[:proc])
475
489
  end
File without changes
File without changes
@@ -0,0 +1,3 @@
1
+ module Scorched
2
+ Match = Struct.new(:mapping, :captures, :path, :failed_condition)
3
+ end
@@ -8,17 +8,17 @@ module Scorched
8
8
 
9
9
  # Returns a hash of captured strings from the last matched URL in the breadcrumb.
10
10
  def captures
11
- breadcrumb.last ? breadcrumb.last[:captures] : []
11
+ breadcrumb.last ? breadcrumb.last.captures : []
12
12
  end
13
13
 
14
14
  # Returns an array of capture arrays; one for each mapping that's been hit during the request processing so far.
15
15
  def all_captures
16
- breadcrumb.map { |v| v[:captures] }
16
+ breadcrumb.map { |match| match.captures }
17
17
  end
18
18
 
19
19
  # The portion of the path that's currently been matched by one or more mappings.
20
20
  def matched_path
21
- join_paths(breadcrumb.map{|v| v[:path]})
21
+ join_paths(breadcrumb.map{ |match| match.path })
22
22
  end
23
23
 
24
24
  # The remaining portion of the path that has yet to be matched by any mappings.
@@ -16,8 +16,10 @@ module Scorched
16
16
  end
17
17
 
18
18
  # Automatically wraps the assigned value in an array if it doesn't respond to ``each``.
19
+ # Also filters out non-true values and empty strings.
19
20
  def body=(value)
20
- super(value.respond_to?(:each) ? value : [value].compact)
21
+ value = [] if !value || value == ''
22
+ super(value.respond_to?(:each) ? value : [value.to_s])
21
23
  end
22
24
 
23
25
  def finish(*args, &block)
File without changes
@@ -1,3 +1,3 @@
1
1
  module Scorched
2
- VERSION = '0.9'
2
+ VERSION = '0.10'
3
3
  end
@@ -1,7 +1,7 @@
1
1
  require_relative './helper.rb'
2
2
 
3
3
  class CollectionA
4
- include Scorched::Collection('middleware')
4
+ include Scorched::Collection('things')
5
5
  end
6
6
 
7
7
  class CollectionB < CollectionA
@@ -12,35 +12,56 @@ end
12
12
 
13
13
  module Scorched
14
14
  describe Collection do
15
+ before(:each) do
16
+ CollectionA.things.clear
17
+ CollectionB.things.clear
18
+ CollectionC.things.clear
19
+ end
20
+
15
21
  it "defaults to an empty set" do
16
- CollectionA.middleware.should == Set.new
22
+ CollectionA.things.should == Set.new
17
23
  end
18
24
 
19
25
  it "can be set to a given set" do
20
26
  my_set = Set.new(['horse', 'cat', 'dog'])
21
- CollectionA.middleware.replace my_set
22
- CollectionA.middleware.should == my_set
27
+ CollectionA.things.replace my_set
28
+ CollectionA.things.should == my_set
23
29
  end
24
30
 
25
31
  it "automatically converts arrays to sets" do
26
- array = ['horse', 'cat', 'dog']
27
- CollectionA.middleware.replace array
28
- CollectionA.middleware.should == array.to_set
32
+ array = ['small', 'medium', 'large']
33
+ CollectionA.things.replace array
34
+ CollectionA.things.should == array.to_set
29
35
  end
30
36
 
31
37
  it "recursively inherits from parents by default" do
32
- CollectionB.middleware.should == CollectionA.middleware
33
- CollectionC.middleware.should == CollectionA.middleware
38
+ CollectionB.things.should == CollectionA.things
39
+ CollectionC.things.should == CollectionA.things
34
40
  end
35
41
 
36
42
  it "allows values to be overridden without modifying the parent" do
37
- CollectionB.middleware << 'rabbit'
38
- CollectionB.middleware.should include('rabbit')
39
- CollectionA.middleware.should_not include('rabbit')
43
+ CollectionB.things << 'rabbit'
44
+ CollectionB.things.should include('rabbit')
45
+ CollectionA.things.should_not include('rabbit')
46
+ end
47
+
48
+ it "prepends parent values by default" do
49
+ CollectionA.things.replace %w{car house}
50
+ CollectionB.things.replace %w{dog cat}
51
+ CollectionB.things.to_a.should == %w{car house dog cat}
52
+ end
53
+
54
+ it "can be set to append parent values" do
55
+ CollectionB.things.append_parent = true
56
+ CollectionA.things.replace %w{car house}
57
+ CollectionB.things.replace %w{dog cat}
58
+ CollectionB.things.to_a.should == %w{dog cat car house}
40
59
  end
41
60
 
42
61
  it "provides access to a copy of internal set" do
43
- CollectionB.middleware.to_set(false).should == Set.new(['rabbit'])
62
+ CollectionA.things << 'monkey'
63
+ CollectionB.things << 'rabbit'
64
+ CollectionB.things.to_set(false).should == Set.new(['rabbit'])
44
65
  end
45
66
  end
46
67
  end
@@ -14,7 +14,7 @@ module Scorched
14
14
  it "contains a set of default conditions" do
15
15
  app.conditions.should be_a(Options)
16
16
  app.conditions.length.should > 0
17
- app.conditions[:methods].should be_a(Proc)
17
+ app.conditions[:method].should be_a(Proc)
18
18
  end
19
19
 
20
20
  describe "basic route handling" do
@@ -143,21 +143,21 @@ module Scorched
143
143
  describe "conditions" do
144
144
  it "contains a default set of conditions" do
145
145
  app.conditions.should be_a(Options)
146
- app.conditions.should include(:methods, :media_type)
146
+ app.conditions.should include(:method, :media_type)
147
147
  app.conditions.each { |k,v| v.should be_a(Proc) }
148
148
  end
149
149
 
150
150
  it "executes route only if all conditions return true" do
151
- app << {pattern: '/', conditions: {methods: 'POST'}, target: generic_handler}
151
+ app << {pattern: '/', conditions: {method: 'POST'}, target: generic_handler}
152
152
  response = rt.get "/"
153
- response.status.should == 404
153
+ response.status.should be_between(400, 499)
154
154
  response = rt.post "/"
155
155
  response.status.should == 200
156
156
 
157
157
  app.conditions[:has_name] = proc { |name| request.GET['name'] }
158
- app << {pattern: '/about', conditions: {methods: ['GET', 'POST'], has_name: 'Ronald'}, target: generic_handler}
158
+ app << {pattern: '/about', conditions: {method: ['GET', 'POST'], has_name: 'Ronald'}, target: generic_handler}
159
159
  response = rt.get "/about"
160
- response.status.should == 404
160
+ response.status.should be_between(400, 499)
161
161
  response = rt.get "/about", name: 'Ronald'
162
162
  response.status.should == 200
163
163
  end
@@ -170,8 +170,8 @@ module Scorched
170
170
  end
171
171
 
172
172
  it "falls through to next route when conditions are not met" do
173
- app << {pattern: '/', conditions: {methods: 'POST'}, target: proc { |env| [200, {}, ['post']] }}
174
- app << {pattern: '/', conditions: {methods: 'GET'}, target: proc { |env| [200, {}, ['get']] }}
173
+ app << {pattern: '/', conditions: {method: 'POST'}, target: proc { |env| [200, {}, ['post']] }}
174
+ app << {pattern: '/', conditions: {method: 'GET'}, target: proc { |env| [200, {}, ['get']] }}
175
175
  rt.get("/").body.should == 'get'
176
176
  rt.post("/").body.should == 'post'
177
177
  end
@@ -179,9 +179,9 @@ module Scorched
179
179
 
180
180
  describe "route helpers" do
181
181
  it "allows end points to be defined more succinctly" do
182
- route_proc = app.route('/*', 2, methods: 'GET') { |capture| capture }
182
+ route_proc = app.route('/*', 2, method: 'GET') { |capture| capture }
183
183
  mapping = app.mappings.first
184
- mapping.should == {pattern: mapping[:pattern], priority: 2, conditions: {methods: 'GET'}, target: route_proc}
184
+ mapping.should == {pattern: mapping[:pattern], priority: 2, conditions: {method: 'GET'}, target: route_proc}
185
185
  rt.get('/about').body.should == 'about'
186
186
  end
187
187
 
@@ -249,11 +249,11 @@ module Scorched
249
249
  end
250
250
 
251
251
  it "can take mapping options" do
252
- app.controller priority: -1, conditions: {methods: 'POST'} do
252
+ app.controller priority: -1, conditions: {method: 'POST'} do
253
253
  route('/') { 'ok' }
254
254
  end
255
255
  app.mappings.first[:priority].should == -1
256
- rt.get('/').status.should == 404
256
+ rt.get('/').status.should be_between(400, 499)
257
257
  rt.post('/').body.should == 'ok'
258
258
  end
259
259
 
@@ -303,14 +303,24 @@ module Scorched
303
303
 
304
304
  they "can take an optional set of conditions" do
305
305
  counter = 0
306
- app.before(methods: ['GET', 'PUT']) { counter += 1 }
307
- app.after(methods: ['GET', 'PUT']) { counter += 1 }
306
+ app.before(method: ['GET', 'PUT']) { counter += 1 }
307
+ app.after(method: ['GET', 'PUT']) { counter += 1 }
308
308
  rt.post('/')
309
309
  rt.get('/')
310
310
  rt.put('/')
311
311
  counter.should == 4
312
312
  end
313
313
 
314
+ they "execute in the order they're defined" do
315
+ order = []
316
+ app.before { order << :first }
317
+ app.before { order << :second }
318
+ app.after { order << :third }
319
+ app.after { order << :fourth }
320
+ rt.get('/')
321
+ order.should == %i{first second third fourth}
322
+ end
323
+
314
324
  describe "nesting" do
315
325
  example "filters inherit but only run once" do
316
326
  before_counter, after_counter = 0, 0
@@ -331,26 +341,48 @@ module Scorched
331
341
  after_counter.should == 1
332
342
  end
333
343
 
334
- example "before filters run from outermost to inner" do
344
+ example "before filters run from outermost to innermost" do
335
345
  order = []
336
346
  app.before { order << :outer }
347
+ app.before { order << :outer2 }
337
348
  app.controller do
338
349
  before { order << :inner }
350
+ before { order << :inner2 }
339
351
  get('/') { }
340
352
  end
341
353
  rt.get('/')
342
- order.should == [:outer, :inner]
354
+ order.should == %i{outer outer2 inner inner2}
343
355
  end
344
356
 
345
357
  example "after filters run from innermost to outermost" do
346
358
  order = []
347
359
  app.after { order << :outer }
360
+ app.after { order << :outer2 }
348
361
  app.controller do
349
362
  get('/') { }
350
363
  after { order << :inner }
364
+ after { order << :inner2 }
351
365
  end
352
366
  rt.get('/')
353
- order.should == [:inner, :outer]
367
+ order.should == %i{inner inner2 outer outer2}
368
+ end
369
+
370
+ example "inherited filters which fail to satisfy their conditions are re-evaluated at every level" do
371
+ order = []
372
+ sub_class = app.controller do
373
+ before { order << :third }
374
+ get('/hello') { }
375
+ end
376
+ app.before(status: 500) do
377
+ order << :second
378
+ self.class.should == sub_class
379
+ end
380
+ app.before do
381
+ order << :first
382
+ response.status = 500
383
+ end
384
+ rt.get('/hello')
385
+ order.should == %i{first second third}
354
386
  end
355
387
  end
356
388
  end
@@ -432,7 +464,7 @@ module Scorched
432
464
  end
433
465
 
434
466
  they "can take an optional set of conditions" do
435
- app.error(methods: ['GET', 'PUT']) { true }
467
+ app.error(method: ['GET', 'PUT']) { true }
436
468
  expect {
437
469
  rt.post('/')
438
470
  }.to raise_error(StandardError)
@@ -585,6 +617,26 @@ module Scorched
585
617
  end
586
618
  end
587
619
 
620
+ describe :show_http_error_pages do
621
+ it "shows HTTP error pages for errors 400 to 599" do
622
+ app.config[:show_http_error_pages] = true
623
+ app.get('/') { response.status = 501; '' }
624
+ app.get('/unknown') { response.status = 480; nil }
625
+ rt.get('/').body.should include('501 Not Implemented')
626
+ rt.post('/').body.should include('405 Method Not Allowed')
627
+ rt.get('/unknown').body.should include('480 ')
628
+ end
629
+
630
+ it "can be disabled" do
631
+ app.config[:show_http_error_pages] = false
632
+ app.get('/') { response.status = 501; '' }
633
+ app.get('/unknown') { response.status = 480; nil }
634
+ rt.get('/').body.should_not include('501 Not Implemented')
635
+ rt.post('/').body.should_not include('405 Method Not Allowed')
636
+ rt.post('/unknown').body.should_not include('408 ')
637
+ end
638
+ end
639
+
588
640
  describe :auto_pass do
589
641
  it "passes to the outer controller without running any filters, if no match" do
590
642
  sub = Class.new(Scorched::Controller) do
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scorched
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.9'
4
+ version: '0.10'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tom Wardrop
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-03-28 00:00:00.000000000 Z
11
+ date: 2013-03-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -112,6 +112,7 @@ files:
112
112
  - lib/scorched/controller.rb
113
113
  - lib/scorched/dynamic_delegate.rb
114
114
  - lib/scorched/error.rb
115
+ - lib/scorched/match.rb
115
116
  - lib/scorched/options.rb
116
117
  - lib/scorched/request.rb
117
118
  - lib/scorched/response.rb
@@ -162,3 +163,4 @@ test_files:
162
163
  - spec/controller_spec.rb
163
164
  - spec/options_spec.rb
164
165
  - spec/request_spec.rb
166
+ has_rdoc: