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