roda 0.9.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG +62 -0
- data/README.rdoc +362 -167
- data/Rakefile +2 -2
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +553 -180
- data/lib/roda/plugins/_erubis_escaping.rb +28 -0
- data/lib/roda/plugins/all_verbs.rb +7 -9
- data/lib/roda/plugins/backtracking_array.rb +92 -0
- data/lib/roda/plugins/content_for.rb +46 -0
- data/lib/roda/plugins/csrf.rb +60 -0
- data/lib/roda/plugins/flash.rb +53 -7
- data/lib/roda/plugins/halt.rb +8 -14
- data/lib/roda/plugins/head.rb +56 -0
- data/lib/roda/plugins/header_matchers.rb +2 -2
- data/lib/roda/plugins/json.rb +84 -0
- data/lib/roda/plugins/multi_route.rb +50 -10
- data/lib/roda/plugins/not_allowed.rb +140 -0
- data/lib/roda/plugins/pass.rb +13 -6
- data/lib/roda/plugins/per_thread_caching.rb +70 -0
- data/lib/roda/plugins/render.rb +20 -33
- data/lib/roda/plugins/render_each.rb +61 -0
- data/lib/roda/plugins/symbol_matchers.rb +79 -0
- data/lib/roda/plugins/symbol_views.rb +40 -0
- data/lib/roda/plugins/view_subdirs.rb +53 -0
- data/lib/roda/version.rb +3 -0
- data/spec/matchers_spec.rb +61 -5
- data/spec/plugin/_erubis_escaping_spec.rb +29 -0
- data/spec/plugin/backtracking_array_spec.rb +38 -0
- data/spec/plugin/content_for_spec.rb +34 -0
- data/spec/plugin/csrf_spec.rb +49 -0
- data/spec/plugin/flash_spec.rb +69 -5
- data/spec/plugin/head_spec.rb +35 -0
- data/spec/plugin/json_spec.rb +50 -0
- data/spec/plugin/multi_route_spec.rb +22 -6
- data/spec/plugin/not_allowed_spec.rb +55 -0
- data/spec/plugin/pass_spec.rb +8 -2
- data/spec/plugin/per_thread_caching_spec.rb +28 -0
- data/spec/plugin/render_each_spec.rb +30 -0
- data/spec/plugin/render_spec.rb +7 -1
- data/spec/plugin/symbol_matchers_spec.rb +68 -0
- data/spec/plugin/symbol_views_spec.rb +32 -0
- data/spec/plugin/view_subdirs_spec.rb +45 -0
- data/spec/plugin_spec.rb +11 -1
- data/spec/redirect_spec.rb +21 -4
- data/spec/request_spec.rb +9 -0
- metadata +49 -5
data/Rakefile
CHANGED
@@ -3,7 +3,7 @@ require "rake/clean"
|
|
3
3
|
|
4
4
|
NAME = 'roda'
|
5
5
|
VERS = lambda do
|
6
|
-
require File.expand_path("../lib/roda.rb", __FILE__)
|
6
|
+
require File.expand_path("../lib/roda/version.rb", __FILE__)
|
7
7
|
Roda::RodaVersion
|
8
8
|
end
|
9
9
|
CLEAN.include ["#{NAME}-*.gem", "rdoc", "coverage", "www/public/*.html", "www/public/rdoc"]
|
@@ -34,7 +34,7 @@ rescue LoadError
|
|
34
34
|
end
|
35
35
|
|
36
36
|
RDOC_OPTS = RDOC_DEFAULT_OPTS + ['--main', 'README.rdoc']
|
37
|
-
RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb"
|
37
|
+
RDOC_FILES = %w"README.rdoc CHANGELOG MIT-LICENSE lib/**/*.rb" + Dir["doc/*.rdoc"] + Dir['doc/release_notes/*.txt']
|
38
38
|
|
39
39
|
rdoc_task_class.new do |rdoc|
|
40
40
|
rdoc.rdoc_dir = "rdoc"
|
@@ -0,0 +1,329 @@
|
|
1
|
+
= New Plugins
|
2
|
+
|
3
|
+
* A csrf plugin has been added for CSRF prevention, using
|
4
|
+
Rack::Csrf. It also adds helper methods for views such as
|
5
|
+
csrf_tag.
|
6
|
+
|
7
|
+
* A symbol_matchers plugin has been added, for customizing
|
8
|
+
the regexps used per symbol. This also affects the use
|
9
|
+
of embedded colons in strings. This supports the following
|
10
|
+
symbol regexps by default:
|
11
|
+
|
12
|
+
:d :: (\d+), a decimal segment
|
13
|
+
:format :: (?:\.(\w+))?, an optional format/extension
|
14
|
+
:opt :: (?:\/([^\/]+))?, an optional segment
|
15
|
+
:optd :: (?:\/(\d+))?, an optional decimal segment
|
16
|
+
:rest :: (.*), all remaining characters, if any
|
17
|
+
:w :: (\w+), a alphanumeric segment
|
18
|
+
|
19
|
+
This allows you to write code such as:
|
20
|
+
|
21
|
+
plugin :symbol_matchers
|
22
|
+
|
23
|
+
route do |r|
|
24
|
+
r.is "track/:d" do
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
And have it only match routes such as /track/123, not
|
29
|
+
/track/abc.
|
30
|
+
|
31
|
+
Note that :opt, :optd, and :format are only going to make sense
|
32
|
+
when used as embedded colons in strings, due to how segment matching
|
33
|
+
works.
|
34
|
+
|
35
|
+
You can add your own symbol matchers using the symbol_matcher
|
36
|
+
class method:
|
37
|
+
|
38
|
+
plugin :symbol_matchers
|
39
|
+
symbol_matcher :slug, /([\w-]+)/
|
40
|
+
|
41
|
+
route do |r|
|
42
|
+
r.on :slug do
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
* A symbol_views plugin has been added, which allows match blocks to
|
47
|
+
return symbols, which are interpreted as template names:
|
48
|
+
|
49
|
+
plugin :symbol_views
|
50
|
+
|
51
|
+
route do |r|
|
52
|
+
:template_name # same as view :template_name
|
53
|
+
end
|
54
|
+
|
55
|
+
* A json plugin has been added, which allows match blocks to return
|
56
|
+
arrays or hashes, and uses a JSON version of them as the response
|
57
|
+
body:
|
58
|
+
|
59
|
+
plugin :json
|
60
|
+
|
61
|
+
route do |r|
|
62
|
+
{'a'=>[1,2,3]} # response: {"a":[1,2,3]}
|
63
|
+
end
|
64
|
+
|
65
|
+
This also sets the Content-Type of the response to application/json.
|
66
|
+
|
67
|
+
To convert additional object types to JSON, you can modify
|
68
|
+
json_response_classes:
|
69
|
+
|
70
|
+
plugin :json
|
71
|
+
json_response_classes << Sequel::Model
|
72
|
+
|
73
|
+
* A view_subdirs plugin has been added for setting a default
|
74
|
+
subdirectory to use for views:
|
75
|
+
|
76
|
+
Roda.route do |r|
|
77
|
+
r.on "admin" do
|
78
|
+
set_view_subdir "admin"
|
79
|
+
|
80
|
+
r.is do
|
81
|
+
view "index" # uses admin/index view
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
* A render_each plugin has been added, for rendering the same
|
87
|
+
template for multiple objects, and returning the concatenation
|
88
|
+
of all of the output:
|
89
|
+
|
90
|
+
<%= render_each([1,2,3], 'number') %>
|
91
|
+
|
92
|
+
This renders the number template 3 times. Each time the template
|
93
|
+
is rendered, a local variable named number will be present with
|
94
|
+
the current entry in the enumerable. You can control the name of
|
95
|
+
the local variable using the :local option:
|
96
|
+
|
97
|
+
<%= render_each([1,2,3], 'number', :local=>:n) %>
|
98
|
+
|
99
|
+
* A content_for plugin has been added, for storing content in one
|
100
|
+
template and retrieving that content in a different template (such
|
101
|
+
as the layout). To set content, you call content_for with a block:
|
102
|
+
|
103
|
+
<% content_for :foo do %>
|
104
|
+
content for foo
|
105
|
+
<% end %>
|
106
|
+
|
107
|
+
To retrieve content, you call content_for without a block:
|
108
|
+
|
109
|
+
<%= content_for :foo %>
|
110
|
+
|
111
|
+
This plugin probably only works when using erb templates.
|
112
|
+
|
113
|
+
* A not_allowed plugin has been added, for automatically returning 405
|
114
|
+
Method Not Allowed responses when a route is handled for a different
|
115
|
+
request method than the one used. For this routing tree:
|
116
|
+
|
117
|
+
plugin :not_allowed
|
118
|
+
|
119
|
+
route do |r|
|
120
|
+
r.get "foo" do
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
If you submit a POST /foo request, it will return a 405 error
|
125
|
+
instead of a 404 error.
|
126
|
+
|
127
|
+
This also handles cases when multiple methods are supported for
|
128
|
+
a single path, so for this routing tree:
|
129
|
+
|
130
|
+
route do |r|
|
131
|
+
r.is "foo" do
|
132
|
+
r.get do
|
133
|
+
end
|
134
|
+
r.post do
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
If you submit a DELETE /foo request, it will return a 405 error
|
140
|
+
instead of a 404 error.
|
141
|
+
|
142
|
+
* A head plugin has been added, automatically handling HEAD requests
|
143
|
+
the same as GET requests, except returning an empty body. So for
|
144
|
+
this routing tree:
|
145
|
+
|
146
|
+
plugin :head
|
147
|
+
|
148
|
+
route do |r|
|
149
|
+
r.get "foo" do
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
A request for HEAD /foo will return a 200 result instead of a 404
|
154
|
+
error.
|
155
|
+
|
156
|
+
* A backtracking_array plugin has been added, which makes matching
|
157
|
+
backtrack to the next entry in an array if a later matcher fails.
|
158
|
+
For example, the following code does not match /foo/bar by
|
159
|
+
default in Roda:
|
160
|
+
|
161
|
+
r.is ['foo', 'foo/bar'] do
|
162
|
+
end
|
163
|
+
|
164
|
+
This is because the 'foo' entry in the array matches, so the
|
165
|
+
array matches. However, after the array is matched, the terminal
|
166
|
+
matcher added by r.is fails to match. That causes the routing
|
167
|
+
method not to match the request, so the match block is not called.
|
168
|
+
|
169
|
+
With the backtracking_array plugin, failures of later matchers after
|
170
|
+
an array matcher backtrack so the next entry in the array is tried.
|
171
|
+
|
172
|
+
* A per_thread_caching plugin has been added, allowing you to change
|
173
|
+
from a thread-safe shared cache to a per-thread cache, which may
|
174
|
+
be faster on alternative ruby implementations, at the cost of
|
175
|
+
additional memory usage.
|
176
|
+
|
177
|
+
= New Features
|
178
|
+
|
179
|
+
* The hash_matcher class method has been added to make it easier to
|
180
|
+
define custom hash matchers:
|
181
|
+
|
182
|
+
hash_matcher(:foo) do |v|
|
183
|
+
self['foo'] == v
|
184
|
+
end
|
185
|
+
|
186
|
+
route do |r|
|
187
|
+
r.on :foo=>'bar' do
|
188
|
+
# matches when param foo has value bar
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
* An r.root routing method has been added for handling GET
|
193
|
+
requests where the current path is /. This is basically
|
194
|
+
a faster and simpler version of r.get "", except it does
|
195
|
+
not consume the / from the path.
|
196
|
+
|
197
|
+
* The r.halt method now works without an argument, in which
|
198
|
+
case it uses the current response.
|
199
|
+
|
200
|
+
* The r.redirect method now works without an argument for non-GET
|
201
|
+
requests, redirecting to the current path.
|
202
|
+
|
203
|
+
* An :all hash matcher has been added, which takes an array and
|
204
|
+
matches only if all of the elements match. This is mainly
|
205
|
+
designed for usage inside an array matcher, so:
|
206
|
+
|
207
|
+
r.on ["foo", {:all=>["bar", :id]}] do
|
208
|
+
end
|
209
|
+
|
210
|
+
will match either /foo or /bar/123, but not /bar.
|
211
|
+
|
212
|
+
* The render plugin's view method now accepts a :content option,
|
213
|
+
in which case it uses the content directly without running it
|
214
|
+
through the template engine. This is useful if you have
|
215
|
+
arbitrary content you want rendered inside the layout.
|
216
|
+
|
217
|
+
* The render plugin now accepts an :escape option, in which case
|
218
|
+
it will automatically set the default :engine_class for erb
|
219
|
+
templates to an Erubis::EscapedEruby subclass. This changes the
|
220
|
+
behavior of erb templates such that:
|
221
|
+
|
222
|
+
<%= '<escaped>' %> # <escaped>
|
223
|
+
<%== '<not escaped>' %> # <not escaped>
|
224
|
+
|
225
|
+
This makes it easier to protect against XSS attacks in your
|
226
|
+
templates, as long as you only use <%== %> for content that has
|
227
|
+
already been escaped.
|
228
|
+
|
229
|
+
Note that similar behavior is available in Erubis by default,
|
230
|
+
using the :opts=>{:escape_html=>true} render option, but that
|
231
|
+
doesn't handle postfix conditionals in <%= %> tags.
|
232
|
+
|
233
|
+
* The multi_route plugin now has an r.multi_route method, which
|
234
|
+
will attempt to dispatch to one of the named routes based on
|
235
|
+
first segment in the path. So this routing tree:
|
236
|
+
|
237
|
+
plugin :multi_route
|
238
|
+
|
239
|
+
route "a" do |r|
|
240
|
+
r.is "c" do
|
241
|
+
"e"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
route "b" do |r|
|
245
|
+
r.is "d" do
|
246
|
+
"f"
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
route do |r|
|
251
|
+
r.multi_route
|
252
|
+
end
|
253
|
+
|
254
|
+
will return "e" for /a/c and "f" for /b/d.
|
255
|
+
|
256
|
+
* Plugins can now override request and response class methods
|
257
|
+
using RequestClassMethods and ResponseClassMethods modules.
|
258
|
+
|
259
|
+
= Optimizations
|
260
|
+
|
261
|
+
* String, hash, and symbol matchers are now much faster by caching
|
262
|
+
the underlying regexp.
|
263
|
+
|
264
|
+
* String, hash, and symbol matchers are now faster by using a
|
265
|
+
regexp positive lookahead assertion instead of an additional
|
266
|
+
capture.
|
267
|
+
|
268
|
+
* Terminal matching in the r.is, r.get, and r.post routing methods
|
269
|
+
is now faster, as it does not use a hash matcher internally.
|
270
|
+
|
271
|
+
* The routing methods are now faster by reducing the number of
|
272
|
+
Array objects created.
|
273
|
+
|
274
|
+
* Calling routing methods without arguments is now faster.
|
275
|
+
|
276
|
+
* The r.get method is now faster by reducing the number of string
|
277
|
+
allocations.
|
278
|
+
|
279
|
+
* Many request methods are faster by reducing the number of
|
280
|
+
method calls used.
|
281
|
+
|
282
|
+
* Template caching no longer uses a mutex on MRI, since one is
|
283
|
+
not needed for thread safety there.
|
284
|
+
|
285
|
+
= Other Improvements
|
286
|
+
|
287
|
+
* The flash plugin now implements its own flash hash instead of
|
288
|
+
using sinatra-flash. It is now slightly faster and handles nil
|
289
|
+
keys in #keep and #discard.
|
290
|
+
|
291
|
+
* Roda's version is now stored in roda/version.rb so that it can be
|
292
|
+
required without requiring Roda itself.
|
293
|
+
|
294
|
+
= Backwards Compatibility
|
295
|
+
|
296
|
+
* The multi_route plugin's route instance method has been changed
|
297
|
+
to a request method. So the new usage is:
|
298
|
+
|
299
|
+
plugin :multi_route
|
300
|
+
|
301
|
+
route "a" do |r|
|
302
|
+
end
|
303
|
+
|
304
|
+
route do |r|
|
305
|
+
r.route "a" # instead of: route "a"
|
306
|
+
end
|
307
|
+
|
308
|
+
* The session key used for the flash hash in the flash plugin is
|
309
|
+
now :_flash, not :flash.
|
310
|
+
|
311
|
+
* The :extension matcher now longer forces a terminal match, use
|
312
|
+
one of the routing methods that forces a terminal match if you
|
313
|
+
want that behavior.
|
314
|
+
|
315
|
+
* The :term hash matcher has been removed.
|
316
|
+
|
317
|
+
* The r.consume private method now takes the exact regexp to use
|
318
|
+
to search the current path, it no longer enforces a preceeding
|
319
|
+
slash and that the match end on a segment boundary.
|
320
|
+
|
321
|
+
* Dynamically constructing match patterns is now a potential
|
322
|
+
memory leak due to them being cached. So you shouldn't do
|
323
|
+
things like:
|
324
|
+
|
325
|
+
r.on r['param'] do
|
326
|
+
end
|
327
|
+
|
328
|
+
* Many private routing methods were changed or removed, if you were
|
329
|
+
using them, you'll probably need to update your code.
|
data/lib/roda.rb
CHANGED
@@ -1,52 +1,54 @@
|
|
1
1
|
require "rack"
|
2
2
|
require "thread"
|
3
|
+
require "roda/version"
|
3
4
|
|
4
5
|
# The main class for Roda. Roda is built completely out of plugins, with the
|
5
6
|
# default plugin being Roda::RodaPlugins::Base, so this class is mostly empty
|
6
7
|
# except for some constants.
|
7
8
|
class Roda
|
8
|
-
# Roda's version, always specified by a string in \d+\.\d+\.\d+ format.
|
9
|
-
RodaVersion = '0.9.0'.freeze
|
10
|
-
|
11
9
|
# Error class raised by Roda
|
12
10
|
class RodaError < StandardError; end
|
13
11
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
12
|
+
if defined?(RUBY_ENGINE) && RUBY_ENGINE != 'ruby'
|
13
|
+
# A thread safe cache class, offering only #[] and #[]= methods,
|
14
|
+
# each protected by a mutex. Used on non-MRI where Hash is not
|
15
|
+
# thread safe.
|
16
|
+
class RodaCache
|
17
|
+
# Create a new thread safe cache.
|
18
|
+
def initialize
|
19
|
+
@mutex = Mutex.new
|
20
|
+
@hash = {}
|
21
|
+
end
|
19
22
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
+
# Make getting value from underlying hash thread safe.
|
24
|
+
def [](key)
|
25
|
+
@mutex.synchronize{@hash[key]}
|
26
|
+
end
|
23
27
|
|
24
|
-
#
|
25
|
-
|
26
|
-
|
27
|
-
def inspect
|
28
|
-
"#{roda_class.inspect}::RodaRequest"
|
28
|
+
# Make setting value in underlying hash thread safe.
|
29
|
+
def []=(key, value)
|
30
|
+
@mutex.synchronize{@hash[key] = value}
|
29
31
|
end
|
30
32
|
end
|
33
|
+
else
|
34
|
+
# Hashes are already thread-safe in MRI, due to the GVL, so they
|
35
|
+
# can safely be used as a cache.
|
36
|
+
RodaCache = Hash
|
37
|
+
end
|
38
|
+
|
39
|
+
# Base class used for Roda requests. The instance methods for this
|
40
|
+
# class are added by Roda::RodaPlugins::Base::RequestMethods, the
|
41
|
+
# class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
|
42
|
+
class RodaRequest < ::Rack::Request;
|
43
|
+
@roda_class = ::Roda
|
44
|
+
@match_pattern_cache = ::Roda::RodaCache.new
|
31
45
|
end
|
32
46
|
|
33
47
|
# Base class used for Roda responses. The instance methods for this
|
34
|
-
# class are added by Roda::RodaPlugins::Base::ResponseMethods,
|
35
|
-
#
|
48
|
+
# class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
|
49
|
+
# methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
|
36
50
|
class RodaResponse < ::Rack::Response;
|
37
51
|
@roda_class = ::Roda
|
38
|
-
|
39
|
-
class << self
|
40
|
-
# Reference to the Roda class related to this response class.
|
41
|
-
attr_accessor :roda_class
|
42
|
-
|
43
|
-
# Since RodaResponse is anonymously subclassed when Roda is subclassed,
|
44
|
-
# and then assigned to a constant of the Roda subclass, make inspect
|
45
|
-
# reflect the likely name for the class.
|
46
|
-
def inspect
|
47
|
-
"#{roda_class.inspect}::RodaResponse"
|
48
|
-
end
|
49
|
-
end
|
50
52
|
end
|
51
53
|
|
52
54
|
@builder = ::Rack::Builder.new
|
@@ -56,11 +58,8 @@ class Roda
|
|
56
58
|
# Module in which all Roda plugins should be stored. Also contains logic for
|
57
59
|
# registering and loading plugins.
|
58
60
|
module RodaPlugins
|
59
|
-
# Mutex protecting the plugins hash
|
60
|
-
@mutex = ::Mutex.new
|
61
|
-
|
62
61
|
# Stores registered plugins
|
63
|
-
@plugins =
|
62
|
+
@plugins = RodaCache.new
|
64
63
|
|
65
64
|
# If the registered plugin already exists, use it. Otherwise,
|
66
65
|
# require it and return it. This raises a LoadError if such a
|
@@ -68,17 +67,19 @@ class Roda
|
|
68
67
|
# not register itself correctly.
|
69
68
|
def self.load_plugin(name)
|
70
69
|
h = @plugins
|
71
|
-
unless plugin =
|
70
|
+
unless plugin = h[name]
|
72
71
|
require "roda/plugins/#{name}"
|
73
|
-
raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin =
|
72
|
+
raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name]
|
74
73
|
end
|
75
74
|
plugin
|
76
75
|
end
|
77
76
|
|
78
77
|
# Register the given plugin with Roda, so that it can be loaded using #plugin
|
79
|
-
# with a symbol. Should be used by plugin files.
|
78
|
+
# with a symbol. Should be used by plugin files. Example:
|
79
|
+
#
|
80
|
+
# Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule)
|
80
81
|
def self.register_plugin(name, mod)
|
81
|
-
@
|
82
|
+
@plugins[name] = mod
|
82
83
|
end
|
83
84
|
|
84
85
|
# The base plugin for Roda, implementing all default functionality.
|
@@ -101,6 +102,26 @@ class Roda
|
|
101
102
|
app.call(env)
|
102
103
|
end
|
103
104
|
|
105
|
+
# Create a match_#{key} method in the request class using the given
|
106
|
+
# block, so that using a hash key in a request match method will
|
107
|
+
# call the block. The block should return nil or false to not
|
108
|
+
# match, and anything else to match.
|
109
|
+
#
|
110
|
+
# class App < Roda
|
111
|
+
# hash_matcher(:foo) do |v|
|
112
|
+
# self['foo'] == v
|
113
|
+
# end
|
114
|
+
#
|
115
|
+
# route do
|
116
|
+
# r.on :foo=>'bar' do
|
117
|
+
# # matches when param foo has value bar
|
118
|
+
# end
|
119
|
+
# end
|
120
|
+
# end
|
121
|
+
def hash_matcher(key, &block)
|
122
|
+
request_module{define_method(:"match_#{key}", &block)}
|
123
|
+
end
|
124
|
+
|
104
125
|
# When inheriting Roda, setup a new rack app builder, copy the
|
105
126
|
# default middleware and opts into the subclass, and set the
|
106
127
|
# request and response classes in the subclasses to be subclasses
|
@@ -115,6 +136,7 @@ class Roda
|
|
115
136
|
|
116
137
|
request_class = Class.new(self::RodaRequest)
|
117
138
|
request_class.roda_class = subclass
|
139
|
+
request_class.match_pattern_cache = thread_safe_cache
|
118
140
|
subclass.const_set(:RodaRequest, request_class)
|
119
141
|
|
120
142
|
response_class = Class.new(self::RodaResponse)
|
@@ -125,6 +147,9 @@ class Roda
|
|
125
147
|
# Load a new plugin into the current class. A plugin can be a module
|
126
148
|
# which is used directly, or a symbol represented a registered plugin
|
127
149
|
# which will be required and then used.
|
150
|
+
#
|
151
|
+
# Roda.plugin PluginModule
|
152
|
+
# Roda.plugin :csrf
|
128
153
|
def plugin(mixin, *args, &block)
|
129
154
|
if mixin.is_a?(Symbol)
|
130
155
|
mixin = RodaPlugins.load_plugin(mixin)
|
@@ -143,9 +168,15 @@ class Roda
|
|
143
168
|
if defined?(mixin::RequestMethods)
|
144
169
|
self::RodaRequest.send(:include, mixin::RequestMethods)
|
145
170
|
end
|
171
|
+
if defined?(mixin::RequestClassMethods)
|
172
|
+
self::RodaRequest.extend mixin::RequestClassMethods
|
173
|
+
end
|
146
174
|
if defined?(mixin::ResponseMethods)
|
147
175
|
self::RodaResponse.send(:include, mixin::ResponseMethods)
|
148
176
|
end
|
177
|
+
if defined?(mixin::ResponseClassMethods)
|
178
|
+
self::RodaResponse.extend mixin::ResponseClassMethods
|
179
|
+
end
|
149
180
|
|
150
181
|
if mixin.respond_to?(:configure)
|
151
182
|
mixin.configure(self, *args, &block)
|
@@ -154,28 +185,71 @@ class Roda
|
|
154
185
|
|
155
186
|
# Include the given module in the request class. If a block
|
156
187
|
# is provided instead of a module, create a module using the
|
157
|
-
# the block.
|
188
|
+
# the block. Example:
|
189
|
+
#
|
190
|
+
# Roda.request_module SomeModule
|
191
|
+
#
|
192
|
+
# Roda.request_module do
|
193
|
+
# def description
|
194
|
+
# "#{request_method} #{path_info}"
|
195
|
+
# end
|
196
|
+
# end
|
197
|
+
#
|
198
|
+
# Roda.route do |r|
|
199
|
+
# r.description
|
200
|
+
# end
|
158
201
|
def request_module(mod = nil, &block)
|
159
202
|
module_include(:request, mod, &block)
|
160
203
|
end
|
161
204
|
|
162
205
|
# Include the given module in the response class. If a block
|
163
206
|
# is provided instead of a module, create a module using the
|
164
|
-
# the block.
|
207
|
+
# the block. Example:
|
208
|
+
#
|
209
|
+
# Roda.response_module SomeModule
|
210
|
+
#
|
211
|
+
# Roda.response_module do
|
212
|
+
# def error!
|
213
|
+
# self.status = 500
|
214
|
+
# end
|
215
|
+
# end
|
216
|
+
#
|
217
|
+
# Roda.route do |r|
|
218
|
+
# response.error!
|
219
|
+
# end
|
165
220
|
def response_module(mod = nil, &block)
|
166
221
|
module_include(:response, mod, &block)
|
167
222
|
end
|
168
223
|
|
169
|
-
# Setup
|
170
|
-
# rack application using the stored middleware.
|
224
|
+
# Setup routing tree for the current Roda application, and build the
|
225
|
+
# underlying rack application using the stored middleware. Requires
|
226
|
+
# a block, which is yielded the request. By convention, the block
|
227
|
+
# argument should be named +r+. Example:
|
228
|
+
#
|
229
|
+
# Roda.route do |r|
|
230
|
+
# r.root do
|
231
|
+
# "Root"
|
232
|
+
# end
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# This should only be called once per class, and if called multiple
|
236
|
+
# times will overwrite the previous routing.
|
171
237
|
def route(&block)
|
172
238
|
@middleware.each{|a, b| @builder.use(*a, &b)}
|
173
239
|
@builder.run lambda{|env| new.call(env, &block)}
|
174
240
|
@app = @builder.to_app
|
175
241
|
end
|
176
242
|
|
243
|
+
# A new thread safe cache instance. This is a method so it can be
|
244
|
+
# easily overridden for alternative implementations.
|
245
|
+
def thread_safe_cache
|
246
|
+
RodaCache.new
|
247
|
+
end
|
248
|
+
|
177
249
|
# Add a middleware to use for the rack application. Must be
|
178
|
-
# called before calling #route.
|
250
|
+
# called before calling #route to have an effect. Example:
|
251
|
+
#
|
252
|
+
# Roda.use Rack::Session::Cookie, :secret=>ENV['secret']
|
179
253
|
def use(*args, &block)
|
180
254
|
@middleware << [args, block]
|
181
255
|
end
|
@@ -214,25 +288,34 @@ class Roda
|
|
214
288
|
|
215
289
|
# Create a request and response of the appopriate
|
216
290
|
# class, the instance_exec the route block with
|
217
|
-
# the request, handling any halts.
|
291
|
+
# the request, handling any halts. This is not usually
|
292
|
+
# called directly.
|
218
293
|
def call(env, &block)
|
219
294
|
@_request = self.class::RodaRequest.new(self, env)
|
220
295
|
@_response = self.class::RodaResponse.new
|
221
296
|
_route(&block)
|
222
297
|
end
|
223
298
|
|
224
|
-
# The environment for the current request.
|
299
|
+
# The environment hash for the current request. Example:
|
300
|
+
#
|
301
|
+
# env['REQUEST_METHOD'] # => 'GET'
|
225
302
|
def env
|
226
303
|
request.env
|
227
304
|
end
|
228
305
|
|
229
306
|
# The class-level options hash. This should probably not be
|
230
|
-
# modified at the instance level.
|
307
|
+
# modified at the instance level. Example:
|
308
|
+
#
|
309
|
+
# Roda.plugin :render
|
310
|
+
# Roda.route do |r|
|
311
|
+
# opts[:render_opts].inspect
|
312
|
+
# end
|
231
313
|
def opts
|
232
314
|
self.class.opts
|
233
315
|
end
|
234
316
|
|
235
317
|
# The instance of the request class related to this request.
|
318
|
+
# This is the same object yielded by Roda.route.
|
236
319
|
def request
|
237
320
|
@_request
|
238
321
|
end
|
@@ -254,12 +337,61 @@ class Roda
|
|
254
337
|
# behavior after the request and response have been setup.
|
255
338
|
def _route(&block)
|
256
339
|
catch(:halt) do
|
257
|
-
request.
|
340
|
+
request.block_result(instance_exec(@_request, &block))
|
258
341
|
response.finish
|
259
342
|
end
|
260
343
|
end
|
261
344
|
end
|
262
345
|
|
346
|
+
# Class methods for RodaRequest
|
347
|
+
module RequestClassMethods
|
348
|
+
# Reference to the Roda class related to this request class.
|
349
|
+
attr_accessor :roda_class
|
350
|
+
|
351
|
+
# The cache to use for match patterns for this request class.
|
352
|
+
attr_accessor :match_pattern_cache
|
353
|
+
|
354
|
+
# Return the cached pattern for the given object. If the object is
|
355
|
+
# not already cached, yield to get the basic pattern, and convert the
|
356
|
+
# basic pattern to a pattern that does not partial segments.
|
357
|
+
def cached_matcher(obj)
|
358
|
+
cache = @match_pattern_cache
|
359
|
+
|
360
|
+
unless pattern = cache[obj]
|
361
|
+
pattern = cache[obj] = consume_pattern(yield)
|
362
|
+
end
|
363
|
+
|
364
|
+
pattern
|
365
|
+
end
|
366
|
+
|
367
|
+
# Define a verb method in the given that will yield to the match block
|
368
|
+
# if the request method matches and there are either no arguments or
|
369
|
+
# there is a successful terminal match on the arguments.
|
370
|
+
def def_verb_method(mod, verb)
|
371
|
+
mod.class_eval(<<-END, __FILE__, __LINE__+1)
|
372
|
+
def #{verb}(*args, &block)
|
373
|
+
_verb(args, &block) if #{verb == :get ? :is_get : verb}?
|
374
|
+
end
|
375
|
+
END
|
376
|
+
end
|
377
|
+
|
378
|
+
# Since RodaRequest is anonymously subclassed when Roda is subclassed,
|
379
|
+
# and then assigned to a constant of the Roda subclass, make inspect
|
380
|
+
# reflect the likely name for the class.
|
381
|
+
def inspect
|
382
|
+
"#{roda_class.inspect}::RodaRequest"
|
383
|
+
end
|
384
|
+
|
385
|
+
private
|
386
|
+
|
387
|
+
# The pattern to use for consuming, based on the given argument. The returned
|
388
|
+
# pattern requires the path starts with a string and does not match partial
|
389
|
+
# segments.
|
390
|
+
def consume_pattern(pattern)
|
391
|
+
/\A(\/(?:#{pattern}))(?=\/|\z)/
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
263
395
|
# Instance methods for RodaRequest, mostly related to handling routing
|
264
396
|
# for the request.
|
265
397
|
module RequestMethods
|
@@ -267,8 +399,16 @@ class Roda
|
|
267
399
|
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
268
400
|
REQUEST_METHOD = "REQUEST_METHOD".freeze
|
269
401
|
EMPTY_STRING = "".freeze
|
270
|
-
|
402
|
+
SLASH = "/".freeze
|
271
403
|
SEGMENT = "([^\\/]+)".freeze
|
404
|
+
TERM_INSPECT = "TERM".freeze
|
405
|
+
GET_REQUEST_METHOD = 'GET'.freeze
|
406
|
+
|
407
|
+
TERM = Object.new
|
408
|
+
def TERM.inspect
|
409
|
+
TERM_INSPECT
|
410
|
+
end
|
411
|
+
TERM.freeze
|
272
412
|
|
273
413
|
# The current captures for the request. This gets modified as routing
|
274
414
|
# occurs.
|
@@ -286,102 +426,315 @@ class Roda
|
|
286
426
|
end
|
287
427
|
|
288
428
|
# As request routing modifies SCRIPT_NAME and PATH_INFO, this exists
|
289
|
-
# as a helper method to get the full
|
429
|
+
# as a helper method to get the full path of the request.
|
430
|
+
#
|
431
|
+
# r.env['SCRIPT_NAME'] = '/foo'
|
432
|
+
# r.env['PATH_INFO'] = '/bar'
|
433
|
+
# r.full_path_info
|
434
|
+
# # => '/foo/bar'
|
290
435
|
def full_path_info
|
291
|
-
"#{env[SCRIPT_NAME]}#{env[PATH_INFO]}"
|
292
|
-
end
|
293
|
-
|
294
|
-
# If this is not a GET method, returns immediately. Otherwise, calls
|
295
|
-
# #is if there are any arguments, or #on if there are no arguments.
|
296
|
-
def get(*args, &block)
|
297
|
-
is_or_on(*args, &block) if get?
|
436
|
+
"#{@env[SCRIPT_NAME]}#{@env[PATH_INFO]}"
|
298
437
|
end
|
299
438
|
|
300
439
|
# Immediately stop execution of the route block and return the given
|
301
|
-
# rack response array of status, headers, and body.
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
#
|
440
|
+
# rack response array of status, headers, and body. If no argument
|
441
|
+
# is given, uses the current response.
|
442
|
+
#
|
443
|
+
# r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
|
444
|
+
#
|
445
|
+
# response.status = 200
|
446
|
+
# response['Content-Type'] = 'text/html'
|
447
|
+
# response.write 'Hello World!'
|
448
|
+
# r.halt
|
449
|
+
def halt(res=response.finish)
|
450
|
+
throw :halt, res
|
451
|
+
end
|
452
|
+
|
453
|
+
# Optimized method for whether this request is a +GET+ request.
|
454
|
+
# Similar to the default Rack::Request get? method, but can be
|
455
|
+
# overridden without changing rack's behavior.
|
456
|
+
def is_get?
|
457
|
+
@env[REQUEST_METHOD] == GET_REQUEST_METHOD
|
458
|
+
end
|
459
|
+
|
460
|
+
# Handle match block return values. By default, if a string is given
|
307
461
|
# and the response is empty, use the string as the response body.
|
308
|
-
def
|
462
|
+
def block_result(result)
|
309
463
|
res = response
|
310
|
-
if
|
311
|
-
res.write(
|
464
|
+
if res.empty? && (body = block_result_body(result))
|
465
|
+
res.write(body)
|
312
466
|
end
|
313
467
|
end
|
314
468
|
|
315
469
|
# Show information about current request, including request class,
|
316
470
|
# request method and full path.
|
471
|
+
#
|
472
|
+
# r.inspect
|
473
|
+
# # => '#<Roda::RodaRequest GET /foo/bar>'
|
317
474
|
def inspect
|
318
|
-
"#<#{self.class.inspect} #{env[REQUEST_METHOD]} #{full_path_info}>"
|
319
|
-
end
|
320
|
-
|
321
|
-
#
|
322
|
-
#
|
475
|
+
"#<#{self.class.inspect} #{@env[REQUEST_METHOD]} #{full_path_info}>"
|
476
|
+
end
|
477
|
+
|
478
|
+
# Does a terminal match on the current path, matching only if the arguments
|
479
|
+
# have fully matched the path. If it matches, the match block is
|
480
|
+
# executed, and when the match block returns, the rack response is
|
481
|
+
# returned.
|
482
|
+
#
|
483
|
+
# r.path_info
|
484
|
+
# # => "/foo/bar"
|
485
|
+
#
|
486
|
+
# r.is 'foo' do
|
487
|
+
# # does not match, as path isn't fully matched (/bar remaining)
|
488
|
+
# end
|
489
|
+
#
|
490
|
+
# r.is 'foo/bar' do
|
491
|
+
# # matches as path is empty after matching
|
492
|
+
# end
|
493
|
+
#
|
494
|
+
# If no arguments are given, matches if the path is already fully matched.
|
495
|
+
#
|
496
|
+
# r.on 'foo/bar' do
|
497
|
+
# r.is do
|
498
|
+
# # matches as path is already empty
|
499
|
+
# end
|
500
|
+
# end
|
501
|
+
#
|
502
|
+
# Note that this matches only if the path after matching the arguments
|
503
|
+
# is empty, not if it still contains a trailing slash:
|
504
|
+
#
|
505
|
+
# r.path_info
|
506
|
+
# # => "/foo/bar/"
|
507
|
+
#
|
508
|
+
# r.is 'foo/bar' do
|
509
|
+
# # does not match, as path isn't fully matched (/ remaining)
|
510
|
+
# end
|
511
|
+
#
|
512
|
+
# r.is 'foo/bar/' do
|
513
|
+
# # matches as path is empty after matching
|
514
|
+
# end
|
515
|
+
#
|
516
|
+
# r.on 'foo/bar' do
|
517
|
+
# r.is "" do
|
518
|
+
# # matches as path is empty after matching
|
519
|
+
# end
|
520
|
+
# end
|
323
521
|
def is(*args, &block)
|
324
|
-
args
|
325
|
-
|
522
|
+
if args.empty?
|
523
|
+
if @env[PATH_INFO] == EMPTY_STRING
|
524
|
+
always(&block)
|
525
|
+
end
|
526
|
+
else
|
527
|
+
args << TERM
|
528
|
+
if_match(args, &block)
|
529
|
+
end
|
326
530
|
end
|
327
531
|
|
328
|
-
#
|
329
|
-
#
|
330
|
-
#
|
331
|
-
#
|
332
|
-
#
|
532
|
+
# Does a match on the path, matching only if the arguments
|
533
|
+
# have matched the path. Because this doesn't fully match the
|
534
|
+
# path, this is usually used to setup branches of the routing tree,
|
535
|
+
# not for final handling of the request.
|
536
|
+
#
|
537
|
+
# r.path_info
|
538
|
+
# # => "/foo/bar"
|
539
|
+
#
|
540
|
+
# r.on 'foo' do
|
541
|
+
# # matches, path is /bar after matching
|
542
|
+
# end
|
543
|
+
#
|
544
|
+
# r.on 'bar' do
|
545
|
+
# # does not match
|
546
|
+
# end
|
547
|
+
#
|
548
|
+
# Like other routing methods, If it matches, the match block is
|
549
|
+
# executed, and when the match block returns, the rack response is
|
550
|
+
# returned. However, in general you will call another routing method
|
551
|
+
# inside the match block that fully matches the path and does the
|
552
|
+
# final handling for the request:
|
553
|
+
#
|
554
|
+
# r.on 'foo' do
|
555
|
+
# r.is 'bar' do
|
556
|
+
# # handle /foo/bar request
|
557
|
+
# end
|
558
|
+
# end
|
333
559
|
def on(*args, &block)
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
#
|
339
|
-
# Short circuit examples:
|
340
|
-
# on true, false do
|
341
|
-
#
|
342
|
-
# # PATH_INFO=/user
|
343
|
-
# on true, "signup"
|
344
|
-
return unless args.all?{|arg| match(arg)}
|
345
|
-
|
346
|
-
# The captures we yield here were generated and assembled
|
347
|
-
# by evaluating each of the `arg`s above. Most of these
|
348
|
-
# are carried out by #consume.
|
349
|
-
handle_on_result(yield(*captures))
|
350
|
-
|
351
|
-
_halt response.finish
|
560
|
+
if args.empty?
|
561
|
+
always(&block)
|
562
|
+
else
|
563
|
+
if_match(args, &block)
|
352
564
|
end
|
353
565
|
end
|
354
566
|
|
355
|
-
#
|
356
|
-
#
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
# The response related to the current request.
|
567
|
+
# The response related to the current request. See ResponseMethods for
|
568
|
+
# instance methods for the response, but in general the most common usage
|
569
|
+
# is to override the response status and headers:
|
570
|
+
#
|
571
|
+
# response.status = 200
|
572
|
+
# response['Header-Name'] = 'Header value'
|
362
573
|
def response
|
363
574
|
scope.response
|
364
575
|
end
|
365
576
|
|
366
|
-
# Immediately redirect to the
|
367
|
-
|
577
|
+
# Immediately redirect to the path using the status code. This ends
|
578
|
+
# the processing of the request:
|
579
|
+
#
|
580
|
+
# r.redirect '/page1', 301 if r['param'] == 'value1'
|
581
|
+
# r.redirect '/page2' # uses 302 status code
|
582
|
+
# response.status = 404 # not reached
|
583
|
+
#
|
584
|
+
# If you do not provide a path, by default it will redirect to the same
|
585
|
+
# path if the request is not a +GET+ request. This is designed to make
|
586
|
+
# it easy to use where a +POST+ request to a URL changes state, +GET+
|
587
|
+
# returns the current state, and you want to show the current state
|
588
|
+
# after changing:
|
589
|
+
#
|
590
|
+
# r.is "foo" do
|
591
|
+
# r.get do
|
592
|
+
# # show state
|
593
|
+
# end
|
594
|
+
#
|
595
|
+
# r.post do
|
596
|
+
# # change state
|
597
|
+
# r.redirect
|
598
|
+
# end
|
599
|
+
# end
|
600
|
+
def redirect(path=default_redirect_path, status=302)
|
368
601
|
response.redirect(path, status)
|
369
|
-
|
602
|
+
throw :halt, response.finish
|
603
|
+
end
|
604
|
+
|
605
|
+
# Routing matches that only matches +GET+ requests where the current
|
606
|
+
# path is +/+. If it matches, the match block is executed, and when
|
607
|
+
# the match block returns, the rack response is returned.
|
608
|
+
#
|
609
|
+
# [r.request_method, r.path_info]
|
610
|
+
# # => ['GET', '/']
|
611
|
+
#
|
612
|
+
# r.root do
|
613
|
+
# # matches
|
614
|
+
# end
|
615
|
+
#
|
616
|
+
# This is usuable inside other match blocks:
|
617
|
+
#
|
618
|
+
# [r.request_method, r.path_info]
|
619
|
+
# # => ['GET', '/foo/']
|
620
|
+
#
|
621
|
+
# r.on 'foo' do
|
622
|
+
# r.root do
|
623
|
+
# # matches
|
624
|
+
# end
|
625
|
+
# end
|
626
|
+
#
|
627
|
+
# Note that this does not match non-+GET+ requests:
|
628
|
+
#
|
629
|
+
# [r.request_method, r.path_info]
|
630
|
+
# # => ['POST', '/']
|
631
|
+
#
|
632
|
+
# r.root do
|
633
|
+
# # does not match
|
634
|
+
# end
|
635
|
+
#
|
636
|
+
# Use <tt>r.post ""</tt> for +POST+ requests where the current path
|
637
|
+
# is +/+.
|
638
|
+
#
|
639
|
+
# Nor does it match empty paths:
|
640
|
+
#
|
641
|
+
# [r.request_method, r.path_info]
|
642
|
+
# # => ['GET', '/foo']
|
643
|
+
#
|
644
|
+
# r.on 'foo' do
|
645
|
+
# r.root do
|
646
|
+
# # does not match
|
647
|
+
# end
|
648
|
+
# end
|
649
|
+
#
|
650
|
+
# Use <tt>r.get true</tt> to handle +GET+ requests where the current
|
651
|
+
# path is empty.
|
652
|
+
def root(&block)
|
653
|
+
if @env[PATH_INFO] == SLASH && is_get?
|
654
|
+
always(&block)
|
655
|
+
end
|
370
656
|
end
|
371
657
|
|
372
|
-
# Call the given rack app with the environment and
|
373
|
-
# the
|
658
|
+
# Call the given rack app with the environment and return the response
|
659
|
+
# from the rack app as the response for this request. This ends
|
660
|
+
# the processing of the request:
|
661
|
+
#
|
662
|
+
# r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
|
663
|
+
# r.run(proc{[404, {}, []]})
|
664
|
+
# response.status = 404 # not reached
|
374
665
|
def run(app)
|
375
|
-
|
666
|
+
throw :halt, app.call(@env)
|
376
667
|
end
|
377
668
|
|
378
669
|
private
|
379
670
|
|
380
|
-
#
|
381
|
-
#
|
382
|
-
#
|
383
|
-
def
|
384
|
-
|
671
|
+
# Match any of the elements in the given array. Return at the
|
672
|
+
# first match without evaluating future matches. Returns false
|
673
|
+
# if no elements in the array match.
|
674
|
+
def _match_array(matcher)
|
675
|
+
matcher.any? do |m|
|
676
|
+
if matched = match(m)
|
677
|
+
if m.is_a?(String)
|
678
|
+
captures.push(m)
|
679
|
+
end
|
680
|
+
end
|
681
|
+
|
682
|
+
matched
|
683
|
+
end
|
684
|
+
end
|
685
|
+
|
686
|
+
# Match the given regexp exactly if it matches a full segment.
|
687
|
+
def _match_regexp(re)
|
688
|
+
consume(self.class.cached_matcher(re){re})
|
689
|
+
end
|
690
|
+
|
691
|
+
# Match the given hash if all hash matchers match.
|
692
|
+
def _match_hash(hash)
|
693
|
+
hash.all?{|k,v| send("match_#{k}", v)}
|
694
|
+
end
|
695
|
+
|
696
|
+
# Match the given string to the request path. Regexp escapes the
|
697
|
+
# string so that regexp metacharacters are not matched, and recognizes
|
698
|
+
# colon tokens for placeholders.
|
699
|
+
def _match_string(str)
|
700
|
+
consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
|
701
|
+
end
|
702
|
+
|
703
|
+
# Match the given symbol if any segment matches.
|
704
|
+
def _match_symbol(sym)
|
705
|
+
consume(self.class.cached_matcher(sym){_match_symbol_regexp(sym)})
|
706
|
+
end
|
707
|
+
|
708
|
+
# The regular expression to use for matching symbols. By default, any non-empty
|
709
|
+
# segment matches.
|
710
|
+
def _match_symbol_regexp(s)
|
711
|
+
SEGMENT
|
712
|
+
end
|
713
|
+
|
714
|
+
# Backbone of the verb method support, using a terminal match if
|
715
|
+
# args is not empty, or a regular match if it is empty.
|
716
|
+
def _verb(args, &block)
|
717
|
+
if args.empty?
|
718
|
+
always(&block)
|
719
|
+
else
|
720
|
+
args << TERM
|
721
|
+
if_match(args, &block)
|
722
|
+
end
|
723
|
+
end
|
724
|
+
|
725
|
+
# Yield to the match block and return rack response after the block returns.
|
726
|
+
def always
|
727
|
+
block_result(yield)
|
728
|
+
throw :halt, response.finish
|
729
|
+
end
|
730
|
+
|
731
|
+
# The body to use for the response if the response does not return
|
732
|
+
# a body. By default, a String is returned directly, and nil is
|
733
|
+
# returned otherwise.
|
734
|
+
def block_result_body(result)
|
735
|
+
if result.is_a?(String)
|
736
|
+
result
|
737
|
+
end
|
385
738
|
end
|
386
739
|
|
387
740
|
# Attempts to match the pattern to the current path. If there is no
|
@@ -389,43 +742,67 @@ class Roda
|
|
389
742
|
# SCRIPT_NAME to include the matched path, removes the matched
|
390
743
|
# path from PATH_INFO, and updates captures with any regex captures.
|
391
744
|
def consume(pattern)
|
392
|
-
|
393
|
-
|
394
|
-
return false unless matchdata
|
745
|
+
env = @env
|
746
|
+
return unless matchdata = env[PATH_INFO].match(pattern)
|
395
747
|
|
396
748
|
vars = matchdata.captures
|
397
749
|
|
398
750
|
# Don't mutate SCRIPT_NAME, breaks try
|
399
751
|
env[SCRIPT_NAME] += vars.shift
|
400
|
-
env[PATH_INFO] =
|
752
|
+
env[PATH_INFO] = matchdata.post_match
|
401
753
|
|
402
754
|
captures.concat(vars)
|
403
755
|
end
|
404
756
|
|
405
|
-
#
|
406
|
-
#
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
757
|
+
# The default path to use for redirects when a path is not given.
|
758
|
+
# For non-GET requests, redirects to the current path, which will
|
759
|
+
# trigger a GET request. This is to make the common case where
|
760
|
+
# a POST request will redirect to a GET request at the same location
|
761
|
+
# will work fine.
|
762
|
+
#
|
763
|
+
# If the current request is a GET request, raise an error, as otherwise
|
764
|
+
# it is easy to create an infinite redirect.
|
765
|
+
def default_redirect_path
|
766
|
+
raise RodaError, "must provide path argument to redirect for get requests" if is_get?
|
767
|
+
full_path_info
|
768
|
+
end
|
769
|
+
|
770
|
+
# If all of the arguments match, yields to the match block and
|
771
|
+
# returns the rack response when the block returns. If any of
|
772
|
+
# the match arguments doesn't match, does nothing.
|
773
|
+
def if_match(args)
|
774
|
+
env = @env
|
775
|
+
script = env[SCRIPT_NAME]
|
776
|
+
path = env[PATH_INFO]
|
777
|
+
|
778
|
+
# For every block, we make sure to reset captures so that
|
779
|
+
# nesting matchers won't mess with each other's captures.
|
780
|
+
captures.clear
|
414
781
|
|
782
|
+
return unless match_all(args)
|
783
|
+
block_result(yield(*captures))
|
784
|
+
throw :halt, response.finish
|
785
|
+
ensure
|
786
|
+
env[SCRIPT_NAME] = script
|
787
|
+
env[PATH_INFO] = path
|
788
|
+
end
|
789
|
+
|
415
790
|
# Attempt to match the argument to the given request, handling
|
416
791
|
# common ruby types.
|
417
792
|
def match(matcher)
|
418
793
|
case matcher
|
419
794
|
when String
|
420
|
-
|
795
|
+
_match_string(matcher)
|
421
796
|
when Regexp
|
422
|
-
|
797
|
+
_match_regexp(matcher)
|
423
798
|
when Symbol
|
424
|
-
|
799
|
+
_match_symbol(matcher)
|
800
|
+
when TERM
|
801
|
+
@env[PATH_INFO] == EMPTY_STRING
|
425
802
|
when Hash
|
426
|
-
matcher
|
803
|
+
_match_hash(matcher)
|
427
804
|
when Array
|
428
|
-
|
805
|
+
_match_array(matcher)
|
429
806
|
when Proc
|
430
807
|
matcher.call
|
431
808
|
else
|
@@ -433,25 +810,15 @@ class Roda
|
|
433
810
|
end
|
434
811
|
end
|
435
812
|
|
436
|
-
# Match
|
437
|
-
|
438
|
-
|
439
|
-
def match_array(matcher)
|
440
|
-
matcher.any? do |m|
|
441
|
-
if matched = match(m)
|
442
|
-
if m.is_a?(String)
|
443
|
-
captures.push(m)
|
444
|
-
end
|
445
|
-
end
|
446
|
-
|
447
|
-
matched
|
448
|
-
end
|
813
|
+
# Match only if all of the arguments in the given array match.
|
814
|
+
def match_all(args)
|
815
|
+
args.all?{|arg| match(arg)}
|
449
816
|
end
|
450
817
|
|
451
818
|
# Match files with the given extension. Requires that the
|
452
819
|
# request path end with the extension.
|
453
820
|
def match_extension(ext)
|
454
|
-
consume(
|
821
|
+
consume(self.class.cached_matcher([:extension, ext]){/([^\\\/]+)\.#{ext}/})
|
455
822
|
end
|
456
823
|
|
457
824
|
# Match by request method. This can be an array if you want
|
@@ -460,7 +827,7 @@ class Roda
|
|
460
827
|
if type.is_a?(Array)
|
461
828
|
type.any?{|t| match_method(t)}
|
462
829
|
else
|
463
|
-
type.to_s.upcase == env[REQUEST_METHOD]
|
830
|
+
type.to_s.upcase == @env[REQUEST_METHOD]
|
464
831
|
end
|
465
832
|
end
|
466
833
|
|
@@ -479,37 +846,18 @@ class Roda
|
|
479
846
|
captures << v
|
480
847
|
end
|
481
848
|
end
|
849
|
+
end
|
482
850
|
|
483
|
-
|
484
|
-
|
485
|
-
#
|
486
|
-
|
487
|
-
str = Regexp.escape(str)
|
488
|
-
str.gsub!(/:\w+/, SEGMENT)
|
489
|
-
consume(str)
|
490
|
-
end
|
491
|
-
|
492
|
-
# Only match if the request path is empty, which usually indicates it
|
493
|
-
# has already been fully matched.
|
494
|
-
def match_term(term)
|
495
|
-
!(term ^ (env[PATH_INFO] == EMPTY_STRING))
|
496
|
-
end
|
497
|
-
|
498
|
-
# Yield to the given block, clearing any captures before
|
499
|
-
# yielding and restoring the SCRIPT_NAME and PATH_INFO on exit.
|
500
|
-
def try
|
501
|
-
script = env[SCRIPT_NAME]
|
502
|
-
path = env[PATH_INFO]
|
503
|
-
|
504
|
-
# For every block, we make sure to reset captures so that
|
505
|
-
# nesting matchers won't mess with each other's captures.
|
506
|
-
captures.clear
|
507
|
-
|
508
|
-
yield
|
851
|
+
# Class methods for RodaResponse
|
852
|
+
module ResponseClassMethods
|
853
|
+
# Reference to the Roda class related to this response class.
|
854
|
+
attr_accessor :roda_class
|
509
855
|
|
510
|
-
|
511
|
-
|
512
|
-
|
856
|
+
# Since RodaResponse is anonymously subclassed when Roda is subclassed,
|
857
|
+
# and then assigned to a constant of the Roda subclass, make inspect
|
858
|
+
# reflect the likely name for the class.
|
859
|
+
def inspect
|
860
|
+
"#{roda_class.inspect}::RodaResponse"
|
513
861
|
end
|
514
862
|
end
|
515
863
|
|
@@ -535,12 +883,16 @@ class Roda
|
|
535
883
|
@length = 0
|
536
884
|
end
|
537
885
|
|
538
|
-
# Return the response header with the given key.
|
886
|
+
# Return the response header with the given key. Example:
|
887
|
+
#
|
888
|
+
# response['Content-Type'] # => 'text/html'
|
539
889
|
def [](key)
|
540
890
|
@headers[key]
|
541
891
|
end
|
542
892
|
|
543
893
|
# Set the response header with the given key to the given value.
|
894
|
+
#
|
895
|
+
# response['Content-Type'] = 'application/json'
|
544
896
|
def []=(key, value)
|
545
897
|
@headers[key] = value
|
546
898
|
end
|
@@ -558,19 +910,29 @@ class Roda
|
|
558
910
|
# Modify the headers to include a Set-Cookie value that
|
559
911
|
# deletes the cookie. A value hash can be provided to
|
560
912
|
# override the default one used to delete the cookie.
|
913
|
+
# Example:
|
914
|
+
#
|
915
|
+
# response.delete_cookie('foo')
|
916
|
+
# response.delete_cookie('foo', :domain=>'example.org')
|
561
917
|
def delete_cookie(key, value = {})
|
562
918
|
::Rack::Utils.delete_cookie_header!(@headers, key, value)
|
563
919
|
end
|
564
920
|
|
565
921
|
# Whether the response body has been written to yet. Note
|
566
922
|
# that writing an empty string to the response body marks
|
567
|
-
# the response as not empty.
|
923
|
+
# the response as not empty. Example:
|
924
|
+
#
|
925
|
+
# response.empty? # => true
|
926
|
+
# response.write('a')
|
927
|
+
# response.empty? # => false
|
568
928
|
def empty?
|
569
929
|
@body.empty?
|
570
930
|
end
|
571
931
|
|
572
932
|
# Return the rack response array of status, headers, and body
|
573
|
-
# for the current response.
|
933
|
+
# for the current response. Example:
|
934
|
+
#
|
935
|
+
# response.finish # => [200, {'Content-Type'=>'text/html'}, []]
|
574
936
|
def finish
|
575
937
|
b = @body
|
576
938
|
s = (@status ||= b.empty? ? 404 : 200)
|
@@ -578,19 +940,28 @@ class Roda
|
|
578
940
|
end
|
579
941
|
|
580
942
|
# Set the Location header to the given path, and the status
|
581
|
-
# to the given status.
|
943
|
+
# to the given status. Example:
|
944
|
+
#
|
945
|
+
# response.redirect('foo', 301)
|
946
|
+
# response.redirect('bar')
|
582
947
|
def redirect(path, status = 302)
|
583
948
|
@headers[LOCATION] = path
|
584
949
|
@status = status
|
585
950
|
end
|
586
951
|
|
587
952
|
# Set the cookie with the given key in the headers.
|
953
|
+
#
|
954
|
+
# response.set_cookie('foo', 'bar')
|
955
|
+
# response.set_cookie('foo', :value=>'bar', :domain=>'example.org')
|
588
956
|
def set_cookie(key, value)
|
589
957
|
::Rack::Utils.set_cookie_header!(@headers, key, value)
|
590
958
|
end
|
591
959
|
|
592
960
|
# Write to the response body. Updates Content-Length header
|
593
|
-
# with the size of the string written. Returns nil.
|
961
|
+
# with the size of the string written. Returns nil. Example:
|
962
|
+
#
|
963
|
+
# response.write('foo')
|
964
|
+
# response['Content-Length'] # =>'3'
|
594
965
|
def write(str)
|
595
966
|
s = str.to_s
|
596
967
|
|
@@ -605,4 +976,6 @@ class Roda
|
|
605
976
|
|
606
977
|
extend RodaPlugins::Base::ClassMethods
|
607
978
|
plugin RodaPlugins::Base
|
979
|
+
RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :get)
|
980
|
+
RodaRequest.def_verb_method(RodaPlugins::Base::RequestMethods, :post)
|
608
981
|
end
|