roda 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +62 -0
- data/README.rdoc +362 -167
- data/Rakefile +2 -2
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +553 -180
- data/lib/roda/plugins/_erubis_escaping.rb +28 -0
- data/lib/roda/plugins/all_verbs.rb +7 -9
- data/lib/roda/plugins/backtracking_array.rb +92 -0
- data/lib/roda/plugins/content_for.rb +46 -0
- data/lib/roda/plugins/csrf.rb +60 -0
- data/lib/roda/plugins/flash.rb +53 -7
- data/lib/roda/plugins/halt.rb +8 -14
- data/lib/roda/plugins/head.rb +56 -0
- data/lib/roda/plugins/header_matchers.rb +2 -2
- data/lib/roda/plugins/json.rb +84 -0
- data/lib/roda/plugins/multi_route.rb +50 -10
- data/lib/roda/plugins/not_allowed.rb +140 -0
- data/lib/roda/plugins/pass.rb +13 -6
- data/lib/roda/plugins/per_thread_caching.rb +70 -0
- data/lib/roda/plugins/render.rb +20 -33
- data/lib/roda/plugins/render_each.rb +61 -0
- data/lib/roda/plugins/symbol_matchers.rb +79 -0
- data/lib/roda/plugins/symbol_views.rb +40 -0
- data/lib/roda/plugins/view_subdirs.rb +53 -0
- data/lib/roda/version.rb +3 -0
- data/spec/matchers_spec.rb +61 -5
- data/spec/plugin/_erubis_escaping_spec.rb +29 -0
- data/spec/plugin/backtracking_array_spec.rb +38 -0
- data/spec/plugin/content_for_spec.rb +34 -0
- data/spec/plugin/csrf_spec.rb +49 -0
- data/spec/plugin/flash_spec.rb +69 -5
- data/spec/plugin/head_spec.rb +35 -0
- data/spec/plugin/json_spec.rb +50 -0
- data/spec/plugin/multi_route_spec.rb +22 -6
- data/spec/plugin/not_allowed_spec.rb +55 -0
- data/spec/plugin/pass_spec.rb +8 -2
- data/spec/plugin/per_thread_caching_spec.rb +28 -0
- data/spec/plugin/render_each_spec.rb +30 -0
- data/spec/plugin/render_spec.rb +7 -1
- data/spec/plugin/symbol_matchers_spec.rb +68 -0
- data/spec/plugin/symbol_views_spec.rb +32 -0
- data/spec/plugin/view_subdirs_spec.rb +45 -0
- data/spec/plugin_spec.rb +11 -1
- data/spec/redirect_spec.rb +21 -4
- data/spec/request_spec.rb +9 -0
- metadata +49 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ee4a240b4a2d17e55d10cc84f60b28ffd8fcf76
|
4
|
+
data.tar.gz: 50af03ce96aadac19fa0e9c81e0cb0ea86e90461
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6f3d44551360e5c38fbbe07826793a7fe986bb6c017bb9136e9494c7a8ec15741b74693f725be32cee455e4d10b6bf4083d44f32e8427b9b76296b5d4dc61090
|
7
|
+
data.tar.gz: db885f610de332b033bb565eadc86239c842e28e49d352cbb45513fed0404b934320ab9b1f9a09b30844e84f08a759cc931dde9ddbba55c1e4b46327f5b4c925
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,65 @@
|
|
1
|
+
= 1.0.0 (2014-08-19)
|
2
|
+
|
3
|
+
* Don't have :extension hash matcher force a terminal match (jeremyevans)
|
4
|
+
|
5
|
+
* Add :content option to view method in render plugin to use given content instead of rendering a template (jeremyevans)
|
6
|
+
|
7
|
+
* Add :escape option to render plugin for using erb templates where <%= %> escapes and <%== %> does not (jeremyevans)
|
8
|
+
|
9
|
+
* Make multi_route plugin route("route_name") method a request method instead of an instance method (jeremyevans)
|
10
|
+
|
11
|
+
* Add r.multi_route method to multi_route plugin, for dispatching to named route based on first segment in path (jeremyevans)
|
12
|
+
|
13
|
+
* Allow non-GET requests to use r.redirect with no argument, redirecting to current path (jeremyevans)
|
14
|
+
|
15
|
+
* Add head plugin, for handling HEAD requests like GET requests with an empty body (jeremyevans)
|
16
|
+
|
17
|
+
* Optimize consuming patterns by using a positive lookahead assertion (jeremyevans)
|
18
|
+
|
19
|
+
* Add not_allowed plugin, for automatically returning 405 Method Not Allowed responses (jeremyevans)
|
20
|
+
|
21
|
+
* Optimize match blocks with no arguments (jeremyevans)
|
22
|
+
|
23
|
+
* Add content_for plugin, for storing content in one template and retrieving it in another (jeremyevans)
|
24
|
+
|
25
|
+
* Add render_each plugin, for rendering a template for each value in an enumerable (jeremyevans)
|
26
|
+
|
27
|
+
* Add backtracking_array plugin, allowing array matchers to backtrack if later matchers do not match (jeremyevans)
|
28
|
+
|
29
|
+
* Add :all hash matcher, allowing array matchers to include conditions where you want to match multiple conditions (jeremyevans)
|
30
|
+
|
31
|
+
* Add json plugin, allowing match blocks to return arrays/hashes, returning JSON (jeremyevans)
|
32
|
+
|
33
|
+
* Add view_subdirs plugin, for setting a subdirectory for views on a per-request basis (jeremyevans)
|
34
|
+
|
35
|
+
* Allow default halt method to take no arguments, and use the current response (jeremyevans)
|
36
|
+
|
37
|
+
* Add symbol_views plugin, allowing match blocks to return a template name symbol (jeremyevans)
|
38
|
+
|
39
|
+
* Add per_thread_caching plugin, for using separate caches per thread instead of shared thread-safe caches (jeremyevans)
|
40
|
+
|
41
|
+
* Add hash_matcher class method, for easily creating hash match methods (jeremyevans)
|
42
|
+
|
43
|
+
* Add symbol_matchers plugin, for using symbol-specific matching regexps (jeremyevans)
|
44
|
+
|
45
|
+
* Add csrf plugin for csrf protection using rack_csrf (jeremyevans)
|
46
|
+
|
47
|
+
* Optimize r.is, r.get, r.post and similar methods by reducing the number of Array objects created (jeremyevans)
|
48
|
+
|
49
|
+
* Support RequestClassMethods and ResponseClassMethods in plugins (jeremyevans)
|
50
|
+
|
51
|
+
* Add Roda::RodaCache for a thread safe cache, currently used for match patterns, templates, and plugins (jeremyevans)
|
52
|
+
|
53
|
+
* Optimize matching by caching consume regexp for strings, regexp, symbol, and :extension matchers (jeremyevans)
|
54
|
+
|
55
|
+
* Add r.root for GET / requests, for easier to read version of r.get "" (jeremyevans)
|
56
|
+
|
57
|
+
* Optimize r.is terminal matcher, remove :term hash matcher (jeremyevans)
|
58
|
+
|
59
|
+
* Make flash plugin no longer depend on sinatra-flash (jeremyevans)
|
60
|
+
|
61
|
+
* Move version file to roda/version so it can be required separately without loading dependencies (jeremyevans)
|
62
|
+
|
1
63
|
= 0.9.0 (2014-07-30)
|
2
64
|
|
3
65
|
* Initial public release
|
data/README.rdoc
CHANGED
@@ -14,6 +14,16 @@ Bugs :: http://github.com/jeremyevans/roda/issues
|
|
14
14
|
Google Group :: http://groups.google.com/group/ruby-roda
|
15
15
|
IRC :: irc://chat.freenode.net/#roda
|
16
16
|
|
17
|
+
== Inspiration
|
18
|
+
|
19
|
+
Roda was inspired by {Sinatra}[http://www.sinatrarb.com] and {Cuba}[http://cuba.is],
|
20
|
+
two other Ruby web frameworks. It started out as a fork of Cuba, from which it borrows
|
21
|
+
the idea of using a routing tree (which Cuba in turn took from
|
22
|
+
{Rum}[https://github.com/chneukirchen/rum]). From Sinatra it takes the ideas that
|
23
|
+
route blocks should return the request bodies and that routes should be canonical.
|
24
|
+
It pilfers the idea for an extensible plugin system from the Ruby database library
|
25
|
+
{Sequel}[http://sequel.jeremyevans.net].
|
26
|
+
|
17
27
|
== Usage
|
18
28
|
|
19
29
|
Here's a simple application, showing how the routing tree works:
|
@@ -25,25 +35,30 @@ Here's a simple application, showing how the routing tree works:
|
|
25
35
|
use Rack::Session::Cookie, :secret => ENV['SECRET']
|
26
36
|
|
27
37
|
route do |r|
|
28
|
-
#
|
29
|
-
r.
|
38
|
+
# GET / request
|
39
|
+
r.root do
|
40
|
+
r.redirect "/hello"
|
41
|
+
end
|
30
42
|
|
31
|
-
|
32
|
-
|
33
|
-
r.redirect "/hello"
|
34
|
-
end
|
43
|
+
# /hello branch
|
44
|
+
r.on "hello" do
|
35
45
|
|
36
|
-
#
|
37
|
-
r.
|
46
|
+
# GET /hello/world request
|
47
|
+
r.get "world" do
|
48
|
+
"Hello world!"
|
49
|
+
end
|
38
50
|
|
39
|
-
|
40
|
-
|
41
|
-
|
51
|
+
# /hello request
|
52
|
+
r.is do
|
53
|
+
# GET /hello request
|
54
|
+
r.get do
|
55
|
+
"Hello!"
|
42
56
|
end
|
43
57
|
|
44
|
-
#
|
45
|
-
r.
|
46
|
-
"
|
58
|
+
# POST /hello request
|
59
|
+
r.post do
|
60
|
+
puts "Someone said hello!"
|
61
|
+
r.redirect
|
47
62
|
end
|
48
63
|
end
|
49
64
|
end
|
@@ -52,8 +67,6 @@ Here's a simple application, showing how the routing tree works:
|
|
52
67
|
|
53
68
|
run App.app
|
54
69
|
|
55
|
-
You can now run +rackup+ and enjoy what you have just created.
|
56
|
-
|
57
70
|
Here's a breakdown of what is going on in the above block:
|
58
71
|
|
59
72
|
After requiring the library and subclassing Roda, the +use+ method
|
@@ -66,34 +79,120 @@ with some additional methods for matching routes. By
|
|
66
79
|
convention, this argument should be named +r+.
|
67
80
|
|
68
81
|
The primary way routes are matched in Roda is by calling
|
69
|
-
+r.on+,
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
82
|
+
+r.on+, +r.is+, +r.root+, +r.get+, or +r.post+. These methods are
|
83
|
+
calling the routing methods, and each of them takes a block. The
|
84
|
+
block is referred to as a match block.
|
85
|
+
|
86
|
+
Each routing method takes each of the arguments (called matchers)
|
87
|
+
given and tries to match them to the current request. If it is
|
88
|
+
able to match all of the arguments, it yields to the match block,
|
89
|
+
otherwise the block is skipped and execution continues.
|
90
|
+
|
91
|
+
+r.on+ matches if all of the arguments match.
|
92
|
+
+r.is+ matches if all of the arguments match, and there are no
|
93
|
+
further entries in the path after matching.
|
94
|
+
+r.get+ when called without arguments matches any +GET+ request.
|
95
|
+
+r.get+ when called with any arguments matches only if the
|
96
|
+
current request is a +GET+ request and there are no further entries
|
97
|
+
in the path after matching.
|
98
|
+
+r.root+ only matches a +GET+ request where the current path is +/+.
|
99
|
+
|
100
|
+
If a routing method matches and control is yielded to the match
|
101
|
+
block, whenever the match block returns, Roda will return the
|
102
|
+
rack response array of status, headers, and body, to the caller.
|
103
|
+
|
104
|
+
If the match block returns a string and the response body hasn't
|
105
|
+
already been written to, the block return value will interpreted
|
106
|
+
as the body for the response. If none of the routing methods match
|
107
|
+
and the route block returns a string, it will be interpreted as the
|
108
|
+
body for the response.
|
86
109
|
|
87
110
|
+r.redirect+ immediately returns the response, allowing for
|
88
|
-
code such as <tt>r.redirect(path) if some_condition</tt>.
|
111
|
+
code such as <tt>r.redirect(path) if some_condition</tt>. If
|
112
|
+
called without arguments, it redirects to the current path if
|
113
|
+
the current request method is not +GET+.
|
89
114
|
|
90
115
|
The +.app+ at the end is an optimization, which you can leave
|
91
116
|
off, but which saves a few methods call for every response.
|
92
117
|
|
118
|
+
== The Routing Tree
|
119
|
+
|
120
|
+
Roda is called a routing tree web framework because the way most
|
121
|
+
sites are structured, routing takes the form of a tree based on the
|
122
|
+
URL structure of the site. In general, +r.on+ is used to split the
|
123
|
+
tree into different branches, and +r.is+ is finalizes the routing,
|
124
|
+
where the request is actually handled.
|
125
|
+
|
126
|
+
So a simple routing tree may look something like this:
|
127
|
+
|
128
|
+
r.on "a" do # /a branch
|
129
|
+
r.on "b" do # /a/b branch
|
130
|
+
r.is "c" do # /a/b/c request
|
131
|
+
r.get do end # GET /a/b/c request
|
132
|
+
r.post do end # POST /a/b/c request
|
133
|
+
end
|
134
|
+
r.get "d" do end # GET /a/b/d request
|
135
|
+
r.post "e" do end # POST /a/b/e request
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
It's also possible to handle the same requests, but structure the
|
140
|
+
routing tree by first branching on the request method:
|
141
|
+
|
142
|
+
r.get do # GET
|
143
|
+
r.on "a" do # GET /a branch
|
144
|
+
r.on "b" do # GET /a/b branch
|
145
|
+
r.is "c" do end # GET /a/b/c request
|
146
|
+
r.is "d" do end # GET /a/b/d request
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
r.post do # POST
|
152
|
+
r.on "a" do # POST /a branch
|
153
|
+
r.on "b" do # POST /a/b branch
|
154
|
+
r.is "c" do end # POST /a/b/c request
|
155
|
+
r.is "e" do end # POST /a/b/e request
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
This allows you to easily separate your +GET+ request handling from
|
161
|
+
your +POST+ request handling. If you only have a small number of
|
162
|
+
+POST+ request URLs and a large number of +GET+ request URLs, this
|
163
|
+
may make things easier.
|
164
|
+
|
165
|
+
However, in general routing first by the path and last by the
|
166
|
+
request method is likely to lead to simpler and DRYer code. This
|
167
|
+
is because at any point during the routing, you can act on the
|
168
|
+
request. For example, if all requests in the +/a+ branch need
|
169
|
+
need access permission +A+ and all requests in the +/a/b+ branch
|
170
|
+
need access permission +B+, you can easily handle this in the
|
171
|
+
routing tree:
|
172
|
+
|
173
|
+
r.on "a" do # /a branch
|
174
|
+
check_perm(:A)
|
175
|
+
r.on "b" do # /a/b branch
|
176
|
+
check_perm(:B)
|
177
|
+
r.is "c" do # /a/b/c request
|
178
|
+
r.get do end # GET /a/b/c request
|
179
|
+
r.post do end # POST /a/b/c request
|
180
|
+
end
|
181
|
+
r.get "d" do end # GET /a/b/d request
|
182
|
+
r.post "e" do end # POST /a/b/e request
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
Being able to operate on the request at any point during the
|
187
|
+
the routing is one of the major advantages of Roda compared
|
188
|
+
to other web frameworks that do not use a routing tree.
|
189
|
+
|
93
190
|
== Matchers
|
94
191
|
|
95
|
-
|
96
|
-
|
192
|
+
Other than +r.root+, the routing methods all take arguments called
|
193
|
+
matchers. If all of the matchers match, the routing method yields to
|
194
|
+
the match block. Here's an example showcasing how different
|
195
|
+
matchers work:
|
97
196
|
|
98
197
|
class App < Roda
|
99
198
|
route do |r|
|
@@ -101,7 +200,7 @@ are arguments passed to +r.on+.
|
|
101
200
|
r.get do
|
102
201
|
|
103
202
|
# /
|
104
|
-
r.
|
203
|
+
r.root do
|
105
204
|
"Home"
|
106
205
|
end
|
107
206
|
|
@@ -126,7 +225,6 @@ are arguments passed to +r.on+.
|
|
126
225
|
|
127
226
|
# /username/foobar/posts
|
128
227
|
r.is "posts" do
|
129
|
-
|
130
228
|
# You can access user here, because the blocks are closures.
|
131
229
|
"Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
|
132
230
|
end
|
@@ -148,7 +246,7 @@ are arguments passed to +r.on+.
|
|
148
246
|
r.is "login" do
|
149
247
|
|
150
248
|
# POST /login, user: foo, pass: baz
|
151
|
-
r.on
|
249
|
+
r.on({:param=>"user"}, {:param=>"pass"}) do |user, pass|
|
152
250
|
"#{user}:#{pass}" #=> "foo:baz"
|
153
251
|
end
|
154
252
|
|
@@ -170,75 +268,75 @@ The +/+ here is considered the empty segment.
|
|
170
268
|
If it does not contain a colon or slash, it matches single segment
|
171
269
|
with the text of the string, preceeded by a slash.
|
172
270
|
|
173
|
-
"" matches "/"
|
174
|
-
"foo" matches "/foo"
|
175
|
-
"foo" does not match "/food"
|
271
|
+
"" # matches "/"
|
272
|
+
"foo" # matches "/foo"
|
273
|
+
"foo" # does not match "/food"
|
176
274
|
|
177
275
|
If it contains any slashes, it matches one additional segment for
|
178
276
|
each slash:
|
179
277
|
|
180
|
-
"foo/bar" matches "/foo/bar"
|
181
|
-
"foo/bar" does not match "/foo/bard"
|
278
|
+
"foo/bar" # matches "/foo/bar"
|
279
|
+
"foo/bar" # does not match "/foo/bard"
|
182
280
|
|
183
281
|
If it contains a colon followed by any <tt>\\w</tt> characters, the colon and
|
184
282
|
remaing <tt>\\w</tt> characters matches any nonempty segment that contains at
|
185
283
|
least one character:
|
186
284
|
|
187
|
-
"foo/:id" matches "/foo/bar", "/foo/baz", etc.
|
188
|
-
"foo/:id" does not match "/fo/bar"
|
285
|
+
"foo/:id" # matches "/foo/bar", "/foo/baz", etc.
|
286
|
+
"foo/:id" # does not match "/fo/bar"
|
189
287
|
|
190
288
|
You can use multiple colons in a string:
|
191
289
|
|
192
|
-
":x/:y" matches "/foo/bar", "/bar/foo" etc.
|
193
|
-
":x/:y" does not match "/foo", "/bar/"
|
290
|
+
":x/:y" # matches "/foo/bar", "/bar/foo" etc.
|
291
|
+
":x/:y" # does not match "/foo", "/bar/"
|
194
292
|
|
195
293
|
You can prefix colons:
|
196
294
|
|
197
|
-
"foo:x/bar:y" matches "/food/bard", "/fool/bart", etc.
|
198
|
-
"foo:x/bar:y" does not match "/foo/bart", "/fool/bar", etc.
|
295
|
+
"foo:x/bar:y" # matches "/food/bard", "/fool/bart", etc.
|
296
|
+
"foo:x/bar:y" # does not match "/foo/bart", "/fool/bar", etc.
|
199
297
|
|
200
298
|
If any colons are used, the block will yield one argument for
|
201
299
|
each segment matched containing the matched text. So:
|
202
300
|
|
203
|
-
"foo:x/:y" matching "/fool/bar" yields "l", "bar"
|
301
|
+
"foo:x/:y" # matching "/fool/bar" yields "l", "bar"
|
204
302
|
|
205
303
|
Colons that are not followed by a <tt>\\w</tt> character are matched literally:
|
206
304
|
|
207
|
-
":/a" matches "/:/a"
|
305
|
+
":/a" # matches "/:/a"
|
208
306
|
|
209
307
|
Note that strings are regexp escaped before being used in a regular
|
210
308
|
expression, so:
|
211
309
|
|
212
|
-
"\\d+(/\\w+)?" matches "
|
213
|
-
"\\d
|
310
|
+
"\\d+(/\\w+)?" # matches "/\d+(/\w+)?"
|
311
|
+
"\\d+(/\\w+)?" # does not match "/123/abc"
|
214
312
|
|
215
313
|
=== Regexp
|
216
314
|
|
217
315
|
Regexps match one or more segments by looking for the pattern preceeded by a
|
218
316
|
slash:
|
219
317
|
|
220
|
-
/foo\w+/ matches "/foobar"
|
221
|
-
/foo\w+/ does not match "/foo/bar"
|
318
|
+
/foo\w+/ # matches "/foobar"
|
319
|
+
/foo\w+/ # does not match "/foo/bar"
|
222
320
|
|
223
321
|
If any patterns are captured by the regexp, they are yielded:
|
224
322
|
|
225
|
-
/foo\w+/ matches "/foobar", yields nothing
|
226
|
-
/foo(\w+)/ matches "/foobar", yields "bar"
|
323
|
+
/foo\w+/ # matches "/foobar", yields nothing
|
324
|
+
/foo(\w+)/ # matches "/foobar", yields "bar"
|
227
325
|
|
228
326
|
=== Symbol
|
229
327
|
|
230
328
|
Symbols match any nonempty segment, yielding the segment except for the
|
231
329
|
preceeding slash:
|
232
330
|
|
233
|
-
:id matches "/foo" yields "foo"
|
234
|
-
:id does not match "/"
|
331
|
+
:id # matches "/foo" yields "foo"
|
332
|
+
:id # does not match "/"
|
235
333
|
|
236
334
|
=== Proc
|
237
335
|
|
238
336
|
Procs match unless they return false or nil:
|
239
337
|
|
240
|
-
proc{true} matches anything
|
241
|
-
proc{false} does not match anything
|
338
|
+
proc{true} # matches anything
|
339
|
+
proc{false} # does not match anything
|
242
340
|
|
243
341
|
Procs don't capture anything by default, but they can if you add
|
244
342
|
the captured text to +r.captures+.
|
@@ -253,75 +351,81 @@ condition). Evaluation stops at the first matcher that matches.
|
|
253
351
|
Additionally, if the matched object is a String, the string is yielded.
|
254
352
|
This makes it easy to handle multiple strings without a Regexp:
|
255
353
|
|
256
|
-
|
257
|
-
[] does not match anything
|
354
|
+
['page1', 'page2'] # matches "/page1", "/page2"
|
355
|
+
[] # does not match anything
|
258
356
|
|
259
357
|
=== Hash
|
260
358
|
|
261
|
-
Hashes
|
262
|
-
and match if that matcher returns true.
|
263
|
-
|
359
|
+
Hashes allow easily calling specialized match methods on the request.
|
264
360
|
The default registered matchers included with Roda are documented below.
|
265
|
-
You can add your own hash matchers
|
266
|
-
|
361
|
+
You can add your own hash matchers using the +hash_matcher+ class method,
|
362
|
+
which creates an appropriate request match method. The +hash_matcher+
|
363
|
+
block will be called with the value of the hash.
|
267
364
|
|
268
365
|
class App < Roda
|
269
|
-
|
270
|
-
|
271
|
-
...
|
272
|
-
end
|
366
|
+
hash_matcher(:foo) do |v|
|
367
|
+
# ...
|
273
368
|
end
|
274
|
-
|
369
|
+
|
275
370
|
route do |r|
|
276
371
|
r.on :foo=>'bar' do
|
277
|
-
...
|
372
|
+
# ...
|
278
373
|
end
|
279
374
|
end
|
280
375
|
end
|
281
376
|
|
377
|
+
==== :all
|
378
|
+
|
379
|
+
The :all matcher matches if all of the entries in the given array matches. So
|
380
|
+
|
381
|
+
r.on :all=>[:a, :b] do
|
382
|
+
# ...
|
383
|
+
end
|
384
|
+
|
385
|
+
is the same as:
|
386
|
+
|
387
|
+
r.on :a, :b do
|
388
|
+
# ...
|
389
|
+
end
|
390
|
+
|
391
|
+
The reason it also exists as a separate hash matcher is so you can use it inside
|
392
|
+
an array matcher. so:
|
393
|
+
|
394
|
+
r.on ['foo', {:all=>['foos', :id]}] do
|
395
|
+
end
|
396
|
+
|
397
|
+
Would match +/foo+ and +/foos/10+, but not +/foos+.
|
282
398
|
|
283
399
|
==== :extension
|
284
400
|
|
285
401
|
The :extension matcher matches any nonempty path ending with the given extension:
|
286
402
|
|
287
|
-
:extension => "css" matches "/foo.css", "/bar.css"
|
288
|
-
:extension => "css" does not match "/foo.css/x", "/foo.bar", "/.css"
|
403
|
+
{:extension => "css"} # matches "/foo.css", "/bar.css"
|
404
|
+
{:extension => "css"} # does not match "/foo.css/x", "/foo.bar", "/.css"
|
289
405
|
|
290
|
-
This matcher yields the part before the extension.
|
291
|
-
matchers, this matcher assumes terminal behavior, it doesn't match if there
|
292
|
-
are additional segments.
|
406
|
+
This matcher yields the part before the extension.
|
293
407
|
|
294
408
|
==== :method
|
295
409
|
|
296
410
|
This matches the method of the request. You can provide an array to specify multiple
|
297
411
|
request methods and match on any of them:
|
298
412
|
|
299
|
-
:method => :post matches POST
|
300
|
-
:method =>
|
413
|
+
{:method => :post} # matches POST
|
414
|
+
{:method => ['post', 'patch']} # matches POST and PATCH
|
301
415
|
|
302
416
|
==== :param
|
303
417
|
|
304
418
|
The :param matcher matches if the given parameter is present, even if empty.
|
305
419
|
|
306
|
-
:param => "user" matches "/foo?user=bar", "/foo?user="
|
307
|
-
:param => "user" does not matches "/foo"
|
420
|
+
{:param => "user"} # matches "/foo?user=bar", "/foo?user="
|
421
|
+
{:param => "user"} # does not matches "/foo"
|
308
422
|
|
309
423
|
==== :param!
|
310
424
|
|
311
425
|
The :param! matcher matches if the given parameter is present and not empty.
|
312
426
|
|
313
|
-
:param! => "user" matches "/foo?user=bar"
|
314
|
-
:param! => "user" does not matches "/foo", "/foo?user="
|
315
|
-
|
316
|
-
==== :term
|
317
|
-
|
318
|
-
The :term matcher matches if true and there are no segments left. This matcher is
|
319
|
-
added by +r.is+ to ensure an exact path match.
|
320
|
-
|
321
|
-
:term => true matches ""
|
322
|
-
:term => true does not match "/"
|
323
|
-
:term => false matches "/"
|
324
|
-
:term => false does not match ""
|
427
|
+
{:param! => "user"} # matches "/foo?user=bar"
|
428
|
+
{:param! => "user"} # does not matches "/foo", "/foo?user="
|
325
429
|
|
326
430
|
=== false, nil
|
327
431
|
|
@@ -343,35 +447,8 @@ You can always set the status code manually via the status attribute
|
|
343
447
|
for the response.
|
344
448
|
|
345
449
|
route do |r|
|
346
|
-
r.get do
|
347
|
-
|
348
|
-
response.status = 200
|
349
|
-
end
|
350
|
-
end
|
351
|
-
end
|
352
|
-
|
353
|
-
== Security
|
354
|
-
|
355
|
-
If you want to protect against some common web application
|
356
|
-
vulnerabilities, you can use
|
357
|
-
{Rack::Protection}[https://github.com/rkh/rack-protection].
|
358
|
-
It is not included by default because there are legitimate
|
359
|
-
uses for plain Roda (for instance, when designing an API).
|
360
|
-
|
361
|
-
If you are using sessions, you should also always set a session
|
362
|
-
secret to some undisclosed value. Keep in mind that the content
|
363
|
-
in the session cookie is not encrypted, just signed to prevent
|
364
|
-
tampering.
|
365
|
-
|
366
|
-
require "roda"
|
367
|
-
require "rack/protection"
|
368
|
-
|
369
|
-
class App < Roda
|
370
|
-
use Rack::Session::Cookie, :secret => ENV['SECRET']
|
371
|
-
use Rack::Protection
|
372
|
-
|
373
|
-
route do |r|
|
374
|
-
# ...
|
450
|
+
r.get "hello" do
|
451
|
+
response.status = 200
|
375
452
|
end
|
376
453
|
end
|
377
454
|
|
@@ -379,22 +456,27 @@ tampering.
|
|
379
456
|
|
380
457
|
The main match method is +r.on+, but as displayed above, you can also
|
381
458
|
use +r.get+ or +r.post+. When called without any arguments, these
|
382
|
-
|
459
|
+
match as long as the request has the appropriate method, so:
|
460
|
+
|
461
|
+
r.get do end
|
462
|
+
|
463
|
+
matches any +GET+ request, and
|
383
464
|
|
384
|
-
|
465
|
+
r.post do end
|
385
466
|
|
386
|
-
|
467
|
+
matches any +POST+ request
|
387
468
|
|
388
|
-
|
469
|
+
If any arguments are given to the method, these match only
|
470
|
+
if the request method matches, all arguments match, and
|
471
|
+
only the path has been fully matched by the arguments. So:
|
389
472
|
|
390
|
-
|
391
|
-
the request has the appropriate method, so:
|
473
|
+
r.post "" do end
|
392
474
|
|
393
|
-
|
475
|
+
matches only +POST+ requests where the current path is +/+.
|
394
476
|
|
395
|
-
|
477
|
+
r.get "a/b" do end
|
396
478
|
|
397
|
-
|
479
|
+
matches only +GET+ requests where the current path is +/a/b+.
|
398
480
|
|
399
481
|
The reason for this difference in behavior is that if you are not
|
400
482
|
providing any arguments, you probably don't want to to also test
|
@@ -407,11 +489,26 @@ you do want, you can provide true as an argument:
|
|
407
489
|
end
|
408
490
|
|
409
491
|
If you want to match the request method and do a partial match
|
410
|
-
on the request path, you need to use
|
411
|
-
hash matcher:
|
492
|
+
on the request path instead of a full match, you need to use
|
493
|
+
+r.on+ with the <tt>:method</tt> hash matcher:
|
412
494
|
|
413
495
|
r.on "foo", :method=>:get do # Matches GET /foo(/.*)?
|
414
|
-
|
496
|
+
end
|
497
|
+
|
498
|
+
== Root Method
|
499
|
+
|
500
|
+
As displayed above, you can also use +r.root+ as a match method. This
|
501
|
+
method matches +GET+ requests where the current path +/+. +r.root+ is
|
502
|
+
similar to <tt>r.get ""</tt>, except that it does not consume the +/+ from the path.
|
503
|
+
|
504
|
+
Unlike the other matching methods, +r.root+ takes no arguments.
|
505
|
+
|
506
|
+
Note that +r.root+ does not match if the path is empty, you should use
|
507
|
+
<tt>r.get true</tt> for that. If you want to match either the
|
508
|
+
the empty path or +/+, you can use <tt>r.get ["", true]</tt>.
|
509
|
+
|
510
|
+
Note that +r.root+ does not match non-GET requests, so to handle
|
511
|
+
<tt>POST /</tt> requests, use <tt>r.post ''</tt>.
|
415
512
|
|
416
513
|
== Request and Response
|
417
514
|
|
@@ -430,8 +527,8 @@ methods, or via plugins.
|
|
430
527
|
|
431
528
|
== Pollution
|
432
529
|
|
433
|
-
Roda tries very hard to avoid polluting the scope
|
434
|
-
block
|
530
|
+
Roda tries very hard to avoid polluting the scope of the +route+
|
531
|
+
block. The only instance variables defined by default in the scope of
|
435
532
|
the +route+ block are <tt>@_request</tt> and <tt>@_response</tt>. The only methods defined
|
436
533
|
(beyond the default methods for +Object+) are: +env+, +opts+, +request+,
|
437
534
|
+response+, +call+, +session+, and +_route+ (private). Constants inside the
|
@@ -468,8 +565,8 @@ in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
|
|
468
565
|
|
469
566
|
== Composition
|
470
567
|
|
471
|
-
You can mount
|
472
|
-
|
568
|
+
You can mount any Rack app (including another Roda app), with its own middlewares,
|
569
|
+
inside a Roda app, using +r.run+:
|
473
570
|
|
474
571
|
class API < Roda
|
475
572
|
use SomeMiddleware
|
@@ -491,13 +588,24 @@ via +r.run+:
|
|
491
588
|
|
492
589
|
run App.app
|
493
590
|
|
494
|
-
|
591
|
+
This will take any path starting with +/api+ and send it to +API+. In this
|
592
|
+
example, +API+ is a Roda app, but it could easily be a Sinatra, Rails, or
|
593
|
+
other Rack app.
|
594
|
+
|
595
|
+
When you use +r.run+, Roda calls the given Rack app (+API+ in this
|
596
|
+
case), and whatever the Rack app returns will be returned as the response
|
597
|
+
for the current application.
|
598
|
+
|
599
|
+
=== multi_route plugin
|
600
|
+
|
601
|
+
If you are just looking to split up the main route block up by branches, you
|
602
|
+
should use the +multi_route+ plugin, which keeps the current scope of
|
495
603
|
the route block:
|
496
604
|
|
497
605
|
class App < Roda
|
498
606
|
plugin :multi_route
|
499
607
|
|
500
|
-
route
|
608
|
+
route "api" do |r|
|
501
609
|
r.is do
|
502
610
|
# ...
|
503
611
|
end
|
@@ -505,19 +613,22 @@ the route block:
|
|
505
613
|
|
506
614
|
route do |r|
|
507
615
|
r.on "api" do
|
508
|
-
route
|
616
|
+
r.route "api"
|
509
617
|
end
|
510
618
|
end
|
511
619
|
end
|
512
620
|
|
513
621
|
run App.app
|
514
622
|
|
623
|
+
This allows you to set instance variables in the main route block, and still
|
624
|
+
have access to them inside the +api+ route block.
|
625
|
+
|
515
626
|
== Testing
|
516
627
|
|
517
628
|
It is very easy to test Roda with {Rack::Test}[https://github.com/brynary/rack-test]
|
518
|
-
or {Capybara}[https://github.com/jnicklas/capybara]. Roda's own tests
|
519
|
-
|
520
|
-
|
629
|
+
or {Capybara}[https://github.com/jnicklas/capybara]. Roda's own tests use
|
630
|
+
{RSpec}[http://rspec.info]. The default rake task will run the specs for Roda, if
|
631
|
+
RSpec is installed.
|
521
632
|
|
522
633
|
== Settings
|
523
634
|
|
@@ -584,11 +695,84 @@ You can override the default rendering options by passing a hash to the plugin,
|
|
584
695
|
or modifying the +render_opts+ hash after loading the plugin:
|
585
696
|
|
586
697
|
class App < Roda
|
587
|
-
plugin :render, :
|
698
|
+
plugin :render, :escape => true # Automatically escape output in erb templates
|
588
699
|
render_opts[:views] = 'admin_views' # Default views directory
|
589
700
|
render_opts[:layout] = "admin_layout" # Default layout template
|
590
701
|
render_opts[:layout_opts] = {:engine=>'haml'} # Default layout template options
|
591
702
|
render_opts[:opts] = {:default_encoding=>'UTF-8'} # Default template options
|
703
|
+
render_opts[:cache] = false # Disable template caching
|
704
|
+
render_opts[:engine] = 'slim' # Tilt engine/template file extension to use
|
705
|
+
end
|
706
|
+
|
707
|
+
== Sessions
|
708
|
+
|
709
|
+
By default, Roda doesn't turn on sessions, but most users are going to
|
710
|
+
want to turn on session support, and the simplest way to do that is to
|
711
|
+
use the <tt>Rack::Session::Cookie</tt> middleware that comes with rack:
|
712
|
+
|
713
|
+
require "roda"
|
714
|
+
|
715
|
+
class App < Roda
|
716
|
+
use Rack::Session::Cookie, :secret => ENV['SECRET']
|
717
|
+
end
|
718
|
+
|
719
|
+
== Security
|
720
|
+
|
721
|
+
Web application security is a very large topic, but here are some
|
722
|
+
things you can do with Roda to prevent some common web application
|
723
|
+
vulnerabilities.
|
724
|
+
|
725
|
+
=== Session Security
|
726
|
+
|
727
|
+
If you are using sessions, you should also always set a session
|
728
|
+
secret using the +:secret+ option as shown above. Make sure this
|
729
|
+
secret is not disclosed, because if an attacker knows the +:secret+
|
730
|
+
value, they can inject arbitrary session values, which in the worst case
|
731
|
+
scenario can lead to remote code execution.
|
732
|
+
|
733
|
+
Keep in mind that with <tt>Rack::Session::Cookie</tt>, the content in
|
734
|
+
the session cookie is not encrypted, just signed to prevent tampering.
|
735
|
+
This means you should not store any data in the session that itself is
|
736
|
+
secret.
|
737
|
+
|
738
|
+
=== Cross Site Request Forgery (CSRF)
|
739
|
+
|
740
|
+
CSRF can be prevented by using the +csrf+ plugin that ships with Roda,
|
741
|
+
which uses the {rack_csrf}[https://github.com/baldowl/rack_csrf]
|
742
|
+
library. Just make sure that you include the CSRF token tags in your
|
743
|
+
html as appropriate.
|
744
|
+
|
745
|
+
It's also possible to use the <tt>Rack::Csrf</tt> middleware directly,
|
746
|
+
you don't have to use the +csrf+ plugin.
|
747
|
+
|
748
|
+
=== Cross Site Scripting (XSS)
|
749
|
+
|
750
|
+
The easiest way to prevent XSS with Roda is to use a template library
|
751
|
+
that automatically escapes output by default. The +:escape+ option
|
752
|
+
to the render plugin sets the ERB template processor to escape by
|
753
|
+
default, so that in your templates:
|
754
|
+
|
755
|
+
<%= '<>' %> # outputs <>
|
756
|
+
<%== '<>' %> # outputs <>
|
757
|
+
|
758
|
+
Note that unlike most other render options, the :escape option
|
759
|
+
must be passed to the <tt>plugin :render</tt> call, it won't be
|
760
|
+
respected if added later.
|
761
|
+
|
762
|
+
This support requires {Erubis}[http://www.kuwata-lab.com/erubis/].
|
763
|
+
|
764
|
+
=== Other
|
765
|
+
|
766
|
+
For prevention of some other vulnerabilities, such as click-jacking,
|
767
|
+
directory traversal, session hijacking, and IP spoofing, consider using
|
768
|
+
{Rack::Protection}[https://github.com/rkh/rack-protection], which is
|
769
|
+
a rack middleware that can be added the usual way:
|
770
|
+
|
771
|
+
require 'roda'
|
772
|
+
require 'rack/protection'
|
773
|
+
|
774
|
+
class App < Roda
|
775
|
+
use Rack::Protection
|
592
776
|
end
|
593
777
|
|
594
778
|
== Plugins
|
@@ -601,27 +785,46 @@ override any Roda method and call +super+ to get the default behavior.
|
|
601
785
|
These plugins ship with roda:
|
602
786
|
|
603
787
|
all_verbs :: Adds routing methods to the request for all http verbs.
|
788
|
+
backtracking_array :: Allows array matchers to backtrack if later matchers
|
789
|
+
do not match.
|
790
|
+
content_for :: Allows storage of content in one template and retrieval of
|
791
|
+
that content in a different template.
|
792
|
+
csrf :: Adds CSRF protection and helper methods using
|
793
|
+
{rack_csrf}[https://github.com/baldowl/rack_csrf].
|
604
794
|
default_headers :: Override the default response headers used.
|
605
795
|
error_handler :: Adds a +error+ block that is called for all responses that
|
606
796
|
raise exceptions.
|
607
|
-
flash :: Adds a flash handler
|
797
|
+
flash :: Adds a flash handler.
|
608
798
|
h :: Adds h method for html escaping.
|
609
799
|
halt :: Augments request#halt method to take status and/or body or status,
|
610
800
|
headers, and body.
|
801
|
+
head :: Treat HEAD requests like GET requests with an empty response body.
|
611
802
|
header_matchers :: Adds host, header, and accept hash matchers.
|
612
803
|
hooks :: Adds before and after methods to run code before and after requests.
|
613
804
|
indifferent_params :: Adds params method with indifferent access to params,
|
614
805
|
allowing use of symbol keys for accessing params.
|
806
|
+
json :: Allows match blocks to return arrays and hashes, using a json
|
807
|
+
representation as the response body.
|
615
808
|
middleware :: Allows the Roda app to be used as a rack middleware, calling the
|
616
809
|
next middleware if no route matches.
|
617
810
|
multi_route :: Adds the ability for multiple named route blocks, with the
|
618
811
|
ability to dispatch to them add any point in the main route block.
|
812
|
+
not_allowed :: Adds support for automatically returning 405 Method Not Allowed
|
813
|
+
responses.
|
619
814
|
not_found :: Adds a +not_found+ block that is called for all 404 responses
|
620
815
|
without bodies.
|
621
816
|
pass :: Adds a pass method allowing you to skip the current +r.on+ block as if
|
622
817
|
it did not match.
|
818
|
+
per_thread_caching :: Switches the thread-safe cache from a shared cache to a
|
819
|
+
per-thread cache.
|
623
820
|
render :: Adds support for rendering templates via tilt, as described above.
|
821
|
+
render_each :: Render a template for each value in an enumerable.
|
624
822
|
streaming :: Adds support for streaming responses.
|
823
|
+
symbol_matchers :: Adds support for symbol-specific matching regexps.
|
824
|
+
symbol_views :: Allows match blocks to return template name symbols, uses the
|
825
|
+
template view as the response body.
|
826
|
+
view_subdirs :: Allows for setting a view subdirectory to use on a per-request
|
827
|
+
basis.
|
625
828
|
|
626
829
|
=== External Plugins
|
627
830
|
|
@@ -633,13 +836,15 @@ autoforme :: Adds support for easily creating a simple administrative front
|
|
633
836
|
|
634
837
|
=== How to create plugins
|
635
838
|
|
636
|
-
Authoring your own plugins is pretty straightforward. Plugins are just modules
|
637
|
-
|
839
|
+
Authoring your own plugins is pretty straightforward. Plugins are just modules,
|
840
|
+
which may contain any of the following modules:
|
638
841
|
|
639
842
|
InstanceMethods :: module included in the Roda class
|
640
843
|
ClassMethods :: module that extends the Roda class
|
641
844
|
RequestMethods :: module included in the class of the request
|
845
|
+
RequestClassMethods :: module extending the class of the request
|
642
846
|
ResponseMethods :: module included in the class of the response
|
847
|
+
ResponseClassMethods :: module extending the class of the response
|
643
848
|
|
644
849
|
If the plugin responds to +load_dependencies+, it will be called first, and should
|
645
850
|
be used if the plugin depends on another plugin.
|
@@ -667,9 +872,9 @@ So a simple plugin to add an instance method would be:
|
|
667
872
|
If you want to ship a Roda plugin in a gem, but still have
|
668
873
|
Roda load it automatically via <tt>Roda.plugin :plugin_name</tt>, you should
|
669
874
|
place it where it can be required via +roda/plugins/plugin_name+, and
|
670
|
-
then have the file register it as a plugin via
|
671
|
-
It's recommended but not required
|
672
|
-
in the <tt>Roda::RodaPlugins</tt> namespace:
|
875
|
+
then have the file register it as a plugin via
|
876
|
+
<tt>Roda::RodaPlugins.register_plugin</tt>. It's recommended but not required
|
877
|
+
that you store your plugin module in the <tt>Roda::RodaPlugins</tt> namespace:
|
673
878
|
|
674
879
|
module Roda
|
675
880
|
module RodaPlugins
|
@@ -680,26 +885,16 @@ in the <tt>Roda::RodaPlugins</tt> namespace:
|
|
680
885
|
end
|
681
886
|
end
|
682
887
|
end
|
683
|
-
end
|
684
888
|
|
685
|
-
|
889
|
+
register_plugin :markdown, Markdown
|
890
|
+
end
|
686
891
|
end
|
687
892
|
|
688
893
|
You should avoid creating your module directly in the +Roda+ namespace
|
689
894
|
to avoid polluting the namespace. Additionally, any instance variables
|
690
|
-
created inside
|
895
|
+
created inside InstanceMethods should be prefixed with an underscore
|
691
896
|
(e.g. <tt>@_variable</tt>) to avoid polluting the scope.
|
692
897
|
|
693
|
-
== Inspiration
|
694
|
-
|
695
|
-
Roda was inspired by {Sinatra}[http://www.sinatrarb.com] and {Cuba}[http://cuba.is],
|
696
|
-
two other Ruby web frameworks. It started out as a fork of Cuba, from which it borrows
|
697
|
-
the idea of using a routing tree (which Cuba in turn took from
|
698
|
-
{Rum}[https://github.com/chneukirchen/rum]). From Sinatra it takes the ideas that
|
699
|
-
route blocks should return the request bodies and that routes should be canonical.
|
700
|
-
It pilfers the idea for an extensible plugin system from the Ruby database library
|
701
|
-
{Sequel}[http://sequel.jeremyevans.net].
|
702
|
-
|
703
898
|
== License
|
704
899
|
|
705
900
|
MIT
|