roda 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +70 -0
- data/README.rdoc +261 -302
- data/Rakefile +1 -1
- data/doc/release_notes/1.2.0.txt +406 -0
- data/lib/roda.rb +206 -124
- data/lib/roda/plugins/all_verbs.rb +11 -10
- data/lib/roda/plugins/assets.rb +5 -5
- data/lib/roda/plugins/backtracking_array.rb +12 -5
- data/lib/roda/plugins/caching.rb +10 -8
- data/lib/roda/plugins/class_level_routing.rb +94 -0
- data/lib/roda/plugins/content_for.rb +6 -0
- data/lib/roda/plugins/default_headers.rb +4 -11
- data/lib/roda/plugins/delay_build.rb +42 -0
- data/lib/roda/plugins/delegate.rb +64 -0
- data/lib/roda/plugins/drop_body.rb +33 -0
- data/lib/roda/plugins/empty_root.rb +48 -0
- data/lib/roda/plugins/environments.rb +68 -0
- data/lib/roda/plugins/error_email.rb +1 -2
- data/lib/roda/plugins/error_handler.rb +1 -1
- data/lib/roda/plugins/halt.rb +7 -5
- data/lib/roda/plugins/head.rb +4 -2
- data/lib/roda/plugins/header_matchers.rb +17 -9
- data/lib/roda/plugins/hooks.rb +16 -32
- data/lib/roda/plugins/json.rb +4 -10
- data/lib/roda/plugins/mailer.rb +233 -0
- data/lib/roda/plugins/match_affix.rb +48 -0
- data/lib/roda/plugins/multi_route.rb +9 -11
- data/lib/roda/plugins/multi_run.rb +81 -0
- data/lib/roda/plugins/named_templates.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +43 -48
- data/lib/roda/plugins/path.rb +63 -2
- data/lib/roda/plugins/render.rb +79 -48
- data/lib/roda/plugins/render_each.rb +6 -0
- data/lib/roda/plugins/sinatra_helpers.rb +523 -0
- data/lib/roda/plugins/slash_path_empty.rb +25 -0
- data/lib/roda/plugins/static_path_info.rb +64 -0
- data/lib/roda/plugins/streaming.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +12 -8
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +33 -0
- data/spec/plugin/backtracking_array_spec.rb +24 -18
- data/spec/plugin/class_level_routing_spec.rb +138 -0
- data/spec/plugin/delay_build_spec.rb +23 -0
- data/spec/plugin/delegate_spec.rb +20 -0
- data/spec/plugin/drop_body_spec.rb +20 -0
- data/spec/plugin/empty_root_spec.rb +14 -0
- data/spec/plugin/environments_spec.rb +31 -0
- data/spec/plugin/h_spec.rb +1 -3
- data/spec/plugin/header_matchers_spec.rb +14 -0
- data/spec/plugin/hooks_spec.rb +3 -5
- data/spec/plugin/mailer_spec.rb +191 -0
- data/spec/plugin/match_affix_spec.rb +22 -0
- data/spec/plugin/multi_run_spec.rb +31 -0
- data/spec/plugin/named_templates_spec.rb +65 -0
- data/spec/plugin/path_spec.rb +66 -2
- data/spec/plugin/render_spec.rb +46 -1
- data/spec/plugin/sinatra_helpers_spec.rb +534 -0
- data/spec/plugin/slash_path_empty_spec.rb +22 -0
- data/spec/plugin/static_path_info_spec.rb +50 -0
- data/spec/request_spec.rb +23 -0
- data/spec/response_spec.rb +12 -1
- metadata +48 -6
data/Rakefile
CHANGED
@@ -17,7 +17,7 @@ end
|
|
17
17
|
|
18
18
|
### RDoc
|
19
19
|
|
20
|
-
RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework']
|
20
|
+
RDOC_DEFAULT_OPTS = ["--line-numbers", "--inline-source", '--title', 'Roda: Routing tree web framework toolkit']
|
21
21
|
|
22
22
|
begin
|
23
23
|
gem 'hanna-nouveau'
|
@@ -0,0 +1,406 @@
|
|
1
|
+
= New Plugins
|
2
|
+
|
3
|
+
* A static_path_info plugin has been added, which doesn't modify
|
4
|
+
SCRIPT_NAME/PATH_INFO during routing, only before dispatching
|
5
|
+
the request to another rack application via r.run. This is
|
6
|
+
faster and avoids problems caused by changing SCRIPT_NAME/PATH_INFO
|
7
|
+
during routing, such as methods that return paths that depend on
|
8
|
+
SCRIPT_NAME. This behavior will become Roda's default starting
|
9
|
+
in Roda 2, and it is recommended that all Roda apps use it.
|
10
|
+
|
11
|
+
* A mailer plugin has been added, which allows you to use Roda's
|
12
|
+
render plugin to create email bodies, and allows you to use Roda's
|
13
|
+
routing tree features to DRY up your mailing code similar to how it
|
14
|
+
DRYs up your web code.
|
15
|
+
|
16
|
+
Here is an example routing tree using the mailer plugin:
|
17
|
+
|
18
|
+
class Mailer < Roda
|
19
|
+
plugin :render
|
20
|
+
plugin :mailer
|
21
|
+
|
22
|
+
route do |r|
|
23
|
+
r.on "user/:d" do |user_id|
|
24
|
+
# DRY up code by setting shared behavior in higher level
|
25
|
+
# branches, instead of duplicating it inside each subtree.
|
26
|
+
@user = User[user_id]
|
27
|
+
from 'notifications@example.com'
|
28
|
+
to @user.email
|
29
|
+
|
30
|
+
r.mail "open_account" do
|
31
|
+
subject 'Welcome to example.com'
|
32
|
+
render(:open_account)
|
33
|
+
end
|
34
|
+
|
35
|
+
r.mail "close_account" do
|
36
|
+
subject 'Thank you for using example.com'
|
37
|
+
render(:close_account)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
With your routing tree setup, you can use the sendmail method to
|
44
|
+
send email:
|
45
|
+
|
46
|
+
Mailer.sendmail("/user/1/open_account")
|
47
|
+
|
48
|
+
If you want a Mail::Message object returned for further modification
|
49
|
+
before sending, you can use mail instead of of sendmail:
|
50
|
+
|
51
|
+
Mailer.mail("/user/2/close_account").deliver
|
52
|
+
|
53
|
+
* A delegate plugin has been added, allowing you to easily create
|
54
|
+
methods in the route block scope that delegate to the request or
|
55
|
+
response. While Roda does not pollute your namespaces by default,
|
56
|
+
this allows you to choose to do so yourself if you find it offers
|
57
|
+
a nicer API. Example:
|
58
|
+
|
59
|
+
class App < Roda
|
60
|
+
plugin :delegate
|
61
|
+
request_delegate :root, :on, :is, :get, :post, :redirect
|
62
|
+
|
63
|
+
route do |r|
|
64
|
+
root do
|
65
|
+
redirect "/hello"
|
66
|
+
end
|
67
|
+
|
68
|
+
on "hello" do
|
69
|
+
get "world" do
|
70
|
+
"Hello world!"
|
71
|
+
end
|
72
|
+
|
73
|
+
is do
|
74
|
+
get do
|
75
|
+
"Hello!"
|
76
|
+
end
|
77
|
+
|
78
|
+
post do
|
79
|
+
puts "Someone said hello!"
|
80
|
+
redirect
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
* A class_level_routing plugin has been added, allowing you to define
|
88
|
+
your routes at the class level if desired. The routes defined at
|
89
|
+
the class level can still use a routing tree for further routing.
|
90
|
+
Example:
|
91
|
+
|
92
|
+
class App < Roda
|
93
|
+
plugin :class_level_routing
|
94
|
+
|
95
|
+
root do
|
96
|
+
request.redirect "/hello"
|
97
|
+
end
|
98
|
+
|
99
|
+
get "hello/world" do
|
100
|
+
"Hello world!"
|
101
|
+
end
|
102
|
+
|
103
|
+
is "hello" do
|
104
|
+
request.get do
|
105
|
+
"Hello!"
|
106
|
+
end
|
107
|
+
|
108
|
+
request.post do
|
109
|
+
puts "Someone said hello!"
|
110
|
+
request.redirect
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
* A named_templates plugin has been added, for creating inline
|
116
|
+
templates associated with a given name, that are used by
|
117
|
+
the render plugin's render/view method in preference to
|
118
|
+
templates stored in the filesystem. This makes it simpler to
|
119
|
+
ship single-file Roda applications that use templates. Example:
|
120
|
+
|
121
|
+
class App < Roda
|
122
|
+
plugin :named_templates
|
123
|
+
|
124
|
+
template :layout do
|
125
|
+
"<html><body><%= yield %></body></html>"
|
126
|
+
end
|
127
|
+
template :index do
|
128
|
+
"<p>Hello <%= @user %>!</p>"
|
129
|
+
end
|
130
|
+
|
131
|
+
route do |r|
|
132
|
+
@user = 'You'
|
133
|
+
render(:index)
|
134
|
+
end
|
135
|
+
# => "<html><body><p>Hello You!</p></body></html>"
|
136
|
+
end
|
137
|
+
|
138
|
+
* A multi_run plugin has been added, for dispatching to multiple
|
139
|
+
rack applications based on the request path prefix. This
|
140
|
+
provides a similar API as the multi_route plugin, but allows
|
141
|
+
you to separate your applications per routing subtree, as
|
142
|
+
opposed to multi_route which uses the same application for
|
143
|
+
all routing subtrees.
|
144
|
+
|
145
|
+
With the multi_run plugin, you call the class level run method
|
146
|
+
with the routing prefix and the rack application to use, and
|
147
|
+
you call r.multi_run to dispatch to all of the applications
|
148
|
+
based on the prefix.
|
149
|
+
|
150
|
+
class App < Roda
|
151
|
+
plugin :multi_run
|
152
|
+
|
153
|
+
run "foo", Foo
|
154
|
+
run "bar", Bar
|
155
|
+
run "baz", Baz
|
156
|
+
|
157
|
+
route do |r|
|
158
|
+
r.multi_run
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
In this case, Foo, Bar, and Baz, can be subclasses of App, which
|
163
|
+
allows them to share methods that should be shared, but still
|
164
|
+
define methods themselves that are not shared by the other
|
165
|
+
applications.
|
166
|
+
|
167
|
+
* A sinatra_helpers plugin has been added, that ports over most
|
168
|
+
of the Sinatra::Helpers methods that haven't already been added
|
169
|
+
by other plugins. All of the methods are added either to the
|
170
|
+
request or response class as appropriate. By default, delegate
|
171
|
+
methods are also added to the route block scope, but you can
|
172
|
+
turn this off by passing a :delegate=>false option when loading
|
173
|
+
the plugin, which avoids polluting the route block namespace.
|
174
|
+
|
175
|
+
The sinatra_helpers plugin adds the following request methods:
|
176
|
+
|
177
|
+
* back
|
178
|
+
* error
|
179
|
+
* not_found
|
180
|
+
* uri
|
181
|
+
* send_file
|
182
|
+
|
183
|
+
And the following response methods:
|
184
|
+
|
185
|
+
* body
|
186
|
+
* body=
|
187
|
+
* status
|
188
|
+
* headers
|
189
|
+
* mime_type
|
190
|
+
* content_type
|
191
|
+
* attachment
|
192
|
+
* informational?
|
193
|
+
* success?
|
194
|
+
* redirect?
|
195
|
+
* client_error?
|
196
|
+
* not_found?
|
197
|
+
* server_error?
|
198
|
+
|
199
|
+
* A slash_path_empty plugin has been added, which changes Roda
|
200
|
+
so that "/" is considered an empty path when doing a
|
201
|
+
terminal match via r.is or r.get/r.post with a path.
|
202
|
+
|
203
|
+
class App < Roda
|
204
|
+
plugin :slash_path_empty
|
205
|
+
|
206
|
+
route do |r|
|
207
|
+
r.get "albums" do
|
208
|
+
# matches both GET /albums and GET /albums/
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
* An empty_root plugin has been added, which makes r.root match
|
214
|
+
the empty string, in addition to /. This can be useful in
|
215
|
+
cases where a partial match on the patch has been completed.
|
216
|
+
|
217
|
+
class App < Roda
|
218
|
+
plugin :empty_root
|
219
|
+
|
220
|
+
route do |r|
|
221
|
+
r.on "albums" do
|
222
|
+
r.root do
|
223
|
+
# matches both GET /albums and GET /albums/
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
* A match_affix plugin has been added, for overriding the default
|
230
|
+
prefix/suffix used in match patterns. For example, if you want
|
231
|
+
to require that a leading / be specified in your routes. and
|
232
|
+
you want to consume any trailing slash:
|
233
|
+
|
234
|
+
class App < Roda
|
235
|
+
plugin :match_affix, "", /(\/|\z)/
|
236
|
+
|
237
|
+
route do |r|
|
238
|
+
r.on "/albums" do |s|
|
239
|
+
# GET /albums # s => ""
|
240
|
+
# GET /albums/ # s => "/"
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
* An environments plugin has been added, giving some simple
|
246
|
+
helpers for executing code in different environments. Example:
|
247
|
+
|
248
|
+
class App < Roda
|
249
|
+
plugin :environments
|
250
|
+
|
251
|
+
environment # => :development
|
252
|
+
development? # => true
|
253
|
+
test? # => false
|
254
|
+
production? # => false
|
255
|
+
|
256
|
+
# Set the environment for the application
|
257
|
+
self.environment = :test
|
258
|
+
test? # => true
|
259
|
+
|
260
|
+
configure do
|
261
|
+
# called, as no environments given
|
262
|
+
end
|
263
|
+
|
264
|
+
configure :development, :production do
|
265
|
+
# not called, as no environments match
|
266
|
+
end
|
267
|
+
|
268
|
+
configure :test do
|
269
|
+
# called, as environment given matches current environment
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
* A drop_body plugin has been added, which automatically drops the
|
274
|
+
body, Content-Type header, and Content-Length header when the
|
275
|
+
response status indicates no body (100-102, 204, 205, 304).
|
276
|
+
|
277
|
+
* A delay_build plugin has been added, which delays building the
|
278
|
+
rack application until Roda.app is called, and only rebuilds the
|
279
|
+
rack application if build! is called. This removes O(n^2)
|
280
|
+
performance in the pathological case of adding a route block
|
281
|
+
and then calling Roda.use many times to add middlewares, though
|
282
|
+
you have to add a few hundred middlewares for the difference
|
283
|
+
to be noticeable.
|
284
|
+
|
285
|
+
= New Features
|
286
|
+
|
287
|
+
* r.remaining_path and r.matched_path have been added for returning
|
288
|
+
the remaining path that will be used for matching, and for
|
289
|
+
returning the path already matched. Currently, these just provide
|
290
|
+
the PATH_INFO and SCRIPT_NAME, but starting in Roda 2 PATH_INFO
|
291
|
+
and SCRIPT_NAME will not be modified during routing, and you'll
|
292
|
+
need to use these methods if you want to find out the remaining
|
293
|
+
or already matched paths.
|
294
|
+
|
295
|
+
* The render plugin now supports a :template option to render/view
|
296
|
+
to specify the template to use, instead of requiring a separate
|
297
|
+
argument.
|
298
|
+
|
299
|
+
* The render plugin now supports a :template_class option, allowing
|
300
|
+
you to override the default template class that Roda would use.
|
301
|
+
|
302
|
+
* The render plugin now supports a :template_block option, specifying
|
303
|
+
the block to pass when creating a template.
|
304
|
+
|
305
|
+
* The path class method added by the path plugin now accepts :name,
|
306
|
+
:url, :url_only, and :add_script_name options:
|
307
|
+
|
308
|
+
:name :: Specifies name for method
|
309
|
+
:url :: Creates a url method in addition to a path method
|
310
|
+
:url_only :: Only creates a url method, not a path method
|
311
|
+
:add_script_name :: prefixes the path with SCRIPT_NAME
|
312
|
+
|
313
|
+
Note that if you plan to use :add_script_name, you should use
|
314
|
+
the static_path_info plugin so that the method created does not
|
315
|
+
return different results depending on where you are in the
|
316
|
+
routing tree.
|
317
|
+
|
318
|
+
* A :user_agent hash matcher has been added to the header_matchers
|
319
|
+
plugin.
|
320
|
+
|
321
|
+
* An inherit_middleware class accessor has been added. This can
|
322
|
+
be set to false if you do not want subclasses to inherit
|
323
|
+
middleware from the superclass. This is useful if the
|
324
|
+
superclass dispatches to the subclass via r.run, as otherwise
|
325
|
+
it would have to run the the same middleware stack twice.
|
326
|
+
|
327
|
+
* A clear_middleware! class accessor has been added, allowing
|
328
|
+
you to clear the current middleware stack.
|
329
|
+
|
330
|
+
* RodaRequest#default_redirect_status has been added, allowing
|
331
|
+
plugins to override the default status used for redirect if
|
332
|
+
a status is not given.
|
333
|
+
|
334
|
+
* Roda{Request,Response}#roda_class has been added, which
|
335
|
+
returns the Roda class related to the given request/response.
|
336
|
+
|
337
|
+
= Other Improvements
|
338
|
+
|
339
|
+
* The render plugin no longer caches templates by default if
|
340
|
+
RACK_ENV is development.
|
341
|
+
|
342
|
+
* When subclassing a Roda app, unfrozen Array/Hash entries in the
|
343
|
+
opts hash are now duped into the subclass, so the subclass
|
344
|
+
no longer needs to dup them manually. Note that plugins that
|
345
|
+
use nested arrays/hashes in the opts hash still need to dup
|
346
|
+
manually inside ClassMethods#inherited. For the plugins where
|
347
|
+
it is possible, it is recommended to store plugin options in a
|
348
|
+
frozen object in the opts hash, and require loading the plugin
|
349
|
+
again to modify the plugin options.
|
350
|
+
|
351
|
+
* Caching of templates is now fixed when the render/view :opts is
|
352
|
+
used to specify template options per-call.
|
353
|
+
|
354
|
+
* An explicit :default_encoding of nil in the render plugin's
|
355
|
+
:opts hash is no longer overwritten with
|
356
|
+
Encoding.default_external.
|
357
|
+
|
358
|
+
* Roda#session now returns the same object as RodaRequest#session.
|
359
|
+
|
360
|
+
* The view_subdirs, content_for, and render_each plugins now all
|
361
|
+
depend on the render plugin.
|
362
|
+
|
363
|
+
* The not_allowed plugin now depends on the all_verbs plugin.
|
364
|
+
|
365
|
+
* Local/instance variables are now used in more places instead of
|
366
|
+
method calls, improving performance.
|
367
|
+
|
368
|
+
= Backwards Compatibility
|
369
|
+
|
370
|
+
* The render plugin's render/view methods no longer pass the given
|
371
|
+
hash directly to the underlying template. To pass options to the
|
372
|
+
template engine, use a separate hash under the :opts key:
|
373
|
+
|
374
|
+
render :file, :opts=>{:foo=>'bar'}
|
375
|
+
|
376
|
+
This is more consistent with the class-level render plugin options,
|
377
|
+
which also uses :opts to pass options to the template engine.
|
378
|
+
|
379
|
+
The :js_opts and :css_opts options to the assets plugin are now
|
380
|
+
passed as the :opts hash, so they continue to affect the template
|
381
|
+
engine, so they no longer specify general render method options.
|
382
|
+
|
383
|
+
* Modifying render_opts :layout after loading the render plugin
|
384
|
+
now has no effect. You need to use plugin :render, :layout=>'...'
|
385
|
+
to set the layout to use now.
|
386
|
+
|
387
|
+
* Default headers are not set on a response until the response is
|
388
|
+
finished. This allows you to check for header presence during
|
389
|
+
routing to detect whether the header was specifically set for the
|
390
|
+
current request.
|
391
|
+
|
392
|
+
* RodaRequest.consume_pattern no longer captures anything by default.
|
393
|
+
Previously, it did so in order to update SCRIPT_NAME, but that is
|
394
|
+
now handled differently. This should only affect external plugins
|
395
|
+
that attempt to override RodaRequest#consume.
|
396
|
+
|
397
|
+
* RodaRequest.def_verb_method has been removed.
|
398
|
+
|
399
|
+
* The hooks, default_headers, json, and multi_route plugins all store
|
400
|
+
their class-level metadata in the opts hash instead of separate
|
401
|
+
class instance variables. This should have no affect unless you
|
402
|
+
were accessing the class instance variables directly.
|
403
|
+
|
404
|
+
* The render plugin internals changed significantly, it now passes
|
405
|
+
internal data using a hash. This should only affect users that
|
406
|
+
were overriding render plugin methods.
|
data/lib/roda.rb
CHANGED
@@ -54,6 +54,7 @@ class Roda
|
|
54
54
|
end
|
55
55
|
|
56
56
|
@app = nil
|
57
|
+
@inherit_middleware = true
|
57
58
|
@middleware = []
|
58
59
|
@opts = {}
|
59
60
|
@route_block = nil
|
@@ -89,13 +90,16 @@ class Roda
|
|
89
90
|
# Methods are put into a plugin so future plugins can easily override
|
90
91
|
# them and call super to get the default behavior.
|
91
92
|
module Base
|
92
|
-
SESSION_KEY = 'rack.session'.freeze
|
93
|
-
|
94
93
|
# Class methods for the Roda class.
|
95
94
|
module ClassMethods
|
96
95
|
# The rack application that this class uses.
|
97
96
|
attr_reader :app
|
98
97
|
|
98
|
+
# Whether middleware from the current class should be inherited by subclasses.
|
99
|
+
# True by default, should be set to false when using a design where the parent
|
100
|
+
# class accepts requests and uses run to dispatch the request to a subclass.
|
101
|
+
attr_accessor :inherit_middleware
|
102
|
+
|
99
103
|
# The settings/options hash for the current class.
|
100
104
|
attr_reader :opts
|
101
105
|
|
@@ -110,6 +114,12 @@ class Roda
|
|
110
114
|
app.call(env)
|
111
115
|
end
|
112
116
|
|
117
|
+
# Clear the middleware stack
|
118
|
+
def clear_middleware!
|
119
|
+
@middleware.clear
|
120
|
+
build_rack_app
|
121
|
+
end
|
122
|
+
|
113
123
|
# Create a match_#{key} method in the request class using the given
|
114
124
|
# block, so that using a hash key in a request match method will
|
115
125
|
# call the block. The block should return nil or false to not
|
@@ -134,8 +144,14 @@ class Roda
|
|
134
144
|
# and setup the request and response subclasses.
|
135
145
|
def inherited(subclass)
|
136
146
|
super
|
137
|
-
subclass.instance_variable_set(:@
|
147
|
+
subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
|
148
|
+
subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
|
138
149
|
subclass.instance_variable_set(:@opts, opts.dup)
|
150
|
+
subclass.opts.to_a.each do |k,v|
|
151
|
+
if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
|
152
|
+
subclass.opts[k] = v.dup
|
153
|
+
end
|
154
|
+
end
|
139
155
|
subclass.instance_variable_set(:@route_block, @route_block)
|
140
156
|
subclass.send(:build_rack_app)
|
141
157
|
|
@@ -155,36 +171,36 @@ class Roda
|
|
155
171
|
#
|
156
172
|
# Roda.plugin PluginModule
|
157
173
|
# Roda.plugin :csrf
|
158
|
-
def plugin(
|
159
|
-
if
|
160
|
-
|
174
|
+
def plugin(plugin, *args, &block)
|
175
|
+
if plugin.is_a?(Symbol)
|
176
|
+
plugin = RodaPlugins.load_plugin(plugin)
|
161
177
|
end
|
162
178
|
|
163
|
-
if
|
164
|
-
|
179
|
+
if plugin.respond_to?(:load_dependencies)
|
180
|
+
plugin.load_dependencies(self, *args, &block)
|
165
181
|
end
|
166
182
|
|
167
|
-
if defined?(
|
168
|
-
include
|
183
|
+
if defined?(plugin::InstanceMethods)
|
184
|
+
include(plugin::InstanceMethods)
|
169
185
|
end
|
170
|
-
if defined?(
|
171
|
-
extend
|
186
|
+
if defined?(plugin::ClassMethods)
|
187
|
+
extend(plugin::ClassMethods)
|
172
188
|
end
|
173
|
-
if defined?(
|
174
|
-
self::RodaRequest.send(:include,
|
189
|
+
if defined?(plugin::RequestMethods)
|
190
|
+
self::RodaRequest.send(:include, plugin::RequestMethods)
|
175
191
|
end
|
176
|
-
if defined?(
|
177
|
-
self::RodaRequest.extend
|
192
|
+
if defined?(plugin::RequestClassMethods)
|
193
|
+
self::RodaRequest.extend(plugin::RequestClassMethods)
|
178
194
|
end
|
179
|
-
if defined?(
|
180
|
-
self::RodaResponse.send(:include,
|
195
|
+
if defined?(plugin::ResponseMethods)
|
196
|
+
self::RodaResponse.send(:include, plugin::ResponseMethods)
|
181
197
|
end
|
182
|
-
if defined?(
|
183
|
-
self::RodaResponse.extend
|
198
|
+
if defined?(plugin::ResponseClassMethods)
|
199
|
+
self::RodaResponse.extend(plugin::ResponseClassMethods)
|
184
200
|
end
|
185
201
|
|
186
|
-
if
|
187
|
-
|
202
|
+
if plugin.respond_to?(:configure)
|
203
|
+
plugin.configure(self, *args, &block)
|
188
204
|
end
|
189
205
|
end
|
190
206
|
|
@@ -315,7 +331,7 @@ class Roda
|
|
315
331
|
#
|
316
332
|
# env['REQUEST_METHOD'] # => 'GET'
|
317
333
|
def env
|
318
|
-
|
334
|
+
@_request.env
|
319
335
|
end
|
320
336
|
|
321
337
|
# The class-level options hash. This should probably not be
|
@@ -340,10 +356,12 @@ class Roda
|
|
340
356
|
@_response
|
341
357
|
end
|
342
358
|
|
343
|
-
# The session for the current request.
|
344
|
-
#
|
359
|
+
# The session hash for the current request. Raises RodaError
|
360
|
+
# if no session existsExample:
|
361
|
+
#
|
362
|
+
# session # => {}
|
345
363
|
def session
|
346
|
-
|
364
|
+
@_request.session
|
347
365
|
end
|
348
366
|
|
349
367
|
private
|
@@ -352,8 +370,9 @@ class Roda
|
|
352
370
|
# behavior after the request and response have been setup.
|
353
371
|
def _route(&block)
|
354
372
|
catch(:halt) do
|
355
|
-
|
356
|
-
|
373
|
+
r = @_request
|
374
|
+
r.block_result(instance_exec(r, &block))
|
375
|
+
@_response.finish
|
357
376
|
end
|
358
377
|
end
|
359
378
|
end
|
@@ -379,17 +398,6 @@ class Roda
|
|
379
398
|
pattern
|
380
399
|
end
|
381
400
|
|
382
|
-
# Define a verb method in the given that will yield to the match block
|
383
|
-
# if the request method matches and there are either no arguments or
|
384
|
-
# there is a successful terminal match on the arguments.
|
385
|
-
def def_verb_method(mod, verb)
|
386
|
-
mod.class_eval(<<-END, __FILE__, __LINE__+1)
|
387
|
-
def #{verb}(*args, &block)
|
388
|
-
_verb(args, &block) if #{verb == :get ? :is_get : verb}?
|
389
|
-
end
|
390
|
-
END
|
391
|
-
end
|
392
|
-
|
393
401
|
# Since RodaRequest is anonymously subclassed when Roda is subclassed,
|
394
402
|
# and then assigned to a constant of the Roda subclass, make inspect
|
395
403
|
# reflect the likely name for the class.
|
@@ -403,7 +411,7 @@ class Roda
|
|
403
411
|
# pattern requires the path starts with a string and does not match partial
|
404
412
|
# segments.
|
405
413
|
def consume_pattern(pattern)
|
406
|
-
/\A
|
414
|
+
/\A\/(?:#{pattern})(?=\/|\z)/
|
407
415
|
end
|
408
416
|
end
|
409
417
|
|
@@ -418,6 +426,7 @@ class Roda
|
|
418
426
|
SEGMENT = "([^\\/]+)".freeze
|
419
427
|
TERM_INSPECT = "TERM".freeze
|
420
428
|
GET_REQUEST_METHOD = 'GET'.freeze
|
429
|
+
SESSION_KEY = 'rack.session'.freeze
|
421
430
|
|
422
431
|
TERM = Object.new
|
423
432
|
def TERM.inspect
|
@@ -440,15 +449,20 @@ class Roda
|
|
440
449
|
super(env)
|
441
450
|
end
|
442
451
|
|
443
|
-
#
|
444
|
-
#
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
+
# Handle match block return values. By default, if a string is given
|
453
|
+
# and the response is empty, use the string as the response body.
|
454
|
+
def block_result(result)
|
455
|
+
res = response
|
456
|
+
if res.empty? && (body = block_result_body(result))
|
457
|
+
res.write(body)
|
458
|
+
end
|
459
|
+
end
|
460
|
+
|
461
|
+
# Match GET requests. If no arguments are provided, matches all GET
|
462
|
+
# requests, otherwise, matches only GET requests where the arguments
|
463
|
+
# given fully consume the path.
|
464
|
+
def get(*args, &block)
|
465
|
+
_verb(args, &block) if is_get?
|
452
466
|
end
|
453
467
|
|
454
468
|
# Immediately stop execution of the route block and return the given
|
@@ -465,29 +479,13 @@ class Roda
|
|
465
479
|
throw :halt, res
|
466
480
|
end
|
467
481
|
|
468
|
-
# Optimized method for whether this request is a +GET+ request.
|
469
|
-
# Similar to the default Rack::Request get? method, but can be
|
470
|
-
# overridden without changing rack's behavior.
|
471
|
-
def is_get?
|
472
|
-
@env[REQUEST_METHOD] == GET_REQUEST_METHOD
|
473
|
-
end
|
474
|
-
|
475
|
-
# Handle match block return values. By default, if a string is given
|
476
|
-
# and the response is empty, use the string as the response body.
|
477
|
-
def block_result(result)
|
478
|
-
res = response
|
479
|
-
if res.empty? && (body = block_result_body(result))
|
480
|
-
res.write(body)
|
481
|
-
end
|
482
|
-
end
|
483
|
-
|
484
482
|
# Show information about current request, including request class,
|
485
483
|
# request method and full path.
|
486
484
|
#
|
487
485
|
# r.inspect
|
488
486
|
# # => '#<Roda::RodaRequest GET /foo/bar>'
|
489
487
|
def inspect
|
490
|
-
"#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{
|
488
|
+
"#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{path}>"
|
491
489
|
end
|
492
490
|
|
493
491
|
# Does a terminal match on the current path, matching only if the arguments
|
@@ -535,7 +533,7 @@ class Roda
|
|
535
533
|
# end
|
536
534
|
def is(*args, &block)
|
537
535
|
if args.empty?
|
538
|
-
if
|
536
|
+
if empty_path?
|
539
537
|
always(&block)
|
540
538
|
end
|
541
539
|
else
|
@@ -544,6 +542,13 @@ class Roda
|
|
544
542
|
end
|
545
543
|
end
|
546
544
|
|
545
|
+
# Optimized method for whether this request is a +GET+ request.
|
546
|
+
# Similar to the default Rack::Request get? method, but can be
|
547
|
+
# overridden without changing rack's behavior.
|
548
|
+
def is_get?
|
549
|
+
@env[REQUEST_METHOD] == GET_REQUEST_METHOD
|
550
|
+
end
|
551
|
+
|
547
552
|
# Does a match on the path, matching only if the arguments
|
548
553
|
# have matched the path. Because this doesn't fully match the
|
549
554
|
# path, this is usually used to setup branches of the routing tree,
|
@@ -579,14 +584,34 @@ class Roda
|
|
579
584
|
end
|
580
585
|
end
|
581
586
|
|
582
|
-
# The
|
583
|
-
|
584
|
-
|
587
|
+
# The already matched part of the path, including the original SCRIPT_NAME.
|
588
|
+
def matched_path
|
589
|
+
@env[SCRIPT_NAME]
|
590
|
+
end
|
591
|
+
|
592
|
+
# This an an optimized version of Rack::Request#path.
|
585
593
|
#
|
586
|
-
#
|
587
|
-
#
|
588
|
-
|
589
|
-
|
594
|
+
# r.env['SCRIPT_NAME'] = '/foo'
|
595
|
+
# r.env['PATH_INFO'] = '/bar'
|
596
|
+
# r.path
|
597
|
+
# # => '/foo/bar'
|
598
|
+
def path
|
599
|
+
e = @env
|
600
|
+
"#{e[SCRIPT_NAME]}#{e[PATH_INFO]}"
|
601
|
+
end
|
602
|
+
alias full_path_info path
|
603
|
+
|
604
|
+
# The current path to match requests against. This is the same as PATH_INFO
|
605
|
+
# in the environment, which gets updated as the request is being routed.
|
606
|
+
def remaining_path
|
607
|
+
@env[PATH_INFO]
|
608
|
+
end
|
609
|
+
|
610
|
+
# Match POST requests. If no arguments are provided, matches all POST
|
611
|
+
# requests, otherwise, matches only POST requests where the arguments
|
612
|
+
# given fully consume the path.
|
613
|
+
def post(*args, &block)
|
614
|
+
_verb(args, &block) if post?
|
590
615
|
end
|
591
616
|
|
592
617
|
# Immediately redirect to the path using the status code. This ends
|
@@ -612,11 +637,26 @@ class Roda
|
|
612
637
|
# r.redirect
|
613
638
|
# end
|
614
639
|
# end
|
615
|
-
def redirect(path=default_redirect_path, status=
|
640
|
+
def redirect(path=default_redirect_path, status=default_redirect_status)
|
616
641
|
response.redirect(path, status)
|
617
642
|
throw :halt, response.finish
|
618
643
|
end
|
619
644
|
|
645
|
+
# The response related to the current request. See ResponseMethods for
|
646
|
+
# instance methods for the response, but in general the most common usage
|
647
|
+
# is to override the response status and headers:
|
648
|
+
#
|
649
|
+
# response.status = 200
|
650
|
+
# response['Header-Name'] = 'Header value'
|
651
|
+
def response
|
652
|
+
scope.response
|
653
|
+
end
|
654
|
+
|
655
|
+
# Return the Roda class related to this request.
|
656
|
+
def roda_class
|
657
|
+
self.class.roda_class
|
658
|
+
end
|
659
|
+
|
620
660
|
# Routing matches that only matches +GET+ requests where the current
|
621
661
|
# path is +/+. If it matches, the match block is executed, and when
|
622
662
|
# the match block returns, the rack response is returned.
|
@@ -665,7 +705,7 @@ class Roda
|
|
665
705
|
# Use <tt>r.get true</tt> to handle +GET+ requests where the current
|
666
706
|
# path is empty.
|
667
707
|
def root(&block)
|
668
|
-
if
|
708
|
+
if remaining_path == SLASH && is_get?
|
669
709
|
always(&block)
|
670
710
|
end
|
671
711
|
end
|
@@ -681,6 +721,12 @@ class Roda
|
|
681
721
|
throw :halt, app.call(@env)
|
682
722
|
end
|
683
723
|
|
724
|
+
# The session for the current request. Raises a RodaError if
|
725
|
+
# a session handler has not been loaded.
|
726
|
+
def session
|
727
|
+
@env[SESSION_KEY] || raise(RodaError, "You're missing a session handler. You can get started by adding use Rack::Session::Cookie")
|
728
|
+
end
|
729
|
+
|
684
730
|
private
|
685
731
|
|
686
732
|
# Match any of the elements in the given array. Return at the
|
@@ -690,7 +736,7 @@ class Roda
|
|
690
736
|
matcher.any? do |m|
|
691
737
|
if matched = match(m)
|
692
738
|
if m.is_a?(String)
|
693
|
-
captures.push(m)
|
739
|
+
@captures.push(m)
|
694
740
|
end
|
695
741
|
end
|
696
742
|
|
@@ -698,16 +744,16 @@ class Roda
|
|
698
744
|
end
|
699
745
|
end
|
700
746
|
|
701
|
-
# Match the given regexp exactly if it matches a full segment.
|
702
|
-
def _match_regexp(re)
|
703
|
-
consume(self.class.cached_matcher(re){re})
|
704
|
-
end
|
705
|
-
|
706
747
|
# Match the given hash if all hash matchers match.
|
707
748
|
def _match_hash(hash)
|
708
749
|
hash.all?{|k,v| send("match_#{k}", v)}
|
709
750
|
end
|
710
751
|
|
752
|
+
# Match the given regexp exactly if it matches a full segment.
|
753
|
+
def _match_regexp(re)
|
754
|
+
consume(self.class.cached_matcher(re){re})
|
755
|
+
end
|
756
|
+
|
711
757
|
# Match the given string to the request path. Regexp escapes the
|
712
758
|
# string so that regexp metacharacters are not matched, and recognizes
|
713
759
|
# colon tokens for placeholders.
|
@@ -757,16 +803,10 @@ class Roda
|
|
757
803
|
# SCRIPT_NAME to include the matched path, removes the matched
|
758
804
|
# path from PATH_INFO, and updates captures with any regex captures.
|
759
805
|
def consume(pattern)
|
760
|
-
|
761
|
-
|
762
|
-
|
763
|
-
|
764
|
-
|
765
|
-
# Don't mutate SCRIPT_NAME, breaks try
|
766
|
-
env[SCRIPT_NAME] += vars.shift
|
767
|
-
env[PATH_INFO] = matchdata.post_match
|
768
|
-
|
769
|
-
captures.concat(vars)
|
806
|
+
if matchdata = remaining_path.match(pattern)
|
807
|
+
update_remaining_path(matchdata.post_match)
|
808
|
+
@captures.concat(matchdata.captures)
|
809
|
+
end
|
770
810
|
end
|
771
811
|
|
772
812
|
# The default path to use for redirects when a path is not given.
|
@@ -779,29 +819,47 @@ class Roda
|
|
779
819
|
# it is easy to create an infinite redirect.
|
780
820
|
def default_redirect_path
|
781
821
|
raise RodaError, "must provide path argument to redirect for get requests" if is_get?
|
782
|
-
|
822
|
+
path
|
823
|
+
end
|
824
|
+
|
825
|
+
# The default status to use for redirects if a status is not provided,
|
826
|
+
# 302 by default.
|
827
|
+
def default_redirect_status
|
828
|
+
302
|
829
|
+
end
|
830
|
+
|
831
|
+
# Whether the current path is considered empty.
|
832
|
+
def empty_path?
|
833
|
+
remaining_path == EMPTY_STRING
|
783
834
|
end
|
784
835
|
|
785
836
|
# If all of the arguments match, yields to the match block and
|
786
837
|
# returns the rack response when the block returns. If any of
|
787
838
|
# the match arguments doesn't match, does nothing.
|
788
839
|
def if_match(args)
|
840
|
+
keep_remaining_path do
|
841
|
+
# For every block, we make sure to reset captures so that
|
842
|
+
# nesting matchers won't mess with each other's captures.
|
843
|
+
@captures.clear
|
844
|
+
|
845
|
+
return unless match_all(args)
|
846
|
+
block_result(yield(*captures))
|
847
|
+
throw :halt, response.finish
|
848
|
+
end
|
849
|
+
end
|
850
|
+
|
851
|
+
# Yield to the block, restoring SCRIPT_NAME and PATH_INFO to
|
852
|
+
# their initial values before returning from the block.
|
853
|
+
def keep_remaining_path
|
789
854
|
env = @env
|
790
|
-
script = env[SCRIPT_NAME]
|
791
|
-
path = env[PATH_INFO]
|
792
|
-
|
793
|
-
# For every block, we make sure to reset captures so that
|
794
|
-
# nesting matchers won't mess with each other's captures.
|
795
|
-
captures.clear
|
796
|
-
|
797
|
-
return unless match_all(args)
|
798
|
-
block_result(yield(*captures))
|
799
|
-
throw :halt, response.finish
|
855
|
+
script = env[sn = SCRIPT_NAME]
|
856
|
+
path = env[pi = PATH_INFO]
|
857
|
+
yield
|
800
858
|
ensure
|
801
|
-
env[
|
802
|
-
env[
|
859
|
+
env[sn] = script
|
860
|
+
env[pi] = path
|
803
861
|
end
|
804
|
-
|
862
|
+
|
805
863
|
# Attempt to match the argument to the given request, handling
|
806
864
|
# common ruby types.
|
807
865
|
def match(matcher)
|
@@ -813,7 +871,7 @@ class Roda
|
|
813
871
|
when Symbol
|
814
872
|
_match_symbol(matcher)
|
815
873
|
when TERM
|
816
|
-
|
874
|
+
empty_path?
|
817
875
|
when Hash
|
818
876
|
_match_hash(matcher)
|
819
877
|
when Array
|
@@ -850,7 +908,7 @@ class Roda
|
|
850
908
|
# Adds any match to the captures.
|
851
909
|
def match_param(key)
|
852
910
|
if v = self[key]
|
853
|
-
captures << v
|
911
|
+
@captures << v
|
854
912
|
end
|
855
913
|
end
|
856
914
|
|
@@ -858,9 +916,18 @@ class Roda
|
|
858
916
|
# Adds any match to the captures.
|
859
917
|
def match_param!(key)
|
860
918
|
if (v = self[key]) && !v.empty?
|
861
|
-
captures << v
|
919
|
+
@captures << v
|
862
920
|
end
|
863
921
|
end
|
922
|
+
|
923
|
+
# Update PATH_INFO and SCRIPT_NAME based on the matchend and remaining variables.
|
924
|
+
def update_remaining_path(remaining)
|
925
|
+
e = @env
|
926
|
+
|
927
|
+
# Don't mutate SCRIPT_NAME, breaks try
|
928
|
+
e[SCRIPT_NAME] += e[pi = PATH_INFO].chomp(remaining)
|
929
|
+
e[pi] = remaining
|
930
|
+
end
|
864
931
|
end
|
865
932
|
|
866
933
|
# Class methods for RodaResponse
|
@@ -879,21 +946,20 @@ class Roda
|
|
879
946
|
# Instance methods for RodaResponse
|
880
947
|
module ResponseMethods
|
881
948
|
CONTENT_LENGTH = "Content-Length".freeze
|
882
|
-
|
883
|
-
DEFAULT_CONTENT_TYPE = "text/html".freeze
|
949
|
+
DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
|
884
950
|
LOCATION = "Location".freeze
|
885
951
|
|
952
|
+
# The hash of response headers for the current response.
|
953
|
+
attr_reader :headers
|
954
|
+
|
886
955
|
# The status code to use for the response. If none is given, will use 200
|
887
956
|
# code for non-empty responses and a 404 code for empty responses.
|
888
957
|
attr_accessor :status
|
889
958
|
|
890
|
-
# The hash of response headers for the current response.
|
891
|
-
attr_reader :headers
|
892
|
-
|
893
959
|
# Set the default headers when creating a response.
|
894
960
|
def initialize
|
895
961
|
@status = nil
|
896
|
-
@headers =
|
962
|
+
@headers = {}
|
897
963
|
@body = []
|
898
964
|
@length = 0
|
899
965
|
end
|
@@ -912,14 +978,9 @@ class Roda
|
|
912
978
|
@headers[key] = value
|
913
979
|
end
|
914
980
|
|
915
|
-
# Show response class, status code, response headers, and response body
|
916
|
-
def inspect
|
917
|
-
"#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
|
918
|
-
end
|
919
|
-
|
920
981
|
# The default headers to use for responses.
|
921
982
|
def default_headers
|
922
|
-
|
983
|
+
DEFAULT_HEADERS
|
923
984
|
end
|
924
985
|
|
925
986
|
# Modify the headers to include a Set-Cookie value that
|
@@ -959,8 +1020,9 @@ class Roda
|
|
959
1020
|
def finish
|
960
1021
|
b = @body
|
961
1022
|
s = (@status ||= b.empty? ? 404 : 200)
|
1023
|
+
set_default_headers
|
962
1024
|
h = @headers
|
963
|
-
h[CONTENT_LENGTH]
|
1025
|
+
h[CONTENT_LENGTH] ||= @length.to_s
|
964
1026
|
[s, h, b]
|
965
1027
|
end
|
966
1028
|
|
@@ -969,9 +1031,15 @@ class Roda
|
|
969
1031
|
# and doesn't add the Content-Length header or use the existing
|
970
1032
|
# body.
|
971
1033
|
def finish_with_body(body)
|
1034
|
+
set_default_headers
|
972
1035
|
[@status || 200, @headers, body]
|
973
1036
|
end
|
974
1037
|
|
1038
|
+
# Show response class, status code, response headers, and response body
|
1039
|
+
def inspect
|
1040
|
+
"#<#{self.class.inspect} #{@status.inspect} #{@headers.inspect} #{@body.inspect}>"
|
1041
|
+
end
|
1042
|
+
|
975
1043
|
# Set the Location header to the given path, and the status
|
976
1044
|
# to the given status. Example:
|
977
1045
|
#
|
@@ -982,6 +1050,11 @@ class Roda
|
|
982
1050
|
@status = status
|
983
1051
|
end
|
984
1052
|
|
1053
|
+
# Return the Roda class related to this response.
|
1054
|
+
def roda_class
|
1055
|
+
self.class.roda_class
|
1056
|
+
end
|
1057
|
+
|
985
1058
|
# Set the cookie with the given key in the headers.
|
986
1059
|
#
|
987
1060
|
# response.set_cookie('foo', 'bar')
|
@@ -999,12 +1072,21 @@ class Roda
|
|
999
1072
|
@body << s
|
1000
1073
|
nil
|
1001
1074
|
end
|
1075
|
+
|
1076
|
+
private
|
1077
|
+
|
1078
|
+
# For each default header, if a header has not already been set for the
|
1079
|
+
# response, set the header in the response.
|
1080
|
+
def set_default_headers
|
1081
|
+
h = @headers
|
1082
|
+
default_headers.each do |k,v|
|
1083
|
+
h[k] ||= v
|
1084
|
+
end
|
1085
|
+
end
|
1002
1086
|
end
|
1003
1087
|
end
|
1004
1088
|
end
|
1005
1089
|
|
1006
1090
|
extend RodaPlugins::Base::ClassMethods
|
1007
1091
|
plugin RodaPlugins::Base
|
1008
|
-
RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :get)
|
1009
|
-
RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :post)
|
1010
1092
|
end
|