scorched 0.9 → 0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +0 -0
- data/LICENSE +0 -0
- data/Milestones.md +21 -2
- data/README.md +1 -1
- data/docs/02_fundamentals/01_the_controller.md +3 -3
- data/docs/02_fundamentals/02_configuration.md +9 -7
- data/docs/02_fundamentals/03_routing.md +9 -5
- data/docs/02_fundamentals/04_requests_and_responses.md +1 -1
- data/lib/scorched.rb +1 -0
- data/lib/scorched/collection.rb +13 -5
- data/lib/scorched/controller.rb +91 -77
- data/lib/scorched/dynamic_delegate.rb +0 -0
- data/lib/scorched/error.rb +0 -0
- data/lib/scorched/match.rb +3 -0
- data/lib/scorched/request.rb +3 -3
- data/lib/scorched/response.rb +3 -1
- data/lib/scorched/static.rb +0 -0
- data/lib/scorched/version.rb +1 -1
- data/spec/collection_spec.rb +34 -13
- data/spec/controller_spec.rb +70 -18
- data/spec/options_spec.rb +0 -0
- data/spec/public/static.txt +0 -0
- data/spec/request_spec.rb +0 -0
- data/spec/views/composer.erb +0 -0
- data/spec/views/layout.erb +0 -0
- data/spec/views/main.erb +0 -0
- data/spec/views/other.str +0 -0
- data/spec/views/partial.erb +0 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 08889289c8e64bbfd860c495a1e0319c5c1c9310
|
4
|
+
data.tar.gz: 7c203bcdc50f2070e157302e6daa8b442c8a6694
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5aaa29d4c05e8bc4b4a2e11381c6a6030f91cbe0e3d2d7d312eabceabcae3447d742485852bfc115938526e21d32fa1399777737eaae9e71a40fe1f2879e8f11
|
7
|
+
data.tar.gz: 17c71a09fb5cf953a3dd96b880be3d68332622002ca3dfe9c1393b72e3141c63ea1fb4009bb0044f18a923c6f5e95657781a4514e50745f2735895f830fff57f
|
data/Gemfile
CHANGED
File without changes
|
data/LICENSE
CHANGED
File without changes
|
data/Milestones.md
CHANGED
@@ -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`
|
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
|
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,
|
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 <
|
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
|
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: {
|
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,
|
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 `:
|
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
|
-
* `:
|
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
|
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.
|
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.
|
data/lib/scorched.rb
CHANGED
@@ -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'
|
data/lib/scorched/collection.rb
CHANGED
@@ -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
|
-
#
|
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
|
-
|
19
|
-
|
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}
|
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
|
data/lib/scorched/controller.rb
CHANGED
@@ -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
|
-
:
|
15
|
-
:
|
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
|
-
:
|
19
|
-
:
|
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
|
-
|
54
|
-
[*
|
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!(
|
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) }) &&
|
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
|
-
|
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
|
-
|
246
|
+
@_matched = true == matches.each { |match|
|
247
|
+
next if match.failed_condition
|
225
248
|
request.breadcrumb << match
|
226
|
-
|
227
|
-
target = match
|
228
|
-
response.merge! (Proc === target) ? instance_exec(
|
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
|
-
|
231
|
-
|
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
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
#
|
248
|
-
|
249
|
-
|
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
|
-
|
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
|
257
|
-
|
258
|
-
|
259
|
-
|
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
|
-
|
268
|
-
matches
|
286
|
+
}.compact
|
269
287
|
end
|
270
288
|
|
271
|
-
|
272
|
-
|
273
|
-
|
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
|
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
|
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
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
<
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
<
|
455
|
-
<
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
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.
|
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
|
-
|
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
|
data/lib/scorched/error.rb
CHANGED
File without changes
|
data/lib/scorched/request.rb
CHANGED
@@ -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
|
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 { |
|
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{|
|
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.
|
data/lib/scorched/response.rb
CHANGED
@@ -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
|
-
|
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)
|
data/lib/scorched/static.rb
CHANGED
File without changes
|
data/lib/scorched/version.rb
CHANGED
data/spec/collection_spec.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require_relative './helper.rb'
|
2
2
|
|
3
3
|
class CollectionA
|
4
|
-
include Scorched::Collection('
|
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.
|
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.
|
22
|
-
CollectionA.
|
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 = ['
|
27
|
-
CollectionA.
|
28
|
-
CollectionA.
|
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.
|
33
|
-
CollectionC.
|
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.
|
38
|
-
CollectionB.
|
39
|
-
CollectionA.
|
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
|
-
|
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
|
data/spec/controller_spec.rb
CHANGED
@@ -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[:
|
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(:
|
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: {
|
151
|
+
app << {pattern: '/', conditions: {method: 'POST'}, target: generic_handler}
|
152
152
|
response = rt.get "/"
|
153
|
-
response.status.should
|
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: {
|
158
|
+
app << {pattern: '/about', conditions: {method: ['GET', 'POST'], has_name: 'Ronald'}, target: generic_handler}
|
159
159
|
response = rt.get "/about"
|
160
|
-
response.status.should
|
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: {
|
174
|
-
app << {pattern: '/', conditions: {
|
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,
|
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: {
|
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: {
|
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
|
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(
|
307
|
-
app.after(
|
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
|
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 ==
|
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 ==
|
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(
|
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
|
data/spec/options_spec.rb
CHANGED
File without changes
|
data/spec/public/static.txt
CHANGED
File without changes
|
data/spec/request_spec.rb
CHANGED
File without changes
|
data/spec/views/composer.erb
CHANGED
File without changes
|
data/spec/views/layout.erb
CHANGED
File without changes
|
data/spec/views/main.erb
CHANGED
File without changes
|
data/spec/views/other.str
CHANGED
File without changes
|
data/spec/views/partial.erb
CHANGED
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.
|
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-
|
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:
|