roda 0.9.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|