roda 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG +3 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +709 -0
- data/Rakefile +124 -0
- data/lib/roda.rb +608 -0
- data/lib/roda/plugins/all_verbs.rb +48 -0
- data/lib/roda/plugins/default_headers.rb +50 -0
- data/lib/roda/plugins/error_handler.rb +69 -0
- data/lib/roda/plugins/flash.rb +62 -0
- data/lib/roda/plugins/h.rb +24 -0
- data/lib/roda/plugins/halt.rb +79 -0
- data/lib/roda/plugins/header_matchers.rb +57 -0
- data/lib/roda/plugins/hooks.rb +106 -0
- data/lib/roda/plugins/indifferent_params.rb +47 -0
- data/lib/roda/plugins/middleware.rb +88 -0
- data/lib/roda/plugins/multi_route.rb +77 -0
- data/lib/roda/plugins/not_found.rb +62 -0
- data/lib/roda/plugins/pass.rb +34 -0
- data/lib/roda/plugins/render.rb +217 -0
- data/lib/roda/plugins/streaming.rb +165 -0
- data/spec/composition_spec.rb +19 -0
- data/spec/env_spec.rb +11 -0
- data/spec/integration_spec.rb +63 -0
- data/spec/matchers_spec.rb +658 -0
- data/spec/module_spec.rb +29 -0
- data/spec/opts_spec.rb +42 -0
- data/spec/plugin/all_verbs_spec.rb +29 -0
- data/spec/plugin/default_headers_spec.rb +63 -0
- data/spec/plugin/error_handler_spec.rb +67 -0
- data/spec/plugin/flash_spec.rb +59 -0
- data/spec/plugin/h_spec.rb +13 -0
- data/spec/plugin/halt_spec.rb +62 -0
- data/spec/plugin/header_matchers_spec.rb +61 -0
- data/spec/plugin/hooks_spec.rb +97 -0
- data/spec/plugin/indifferent_params_spec.rb +13 -0
- data/spec/plugin/middleware_spec.rb +52 -0
- data/spec/plugin/multi_route_spec.rb +98 -0
- data/spec/plugin/not_found_spec.rb +99 -0
- data/spec/plugin/pass_spec.rb +23 -0
- data/spec/plugin/render_spec.rb +148 -0
- data/spec/plugin/streaming_spec.rb +52 -0
- data/spec/plugin_spec.rb +61 -0
- data/spec/redirect_spec.rb +24 -0
- data/spec/request_spec.rb +55 -0
- data/spec/response_spec.rb +131 -0
- data/spec/session_spec.rb +35 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/version_spec.rb +8 -0
- metadata +148 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: ab3cd69eb4146c87b08dc9d58ac4c6cb40e98e71
|
4
|
+
data.tar.gz: f276b865b831438274335aa559ef1cbfe743f045
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 04b6479269bea8bffa930f735a11d22436d791eb04531ac65248ab88b9eb7096146929c9538ab4589f0160c41839b1c488bfcc71adf0cd6c3fa3376277973b9f
|
7
|
+
data.tar.gz: 603135eaa279af4980f7e799decc28ee288bfbdcb86ebe1ccd66c84ee48c531edbcb032d9f7e1f68d80b08a8a02d1f98cc96e4534f6e7e86b9630428a1f8fc2e
|
data/CHANGELOG
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2014 Jeremy Evans
|
2
|
+
Copyright (c) 2010, 2011 Michel Martens, Damian Janowski and Cyril David
|
3
|
+
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
6
|
+
in the Software without restriction, including without limitation the rights
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
9
|
+
furnished to do so, subject to the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be included in
|
12
|
+
all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
20
|
+
THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,709 @@
|
|
1
|
+
= Roda
|
2
|
+
|
3
|
+
Roda is a routing tree web framework.
|
4
|
+
|
5
|
+
= Installation
|
6
|
+
|
7
|
+
$ gem install roda
|
8
|
+
|
9
|
+
== Resources
|
10
|
+
|
11
|
+
Website :: http://roda.jeremyevans.net
|
12
|
+
Source :: http://github.com/jeremyevans/roda
|
13
|
+
Bugs :: http://github.com/jeremyevans/roda/issues
|
14
|
+
Google Group :: http://groups.google.com/group/ruby-roda
|
15
|
+
IRC :: irc://chat.freenode.net/#roda
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
|
19
|
+
Here's a simple application, showing how the routing tree works:
|
20
|
+
|
21
|
+
# cat config.ru
|
22
|
+
require "roda"
|
23
|
+
|
24
|
+
class App < Roda
|
25
|
+
use Rack::Session::Cookie, :secret => ENV['SECRET']
|
26
|
+
|
27
|
+
route do |r|
|
28
|
+
# matches any GET request
|
29
|
+
r.get do
|
30
|
+
|
31
|
+
# matches GET /
|
32
|
+
r.is "" do
|
33
|
+
r.redirect "/hello"
|
34
|
+
end
|
35
|
+
|
36
|
+
# matches GET /hello or GET /hello/.*
|
37
|
+
r.on "hello" do
|
38
|
+
|
39
|
+
# matches GET /hello/world
|
40
|
+
r.is "world" do
|
41
|
+
"Hello world!"
|
42
|
+
end
|
43
|
+
|
44
|
+
# matches GET /hello
|
45
|
+
r.is do
|
46
|
+
"Hello!"
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
run App.app
|
54
|
+
|
55
|
+
You can now run +rackup+ and enjoy what you have just created.
|
56
|
+
|
57
|
+
Here's a breakdown of what is going on in the above block:
|
58
|
+
|
59
|
+
After requiring the library and subclassing Roda, the +use+ method
|
60
|
+
is called, which loads a rack middleware into the current
|
61
|
+
application.
|
62
|
+
|
63
|
+
The +route+ block is called whenever a new request comes in,
|
64
|
+
and it is yielded an instance of a subclass of <tt>Rack::Request</tt>
|
65
|
+
with some additional methods for matching routes. By
|
66
|
+
convention, this argument should be named +r+.
|
67
|
+
|
68
|
+
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.
|
86
|
+
|
87
|
+
+r.redirect+ immediately returns the response, allowing for
|
88
|
+
code such as <tt>r.redirect(path) if some_condition</tt>.
|
89
|
+
|
90
|
+
The +.app+ at the end is an optimization, which you can leave
|
91
|
+
off, but which saves a few methods call for every response.
|
92
|
+
|
93
|
+
== Matchers
|
94
|
+
|
95
|
+
Here's an example showcasing how different matchers work. Matchers
|
96
|
+
are arguments passed to +r.on+.
|
97
|
+
|
98
|
+
class App < Roda
|
99
|
+
route do |r|
|
100
|
+
# only GET requests
|
101
|
+
r.get do
|
102
|
+
|
103
|
+
# /
|
104
|
+
r.is "" do
|
105
|
+
"Home"
|
106
|
+
end
|
107
|
+
|
108
|
+
# /about
|
109
|
+
r.is "about" do
|
110
|
+
"About"
|
111
|
+
end
|
112
|
+
|
113
|
+
# /styles/basic.css
|
114
|
+
r.is "styles", :extension => "css" do |file|
|
115
|
+
"Filename: #{file}" #=> "Filename: basic"
|
116
|
+
end
|
117
|
+
|
118
|
+
# /post/2011/02/16/hello
|
119
|
+
r.is "post/:y/:m/:d/:slug" do |y, m, d, slug|
|
120
|
+
"#{y}-#{m}-#{d} #{slug}" #=> "2011-02-16 hello"
|
121
|
+
end
|
122
|
+
|
123
|
+
# /username/foobar
|
124
|
+
r.on "username/:username" do |username|
|
125
|
+
user = User.find_by_username(username) # username == "foobar"
|
126
|
+
|
127
|
+
# /username/foobar/posts
|
128
|
+
r.is "posts" do
|
129
|
+
|
130
|
+
# You can access user here, because the blocks are closures.
|
131
|
+
"Total Posts: #{user.posts.size}" #=> "Total Posts: 6"
|
132
|
+
end
|
133
|
+
|
134
|
+
# /username/foobar/following
|
135
|
+
r.is "following" do
|
136
|
+
user.following.size.to_s #=> "1301"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# /search?q=barbaz
|
141
|
+
r.is "search", :param=>"q" do |query|
|
142
|
+
"Searched for #{query}" #=> "Searched for barbaz"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
# only POST requests
|
147
|
+
r.post do
|
148
|
+
r.is "login" do
|
149
|
+
|
150
|
+
# POST /login, user: foo, pass: baz
|
151
|
+
r.on {:param=>"user"}, {:param=>"pass"} do |user, pass|
|
152
|
+
"#{user}:#{pass}" #=> "foo:baz"
|
153
|
+
end
|
154
|
+
|
155
|
+
# If the params user and pass are not provided, this
|
156
|
+
# will get executed.
|
157
|
+
"You need to provide user and pass!"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
Here's a description of the matchers. Note that segment as used
|
164
|
+
here means one part of the path preceeded by a +/+. So a path such
|
165
|
+
as +/foo/bar//baz+ has 4 segments, +/foo+, +/bar+, +/+ and +/baz+.
|
166
|
+
The +/+ here is considered the empty segment.
|
167
|
+
|
168
|
+
=== String
|
169
|
+
|
170
|
+
If it does not contain a colon or slash, it matches single segment
|
171
|
+
with the text of the string, preceeded by a slash.
|
172
|
+
|
173
|
+
"" matches "/"
|
174
|
+
"foo" matches "/foo"
|
175
|
+
"foo" does not match "/food"
|
176
|
+
|
177
|
+
If it contains any slashes, it matches one additional segment for
|
178
|
+
each slash:
|
179
|
+
|
180
|
+
"foo/bar" matches "/foo/bar"
|
181
|
+
"foo/bar" does not match "/foo/bard"
|
182
|
+
|
183
|
+
If it contains a colon followed by any <tt>\\w</tt> characters, the colon and
|
184
|
+
remaing <tt>\\w</tt> characters matches any nonempty segment that contains at
|
185
|
+
least one character:
|
186
|
+
|
187
|
+
"foo/:id" matches "/foo/bar", "/foo/baz", etc.
|
188
|
+
"foo/:id" does not match "/fo/bar"
|
189
|
+
|
190
|
+
You can use multiple colons in a string:
|
191
|
+
|
192
|
+
":x/:y" matches "/foo/bar", "/bar/foo" etc.
|
193
|
+
":x/:y" does not match "/foo", "/bar/"
|
194
|
+
|
195
|
+
You can prefix colons:
|
196
|
+
|
197
|
+
"foo:x/bar:y" matches "/food/bard", "/fool/bart", etc.
|
198
|
+
"foo:x/bar:y" does not match "/foo/bart", "/fool/bar", etc.
|
199
|
+
|
200
|
+
If any colons are used, the block will yield one argument for
|
201
|
+
each segment matched containing the matched text. So:
|
202
|
+
|
203
|
+
"foo:x/:y" matching "/fool/bar" yields "l", "bar"
|
204
|
+
|
205
|
+
Colons that are not followed by a <tt>\\w</tt> character are matched literally:
|
206
|
+
|
207
|
+
":/a" matches "/:/a"
|
208
|
+
|
209
|
+
Note that strings are regexp escaped before being used in a regular
|
210
|
+
expression, so:
|
211
|
+
|
212
|
+
"\\d+(/\\w+)?" matches "\d+(/\w+)?"
|
213
|
+
"\\d+/\\w+" does not match "123/abc"
|
214
|
+
|
215
|
+
=== Regexp
|
216
|
+
|
217
|
+
Regexps match one or more segments by looking for the pattern preceeded by a
|
218
|
+
slash:
|
219
|
+
|
220
|
+
/foo\w+/ matches "/foobar"
|
221
|
+
/foo\w+/ does not match "/foo/bar"
|
222
|
+
|
223
|
+
If any patterns are captured by the regexp, they are yielded:
|
224
|
+
|
225
|
+
/foo\w+/ matches "/foobar", yields nothing
|
226
|
+
/foo(\w+)/ matches "/foobar", yields "bar"
|
227
|
+
|
228
|
+
=== Symbol
|
229
|
+
|
230
|
+
Symbols match any nonempty segment, yielding the segment except for the
|
231
|
+
preceeding slash:
|
232
|
+
|
233
|
+
:id matches "/foo" yields "foo"
|
234
|
+
:id does not match "/"
|
235
|
+
|
236
|
+
=== Proc
|
237
|
+
|
238
|
+
Procs match unless they return false or nil:
|
239
|
+
|
240
|
+
proc{true} matches anything
|
241
|
+
proc{false} does not match anything
|
242
|
+
|
243
|
+
Procs don't capture anything by default, but they can if you add
|
244
|
+
the captured text to +r.captures+.
|
245
|
+
|
246
|
+
=== Arrays
|
247
|
+
|
248
|
+
Arrays match when any of their elements matches. If multiple matchers
|
249
|
+
are given to +r.on+, they all must match (an AND condition), while
|
250
|
+
if an array of matchers is given, only one needs to match (an OR
|
251
|
+
condition). Evaluation stops at the first matcher that matches.
|
252
|
+
|
253
|
+
Additionally, if the matched object is a String, the string is yielded.
|
254
|
+
This makes it easy to handle multiple strings without a Regexp:
|
255
|
+
|
256
|
+
%w'page1 page2' matches "/page1", "/page2"
|
257
|
+
[] does not match anything
|
258
|
+
|
259
|
+
=== Hash
|
260
|
+
|
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
|
+
|
264
|
+
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:
|
267
|
+
|
268
|
+
class App < Roda
|
269
|
+
request_module do
|
270
|
+
def match_foo(v)
|
271
|
+
...
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
route do |r|
|
276
|
+
r.on :foo=>'bar' do
|
277
|
+
...
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
|
283
|
+
==== :extension
|
284
|
+
|
285
|
+
The :extension matcher matches any nonempty path ending with the given extension:
|
286
|
+
|
287
|
+
:extension => "css" matches "/foo.css", "/bar.css"
|
288
|
+
:extension => "css" does not match "/foo.css/x", "/foo.bar", "/.css"
|
289
|
+
|
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.
|
293
|
+
|
294
|
+
==== :method
|
295
|
+
|
296
|
+
This matches the method of the request. You can provide an array to specify multiple
|
297
|
+
request methods and match on any of them:
|
298
|
+
|
299
|
+
:method => :post matches POST
|
300
|
+
:method => %w'post patch' matches POST and PATCH
|
301
|
+
|
302
|
+
==== :param
|
303
|
+
|
304
|
+
The :param matcher matches if the given parameter is present, even if empty.
|
305
|
+
|
306
|
+
:param => "user" matches "/foo?user=bar", "/foo?user="
|
307
|
+
:param => "user" does not matches "/foo"
|
308
|
+
|
309
|
+
==== :param!
|
310
|
+
|
311
|
+
The :param! matcher matches if the given parameter is present and not empty.
|
312
|
+
|
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 ""
|
325
|
+
|
326
|
+
=== false, nil
|
327
|
+
|
328
|
+
If false or nil is given directly as a matcher, it doesn't match anything.
|
329
|
+
|
330
|
+
=== Everything else
|
331
|
+
|
332
|
+
Everything else matches anything.
|
333
|
+
|
334
|
+
== Status codes
|
335
|
+
|
336
|
+
When it comes time to finalize a response, if a status code has not
|
337
|
+
been set manually, it will use a 200 status code if anything has been
|
338
|
+
written to the response, otherwise it will use a 404 status code.
|
339
|
+
This enables the principle of least surprise to work, where if you
|
340
|
+
don't handle an action, a 404 response is assumed.
|
341
|
+
|
342
|
+
You can always set the status code manually via the status attribute
|
343
|
+
for the response.
|
344
|
+
|
345
|
+
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
|
+
# ...
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
== Verb Methods
|
379
|
+
|
380
|
+
The main match method is +r.on+, but as displayed above, you can also
|
381
|
+
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:
|
383
|
+
|
384
|
+
r.get{}
|
385
|
+
|
386
|
+
is syntax sugar for:
|
387
|
+
|
388
|
+
r.on{} if r.get?
|
389
|
+
|
390
|
+
If any arguments are given to the method, these call +r.is+ as long as
|
391
|
+
the request has the appropriate method, so:
|
392
|
+
|
393
|
+
r.post(""){}
|
394
|
+
|
395
|
+
is syntax sugar for:
|
396
|
+
|
397
|
+
r.is(""){} if r.post?
|
398
|
+
|
399
|
+
The reason for this difference in behavior is that if you are not
|
400
|
+
providing any arguments, you probably don't want to to also test
|
401
|
+
for an exact match with the current path. If that is something
|
402
|
+
you do want, you can provide true as an argument:
|
403
|
+
|
404
|
+
r.on "foo" do
|
405
|
+
r.get true do # Matches GET /foo, not GET /foo/.*
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
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:
|
412
|
+
|
413
|
+
r.on "foo", :method=>:get do # Matches GET /foo(/.*)?
|
414
|
+
edn
|
415
|
+
|
416
|
+
== Request and Response
|
417
|
+
|
418
|
+
While the request object is yielded to the route block, it is also
|
419
|
+
available via the +request+ method. Likewise, the response object
|
420
|
+
is available via the +response+ method.
|
421
|
+
|
422
|
+
The request object is an instance of a subclass of <tt>Rack::Request</tt>
|
423
|
+
with some additional methods, and the response object is an
|
424
|
+
instance of a subclass of <tt>Rack::Response</tt> with some additional
|
425
|
+
methods.
|
426
|
+
|
427
|
+
If you want to extend the request and response objects with additional
|
428
|
+
modules, you can do so via the +request_module+ or +response_module+
|
429
|
+
methods, or via plugins.
|
430
|
+
|
431
|
+
== Pollution
|
432
|
+
|
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
|
435
|
+
the +route+ block are <tt>@_request</tt> and <tt>@_response</tt>. The only methods defined
|
436
|
+
(beyond the default methods for +Object+) are: +env+, +opts+, +request+,
|
437
|
+
+response+, +call+, +session+, and +_route+ (private). Constants inside the
|
438
|
+
Roda namespace are all prefixed with +Roda+ (e.g. <tt>Roda::RodaRequest</tt>). This
|
439
|
+
should make it unlikely that Roda will cause a namespace issue with your
|
440
|
+
application code.
|
441
|
+
|
442
|
+
== Captures
|
443
|
+
|
444
|
+
You may have noticed that some matchers yield a value to the block. The rules
|
445
|
+
for determining if a matcher will yield a value are simple:
|
446
|
+
|
447
|
+
1. Regexp captures: <tt>/posts\/(\d+)-(.*)/</tt> will yield two values, corresponding to each capture.
|
448
|
+
2. String placeholders: <tt>"users/:id"</tt> will yield the value in the position of +:id+.
|
449
|
+
3. Symbols: +:foobar+ will yield if a segment is available.
|
450
|
+
4. File extensions: <tt>:extension=>"css"</tt> will yield the basename of the matched file.
|
451
|
+
5. Parameters: <tt>:param=>"user"</tt> will yield the value of the parameter user, if present.
|
452
|
+
|
453
|
+
The first case is important because it shows the underlying effect of regex
|
454
|
+
captures.
|
455
|
+
|
456
|
+
In the second case, the substring +:id+ gets replaced by <tt>([^\\/]+)</tt> and the
|
457
|
+
regexp becomes <tt>/users\/([^\/]+)/</tt> before performing the match, thus it reverts
|
458
|
+
to the first form we saw.
|
459
|
+
|
460
|
+
In the third case, the symbol, no matter what it says, gets replaced
|
461
|
+
by <tt>/([^\\/]+)/</tt>, and again we are in presence of case 1.
|
462
|
+
|
463
|
+
The fourth case, again, reverts to the basic matcher: it generates the string
|
464
|
+
<tt>/([^\/]+?)\.#{ext}\z/</tt> before performing the match.
|
465
|
+
|
466
|
+
The fifth case is different: it checks if the the parameter supplied is present
|
467
|
+
in the request (via POST or QUERY_STRING) and it pushes the value as a capture.
|
468
|
+
|
469
|
+
== Composition
|
470
|
+
|
471
|
+
You can mount a Roda app, along with middlewares, inside another Roda app,
|
472
|
+
via +r.run+:
|
473
|
+
|
474
|
+
class API < Roda
|
475
|
+
use SomeMiddleware
|
476
|
+
|
477
|
+
route do |r|
|
478
|
+
r.is do
|
479
|
+
# ...
|
480
|
+
end
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
class App < Roda
|
485
|
+
route do |r|
|
486
|
+
r.on "api" do
|
487
|
+
r.run API
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
run App.app
|
493
|
+
|
494
|
+
You can also use the +multi_route+ plugin, which keeps the current scope of
|
495
|
+
the route block:
|
496
|
+
|
497
|
+
class App < Roda
|
498
|
+
plugin :multi_route
|
499
|
+
|
500
|
+
route :api do |r|
|
501
|
+
r.is do
|
502
|
+
# ...
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
route do |r|
|
507
|
+
r.on "api" do
|
508
|
+
route :api
|
509
|
+
end
|
510
|
+
end
|
511
|
+
end
|
512
|
+
|
513
|
+
run App.app
|
514
|
+
|
515
|
+
== Testing
|
516
|
+
|
517
|
+
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.
|
521
|
+
|
522
|
+
== Settings
|
523
|
+
|
524
|
+
Each Roda app can store settings in the +opts+ hash. The settings are
|
525
|
+
inherited if you happen to subclass +Roda+.
|
526
|
+
|
527
|
+
Roda.opts[:layout] = "guest"
|
528
|
+
|
529
|
+
class Users < Roda; end
|
530
|
+
class Admin < Roda; end
|
531
|
+
|
532
|
+
Admin.opts[:layout] = "admin"
|
533
|
+
|
534
|
+
Users.opts[:layout] # => 'guest'
|
535
|
+
Admin.opts[:layout] # => 'admin'
|
536
|
+
|
537
|
+
Feel free to store whatever you find convenient. Note that when subclassing,
|
538
|
+
Roda only does a shallow clone of the settings. If you store nested structures
|
539
|
+
and plan to mutate them in subclasses, it is your responsibility to dup the nested
|
540
|
+
structures inside +Roda.inherited+ (making sure to call +super+). The
|
541
|
+
plugins that ship with Roda all handle this. Also, note that this means that
|
542
|
+
future modifications to the parent class after subclassing do not affect the
|
543
|
+
subclass.
|
544
|
+
|
545
|
+
== Rendering
|
546
|
+
|
547
|
+
Roda ships with a +render+ plugin that provides helpers for rendering templates. It uses
|
548
|
+
{Tilt}[https://github.com/rtomayko/tilt], a gem that interfaces with many template
|
549
|
+
engines. The +erb+ engine is used by default.
|
550
|
+
|
551
|
+
Note that in order to use this plugin you need to have Tilt installed, along
|
552
|
+
with the templating engines you want to use.
|
553
|
+
|
554
|
+
This plugin adds the +render+ and +view+ methods, for rendering templates.
|
555
|
+
The difference between +render+ and +view+ is that +view+ will by default
|
556
|
+
attempt to render the template inside the default layout template, where
|
557
|
+
+render+ will just render the template.
|
558
|
+
|
559
|
+
class App < Roda
|
560
|
+
plugin :render
|
561
|
+
|
562
|
+
route do |r|
|
563
|
+
@var = '1'
|
564
|
+
|
565
|
+
r.is "render" do
|
566
|
+
# Renders the views/home.erb template, which will have access to the
|
567
|
+
# instance variable @var, as well as local variable content
|
568
|
+
render("home", :locals=>{:content => "hello, world"})
|
569
|
+
end
|
570
|
+
|
571
|
+
r.is "view" do
|
572
|
+
@var2 = '1'
|
573
|
+
|
574
|
+
# Renders the views/home.erb template, which will have access to the
|
575
|
+
# instance variables @var and @var2, and takes the output of that and
|
576
|
+
# renders it inside views/layout.erb (which should yield where the
|
577
|
+
# content should be inserted).
|
578
|
+
view("home")
|
579
|
+
end
|
580
|
+
end
|
581
|
+
end
|
582
|
+
|
583
|
+
You can override the default rendering options by passing a hash to the plugin,
|
584
|
+
or modifying the +render_opts+ hash after loading the plugin:
|
585
|
+
|
586
|
+
class App < Roda
|
587
|
+
plugin :render, :engine=>'slim' # Tilt engine/template file extension to use
|
588
|
+
render_opts[:views] = 'admin_views' # Default views directory
|
589
|
+
render_opts[:layout] = "admin_layout" # Default layout template
|
590
|
+
render_opts[:layout_opts] = {:engine=>'haml'} # Default layout template options
|
591
|
+
render_opts[:opts] = {:default_encoding=>'UTF-8'} # Default template options
|
592
|
+
end
|
593
|
+
|
594
|
+
== Plugins
|
595
|
+
|
596
|
+
Roda provides a way to extend its functionality with plugins. Plugins can
|
597
|
+
override any Roda method and call +super+ to get the default behavior.
|
598
|
+
|
599
|
+
=== Included Plugins
|
600
|
+
|
601
|
+
These plugins ship with roda:
|
602
|
+
|
603
|
+
all_verbs :: Adds routing methods to the request for all http verbs.
|
604
|
+
default_headers :: Override the default response headers used.
|
605
|
+
error_handler :: Adds a +error+ block that is called for all responses that
|
606
|
+
raise exceptions.
|
607
|
+
flash :: Adds a flash handler, requires sinatra-flash.
|
608
|
+
h :: Adds h method for html escaping.
|
609
|
+
halt :: Augments request#halt method to take status and/or body or status,
|
610
|
+
headers, and body.
|
611
|
+
header_matchers :: Adds host, header, and accept hash matchers.
|
612
|
+
hooks :: Adds before and after methods to run code before and after requests.
|
613
|
+
indifferent_params :: Adds params method with indifferent access to params,
|
614
|
+
allowing use of symbol keys for accessing params.
|
615
|
+
middleware :: Allows the Roda app to be used as a rack middleware, calling the
|
616
|
+
next middleware if no route matches.
|
617
|
+
multi_route :: Adds the ability for multiple named route blocks, with the
|
618
|
+
ability to dispatch to them add any point in the main route block.
|
619
|
+
not_found :: Adds a +not_found+ block that is called for all 404 responses
|
620
|
+
without bodies.
|
621
|
+
pass :: Adds a pass method allowing you to skip the current +r.on+ block as if
|
622
|
+
it did not match.
|
623
|
+
render :: Adds support for rendering templates via tilt, as described above.
|
624
|
+
streaming :: Adds support for streaming responses.
|
625
|
+
|
626
|
+
=== External Plugins
|
627
|
+
|
628
|
+
The following libraries include Roda plugins:
|
629
|
+
|
630
|
+
forme :: Adds support for easy HTML form creation in erb templates.
|
631
|
+
autoforme :: Adds support for easily creating a simple administrative front
|
632
|
+
end for Sequel models.
|
633
|
+
|
634
|
+
=== How to create plugins
|
635
|
+
|
636
|
+
Authoring your own plugins is pretty straightforward. Plugins are just modules
|
637
|
+
that contain one of the following modules:
|
638
|
+
|
639
|
+
InstanceMethods :: module included in the Roda class
|
640
|
+
ClassMethods :: module that extends the Roda class
|
641
|
+
RequestMethods :: module included in the class of the request
|
642
|
+
ResponseMethods :: module included in the class of the response
|
643
|
+
|
644
|
+
If the plugin responds to +load_dependencies+, it will be called first, and should
|
645
|
+
be used if the plugin depends on another plugin.
|
646
|
+
|
647
|
+
If the plugin responds to +configure+, it will be called last, and should be
|
648
|
+
used to configure the plugin.
|
649
|
+
|
650
|
+
Both +load_dependencies+ and +configure+ are called with the additional arguments
|
651
|
+
and block given to the plugin call.
|
652
|
+
|
653
|
+
So a simple plugin to add an instance method would be:
|
654
|
+
|
655
|
+
module MarkdownHelper
|
656
|
+
module InstanceMethods
|
657
|
+
def markdown(str)
|
658
|
+
BlueCloth.new(str).to_html
|
659
|
+
end
|
660
|
+
end
|
661
|
+
end
|
662
|
+
|
663
|
+
Roda.plugin MarkdownHelper
|
664
|
+
|
665
|
+
=== Registering plugins
|
666
|
+
|
667
|
+
If you want to ship a Roda plugin in a gem, but still have
|
668
|
+
Roda load it automatically via <tt>Roda.plugin :plugin_name</tt>, you should
|
669
|
+
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:
|
673
|
+
|
674
|
+
module Roda
|
675
|
+
module RodaPlugins
|
676
|
+
module Markdown
|
677
|
+
module InstanceMethods
|
678
|
+
def markdown(str)
|
679
|
+
BlueCloth.new(str).to_html
|
680
|
+
end
|
681
|
+
end
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
register_plugin :markdown, RodaPlugins::Markdown
|
686
|
+
end
|
687
|
+
|
688
|
+
You should avoid creating your module directly in the +Roda+ namespace
|
689
|
+
to avoid polluting the namespace. Additionally, any instance variables
|
690
|
+
created inside an InstanceMethods should be prefixed with an underscore
|
691
|
+
(e.g. <tt>@_variable</tt>) to avoid polluting the scope.
|
692
|
+
|
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
|
+
== License
|
704
|
+
|
705
|
+
MIT
|
706
|
+
|
707
|
+
== Maintainer
|
708
|
+
|
709
|
+
Jeremy Evans <code@jeremyevans.net>
|