scorched 0.5.1 → 0.5.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Milestones.md +3 -0
- data/README.md +41 -23
- data/docs/01_preface.md +6 -0
- data/docs/02_fundamentals/01_the_controller.md +19 -0
- data/docs/02_fundamentals/02_configuration.md +29 -0
- data/docs/02_fundamentals/03_routing.md +92 -0
- data/docs/02_fundamentals/04_requests_and_responses.md +30 -0
- data/docs/02_fundamentals/05_filters.md +45 -0
- data/docs/02_fundamentals/06_middleware.md +18 -0
- data/docs/02_fundamentals/07_request_and_session_data.md +89 -0
- data/docs/02_fundamentals/08_views.md +4 -0
- data/docs/02_fundamentals/09_sharing_request_state.md +16 -0
- data/docs/03_further_reading/be_creative.md +45 -0
- data/examples/file_upload.ru +44 -0
- data/examples/media_types.ru +25 -1
- data/lib/scorched/controller.rb +23 -24
- data/lib/scorched/request.rb +7 -3
- data/lib/scorched/response.rb +13 -0
- data/lib/scorched/version.rb +1 -1
- data/spec/controller_spec.rb +62 -39
- metadata +14 -7
- data/docs/be_creative.md +0 -32
- data/docs/filters.md +0 -8
- data/docs/routing.md +0 -29
- data/docs/sharing_request_state.md +0 -5
- data/examples/media_types.rb +0 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b0950b3928b9921a50ec7af0cb01869412172f09
|
4
|
+
data.tar.gz: b698623e4b6e40a64cf56ac47b7d20be50df4c2f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e528fe6a63bfdd08127ff405e83541a5a6032fa6db1746be18c50a0e9488de6eba591b720fb218fda4378e42b244eba4246be71fb4bc8c384921cdd9c2c2bca8
|
7
|
+
data.tar.gz: 00f71b40921a073e043a985e693e8a2924c93f16a4221a2f5efcf72588d90515846c67151302cd931d1af7fe112884c17f2424c068bbd82e83170de954f1ce12
|
data/Milestones.md
CHANGED
@@ -3,6 +3,9 @@ Milestones
|
|
3
3
|
|
4
4
|
Changelog
|
5
5
|
---------
|
6
|
+
### v0.5.2
|
7
|
+
* Response content-type now defaults to "text/html;charset=utf-8", rather than empty.
|
8
|
+
|
6
9
|
### v0.5.1
|
7
10
|
* Added URL helpers, #absolute and #url
|
8
11
|
* Render helper now loads files itself as Tilt still has issues with UTF-8 files.
|
data/README.md
CHANGED
@@ -1,20 +1,39 @@
|
|
1
|
-
Scorched
|
2
|
-
|
1
|
+
[Simple, Powerful, Scorched](http://scorchedrb.com)
|
2
|
+
==========================
|
3
3
|
|
4
|
-
|
4
|
+
Scorched is a generic, unopinionated, DRY, light-weight web framework for Ruby. It provides a generic yet powerful set of constructs for processing HTTP requests, with which websites and applications of almost any scale can be built.
|
5
5
|
|
6
|
-
|
6
|
+
If you've used a light-weight DSL-based Ruby web framework before, such as a Sinatra, it should look familiar. Scorched is a true evolutionary enhancement of Sinatra, with more power, focus, and less clutter.
|
7
7
|
|
8
|
-
|
8
|
+
Getting Started
|
9
|
+
---------------
|
9
10
|
|
11
|
+
Install the canister...
|
10
12
|
|
11
|
-
|
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.
|
13
|
+
gem install scorched
|
16
14
|
|
17
|
-
|
15
|
+
Open the valve...
|
16
|
+
|
17
|
+
# ruby
|
18
|
+
# hello_world.ru
|
19
|
+
require 'scorched'
|
20
|
+
class App < Scorched::Controller
|
21
|
+
get '/' do
|
22
|
+
'hello world'
|
23
|
+
end
|
24
|
+
end
|
25
|
+
run App
|
26
|
+
|
27
|
+
And light the flame...
|
28
|
+
|
29
|
+
rackup hello_world.ru
|
30
|
+
|
31
|
+
|
32
|
+
The Errors of Our Past
|
33
|
+
----------------------
|
34
|
+
One of the big mistakes made by a lot of other Ruby frameworks, was to not leverage the power of the class. The consequences of this made for some awkwardness. Helpers for example, are a classical reinvention of what 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. The decision to allow developers to implement helpers and other common functionality as proper methods not only makes Controllers somewhat more predictable and familiar, but of course allow such helpers to be inheritable via plain-old Class inheritance.
|
35
|
+
|
36
|
+
Perhaps another design oversight of other frameworks, has been the 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 configuration, filters, route conditions, etc, applied along the way. This can assist many areas of web development, including security, restful interfaces, and interchangeable content types.
|
18
37
|
|
19
38
|
|
20
39
|
Design Philosophy
|
@@ -33,28 +52,28 @@ Part of what keeps Scorched lightweight, is that unlike other lightweight web fr
|
|
33
52
|
|
34
53
|
First Impressions
|
35
54
|
-----------------
|
36
|
-
Below I present a sample of the API as it currently stands:
|
37
55
|
|
56
|
+
# ruby
|
38
57
|
class MyApp < Scorched::Controller
|
39
|
-
|
58
|
+
|
40
59
|
# From the most simple route possible...
|
41
60
|
get '/' do
|
42
61
|
"Hello World"
|
43
62
|
end
|
44
63
|
|
45
|
-
# To something that gets the muscle's flexing
|
64
|
+
# To something that gets the muscle's flexing
|
46
65
|
route '/articles/:title/::opts', 2, methods: ['GET', 'POST'], content_type: :json do
|
47
66
|
# Do what you want in here. Note, the second argument is the optional route priority.
|
48
67
|
end
|
49
68
|
|
50
69
|
# Anonymous controllers allow for convenient route grouping to which filters and conditions can be applied
|
51
|
-
controller conditions: {
|
70
|
+
controller conditions: {media_type: 'application/json'} do
|
52
71
|
get '/articles/*' do |page|
|
53
72
|
{title: 'Scorched Rocks', body: '...', created_at: '27/08/2012', created_by: 'Bob'}
|
54
73
|
end
|
55
74
|
|
56
75
|
after do
|
57
|
-
response.to_json
|
76
|
+
response.body = response.body.to_json
|
58
77
|
end
|
59
78
|
end
|
60
79
|
|
@@ -63,21 +82,20 @@ Below I present a sample of the API as it currently stands:
|
|
63
82
|
# Do some crazy awesome stuff that no route can resist using.
|
64
83
|
end
|
65
84
|
|
66
|
-
# You can always avoid the routing helpers and add mappings manually. Anything that responds to #call is a valid
|
67
|
-
|
68
|
-
self << {
|
85
|
+
# You can always avoid the routing helpers and add mappings manually. Anything that responds to #call is a valid
|
86
|
+
# target, with the only minor exception being that proc's are instance_exec'd, not call'd.
|
87
|
+
self << {pattern: '/admin', priority: 10, target: My3rdPartyAdminApp}
|
88
|
+
self << {pattern: '**', conditions: {maintenance_mode: true}, target: proc { |env|
|
69
89
|
@request.body << 'Maintenance underway, please be patient.'
|
70
90
|
}}
|
71
91
|
end
|
72
|
-
|
92
|
+
|
73
93
|
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:
|
74
94
|
|
75
95
|
* 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.
|
76
96
|
* 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.
|
77
97
|
* 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.
|
78
|
-
* 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
|
79
|
-
|
80
|
-
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.
|
98
|
+
* 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 flexibility.
|
81
99
|
|
82
100
|
|
83
101
|
Development Progress
|
data/docs/01_preface.md
ADDED
@@ -0,0 +1,6 @@
|
|
1
|
+
Preface
|
2
|
+
=======
|
3
|
+
|
4
|
+
_This is where I share with you all my infinite wisdom. It will come. In the mean time, know that the documentation is currently incomplete, and likely the victim of quite a few typos._
|
5
|
+
|
6
|
+
_As an early adopter, feel free to use the Github issue tracker to ask questions and request assistance._
|
@@ -0,0 +1,19 @@
|
|
1
|
+
The Controller
|
2
|
+
==============
|
3
|
+
|
4
|
+
Scorched consists almost entirely of the ``Scorched::Controller``. The Controller is the class from which your application class inherits. All the code examples provided in the documentation are assumed to be wrapped within a controller class.
|
5
|
+
|
6
|
+
# ruby
|
7
|
+
class MyApp < Scorched::Controller
|
8
|
+
# We are now within the controller class.
|
9
|
+
# Most examples are assumed to be within this context.
|
10
|
+
end
|
11
|
+
|
12
|
+
Your application's root controller (named ``MyApp`` in the example above), should be configured as the _run_ target in your rackup file:
|
13
|
+
|
14
|
+
# ruby
|
15
|
+
# config.ru
|
16
|
+
require './myapp.rb'
|
17
|
+
run MyApp
|
18
|
+
|
19
|
+
The rest of the documentation will detail the Controller more thoroughly.
|
@@ -0,0 +1,29 @@
|
|
1
|
+
Configuration
|
2
|
+
=============
|
3
|
+
|
4
|
+
Scorched includes a few configurable options out of the box. These all have common defaults, or are otherwise left intentionally blank to ensure the developer opts-in to any potentially undesirable or surprising behaviour.
|
5
|
+
|
6
|
+
There are two sets of configurables. Those which apply to views, and everything else. Each set of configuration options is a ``Scorched::Options`` instance. This allows configuration options to be inherited and subsequently overriden by child classes. This is handy in many instances, but a common requirement might be to change the view directory or default layout of some sub-controller.
|
7
|
+
|
8
|
+
Options
|
9
|
+
-------
|
10
|
+
|
11
|
+
Each configuration is listed below, with the default value of each included.
|
12
|
+
|
13
|
+
* ``config[:strip_trailing_slash] = :redirect``
|
14
|
+
Controls how trailing forward slashes in requests are handled.
|
15
|
+
* ``:redirect`` - Strips and redirects URL's ending in a forward slash
|
16
|
+
* ``:ignore`` - Internally ignores trailing slash
|
17
|
+
* ``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] = Logger.new(STDOUT)`` - Currently does nothing until logging is added to Scorched.
|
21
|
+
|
22
|
+
The follow view configuration options can all be overriden when calling ``render``.
|
23
|
+
|
24
|
+
* ``view_config[:dir] = 'views'``
|
25
|
+
The directory containing all the view templates, relative to the current working directory.
|
26
|
+
* ``view_config[:layout] = false``
|
27
|
+
The default layout to use when rendering views.
|
28
|
+
* ``view_config[:engine] = :erb``
|
29
|
+
The default rendering engine. This is used when ``render`` is given a filename with no extension, or a string.
|
@@ -0,0 +1,92 @@
|
|
1
|
+
Routing
|
2
|
+
=======
|
3
|
+
|
4
|
+
When Scorched receives a request, the first thing it does is iterate over it's internal mapping hash, looking for the any URL pattern that matches the current URL. If it finds an appropriate match, it invokes the ``call`` method on the target defined for that mapping, unless the target is a ``Proc``, in which case it's invoked via ``instance_exec`` to run it within the context of the controller instance.
|
5
|
+
|
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
|
+
|
8
|
+
# ruby
|
9
|
+
map pattern: '/', priority: -99, conditions: {methods: ['POST', 'PUT', 'DELETE']}, target: proc { |env|
|
10
|
+
[200, {}, 'Bugger off']
|
11
|
+
}
|
12
|
+
|
13
|
+
The position the new mapping is inserted into the mapping hash is determined by it's priority, and the priority of the mappings already defined. This avoids re-sorting the mapping hash every time it's added to. This isn't a performance consideration, but is required to maintain the natural insert order of the mappings which have identical priorities (such as the default 0).
|
14
|
+
|
15
|
+
A ``mapping`` method is also provided as means to access all defined mappings on a controller, but it should be considered read-only for the reasons just stated.
|
16
|
+
|
17
|
+
Route Helpers
|
18
|
+
-------------
|
19
|
+
Adding mappings manually can be a little verbose and painful, which is why Scorched includes a bunch of route helpers which are used in most code examples.
|
20
|
+
|
21
|
+
The main route helper which all others delegate to, is the ``route`` class method. Here's what it looks like in both it's simple and advance form:
|
22
|
+
|
23
|
+
# ruby
|
24
|
+
route '/' do
|
25
|
+
'Well hello there'
|
26
|
+
end
|
27
|
+
|
28
|
+
route '/*', 5, methods: ['POST', 'PUT', 'DELETE'] do |capture|
|
29
|
+
"Hmm trying to change #{capture} I see"
|
30
|
+
end
|
31
|
+
|
32
|
+
You can see pretty clearly how these examples correspond to the pattern, priority, conditions and target options of a manual mapping. The pattern, priority and conditions behave exactly as they do for a manual mapping, with a couple of exceptions.
|
33
|
+
|
34
|
+
The first exception is that the pattern must match to the end of the request path. This is mentioned in the _pattern matching_ section below.
|
35
|
+
|
36
|
+
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.
|
37
|
+
|
38
|
+
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.
|
39
|
+
|
40
|
+
Pattern Matching
|
41
|
+
----------------
|
42
|
+
All patterns attempt to match the remaining unmatched portion of the _request path_; the _request path_ being Rack's
|
43
|
+
``path_info`` request variable. The unmatched path will always begin with a forward slash if the previously matched portion of the path ended immediately before, or included as the last character, a forward slash. As an example, if the request was to "/article/21", then both "/article/" => "/21" and "/article" => "/21" would match.
|
44
|
+
|
45
|
+
All patterns must match from the beginning of the path. So even though the pattern "article" would match "/article/21", it wouldn't count as a match because the match didn't start at a non-zero offset.
|
46
|
+
|
47
|
+
If a pattern contains named captures, unnamed captures will be lost - this is how named regex captures work in Ruby. So if you name one capture, make sure you name any other captures you may want to access.
|
48
|
+
|
49
|
+
Patterns can be defined as either a String or Regexp.
|
50
|
+
|
51
|
+
###String Patterns
|
52
|
+
String patterns are compiled into Regexp patterns corresponding to the following rules:
|
53
|
+
|
54
|
+
* `*` - Matches all characters excluding the forward slash.
|
55
|
+
* `**` - Matches all characters including the forward slash.
|
56
|
+
* `:param` - Same as `*` except the capture is named to whatever the string following the single-colon is.
|
57
|
+
* `::param` - Same as `**` except the capture is named to whatever the string following the double-colon is.
|
58
|
+
* `$` - If placed at the end of a pattern, the pattern only matches if it matches the entire path. For patterns defined using the route helpers, e.g. ``Controller.route``, ``Controller.get``, this is implied.
|
59
|
+
|
60
|
+
###Regex Patterns
|
61
|
+
Regex patterns offer more power and flexibility than string patterns (naturally). The rules for Regex patterns are identical to String patterns, e.g. they must match from the beginning of the path, etc.
|
62
|
+
|
63
|
+
|
64
|
+
Conditions
|
65
|
+
----------
|
66
|
+
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.
|
67
|
+
|
68
|
+
* ``:charset`` - Character sets accepted by the client.
|
69
|
+
* ``:encoding`` - Encodings accepted by the client.
|
70
|
+
* ``:host`` - The host name (i.e. domain name) used in the request.
|
71
|
+
* ``:language`` - Languages accepted by the client.
|
72
|
+
* ``:media_type`` - Media types (i.e. content types) accepted by the client.
|
73
|
+
* ``:methods`` - The request method used, e.g. GET, POST, PUT, ...
|
74
|
+
* ``:user_agent`` - The user agent string provided with the request. Takes a Regexp or String.
|
75
|
+
* ``:status`` - The response status of the request. Intended for use by _after_ filters.
|
76
|
+
|
77
|
+
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.
|
78
|
+
|
79
|
+
# ruby
|
80
|
+
condition[:has_permission] = proc { |v|
|
81
|
+
user.has_permission == v
|
82
|
+
}
|
83
|
+
|
84
|
+
get '/', has_permission: true do
|
85
|
+
'Welcome'
|
86
|
+
end
|
87
|
+
|
88
|
+
get '/', has_permission: false do
|
89
|
+
'Forbidden'
|
90
|
+
end
|
91
|
+
|
92
|
+
Each of the built-in conditions can take a single value, or an array of values, with the exception of the ``:host`` and ``:user_agent`` conditions which support Regexp patterns.
|
@@ -0,0 +1,30 @@
|
|
1
|
+
Requests and Responses
|
2
|
+
======================
|
3
|
+
One of the first things a controller does when it instantiates itself, is make the Rack environment hash accessible via the ``env`` helper, as well as make available a ``Scorched::Request`` and ``Scorched::Response`` object under the respective ``request`` and ``response`` methods.
|
4
|
+
|
5
|
+
The ``Scorched::Request`` and ``Scorched::Response`` classes are children of the corresponding _Rack_ request and response classes, with a little extra functionality tacked on.
|
6
|
+
|
7
|
+
The _request_ object makes accessible all the information associated with the current request, such as the GET and POST data, server and environment information, request headers, and so on. The _response_ is much the same, but in reverse. You'll use the _response_ object to set response headers and manipulate the body of the response.
|
8
|
+
|
9
|
+
Refer to the _Rack_ documentation for more information on the ``Rack::Request`` and ``Rack::Response`` classes.
|
10
|
+
|
11
|
+
|
12
|
+
Scorched Extras
|
13
|
+
---------------
|
14
|
+
As mentioned, Scorched tacks a few extras onto it's ``Scorched::Request`` and ``Scorched::Response`` classes. Most of these extras were added as a requirement of the Scorched controller, but they're just as useful to other developers.
|
15
|
+
|
16
|
+
Refer to the generated API documentation for ``Scorched::Request`` and ``Scorched::Response``.
|
17
|
+
|
18
|
+
|
19
|
+
Halting Requests
|
20
|
+
----------------
|
21
|
+
There may be instances we're you want to shortcut out-of processing the current request. The ``halt`` method allows you to do this, though it's worth clarifying its behaviour.
|
22
|
+
|
23
|
+
When ``halt`` is called within a route, it simply exists out of that route, and begins processing any _after_ filters. Halt can also be used within a _before_ or _after_ filter, in which case any remaining filters in the current controller are skipped.
|
24
|
+
|
25
|
+
Calls to ``halt`` don't propagate up the controller chain. They're local to the controller. A call to ``halt`` is equivalent to doing a ``throw :halt``. Calling ``halt`` is often preferred though because as well as being shorter, it can take an optional argument to set the response status, which is something you typically want to do when halting a request.
|
26
|
+
|
27
|
+
|
28
|
+
Redirections
|
29
|
+
------------
|
30
|
+
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.
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Filters
|
2
|
+
=======
|
3
|
+
Filters serve as a handy place to put functionality and behaviour that's common to a set of routes, or for that matter, a whole website or application. Filters are executed in the context of the controller; the same context as routes. Filters are also inheritable, meaning sub-classes inherit the filters of their parent - this inheritance is enabled through the use of the ``Scorched::Collection`` class, and is implemented such that each filter will only run once per-request.
|
4
|
+
|
5
|
+
There are currently two types of filter in Scorched, both of which are documented below.
|
6
|
+
|
7
|
+
|
8
|
+
Before and After Filters
|
9
|
+
------------------------
|
10
|
+
Before and After filters allow pre- and post-processing of requests. They are executed before and after each request, respectively.
|
11
|
+
|
12
|
+
# ruby
|
13
|
+
before do
|
14
|
+
raise Error, "Must be logged in to access this site" unless session[:logged_in] == true
|
15
|
+
end
|
16
|
+
|
17
|
+
Like routes, filters can have conditions defined on them, for example:
|
18
|
+
|
19
|
+
# ruby
|
20
|
+
after media_type: 'application/json' do
|
21
|
+
response.body.to_json!
|
22
|
+
end
|
23
|
+
|
24
|
+
Before and after filters run even if no route within the controller matches. This makes them suitable for handling 404 errors for example.
|
25
|
+
|
26
|
+
# ruby
|
27
|
+
after status: 404 do
|
28
|
+
response.body = render(:not_found)
|
29
|
+
end
|
30
|
+
|
31
|
+
Your imagination is the only limitation.
|
32
|
+
|
33
|
+
|
34
|
+
Error Filters
|
35
|
+
-------------
|
36
|
+
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.
|
37
|
+
|
38
|
+
Error filters can handle exceptions raised from within the request target, as well as those raised within _before_ and _after_ filters. The way error filters have been implemented, allows exceptions raised within the request target to be handled before running the _after_ filters. This means that _after_ filters are still run as long as exceptions that occurred within the request target are handled.
|
39
|
+
|
40
|
+
Error filters can target only specific types of exception class, in much the same way as a typical Ruby rescue block.
|
41
|
+
|
42
|
+
# ruby
|
43
|
+
error PermissionError do |e|
|
44
|
+
flash[:error] = "You do not have the appropriate permission to perform that action: #{e.message}"
|
45
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
Middleware
|
2
|
+
==========
|
3
|
+
|
4
|
+
While middleware can be added in your _rackup_ file to wrap your Scorched application, it can be more desirable to add the middleware at the controller level. Scorched itself requires this as it needs to include a set of Rack middleware out-of-the-box. Developers can't be expected to manually add these to their rackup file for every Scorched application.
|
5
|
+
|
6
|
+
Like _filters_, middleware is inheritable thanks to it's use of the ``Scorched::Collection`` class. Also like _filters_, middleware proc's are only run once per request, which prevents unintended double-loading of middleware.
|
7
|
+
|
8
|
+
Adding middleware to a Scorched controller involves pushing a proc onto the end of the middleware collection, accessible via the ``middleware`` accessor method. The given proc is ``instance_exec``'d in the context of a Rack builder object, and so can be used for more than just loading middleware.
|
9
|
+
|
10
|
+
# ruby
|
11
|
+
middleware << proc do
|
12
|
+
use Rack::Session::Cookie, secret: 'blah'
|
13
|
+
# Stolen from Rack's own documentation...
|
14
|
+
map "/lobster" do
|
15
|
+
use Rack::Lint
|
16
|
+
run Rack::Lobster.new
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
Request and Session Data
|
2
|
+
========================
|
3
|
+
|
4
|
+
GET and POST Data
|
5
|
+
-----------------
|
6
|
+
Many ruby frameworks provide helpers for accessing GET and POST data via some kind of generic accessor, such as a ``params`` method, but Rack already provides this functionality out of the box, and more.
|
7
|
+
|
8
|
+
# ruby
|
9
|
+
post '/' do
|
10
|
+
request.GET['view'] # Key/value pairs submitted in the query string of the URL.
|
11
|
+
request.POST['username'] # Key/value pairs submitted in the payload of a POST request.
|
12
|
+
request.params[:username] # A merged hash of GET and POST data.
|
13
|
+
request[:username] # Shortcut to merged hash of GET and POST data.
|
14
|
+
end
|
15
|
+
|
16
|
+
Uploaded files are also accessible as ordinary fields, except the associated value is a hash of properties, instead of a string. An example of an application that accepts file uploads is included in the "examples" directory of the Scorched git repository.
|
17
|
+
|
18
|
+
Cookies
|
19
|
+
-------
|
20
|
+
While Rack provides a relatively simple means of setting, retrieving and deleting cookies, Scorched aggregates those three actions into a single method, ``cookie``, for the sake of brevity and simplicity.
|
21
|
+
|
22
|
+
# ruby
|
23
|
+
def '/' do
|
24
|
+
cookie :previous_page # Retrieves the cookie.
|
25
|
+
cookie :previous_page, '/search' # Sets the cookie
|
26
|
+
cookie :previous_page, nil # Deletes the cookies
|
27
|
+
end
|
28
|
+
|
29
|
+
For each of the above lines, the corresponding Rack methods are called, e.g. ``Rack::Requeste#cookies``, ``Rack::Response#set_cookie`` and ``Rack::Response#delete_cookie``. The values for setting and deleting a cookie can also be a hash, like ``set_cookie`` and ``delete_cookie`` can take directly. Deleting still works when a Hash is provided, as long as the ``value`` property is nil.
|
30
|
+
|
31
|
+
# ruby
|
32
|
+
def '/' do
|
33
|
+
cookie :view, path: '/account', value: 'datasheet' # Sets the cookie
|
34
|
+
cookie :view, path: '/account', value: nil # Deletes the cookie
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
Sessions
|
39
|
+
--------
|
40
|
+
Sessions are completely handled by Rack. For conveniance, a ``session`` helper is provided. This merely acts as an alias to ``request['rack.session']`` however. It was raise an exception if called without any Rack session middleware loaded, such as ``Rack::Session::Cookie``.
|
41
|
+
|
42
|
+
# ruby
|
43
|
+
class App < Scorched::Controller
|
44
|
+
middleware << proc {
|
45
|
+
use Rack::Session::Cookie, secret: 'blah'
|
46
|
+
}
|
47
|
+
|
48
|
+
get '/' do
|
49
|
+
session['logged_in'] ? 'You're currently logged in.' : 'Please login.'
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
###Flash
|
54
|
+
A common requirement for websites, and especially web applications, is to provide a message on the next page load corresponding to an action that a user just performed. A common framework idiom that Scorched happily implements are flash session variables - special session data that lives for only a single page load.
|
55
|
+
|
56
|
+
This isn't as trivial to implement as it may sound at a glance, which is why Scorched provides this helper.
|
57
|
+
|
58
|
+
# ruby
|
59
|
+
get '/' do
|
60
|
+
"<span class="success">#{flash[:success]}</span>" if flash[:success]
|
61
|
+
end
|
62
|
+
|
63
|
+
post '/login' do
|
64
|
+
flash[:success] = 'Logged in successfully.'
|
65
|
+
end
|
66
|
+
|
67
|
+
The flash helper allows multiple sets of flash session data to be stored under different names. Because of how flash sessions are implemented, they're only deleted on the next page load if that particular flash data set is accessed. These properties of flash sessions can satisfy some interesting use cases. Here's a very uninteresting example:
|
68
|
+
|
69
|
+
# ruby
|
70
|
+
class App < Scorched::Controller
|
71
|
+
get '/' do
|
72
|
+
"<span class="success">#{flash[:success]}</span>" if flash[:success]
|
73
|
+
end
|
74
|
+
|
75
|
+
post '/login' do
|
76
|
+
flash[:success] = 'Logged in successfully.'
|
77
|
+
if user.membership_type == 'vip' && user.membership_expiry < 5
|
78
|
+
flash(:vip)[:warning] = 'Your VIP membership is about to expire, please renew it.'
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
controller pattern: '/vip' do
|
83
|
+
get '/' do
|
84
|
+
"<span class="warning">#{flash(:vip)[:warning]}</span>" if flash(:vip)[:warning]
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
In the rather contrived example above, when a VIP user logs in, a message is generated and stored as a flash session variable in the ``:vip`` flash data set. Because the ``:vip`` flash data set isn't accessed on the main page, it lives on until it's finally re-accessed on the VIP page.
|
@@ -0,0 +1,16 @@
|
|
1
|
+
Sharing and Managing Request State
|
2
|
+
==================================
|
3
|
+
Because Scorched allows sub-controllers to an arbitrary depth and treats all controllers equal (i.e it has no concept of a _root_ controller), it means instance variables and the likes 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 frameworks such as Sinatra.
|
4
|
+
|
5
|
+
The only data exchanged between controllers is the Rack environment hash. Using the Rack environment hash is the only thread-safe way to manage the request state between controllers. As an example use case, consider the Scorched implementation of flash session data. Flash session data must be shared between controllers throughout the life of the request. The rack environment hash is the only suitable place to store this data.
|
6
|
+
|
7
|
+
The Rack idiom is to namespace your keys, typically with your project name, using dots as delimiters. You can further group your keys using more dot-delimited namespaces. Perhaps an example is due:
|
8
|
+
|
9
|
+
# ruby
|
10
|
+
before user_agent: /MSIE|Windows/ do
|
11
|
+
env['myapp.dangerous_request'] = true
|
12
|
+
end
|
13
|
+
|
14
|
+
get '/' do
|
15
|
+
"Welcome to #{env['myapp.settings.site_name']}!"
|
16
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Be Creative
|
2
|
+
===========
|
3
|
+
Getting the most out of Scorched requires a bit of creative thinking. A couple of examples are given below.
|
4
|
+
|
5
|
+
Effortless REST
|
6
|
+
---------------
|
7
|
+
An DRY way to serve multiple content-types:
|
8
|
+
|
9
|
+
# ruby
|
10
|
+
class App < Scorched::Controller
|
11
|
+
def view(view = nil)
|
12
|
+
view ? env['app.view'] = view : env['app.view']
|
13
|
+
end
|
14
|
+
|
15
|
+
after do
|
16
|
+
data = response.body.join('')
|
17
|
+
response['Content-type'] = 'text/html'
|
18
|
+
if check_condition?(:media_type, 'text/html')
|
19
|
+
response.body = render(view, locals: {data: data})
|
20
|
+
elsif check_condition?(:media_type, 'application/json')
|
21
|
+
response['Content-type'] = 'application/json'
|
22
|
+
response.body = data.to_json
|
23
|
+
elsif check_condition?(:media_type, 'application/pdf')
|
24
|
+
response['Content-type'] = 'text/plain'
|
25
|
+
response.status = 406
|
26
|
+
response.body = 'PDF rendering service currently unavailable.'
|
27
|
+
else
|
28
|
+
response.body = render(view, locals: {data: data})
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
get '/' do
|
33
|
+
view :index
|
34
|
+
[
|
35
|
+
{title: 'Sweet Purple Unicorns', date: '08/03/2013'},
|
36
|
+
{title: 'Mellow Grass Men', date: '21/03/2013'}
|
37
|
+
]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
Authentication and Permissions
|
43
|
+
------------------------------
|
44
|
+
|
45
|
+
_Example coming soon._
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require File.expand_path('../../lib/scorched.rb', __FILE__)
|
2
|
+
|
3
|
+
class MediaTypesExample < Scorched::Controller
|
4
|
+
|
5
|
+
get '/' do
|
6
|
+
response['Content-Type'] = 'text/html'
|
7
|
+
<<-HTML
|
8
|
+
<form method="POST" action="#{absolute(request.matched_path)}" enctype="multipart/form-data">
|
9
|
+
<input type="file" name="example_file" />
|
10
|
+
<input type="submit" value="Submit" />
|
11
|
+
</form>
|
12
|
+
HTML
|
13
|
+
end
|
14
|
+
|
15
|
+
post '/' do
|
16
|
+
example_file = request[:example_file]
|
17
|
+
<<-HTML
|
18
|
+
We know the following about the received file.
|
19
|
+
<ul>
|
20
|
+
<li><strong>Name:</strong> #{example_file[:filename]}</li>
|
21
|
+
<li><strong>Supposed Type:</strong> #{example_file[:type]}</li>
|
22
|
+
<li><strong>Actual Type:</strong> <em>Just pretend we are using MimeMagic</em></li>
|
23
|
+
<li><strong>Size:</strong> #{format_byte_size example_file[:tempfile].size}</li>
|
24
|
+
</ul>
|
25
|
+
HTML
|
26
|
+
end
|
27
|
+
|
28
|
+
after do
|
29
|
+
response['Content-Type'] = 'text/html; charset=utf-8' unless response['Content-Type']
|
30
|
+
end
|
31
|
+
|
32
|
+
# My self-proclaimed awesome byte size formatter: https://gist.github.com/Wardrop/4952405
|
33
|
+
def format_byte_size(bytes, opts = {})
|
34
|
+
opts = {binary: true, precision: 2, as_bits: false}.merge(opts)
|
35
|
+
suffixes = ['bytes', 'KB', 'MB', 'GB', 'TB', 'PB','EB', 'ZB', 'YB']
|
36
|
+
opts[:as_bits] && suffixes[0] = 'bits' && suffixes[1..-1].each { |v| v.downcase! } && bytes *= 8
|
37
|
+
opts[:binary] ? base = 1024 : (base = 1000) && suffixes[1..-1].each { |v| v.insert(1,'i') }
|
38
|
+
exp = Math.log(bytes, base).floor
|
39
|
+
"#{(bytes.to_f / (base ** exp)).round(opts[:precision])} #{suffixes[exp]}"
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
|
44
|
+
run MediaTypesExample
|
data/examples/media_types.ru
CHANGED
@@ -1,2 +1,26 @@
|
|
1
|
-
require '
|
1
|
+
require 'json'
|
2
|
+
require File.expand_path('../../lib/scorched.rb', __FILE__)
|
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
|
+
controller pattern: '/*' do
|
15
|
+
get '/*' do |*captures|
|
16
|
+
request.breadcrumb.inspect
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
get '/,', media_type: 'application/json' do
|
21
|
+
{name: 'John Falkon', age: 39, occupation: 'Carpet Cleaner'}.to_json
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
|
2
26
|
run MediaTypesExample
|
data/lib/scorched/controller.rb
CHANGED
@@ -15,7 +15,7 @@ module Scorched
|
|
15
15
|
}
|
16
16
|
|
17
17
|
view_config << {
|
18
|
-
:dir => 'views', # The directory containing all the view templates, relative to the
|
18
|
+
:dir => 'views', # The directory containing all the view templates, relative to the current working directory.
|
19
19
|
:layout => false, # The default layout template to use, relative to the view directory. Set to false for no default layout.
|
20
20
|
:engine => :erb
|
21
21
|
}
|
@@ -84,17 +84,17 @@ module Scorched
|
|
84
84
|
# Generates and assigns mapping hash from the given arguments.
|
85
85
|
#
|
86
86
|
# Accepts the following keyword arguments:
|
87
|
-
# :
|
87
|
+
# :pattern - The url pattern to match on. Required.
|
88
88
|
# :target - A proc to execute, or some other object that responds to #call. Required.
|
89
89
|
# :priority - Negative or positive integer for giving a priority to the mapped item.
|
90
90
|
# :conditions - A hash of condition:value pairs
|
91
91
|
# Raises ArgumentError if required key values are not provided.
|
92
|
-
def map(
|
93
|
-
raise ArgumentError, "Mapping must specify url pattern and target" unless
|
92
|
+
def map(pattern: nil, priority: nil, conditions: {}, target: nil)
|
93
|
+
raise ArgumentError, "Mapping must specify url pattern and target" unless pattern && target
|
94
94
|
priority = priority.to_i
|
95
95
|
insert_pos = mappings.take_while { |v| priority <= v[:priority] }.length
|
96
96
|
mappings.insert(insert_pos, {
|
97
|
-
|
97
|
+
pattern: compile(pattern),
|
98
98
|
priority: priority,
|
99
99
|
conditions: conditions,
|
100
100
|
target: target
|
@@ -113,17 +113,17 @@ module Scorched
|
|
113
113
|
# (or inherits from) a Scorched::Controller.
|
114
114
|
def controller(parent_class = self, **mapping, &block)
|
115
115
|
c = Class.new(parent_class, &block)
|
116
|
-
self << {
|
116
|
+
self << {pattern: '/', target: c}.merge(mapping)
|
117
117
|
c
|
118
118
|
end
|
119
119
|
|
120
120
|
# Generates and returns a new route proc from the given block, and optionally maps said proc using the given args.
|
121
|
-
def route(
|
121
|
+
def route(pattern = nil, priority = nil, **conditions, &block)
|
122
122
|
target = lambda do |env|
|
123
|
-
env['
|
124
|
-
env['
|
123
|
+
env['scorched.response'].body = instance_exec(*env['scorched.request'].captures, &block)
|
124
|
+
env['scorched.response']
|
125
125
|
end
|
126
|
-
self << {
|
126
|
+
self << {pattern: compile(pattern, true), priority: priority, conditions: conditions, target: target} if pattern
|
127
127
|
target
|
128
128
|
end
|
129
129
|
|
@@ -151,11 +151,11 @@ module Scorched
|
|
151
151
|
# Parses and compiles the given URL string pattern into a regex if not already, returning the resulting regexp
|
152
152
|
# object. Accepts an optional _match_to_end_ argument which will ensure the generated pattern matches to the end
|
153
153
|
# of the string.
|
154
|
-
def compile(
|
155
|
-
return
|
156
|
-
raise Error, "Can't compile URL of type #{
|
157
|
-
match_to_end = !!
|
158
|
-
|
154
|
+
def compile(pattern, match_to_end = false)
|
155
|
+
return pattern if Regexp === pattern
|
156
|
+
raise Error, "Can't compile URL of type #{pattern.class}. Must be String or Regexp." unless String === pattern
|
157
|
+
match_to_end = !!pattern.sub!(/\$$/, '') || match_to_end
|
158
|
+
regex_pattern = pattern.split(%r{(\*{1,2}|(?<!\\):{1,2}[^/*$]+)}).each_slice(2).map { |unmatched, match|
|
159
159
|
Regexp.escape(unmatched) << begin
|
160
160
|
if %w{* **}.include? match
|
161
161
|
match == '*' ? "([^/]+)" : "(.+)"
|
@@ -166,8 +166,8 @@ module Scorched
|
|
166
166
|
end
|
167
167
|
end
|
168
168
|
}.join
|
169
|
-
|
170
|
-
Regexp.new(
|
169
|
+
regex_pattern << '$' if match_to_end
|
170
|
+
Regexp.new(regex_pattern)
|
171
171
|
end
|
172
172
|
end
|
173
173
|
|
@@ -179,8 +179,8 @@ module Scorched
|
|
179
179
|
define_singleton_method :env do
|
180
180
|
env
|
181
181
|
end
|
182
|
-
env['
|
183
|
-
env['
|
182
|
+
env['scorched.request'] ||= Request.new(env)
|
183
|
+
env['scorched.response'] ||= Response.new
|
184
184
|
end
|
185
185
|
|
186
186
|
def action
|
@@ -232,7 +232,7 @@ module Scorched
|
|
232
232
|
to_match = to_match.chomp('/') if config[:strip_trailing_slash] == :ignore && to_match =~ %r{./$}
|
233
233
|
matches = []
|
234
234
|
mappings.each do |m|
|
235
|
-
m[:
|
235
|
+
m[:pattern].match(to_match) do |match_data|
|
236
236
|
if match_data.pre_match == ''
|
237
237
|
if check_conditions?(m[:conditions])
|
238
238
|
if match_data.names.empty?
|
@@ -240,7 +240,7 @@ module Scorched
|
|
240
240
|
else
|
241
241
|
captures = Hash[match_data.names.map{|v| v.to_sym}.zip match_data.captures]
|
242
242
|
end
|
243
|
-
matches << {mapping: m, captures: captures,
|
243
|
+
matches << {mapping: m, captures: captures, path: match_data.to_s}
|
244
244
|
break if short_circuit
|
245
245
|
end
|
246
246
|
end
|
@@ -274,12 +274,12 @@ module Scorched
|
|
274
274
|
|
275
275
|
# Convenience method for accessing Rack request.
|
276
276
|
def request
|
277
|
-
env['
|
277
|
+
env['scorched.request']
|
278
278
|
end
|
279
279
|
|
280
280
|
# Convenience method for accessing Rack response.
|
281
281
|
def response
|
282
|
-
env['
|
282
|
+
env['scorched.response']
|
283
283
|
end
|
284
284
|
|
285
285
|
# Convenience method for accessing Rack session.
|
@@ -375,7 +375,6 @@ module Scorched
|
|
375
375
|
else
|
376
376
|
uri.to_s
|
377
377
|
end
|
378
|
-
|
379
378
|
end
|
380
379
|
|
381
380
|
# Takes an optional path, relative to the applications root URL, and returns an absolute path.
|
data/lib/scorched/request.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
module Scorched
|
2
2
|
class Request < Rack::Request
|
3
|
-
# Keeps track of the matched URL portions and what object handled them.
|
3
|
+
# Keeps track of the matched URL portions and what object handled them. Useful for debugging and building
|
4
|
+
# breadcrumb navigation.
|
4
5
|
def breadcrumb
|
5
6
|
env['breadcrumb'] ||= []
|
6
7
|
end
|
@@ -10,17 +11,20 @@ module Scorched
|
|
10
11
|
breadcrumb.last ? breadcrumb.last[:captures] : []
|
11
12
|
end
|
12
13
|
|
14
|
+
# Returns an array of capture arrays; one for each mapping that's been hit during the request processing so far.
|
13
15
|
def all_captures
|
14
16
|
breadcrumb.map { |v| v[:captures] }
|
15
17
|
end
|
16
18
|
|
19
|
+
# The portion of the path that's currently been matched by one or more mappings.
|
17
20
|
def matched_path
|
18
|
-
join_paths(breadcrumb.map{|v| v[:
|
21
|
+
join_paths(breadcrumb.map{|v| v[:path]})
|
19
22
|
end
|
20
23
|
|
24
|
+
# The remaining portion of the path that has yet to be matched by any mappings.
|
21
25
|
def unmatched_path
|
22
26
|
path = path_info.partition(matched_path).last
|
23
|
-
path[0,0] = '/'
|
27
|
+
path[0,0] = '/' if path.empty? || matched_path[-1] == '/'
|
24
28
|
path
|
25
29
|
end
|
26
30
|
|
data/lib/scorched/response.rb
CHANGED
@@ -14,5 +14,18 @@ module Scorched
|
|
14
14
|
self.status, @header, self.body = response
|
15
15
|
end
|
16
16
|
end
|
17
|
+
|
18
|
+
# Automatically wraps the assigned value in an array if it doesn't respond to ``each``.
|
19
|
+
def body=(value)
|
20
|
+
super(value.respond_to?(:each) ? value : [value])
|
21
|
+
end
|
22
|
+
|
23
|
+
def finish(*args, &block)
|
24
|
+
self['Content-Type'] ||= 'text/html;charset=utf-8'
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
alias :to_a :finish
|
29
|
+
alias :to_ary :finish
|
17
30
|
end
|
18
31
|
end
|
data/lib/scorched/version.rb
CHANGED
data/spec/controller_spec.rb
CHANGED
@@ -24,13 +24,13 @@ module Scorched
|
|
24
24
|
end
|
25
25
|
|
26
26
|
it "handles a root rack call correctly" do
|
27
|
-
app << {
|
27
|
+
app << {pattern: '/$', target: generic_handler}
|
28
28
|
response = rt.get '/'
|
29
29
|
response.status.should == 200
|
30
30
|
end
|
31
31
|
|
32
32
|
it "does not maintain state between requests" do
|
33
|
-
app << {
|
33
|
+
app << {pattern: '/state', target: proc { |env| [200, {}, [@state = 1 + @state.to_i]] }}
|
34
34
|
response = rt.get '/state'
|
35
35
|
response.body.should == '1'
|
36
36
|
response = rt.get '/state'
|
@@ -39,7 +39,7 @@ module Scorched
|
|
39
39
|
|
40
40
|
it "raises exception when invalid mapping hash given" do
|
41
41
|
expect {
|
42
|
-
app << {
|
42
|
+
app << {pattern: '/'}
|
43
43
|
}.to raise_error(ArgumentError)
|
44
44
|
expect {
|
45
45
|
app << {target: generic_handler}
|
@@ -49,70 +49,90 @@ module Scorched
|
|
49
49
|
|
50
50
|
describe "URL matching" do
|
51
51
|
it 'always matches from the beginning of the URL' do
|
52
|
-
app << {
|
52
|
+
app << {pattern: 'about', target: generic_handler}
|
53
53
|
response = rt.get '/about'
|
54
54
|
response.status.should == 404
|
55
55
|
end
|
56
56
|
|
57
57
|
it "matches eagerly by default" do
|
58
|
-
|
59
|
-
app << {
|
60
|
-
|
58
|
+
req = nil
|
59
|
+
app << {pattern: '/*', target: proc do |env|
|
60
|
+
req = request; [200, {}, ['ok']]
|
61
61
|
end}
|
62
62
|
response = rt.get '/about'
|
63
|
-
|
63
|
+
req.captures.should == ['about']
|
64
64
|
end
|
65
65
|
|
66
66
|
it "can be forced to match end of URL" do
|
67
|
-
app << {
|
67
|
+
app << {pattern: '/about$', target: generic_handler}
|
68
68
|
response = rt.get '/about/us'
|
69
69
|
response.status.should == 404
|
70
|
-
app << {
|
70
|
+
app << {pattern: '/about', target: generic_handler}
|
71
71
|
response = rt.get '/about/us'
|
72
72
|
response.status.should == 200
|
73
73
|
end
|
74
74
|
|
75
|
+
it "unmatched path doesn't always begin with a forward slash" do
|
76
|
+
gh = generic_handler
|
77
|
+
app << {pattern: '/ab', target: Class.new(Scorched::Controller) do
|
78
|
+
map(pattern: 'out', target: gh)
|
79
|
+
end}
|
80
|
+
rt.get('/about').body.should == "ok"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "unmatched path begins with forward slash if last match was up to or included a forward slash" do
|
84
|
+
gh = generic_handler
|
85
|
+
app << {pattern: '/about/', target: Class.new(Scorched::Controller) do
|
86
|
+
map(pattern: '/us', target: gh)
|
87
|
+
end}
|
88
|
+
app << {pattern: '/contact', target: Class.new(Scorched::Controller) do
|
89
|
+
map(pattern: '/us', target: gh)
|
90
|
+
end}
|
91
|
+
rt.get('/about/us').body.should == "ok"
|
92
|
+
rt.get('/contact/us').body.should == "ok"
|
93
|
+
end
|
94
|
+
|
75
95
|
it "can match anonymous wildcards" do
|
76
|
-
|
77
|
-
app << {
|
78
|
-
|
96
|
+
req = nil
|
97
|
+
app << {pattern: '/anon/*/**', target: proc do |env|
|
98
|
+
req = request; [200, {}, ['ok']]
|
79
99
|
end}
|
80
100
|
response = rt.get '/anon/jeff/has/crabs'
|
81
|
-
|
101
|
+
req.captures.should == ['jeff', 'has/crabs']
|
82
102
|
end
|
83
103
|
|
84
104
|
it "can match named wildcards (ignoring anonymous captures)" do
|
85
|
-
|
86
|
-
app << {
|
87
|
-
|
105
|
+
req = nil
|
106
|
+
app << {pattern: '/anon/:name/*/::infliction', target: proc do |env|
|
107
|
+
req = request; [200, {}, ['ok']]
|
88
108
|
end}
|
89
109
|
response = rt.get '/anon/jeff/smith/has/crabs'
|
90
|
-
|
110
|
+
req.captures.should == {name: 'jeff', infliction: 'has/crabs'}
|
91
111
|
end
|
92
112
|
|
93
113
|
it "can match regex and preserve anonymous captures" do
|
94
|
-
|
95
|
-
app << {
|
96
|
-
|
114
|
+
req = nil
|
115
|
+
app << {pattern: %r{/anon/([^/]+)/(.+)}, target: proc do |env|
|
116
|
+
req = request; [200, {}, ['ok']]
|
97
117
|
end}
|
98
118
|
response = rt.get '/anon/jeff/has/crabs'
|
99
|
-
|
119
|
+
req.captures.should == ['jeff', 'has/crabs']
|
100
120
|
end
|
101
121
|
|
102
122
|
it "can match regex and preserve named captures (ignoring anonymous captures)" do
|
103
|
-
|
104
|
-
app << {
|
105
|
-
|
123
|
+
req = nil
|
124
|
+
app << {pattern: %r{/anon/(?<name>[^/]+)/([^/]+)/(?<infliction>.+)}, target: proc do |env|
|
125
|
+
req = request; [200, {}, ['ok']]
|
106
126
|
end}
|
107
127
|
response = rt.get '/anon/jeff/smith/has/crabs'
|
108
|
-
|
128
|
+
req.captures.should == {name: 'jeff', infliction: 'has/crabs'}
|
109
129
|
end
|
110
130
|
|
111
131
|
it "matches routes based on priority, otherwise giving precedence to those defined first" do
|
112
|
-
app << {
|
113
|
-
app << {
|
114
|
-
app << {
|
115
|
-
app << {
|
132
|
+
app << {pattern: '/', priority: -1, target: proc { |env| self.class.mappings.shift; [200, {}, ['four']] }}
|
133
|
+
app << {pattern: '/', target: proc { |env| self.class.mappings.shift; [200, {}, ['two']] }}
|
134
|
+
app << {pattern: '/', target: proc { |env| self.class.mappings.shift; [200, {}, ['three']] }}
|
135
|
+
app << {pattern: '/', priority: 2, target: proc { |env| self.class.mappings.shift; [200, {}, ['one']] }}
|
116
136
|
rt.get('/').body.should == 'one'
|
117
137
|
rt.get('/').body.should == 'two'
|
118
138
|
rt.get('/').body.should == 'three'
|
@@ -128,14 +148,14 @@ module Scorched
|
|
128
148
|
end
|
129
149
|
|
130
150
|
it "executes route only if all conditions return true" do
|
131
|
-
app << {
|
151
|
+
app << {pattern: '/', conditions: {methods: 'POST'}, target: generic_handler}
|
132
152
|
response = rt.get "/"
|
133
153
|
response.status.should == 404
|
134
154
|
response = rt.post "/"
|
135
155
|
response.status.should == 200
|
136
156
|
|
137
157
|
app.conditions[:has_name] = proc { |name| request.GET['name'] }
|
138
|
-
app << {
|
158
|
+
app << {pattern: '/about', conditions: {methods: ['GET', 'POST'], has_name: 'Ronald'}, target: generic_handler}
|
139
159
|
response = rt.get "/about"
|
140
160
|
response.status.should == 404
|
141
161
|
response = rt.get "/about", name: 'Ronald'
|
@@ -143,15 +163,15 @@ module Scorched
|
|
143
163
|
end
|
144
164
|
|
145
165
|
it "raises exception when condition doesn't exist or is invalid" do
|
146
|
-
app << {
|
166
|
+
app << {pattern: '/', conditions: {surprise_christmas_turkey: true}, target: generic_handler}
|
147
167
|
expect {
|
148
168
|
rt.get "/"
|
149
169
|
}.to raise_error(Scorched::Error)
|
150
170
|
end
|
151
171
|
|
152
172
|
it "falls through to next route when conditions are not met" do
|
153
|
-
app << {
|
154
|
-
app << {
|
173
|
+
app << {pattern: '/', conditions: {methods: 'POST'}, target: proc { |env| [200, {}, ['post']] }}
|
174
|
+
app << {pattern: '/', conditions: {methods: 'GET'}, target: proc { |env| [200, {}, ['get']] }}
|
155
175
|
rt.get("/").body.should == 'get'
|
156
176
|
rt.post("/").body.should == 'post'
|
157
177
|
end
|
@@ -161,7 +181,7 @@ module Scorched
|
|
161
181
|
it "allows end points to be defined more succinctly" do
|
162
182
|
route_proc = app.route('/*', 2, methods: 'GET') { |capture| capture }
|
163
183
|
mapping = app.mappings.first
|
164
|
-
mapping.should == {
|
184
|
+
mapping.should == {pattern: mapping[:pattern], priority: 2, conditions: {methods: 'GET'}, target: route_proc}
|
165
185
|
rt.get('/about').body.should == 'about'
|
166
186
|
end
|
167
187
|
|
@@ -170,7 +190,7 @@ module Scorched
|
|
170
190
|
wrapped_block = app.route(&block)
|
171
191
|
app.mappings.length.should == 0
|
172
192
|
block.should_not == wrapped_block
|
173
|
-
app << {
|
193
|
+
app << {pattern: '/*', target: wrapped_block}
|
174
194
|
rt.get('/turkey').body.should == 'turkey'
|
175
195
|
end
|
176
196
|
|
@@ -208,7 +228,7 @@ module Scorched
|
|
208
228
|
end
|
209
229
|
|
210
230
|
it "should ignore the already matched portions of the path" do
|
211
|
-
app.controller
|
231
|
+
app.controller pattern: '/article' do
|
212
232
|
get('/*') { |title| title }
|
213
233
|
end
|
214
234
|
rt.get('/article/hello-world').body.should == 'hello-world'
|
@@ -391,7 +411,7 @@ module Scorched
|
|
391
411
|
get '/'do
|
392
412
|
request.env['scorched.simple_counter']
|
393
413
|
end
|
394
|
-
controller
|
414
|
+
controller pattern: '/sub_controller' do
|
395
415
|
get '/' do
|
396
416
|
request.env['scorched.simple_counter']
|
397
417
|
end
|
@@ -549,6 +569,7 @@ module Scorched
|
|
549
569
|
app.post('/') { cookie :test, 'hello' }
|
550
570
|
app.post('/goodbye') { cookie :test, {value: 'goodbye', expires: Time.now() + 999999 } }
|
551
571
|
app.delete('/') { cookie :test, nil }
|
572
|
+
app.delete('/alt') { cookie :test, {value: nil} }
|
552
573
|
|
553
574
|
rt.get('/').body.should == ''
|
554
575
|
rt.post('/')
|
@@ -557,6 +578,8 @@ module Scorched
|
|
557
578
|
rt.get('/').body.should == 'goodbye'
|
558
579
|
rt.delete('/')
|
559
580
|
rt.get('/').body.should == ''
|
581
|
+
rt.delete('/alt')
|
582
|
+
rt.get('/').body.should == ''
|
560
583
|
end
|
561
584
|
end
|
562
585
|
|
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.5.
|
4
|
+
version: 0.5.2
|
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-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -91,11 +91,18 @@ files:
|
|
91
91
|
- LICENSE
|
92
92
|
- Milestones.md
|
93
93
|
- README.md
|
94
|
-
- docs/
|
95
|
-
- docs/
|
96
|
-
- docs/
|
97
|
-
- docs/
|
98
|
-
-
|
94
|
+
- docs/01_preface.md
|
95
|
+
- docs/02_fundamentals/01_the_controller.md
|
96
|
+
- docs/02_fundamentals/02_configuration.md
|
97
|
+
- docs/02_fundamentals/03_routing.md
|
98
|
+
- docs/02_fundamentals/04_requests_and_responses.md
|
99
|
+
- docs/02_fundamentals/05_filters.md
|
100
|
+
- docs/02_fundamentals/06_middleware.md
|
101
|
+
- docs/02_fundamentals/07_request_and_session_data.md
|
102
|
+
- docs/02_fundamentals/08_views.md
|
103
|
+
- docs/02_fundamentals/09_sharing_request_state.md
|
104
|
+
- docs/03_further_reading/be_creative.md
|
105
|
+
- examples/file_upload.ru
|
99
106
|
- examples/media_types.ru
|
100
107
|
- lib/scorched.rb
|
101
108
|
- lib/scorched/collection.rb
|
data/docs/be_creative.md
DELETED
@@ -1,32 +0,0 @@
|
|
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
|
-
|
data/docs/filters.md
DELETED
@@ -1,8 +0,0 @@
|
|
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.
|
data/docs/routing.md
DELETED
@@ -1,29 +0,0 @@
|
|
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).
|
@@ -1,5 +0,0 @@
|
|
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.
|
data/examples/media_types.rb
DELETED
@@ -1,18 +0,0 @@
|
|
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
|