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.
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