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.
@@ -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
@@ -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
@@ -52,9 +52,14 @@ class Roda
52
52
  # plugin :symbol_views
53
53
  # plugin :json
54
54
  # route do |r|
55
- # r.halt(:template)
56
- # r.halt(500, [{'error'=>'foo'}])
57
- # r.halt(500, 'header=>'value', :other_template)
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
- def initialize(*)
60
- super
61
- params['captures'] = []
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
@@ -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