roda 0.9.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +362 -167
  4. data/Rakefile +2 -2
  5. data/doc/release_notes/1.0.0.txt +329 -0
  6. data/lib/roda.rb +553 -180
  7. data/lib/roda/plugins/_erubis_escaping.rb +28 -0
  8. data/lib/roda/plugins/all_verbs.rb +7 -9
  9. data/lib/roda/plugins/backtracking_array.rb +92 -0
  10. data/lib/roda/plugins/content_for.rb +46 -0
  11. data/lib/roda/plugins/csrf.rb +60 -0
  12. data/lib/roda/plugins/flash.rb +53 -7
  13. data/lib/roda/plugins/halt.rb +8 -14
  14. data/lib/roda/plugins/head.rb +56 -0
  15. data/lib/roda/plugins/header_matchers.rb +2 -2
  16. data/lib/roda/plugins/json.rb +84 -0
  17. data/lib/roda/plugins/multi_route.rb +50 -10
  18. data/lib/roda/plugins/not_allowed.rb +140 -0
  19. data/lib/roda/plugins/pass.rb +13 -6
  20. data/lib/roda/plugins/per_thread_caching.rb +70 -0
  21. data/lib/roda/plugins/render.rb +20 -33
  22. data/lib/roda/plugins/render_each.rb +61 -0
  23. data/lib/roda/plugins/symbol_matchers.rb +79 -0
  24. data/lib/roda/plugins/symbol_views.rb +40 -0
  25. data/lib/roda/plugins/view_subdirs.rb +53 -0
  26. data/lib/roda/version.rb +3 -0
  27. data/spec/matchers_spec.rb +61 -5
  28. data/spec/plugin/_erubis_escaping_spec.rb +29 -0
  29. data/spec/plugin/backtracking_array_spec.rb +38 -0
  30. data/spec/plugin/content_for_spec.rb +34 -0
  31. data/spec/plugin/csrf_spec.rb +49 -0
  32. data/spec/plugin/flash_spec.rb +69 -5
  33. data/spec/plugin/head_spec.rb +35 -0
  34. data/spec/plugin/json_spec.rb +50 -0
  35. data/spec/plugin/multi_route_spec.rb +22 -6
  36. data/spec/plugin/not_allowed_spec.rb +55 -0
  37. data/spec/plugin/pass_spec.rb +8 -2
  38. data/spec/plugin/per_thread_caching_spec.rb +28 -0
  39. data/spec/plugin/render_each_spec.rb +30 -0
  40. data/spec/plugin/render_spec.rb +7 -1
  41. data/spec/plugin/symbol_matchers_spec.rb +68 -0
  42. data/spec/plugin/symbol_views_spec.rb +32 -0
  43. data/spec/plugin/view_subdirs_spec.rb +45 -0
  44. data/spec/plugin_spec.rb +11 -1
  45. data/spec/redirect_spec.rb +21 -4
  46. data/spec/request_spec.rb +9 -0
  47. metadata +49 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ab3cd69eb4146c87b08dc9d58ac4c6cb40e98e71
4
- data.tar.gz: f276b865b831438274335aa559ef1cbfe743f045
3
+ metadata.gz: 6ee4a240b4a2d17e55d10cc84f60b28ffd8fcf76
4
+ data.tar.gz: 50af03ce96aadac19fa0e9c81e0cb0ea86e90461
5
5
  SHA512:
6
- metadata.gz: 04b6479269bea8bffa930f735a11d22436d791eb04531ac65248ab88b9eb7096146929c9538ab4589f0160c41839b1c488bfcc71adf0cd6c3fa3376277973b9f
7
- data.tar.gz: 603135eaa279af4980f7e799decc28ee288bfbdcb86ebe1ccd66c84ee48c531edbcb032d9f7e1f68d80b08a8a02d1f98cc96e4534f6e7e86b9630428a1f8fc2e
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
@@ -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
- # matches any GET request
29
- r.get do
38
+ # GET / request
39
+ r.root do
40
+ r.redirect "/hello"
41
+ end
30
42
 
