roda 3.26.0 → 3.27.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 +8 -0
- data/doc/release_notes/3.27.0.txt +15 -0
- data/lib/roda.rb +4 -857
- data/lib/roda/cache.rb +35 -0
- data/lib/roda/plugins.rb +53 -0
- data/lib/roda/plugins/halt.rb +8 -3
- data/lib/roda/plugins/multibyte_string_matcher.rb +57 -0
- data/lib/roda/plugins/params_capturing.rb +5 -3
- data/lib/roda/request.rb +625 -0
- data/lib/roda/response.rb +172 -0
- data/lib/roda/version.rb +1 -1
- data/spec/define_roda_method_spec.rb +1 -1
- data/spec/plugin/json_parser_spec.rb +5 -0
- data/spec/plugin/multibyte_string_matcher_spec.rb +44 -0
- data/spec/plugin/sinatra_helpers_spec.rb +2 -2
- metadata +10 -2
data/lib/roda/cache.rb
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "thread"
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
# A thread safe cache class, offering only #[] and #[]= methods,
|
7
|
+
# each protected by a mutex.
|
8
|
+
class RodaCache
|
9
|
+
# Create a new thread safe cache.
|
10
|
+
def initialize
|
11
|
+
@mutex = Mutex.new
|
12
|
+
@hash = {}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Make getting value from underlying hash thread safe.
|
16
|
+
def [](key)
|
17
|
+
@mutex.synchronize{@hash[key]}
|
18
|
+
end
|
19
|
+
|
20
|
+
# Make setting value in underlying hash thread safe.
|
21
|
+
def []=(key, value)
|
22
|
+
@mutex.synchronize{@hash[key] = value}
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Create a copy of the cache with a separate mutex.
|
28
|
+
def initialize_copy(other)
|
29
|
+
@mutex = Mutex.new
|
30
|
+
other.instance_variable_get(:@mutex).synchronize do
|
31
|
+
@hash = other.instance_variable_get(:@hash).dup
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/roda/plugins.rb
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require_relative "cache"
|
4
|
+
|
5
|
+
class Roda
|
6
|
+
# Module in which all Roda plugins should be stored. Also contains logic for
|
7
|
+
# registering and loading plugins.
|
8
|
+
module RodaPlugins
|
9
|
+
OPTS = {}.freeze
|
10
|
+
EMPTY_ARRAY = [].freeze
|
11
|
+
|
12
|
+
# Stores registered plugins
|
13
|
+
@plugins = RodaCache.new
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Make warn a public method, as it is used for deprecation warnings.
|
17
|
+
# Roda::RodaPlugins.warn can be overridden for custom handling of
|
18
|
+
# deprecation warnings.
|
19
|
+
public :warn
|
20
|
+
end
|
21
|
+
|
22
|
+
# If the registered plugin already exists, use it. Otherwise,
|
23
|
+
# require it and return it. This raises a LoadError if such a
|
24
|
+
# plugin doesn't exist, or a RodaError if it exists but it does
|
25
|
+
# not register itself correctly.
|
26
|
+
def self.load_plugin(name)
|
27
|
+
h = @plugins
|
28
|
+
unless plugin = h[name]
|
29
|
+
require "roda/plugins/#{name}"
|
30
|
+
raise RodaError, "Plugin #{name} did not register itself correctly in Roda::RodaPlugins" unless plugin = h[name]
|
31
|
+
end
|
32
|
+
plugin
|
33
|
+
end
|
34
|
+
|
35
|
+
# Register the given plugin with Roda, so that it can be loaded using #plugin
|
36
|
+
# with a symbol. Should be used by plugin files. Example:
|
37
|
+
#
|
38
|
+
# Roda::RodaPlugins.register_plugin(:plugin_name, PluginModule)
|
39
|
+
def self.register_plugin(name, mod)
|
40
|
+
@plugins[name] = mod
|
41
|
+
end
|
42
|
+
|
43
|
+
# Deprecate the constant with the given name in the given module,
|
44
|
+
# if the ruby version supports it.
|
45
|
+
def self.deprecate_constant(mod, name)
|
46
|
+
# :nocov:
|
47
|
+
if RUBY_VERSION >= '2.3'
|
48
|
+
mod.deprecate_constant(name)
|
49
|
+
end
|
50
|
+
# :nocov:
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
data/lib/roda/plugins/halt.rb
CHANGED
@@ -52,9 +52,14 @@ class Roda
|
|
52
52
|
# plugin :symbol_views
|
53
53
|
# plugin :json
|
54
54
|
# route do |r|
|
55
|
-
#
|
56
|
-
# r.halt(
|
57
|
-
#
|
55
|
+
# # symbol_views plugin, specifying template file to render as body
|
56
|
+
# r.halt(:template) if r.params['a']
|
57
|
+
#
|
58
|
+
# # symbol_views plugin, specifying status code, headers, and template file to render as body
|
59
|
+
# r.halt(500, 'header=>'value', :other_template) if r.params['c']
|
60
|
+
#
|
61
|
+
# # json plugin, specifying status code and JSON body
|
62
|
+
# r.halt(500, [{'error'=>'foo'}]) if r.params['b']
|
58
63
|
# end
|
59
64
|
#
|
60
65
|
# Note that when using the +json+ plugin with the +halt+ plugin, you cannot return a
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The multibyte_string_matcher plugin allows multibyte
|
7
|
+
# strings to be used in matchers. Roda's default string
|
8
|
+
# matcher does not handle multibyte strings for performance
|
9
|
+
# reasons.
|
10
|
+
#
|
11
|
+
# As browsers send multibyte characters in request paths URL
|
12
|
+
# escaped, so this also loads the unescape_path plugin to
|
13
|
+
# unescape the paths.
|
14
|
+
#
|
15
|
+
# plugin :multibyte_string_matcher
|
16
|
+
#
|
17
|
+
# path = "\xD0\xB8".force_encoding('UTF-8')
|
18
|
+
# route do |r|
|
19
|
+
# r.get path do
|
20
|
+
# # GET /\xD0\xB8 (request.path in UTF-8 format)
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# r.get /y-(#{path})/u do |x|
|
24
|
+
# # GET /y-\xD0\xB8 (request.path in UTF-8 format)
|
25
|
+
# x => "\xD0\xB8".force_encoding('BINARY')
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
module MultibyteStringMatcher
|
29
|
+
# Must load unescape_path plugin to decode multibyte
|
30
|
+
# paths, which are submitted escaped.
|
31
|
+
def self.load_dependencies(app)
|
32
|
+
app.plugin :unescape_path
|
33
|
+
end
|
34
|
+
|
35
|
+
module RequestMethods
|
36
|
+
private
|
37
|
+
|
38
|
+
# Use multibyte safe string matcher, using the same
|
39
|
+
# approach as in Roda 3.0.
|
40
|
+
def _match_string(str)
|
41
|
+
rp = @remaining_path
|
42
|
+
if rp.start_with?("/#{str}")
|
43
|
+
last = str.length + 1
|
44
|
+
case rp[last]
|
45
|
+
when "/"
|
46
|
+
@remaining_path = rp[last, rp.length]
|
47
|
+
when nil
|
48
|
+
@remaining_path = ""
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
register_plugin(:multibyte_string_matcher, MultibyteStringMatcher)
|
56
|
+
end
|
57
|
+
end
|
@@ -56,9 +56,11 @@ class Roda
|
|
56
56
|
# symbols that capture multiple values or no values.
|
57
57
|
module ParamsCapturing
|
58
58
|
module RequestMethods
|
59
|
-
|
60
|
-
|
61
|
-
|
59
|
+
# Lazily initialize captures entry when params is called.
|
60
|
+
def params
|
61
|
+
ret = super
|
62
|
+
ret['captures'] ||= []
|
63
|
+
ret
|
62
64
|
end
|
63
65
|
|
64
66
|
private
|
data/lib/roda/request.rb
ADDED
@@ -0,0 +1,625 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
require "rack"
|
4
|
+
require_relative "cache"
|
5
|
+
|
6
|
+
class Roda
|
7
|
+
# Base class used for Roda requests. The instance methods for this
|
8
|
+
# class are added by Roda::RodaPlugins::Base::RequestMethods, the
|
9
|
+
# class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
|
10
|
+
class RodaRequest < ::Rack::Request
|
11
|
+
@roda_class = ::Roda
|
12
|
+
@match_pattern_cache = ::Roda::RodaCache.new
|
13
|
+
end
|
14
|
+
|
15
|
+
module RodaPlugins
|
16
|
+
module Base
|
17
|
+
# Class methods for RodaRequest
|
18
|
+
module RequestClassMethods
|
19
|
+
# Reference to the Roda class related to this request class.
|
20
|
+
attr_accessor :roda_class
|
21
|
+
|
22
|
+
# The cache to use for match patterns for this request class.
|
23
|
+
attr_accessor :match_pattern_cache
|
24
|
+
|
25
|
+
# Return the cached pattern for the given object. If the object is
|
26
|
+
# not already cached, yield to get the basic pattern, and convert the
|
27
|
+
# basic pattern to a pattern that does not partial segments.
|
28
|
+
def cached_matcher(obj)
|
29
|
+
cache = @match_pattern_cache
|
30
|
+
|
31
|
+
unless pattern = cache[obj]
|
32
|
+
pattern = cache[obj] = consume_pattern(yield)
|
33
|
+
end
|
34
|
+
|
35
|
+
pattern
|
36
|
+
end
|
37
|
+
|
38
|
+
# Since RodaRequest is anonymously subclassed when Roda is subclassed,
|
39
|
+
# and then assigned to a constant of the Roda subclass, make inspect
|
40
|
+
# reflect the likely name for the class.
|
41
|
+
def inspect
|
42
|
+
"#{roda_class.inspect}::RodaRequest"
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
# The pattern to use for consuming, based on the given argument. The returned
|
48
|
+
# pattern requires the path starts with a string and does not match partial
|
49
|
+
# segments.
|
50
|
+
def consume_pattern(pattern)
|
51
|
+
/\A\/(?:#{pattern})(?=\/|\z)/
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
# Instance methods for RodaRequest, mostly related to handling routing
|
56
|
+
# for the request.
|
57
|
+
module RequestMethods
|
58
|
+
TERM = Object.new
|
59
|
+
def TERM.inspect
|
60
|
+
"TERM"
|
61
|
+
end
|
62
|
+
TERM.freeze
|
63
|
+
|
64
|
+
# The current captures for the request. This gets modified as routing
|
65
|
+
# occurs.
|
66
|
+
attr_reader :captures
|
67
|
+
|
68
|
+
# The Roda instance related to this request object. Useful if routing
|
69
|
+
# methods need access to the scope of the Roda route block.
|
70
|
+
attr_reader :scope
|
71
|
+
|
72
|
+
# Store the roda instance and environment.
|
73
|
+
def initialize(scope, env)
|
74
|
+
@scope = scope
|
75
|
+
@captures = []
|
76
|
+
@remaining_path = _remaining_path(env)
|
77
|
+
@env = env
|
78
|
+
end
|
79
|
+
|
80
|
+
# Handle match block return values. By default, if a string is given
|
81
|
+
# and the response is empty, use the string as the response body.
|
82
|
+
def block_result(result)
|
83
|
+
res = response
|
84
|
+
if res.empty? && (body = block_result_body(result))
|
85
|
+
res.write(body)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Match GET requests. If no arguments are provided, matches all GET
|
90
|
+
# requests, otherwise, matches only GET requests where the arguments
|
91
|
+
# given fully consume the path.
|
92
|
+
def get(*args, &block)
|
93
|
+
_verb(args, &block) if is_get?
|
94
|
+
end
|
95
|
+
|
96
|
+
# Immediately stop execution of the route block and return the given
|
97
|
+
# rack response array of status, headers, and body. If no argument
|
98
|
+
# is given, uses the current response.
|
99
|
+
#
|
100
|
+
# r.halt [200, {'Content-Type'=>'text/html'}, ['Hello World!']]
|
101
|
+
#
|
102
|
+
# response.status = 200
|
103
|
+
# response['Content-Type'] = 'text/html'
|
104
|
+
# response.write 'Hello World!'
|
105
|
+
# r.halt
|
106
|
+
def halt(res=response.finish)
|
107
|
+
throw :halt, res
|
108
|
+
end
|
109
|
+
|
110
|
+
# Show information about current request, including request class,
|
111
|
+
# request method and full path.
|
112
|
+
#
|
113
|
+
# r.inspect
|
114
|
+
# # => '#<Roda::RodaRequest GET /foo/bar>'
|
115
|
+
def inspect
|
116
|
+
"#<#{self.class.inspect} #{@env["REQUEST_METHOD"]} #{path}>"
|
117
|
+
end
|
118
|
+
|
119
|
+
# Does a terminal match on the current path, matching only if the arguments
|
120
|
+
# have fully matched the path. If it matches, the match block is
|
121
|
+
# executed, and when the match block returns, the rack response is
|
122
|
+
# returned.
|
123
|
+
#
|
124
|
+
# r.remaining_path
|
125
|
+
# # => "/foo/bar"
|
126
|
+
#
|
127
|
+
# r.is 'foo' do
|
128
|
+
# # does not match, as path isn't fully matched (/bar remaining)
|
129
|
+
# end
|
130
|
+
#
|
131
|
+
# r.is 'foo/bar' do
|
132
|
+
# # matches as path is empty after matching
|
133
|
+
# end
|
134
|
+
#
|
135
|
+
# If no arguments are given, matches if the path is already fully matched.
|
136
|
+
#
|
137
|
+
# r.on 'foo/bar' do
|
138
|
+
# r.is do
|
139
|
+
# # matches as path is already empty
|
140
|
+
# end
|
141
|
+
# end
|
142
|
+
#
|
143
|
+
# Note that this matches only if the path after matching the arguments
|
144
|
+
# is empty, not if it still contains a trailing slash:
|
145
|
+
#
|
146
|
+
# r.remaining_path
|
147
|
+
# # => "/foo/bar/"
|
148
|
+
#
|
149
|
+
# r.is 'foo/bar' do
|
150
|
+
# # does not match, as path isn't fully matched (/ remaining)
|
151
|
+
# end
|
152
|
+
#
|
153
|
+
# r.is 'foo/bar/' do
|
154
|
+
# # matches as path is empty after matching
|
155
|
+
# end
|
156
|
+
#
|
157
|
+
# r.on 'foo/bar' do
|
158
|
+
# r.is "" do
|
159
|
+
# # matches as path is empty after matching
|
160
|
+
# end
|
161
|
+
# end
|
162
|
+
def is(*args, &block)
|
163
|
+
if args.empty?
|
164
|
+
if empty_path?
|
165
|
+
always(&block)
|
166
|
+
end
|
167
|
+
else
|
168
|
+
args << TERM
|
169
|
+
if_match(args, &block)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Optimized method for whether this request is a +GET+ request.
|
174
|
+
# Similar to the default Rack::Request get? method, but can be
|
175
|
+
# overridden without changing rack's behavior.
|
176
|
+
def is_get?
|
177
|
+
@env["REQUEST_METHOD"] == 'GET'
|
178
|
+
end
|
179
|
+
|
180
|
+
# Does a match on the path, matching only if the arguments
|
181
|
+
# have matched the path. Because this doesn't fully match the
|
182
|
+
# path, this is usually used to setup branches of the routing tree,
|
183
|
+
# not for final handling of the request.
|
184
|
+
#
|
185
|
+
# r.remaining_path
|
186
|
+
# # => "/foo/bar"
|
187
|
+
#
|
188
|
+
# r.on 'foo' do
|
189
|
+
# # matches, path is /bar after matching
|
190
|
+
# end
|
191
|
+
#
|
192
|
+
# r.on 'bar' do
|
193
|
+
# # does not match
|
194
|
+
# end
|
195
|
+
#
|
196
|
+
# Like other routing methods, If it matches, the match block is
|
197
|
+
# executed, and when the match block returns, the rack response is
|
198
|
+
# returned. However, in general you will call another routing method
|
199
|
+
# inside the match block that fully matches the path and does the
|
200
|
+
# final handling for the request:
|
201
|
+
#
|
202
|
+
# r.on 'foo' do
|
203
|
+
# r.is 'bar' do
|
204
|
+
# # handle /foo/bar request
|
205
|
+
# end
|
206
|
+
# end
|
207
|
+
def on(*args, &block)
|
208
|
+
if args.empty?
|
209
|
+
always(&block)
|
210
|
+
else
|
211
|
+
if_match(args, &block)
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# The already matched part of the path, including the original SCRIPT_NAME.
|
216
|
+
def matched_path
|
217
|
+
e = @env
|
218
|
+
e["SCRIPT_NAME"] + e["PATH_INFO"].chomp(@remaining_path)
|
219
|
+
end
|
220
|
+
|
221
|
+
# This an an optimized version of Rack::Request#path.
|
222
|
+
#
|
223
|
+
# r.env['SCRIPT_NAME'] = '/foo'
|
224
|
+
# r.env['PATH_INFO'] = '/bar'
|
225
|
+
# r.path
|
226
|
+
# # => '/foo/bar'
|
227
|
+
def path
|
228
|
+
e = @env
|
229
|
+
"#{e["SCRIPT_NAME"]}#{e["PATH_INFO"]}"
|
230
|
+
end
|
231
|
+
|
232
|
+
# The current path to match requests against.
|
233
|
+
attr_reader :remaining_path
|
234
|
+
|
235
|
+
# An alias of remaining_path. If a plugin changes remaining_path then
|
236
|
+
# it should override this method to return the untouched original.
|
237
|
+
def real_remaining_path
|
238
|
+
remaining_path
|
239
|
+
end
|
240
|
+
|
241
|
+
# Match POST requests. If no arguments are provided, matches all POST
|
242
|
+
# requests, otherwise, matches only POST requests where the arguments
|
243
|
+
# given fully consume the path.
|
244
|
+
def post(*args, &block)
|
245
|
+
_verb(args, &block) if post?
|
246
|
+
end
|
247
|
+
|
248
|
+
# Immediately redirect to the path using the status code. This ends
|
249
|
+
# the processing of the request:
|
250
|
+
#
|
251
|
+
# r.redirect '/page1', 301 if r['param'] == 'value1'
|
252
|
+
# r.redirect '/page2' # uses 302 status code
|
253
|
+
# response.status = 404 # not reached
|
254
|
+
#
|
255
|
+
# If you do not provide a path, by default it will redirect to the same
|
256
|
+
# path if the request is not a +GET+ request. This is designed to make
|
257
|
+
# it easy to use where a +POST+ request to a URL changes state, +GET+
|
258
|
+
# returns the current state, and you want to show the current state
|
259
|
+
# after changing:
|
260
|
+
#
|
261
|
+
# r.is "foo" do
|
262
|
+
# r.get do
|
263
|
+
# # show state
|
264
|
+
# end
|
265
|
+
#
|
266
|
+
# r.post do
|
267
|
+
# # change state
|
268
|
+
# r.redirect
|
269
|
+
# end
|
270
|
+
# end
|
271
|
+
def redirect(path=default_redirect_path, status=default_redirect_status)
|
272
|
+
response.redirect(path, status)
|
273
|
+
throw :halt, response.finish
|
274
|
+
end
|
275
|
+
|
276
|
+
# The response related to the current request. See ResponseMethods for
|
277
|
+
# instance methods for the response, but in general the most common usage
|
278
|
+
# is to override the response status and headers:
|
279
|
+
#
|
280
|
+
# response.status = 200
|
281
|
+
# response['Header-Name'] = 'Header value'
|
282
|
+
def response
|
283
|
+
@scope.response
|
284
|
+
end
|
285
|
+
|
286
|
+
# Return the Roda class related to this request.
|
287
|
+
def roda_class
|
288
|
+
self.class.roda_class
|
289
|
+
end
|
290
|
+
|
291
|
+
# Match method that only matches +GET+ requests where the current
|
292
|
+
# path is +/+. If it matches, the match block is executed, and when
|
293
|
+
# the match block returns, the rack response is returned.
|
294
|
+
#
|
295
|
+
# [r.request_method, r.remaining_path]
|
296
|
+
# # => ['GET', '/']
|
297
|
+
#
|
298
|
+
# r.root do
|
299
|
+
# # matches
|
300
|
+
# end
|
301
|
+
#
|
302
|
+
# This is usuable inside other match blocks:
|
303
|
+
#
|
304
|
+
# [r.request_method, r.remaining_path]
|
305
|
+
# # => ['GET', '/foo/']
|
306
|
+
#
|
307
|
+
# r.on 'foo' do
|
308
|
+
# r.root do
|
309
|
+
# # matches
|
310
|
+
# end
|
311
|
+
# end
|
312
|
+
#
|
313
|
+
# Note that this does not match non-+GET+ requests:
|
314
|
+
#
|
315
|
+
# [r.request_method, r.remaining_path]
|
316
|
+
# # => ['POST', '/']
|
317
|
+
#
|
318
|
+
# r.root do
|
319
|
+
# # does not match
|
320
|
+
# end
|
321
|
+
#
|
322
|
+
# Use <tt>r.post ""</tt> for +POST+ requests where the current path
|
323
|
+
# is +/+.
|
324
|
+
#
|
325
|
+
# Nor does it match empty paths:
|
326
|
+
#
|
327
|
+
# [r.request_method, r.remaining_path]
|
328
|
+
# # => ['GET', '/foo']
|
329
|
+
#
|
330
|
+
# r.on 'foo' do
|
331
|
+
# r.root do
|
332
|
+
# # does not match
|
333
|
+
# end
|
334
|
+
# end
|
335
|
+
#
|
336
|
+
# Use <tt>r.get true</tt> to handle +GET+ requests where the current
|
337
|
+
# path is empty.
|
338
|
+
def root(&block)
|
339
|
+
if remaining_path == "/" && is_get?
|
340
|
+
always(&block)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
# Call the given rack app with the environment and return the response
|
345
|
+
# from the rack app as the response for this request. This ends
|
346
|
+
# the processing of the request:
|
347
|
+
#
|
348
|
+
# r.run(proc{[403, {}, []]}) unless r['letmein'] == '1'
|
349
|
+
# r.run(proc{[404, {}, []]})
|
350
|
+
# response.status = 404 # not reached
|
351
|
+
#
|
352
|
+
# This updates SCRIPT_NAME/PATH_INFO based on the current remaining_path
|
353
|
+
# before dispatching to another rack app, so the app still works as
|
354
|
+
# a URL mapper.
|
355
|
+
def run(app)
|
356
|
+
e = @env
|
357
|
+
path = real_remaining_path
|
358
|
+
sn = "SCRIPT_NAME"
|
359
|
+
pi = "PATH_INFO"
|
360
|
+
script_name = e[sn]
|
361
|
+
path_info = e[pi]
|
362
|
+
begin
|
363
|
+
e[sn] += path_info.chomp(path)
|
364
|
+
e[pi] = path
|
365
|
+
throw :halt, app.call(e)
|
366
|
+
ensure
|
367
|
+
e[sn] = script_name
|
368
|
+
e[pi] = path_info
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
# The session for the current request. Raises a RodaError if
|
373
|
+
# a session handler has not been loaded.
|
374
|
+
def session
|
375
|
+
@env['rack.session'] || raise(RodaError, "You're missing a session handler, try using the sessions plugin.")
|
376
|
+
end
|
377
|
+
|
378
|
+
private
|
379
|
+
|
380
|
+
# Match any of the elements in the given array. Return at the
|
381
|
+
# first match without evaluating future matches. Returns false
|
382
|
+
# if no elements in the array match.
|
383
|
+
def _match_array(matcher)
|
384
|
+
matcher.any? do |m|
|
385
|
+
if matched = match(m)
|
386
|
+
if m.is_a?(String)
|
387
|
+
@captures.push(m)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
matched
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
# Match the given class. Currently, the following classes
|
396
|
+
# are supported by default:
|
397
|
+
# Integer :: Match an integer segment, yielding result to block as an integer
|
398
|
+
# String :: Match any non-empty segment, yielding result to block as a string
|
399
|
+
def _match_class(klass)
|
400
|
+
meth = :"_match_class_#{klass}"
|
401
|
+
if respond_to?(meth, true)
|
402
|
+
# Allow calling private methods, as match methods are generally private
|
403
|
+
send(meth)
|
404
|
+
else
|
405
|
+
unsupported_matcher(klass)
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# Match the given hash if all hash matchers match.
|
410
|
+
def _match_hash(hash)
|
411
|
+
# Allow calling private methods, as match methods are generally private
|
412
|
+
hash.all?{|k,v| send("match_#{k}", v)}
|
413
|
+
end
|
414
|
+
|
415
|
+
# Match integer segment, and yield resulting value as an
|
416
|
+
# integer.
|
417
|
+
def _match_class_Integer
|
418
|
+
consume(/\A\/(\d+)(?=\/|\z)/){|i| [i.to_i]}
|
419
|
+
end
|
420
|
+
|
421
|
+
# Match only if all of the arguments in the given array match.
|
422
|
+
# Match the given regexp exactly if it matches a full segment.
|
423
|
+
def _match_regexp(re)
|
424
|
+
consume(self.class.cached_matcher(re){re})
|
425
|
+
end
|
426
|
+
|
427
|
+
# Match the given string to the request path. Matches only if the
|
428
|
+
# request path ends with the string or if the next character in the
|
429
|
+
# request path is a slash (indicating a new segment).
|
430
|
+
def _match_string(str)
|
431
|
+
rp = @remaining_path
|
432
|
+
length = str.length
|
433
|
+
|
434
|
+
match = case rp.rindex(str, length)
|
435
|
+
when nil
|
436
|
+
# segment does not match, most common case
|
437
|
+
return
|
438
|
+
when 1
|
439
|
+
# segment matches, check first character is /
|
440
|
+
rp.getbyte(0) == 47
|
441
|
+
else # must be 0
|
442
|
+
# segment matches at first character, only a match if
|
443
|
+
# empty string given and first character is /
|
444
|
+
length == 0 && rp.getbyte(0) == 47
|
445
|
+
end
|
446
|
+
|
447
|
+
if match
|
448
|
+
length += 1
|
449
|
+
case rp.getbyte(length)
|
450
|
+
when 47
|
451
|
+
# next character is /, update remaining path to rest of string
|
452
|
+
@remaining_path = rp[length, 100000000]
|
453
|
+
when nil
|
454
|
+
# end of string, so remaining path is empty
|
455
|
+
@remaining_path = ""
|
456
|
+
# else
|
457
|
+
# Any other value means this was partial segment match,
|
458
|
+
# so we return nil in that case without updating the
|
459
|
+
# remaining_path. No need for explicit else clause.
|
460
|
+
end
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# Match the given symbol if any segment matches.
|
465
|
+
def _match_symbol(sym=nil)
|
466
|
+
rp = @remaining_path
|
467
|
+
if rp.getbyte(0) == 47
|
468
|
+
if last = rp.index('/', 1)
|
469
|
+
if last > 1
|
470
|
+
@captures << rp[1, last-1]
|
471
|
+
@remaining_path = rp[last, rp.length]
|
472
|
+
end
|
473
|
+
elsif rp.length > 1
|
474
|
+
@captures << rp[1,rp.length]
|
475
|
+
@remaining_path = ""
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# Match any nonempty segment. This should be called without an argument.
|
481
|
+
alias _match_class_String _match_symbol
|
482
|
+
|
483
|
+
# The base remaining path to use.
|
484
|
+
def _remaining_path(env)
|
485
|
+
env["PATH_INFO"]
|
486
|
+
end
|
487
|
+
|
488
|
+
# Backbone of the verb method support, using a terminal match if
|
489
|
+
# args is not empty, or a regular match if it is empty.
|
490
|
+
def _verb(args, &block)
|
491
|
+
if args.empty?
|
492
|
+
always(&block)
|
493
|
+
else
|
494
|
+
args << TERM
|
495
|
+
if_match(args, &block)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
|
499
|
+
# Yield to the match block and return rack response after the block returns.
|
500
|
+
def always
|
501
|
+
block_result(yield)
|
502
|
+
throw :halt, response.finish
|
503
|
+
end
|
504
|
+
|
505
|
+
# The body to use for the response if the response does not already have
|
506
|
+
# a body. By default, a String is returned directly, and nil is
|
507
|
+
# returned otherwise.
|
508
|
+
def block_result_body(result)
|
509
|
+
case result
|
510
|
+
when String
|
511
|
+
result
|
512
|
+
when nil, false
|
513
|
+
# nothing
|
514
|
+
else
|
515
|
+
raise RodaError, "unsupported block result: #{result.inspect}"
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
# Attempts to match the pattern to the current path. If there is no
|
520
|
+
# match, returns false without changes. Otherwise, modifies
|
521
|
+
# SCRIPT_NAME to include the matched path, removes the matched
|
522
|
+
# path from PATH_INFO, and updates captures with any regex captures.
|
523
|
+
def consume(pattern)
|
524
|
+
if matchdata = remaining_path.match(pattern)
|
525
|
+
@remaining_path = matchdata.post_match
|
526
|
+
captures = matchdata.captures
|
527
|
+
captures = yield(*captures) if block_given?
|
528
|
+
@captures.concat(captures)
|
529
|
+
end
|
530
|
+
end
|
531
|
+
|
532
|
+
# The default path to use for redirects when a path is not given.
|
533
|
+
# For non-GET requests, redirects to the current path, which will
|
534
|
+
# trigger a GET request. This is to make the common case where
|
535
|
+
# a POST request will redirect to a GET request at the same location
|
536
|
+
# will work fine.
|
537
|
+
#
|
538
|
+
# If the current request is a GET request, raise an error, as otherwise
|
539
|
+
# it is easy to create an infinite redirect.
|
540
|
+
def default_redirect_path
|
541
|
+
raise RodaError, "must provide path argument to redirect for get requests" if is_get?
|
542
|
+
path
|
543
|
+
end
|
544
|
+
|
545
|
+
# The default status to use for redirects if a status is not provided,
|
546
|
+
# 302 by default.
|
547
|
+
def default_redirect_status
|
548
|
+
302
|
549
|
+
end
|
550
|
+
|
551
|
+
# Whether the current path is considered empty.
|
552
|
+
def empty_path?
|
553
|
+
remaining_path.empty?
|
554
|
+
end
|
555
|
+
|
556
|
+
# If all of the arguments match, yields to the match block and
|
557
|
+
# returns the rack response when the block returns. If any of
|
558
|
+
# the match arguments doesn't match, does nothing.
|
559
|
+
def if_match(args)
|
560
|
+
path = @remaining_path
|
561
|
+
# For every block, we make sure to reset captures so that
|
562
|
+
# nesting matchers won't mess with each other's captures.
|
563
|
+
captures = @captures.clear
|
564
|
+
|
565
|
+
if match_all(args)
|
566
|
+
block_result(yield(*captures))
|
567
|
+
throw :halt, response.finish
|
568
|
+
else
|
569
|
+
@remaining_path = path
|
570
|
+
false
|
571
|
+
end
|
572
|
+
end
|
573
|
+
|
574
|
+
# Attempt to match the argument to the given request, handling
|
575
|
+
# common ruby types.
|
576
|
+
def match(matcher)
|
577
|
+
case matcher
|
578
|
+
when String
|
579
|
+
_match_string(matcher)
|
580
|
+
when Class
|
581
|
+
_match_class(matcher)
|
582
|
+
when TERM
|
583
|
+
empty_path?
|
584
|
+
when Regexp
|
585
|
+
_match_regexp(matcher)
|
586
|
+
when true
|
587
|
+
matcher
|
588
|
+
when Array
|
589
|
+
_match_array(matcher)
|
590
|
+
when Hash
|
591
|
+
_match_hash(matcher)
|
592
|
+
when Symbol
|
593
|
+
_match_symbol(matcher)
|
594
|
+
when false, nil
|
595
|
+
matcher
|
596
|
+
when Proc
|
597
|
+
matcher.call
|
598
|
+
else
|
599
|
+
unsupported_matcher(matcher)
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
# Match only if all of the arguments in the given array match.
|
604
|
+
def match_all(args)
|
605
|
+
args.all?{|arg| match(arg)}
|
606
|
+
end
|
607
|
+
|
608
|
+
# Match by request method. This can be an array if you want
|
609
|
+
# to match on multiple methods.
|
610
|
+
def match_method(type)
|
611
|
+
if type.is_a?(Array)
|
612
|
+
type.any?{|t| match_method(t)}
|
613
|
+
else
|
614
|
+
type.to_s.upcase == @env["REQUEST_METHOD"]
|
615
|
+
end
|
616
|
+
end
|
617
|
+
|
618
|
+
# Handle an unsupported matcher.
|
619
|
+
def unsupported_matcher(matcher)
|
620
|
+
raise RodaError, "unsupported matcher: #{matcher.inspect}"
|
621
|
+
end
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
end
|