31
- # matches GET /
32
- r.is "" do
33
- r.redirect "/hello"
34
- end
43
+ # /hello branch
44
+ r.on "hello" do
35
45
 
36
- # matches GET /hello or GET /hello/.*
37
- r.on "hello" do
46
+ # GET /hello/world request
47
+ r.get "world" do
48
+ "Hello world!"
49
+ end
38
50
 
39
- # matches GET /hello/world
40
- r.is "world" do
41
- "Hello world!"
51
+ # /hello request
52
+ r.is do
53
+ # GET /hello request
54
+ r.get do
55
+ "Hello!"
42
56
  end
43
57
 
44
- # matches GET /hello
45
- r.is do
46
- "Hello!"
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+, or a method like +r.get+ or +r.is+ which calls +r.on+.
70
- +r.on+ takes each of the arguments given and tries to match them to
71
- the current request. If it is able to successfully match
72
- all of the arguments, it yields to the +r.on+ block, otherwise
73
- it returns immediately.
74
-
75
- +r.get+ is a shortcut that matches any GET request, and
76
- +r.is+ is a shortcut that ensures the the exact route is
77
- matched and there are no further entries in the path.
78
-
79
- If +r.on+ matches and control is yielded to the block, whenever
80
- the block returns, the response will be returned. If the block
81
- returns a string and the response body hasn't already been
82
- written to, the block return value will interpreted as the body
83
- for the response. If none of the +r.on+ blocks match and the
84
- route block returns a string, it will be interpreted as the body
85
- for the response.
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
- Here's an example showcasing how different matchers work. Matchers
96
- are arguments passed to +r.on+.
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.is "" do
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 {:param=>"user"}, {:param=>"pass"} do |user, pass|
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 "\d+(/\w+)?"
213
- "\\d+/\\w+" does not match "123/abc"
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
- %w'page1 page2' matches "/page1", "/page2"
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 call a <tt>match_*</tt> method with the given key using the hash value,
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 by adding the appropriate <tt>match_*</tt>
266
- method to the request class using the +request_module+ method:
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
- request_module do
270
- def match_foo(v)
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. Note that unlike other
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 => %w'post patch' matches POST and PATCH
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
- r.is "hello" do
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
- call +r.on+ as long as the request has the appropriate method, so:
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
- r.get{}
465
+ r.post do end
385
466
 
386
- is syntax sugar for:
467
+ matches any +POST+ request
387
468
 
388
- r.on{} if r.get?
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
- If any arguments are given to the method, these call +r.is+ as long as
391
- the request has the appropriate method, so:
473
+ r.post "" do end
392
474
 
393
- r.post(""){}
475
+ matches only +POST+ requests where the current path is +/+.
394
476
 
395
- is syntax sugar for:
477
+ r.get "a/b" do end
396
478
 
397
- r.is(""){} if r.post?
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 +r.on+ with the <tt>:method</tt>
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
- edn
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 in which the +route+
434
- block operates. The only instance variables defined by default in the scope of
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 a Roda app, along with middlewares, inside another Roda app,
472
- via +r.run+:
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
- You can also use the +multi_route+ plugin, which keeps the current scope of
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 :api do |r|
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 :api
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 are written
519
- with a combination of {RSpec}[http://rspec.info] and Rack::Test
520
- The default rake task will run the specs for Roda, if RSpec is installed.
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, :engine=>'slim' # Tilt engine/template file extension to use
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 &lt;&gt;
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, requires sinatra-flash.
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
- that contain one of the following modules:
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 +Roda.register_plugin+.
671
- It's recommended but not required that you store your plugin module
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
- register_plugin :markdown, RodaPlugins::Markdown
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 an InstanceMethods should be prefixed with an underscore
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