roda 3.84.0 → 3.86.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 847704410e729b17a70bb30b3da0e7a2a539320c3394947269ca00c680e57c97
4
- data.tar.gz: 8487b70418d90a811cca396a7c8fe52f87d9c6a93f34c7ca66316d8cb15a30b2
3
+ metadata.gz: a1851a201539b728f1af90ea1b85b2b33a9026f71986cb65c1aabdd079f653ad
4
+ data.tar.gz: f360e0bfeb3442df2fa1f96e28362eee9850c0f54d160bfbfc2b11d62d33ba39
5
5
  SHA512:
6
- metadata.gz: 94e7e1d8318aed73f0e491d8dae066b5a0c05a759cdadd690073d1ca8600acbb6269b0d1ab0cb51e038835563568e2a83db154dcf5ab5f72d4cfc809c0e94da5
7
- data.tar.gz: 52e999ea7a16b47ad65b25ca52208f9b51d13ab8458ca24404e74606a9f3b6c9c5ea18b1bcb088b3e3495ac8f520b895af8eb23da07a85e96881f9008bafda1e
6
+ metadata.gz: d2bef4abc3d5e08ddb5a1c9c27f8b626b631a5951cec7cee062c497074cb7f68e6c8b36e75261cc913a580a87d5111438e8a21488669812c9a3b227bf9609e0b
7
+ data.tar.gz: e668a47039e529aa026e21ddfd6346b3e1fa224f432b46085b33242c9551ced88278328f2d2d471b2593f841da0a882d8f85dd2468277484409f679485a219df
@@ -23,27 +23,28 @@ class Roda
23
23
  module IntegerMatcherMax
24
24
  def self.configure(app, max=nil)
25
25
  if max
26
- app::RodaRequest.class_eval do
27
- define_method(:_match_class_max_Integer){max}
28
- alias_method :_match_class_max_Integer, :_match_class_max_Integer
29
- private :_match_class_max_Integer
26
+ app.class_eval do
27
+ meth = :_max_value_convert_class_Integer
28
+ define_method(meth){max}
29
+ alias_method meth, meth
30
+ private meth
30
31
  end
31
32
  end
32
33
  end
33
34
 
34
- module RequestMethods
35
+ module InstanceMethods
35
36
  private
36
37
 
37
38
  # Do not have the Integer matcher max when over the maximum
38
39
  # configured Integer value.
39
- def _match_class_convert_Integer(value)
40
+ def _convert_class_Integer(value)
40
41
  value = super
41
- value if value <= _match_class_max_Integer
42
+ value if value <= _max_value_convert_class_Integer
42
43
  end
43
44
 
44
45
  # Use 2**63-1 as the default maximum value for the Integer
45
46
  # matcher.
46
- def _match_class_max_Integer
47
+ def _max_value_convert_class_Integer
47
48
  9223372036854775807
48
49
  end
49
50
  end
@@ -52,7 +52,7 @@ class Roda
52
52
  end
53
53
  end
54
54
  elsif matcher == Integer
55
- if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = _match_class_convert_Integer(matchdata[1]))
55
+ if (matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1]))
56
56
  @remaining_path = matchdata.post_match
57
57
  always{yield(value)}
58
58
  end
@@ -151,7 +151,7 @@ class Roda
151
151
  always{yield rp[1, len]}
152
152
  end
153
153
  elsif matcher == Integer
154
- if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = _match_class_convert_Integer(matchdata[1]))
154
+ if (matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)) && (value = scope.send(:_convert_class_Integer, matchdata[1]))
155
155
  @remaining_path = ''
156
156
  always{yield(value)}
157
157
  end
@@ -0,0 +1,107 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ module SymbolClassMatchers_
7
+ module ClassMethods
8
+ private
9
+
10
+ # Backend of symbol_matcher and class_matcher.
11
+ def _symbol_class_matcher(expected_class, obj, matcher, block, &request_class_block)
12
+ unless obj.is_a?(expected_class)
13
+ raise RodaError, "Invalid type passed to class_matcher or symbol_matcher: #{matcher.inspect}"
14
+ end
15
+
16
+ if obj.is_a?(Symbol)
17
+ type = "symbol"
18
+ meth = :"match_symbol_#{obj}"
19
+ else
20
+ type = "class"
21
+ meth = :"_match_class_#{obj}"
22
+ end
23
+
24
+ case matcher
25
+ when Regexp
26
+ regexp = matcher
27
+ consume_regexp = self::RodaRequest.send(:consume_pattern, regexp)
28
+ when Symbol
29
+ unless opts[:symbol_matchers]
30
+ raise RodaError, "cannot provide Symbol matcher to class_matcher unless using symbol_matchers plugin: #{matcher.inspect}"
31
+ end
32
+
33
+ regexp, consume_regexp, matcher_block = opts[:symbol_matchers][matcher]
34
+
35
+ unless regexp
36
+ raise RodaError, "unregistered symbol matcher given to #{type}_matcher: #{matcher.inspect}"
37
+ end
38
+
39
+ block = _merge_matcher_blocks(type, obj, block, matcher_block)
40
+ when Class
41
+ unless opts[:class_matchers]
42
+ raise RodaError, "cannot provide Class matcher to symbol_matcher unless using class_matchers plugin: #{matcher.inspect}"
43
+ end
44
+
45
+ regexp, consume_regexp, matcher_block = opts[:class_matchers][matcher]
46
+ unless regexp
47
+ raise RodaError, "unregistered class matcher given to #{type}_matcher: #{matcher.inspect}"
48
+ end
49
+ block = _merge_matcher_blocks(type, obj, block, matcher_block)
50
+ else
51
+ raise RodaError, "unsupported matcher given to #{type}_matcher: #{matcher.inspect}"
52
+ end
53
+
54
+ if block.is_a?(Symbol)
55
+ convert_meth = block
56
+ elsif block
57
+ convert_meth = :"_convert_#{type}_#{obj}"
58
+ define_method(convert_meth, &block)
59
+ private convert_meth
60
+ end
61
+
62
+ array = opts[:"#{type}_matchers"][obj] = [regexp, consume_regexp, convert_meth].freeze
63
+
64
+ self::RodaRequest.class_eval do
65
+ class_exec(meth, array, &request_class_block)
66
+ private meth
67
+ end
68
+
69
+ nil
70
+ end
71
+
72
+ # If both block and matche_meth are given,
73
+ # define a method for block, and then return a
74
+ # proc that calls matcher_meth first, and only calls
75
+ # the newly defined method with the return values of matcher_meth
76
+ # if matcher_method returns a truthy value.
77
+ # Otherwise, return matcher_meth or block.
78
+ def _merge_matcher_blocks(type, obj, block, matcher_meth)
79
+ if matcher_meth
80
+ if block
81
+ convert_meth = :"_convert_merge_#{type}_#{obj}"
82
+ define_method(convert_meth, &block)
83
+ private convert_meth
84
+
85
+ proc do |*a|
86
+ if captures = send(matcher_meth, *a)
87
+ if captures.is_a?(Array)
88
+ send(convert_meth, *captures)
89
+ else
90
+ send(convert_meth, captures)
91
+ end
92
+ end
93
+ end
94
+ else
95
+ matcher_meth
96
+ end
97
+ else
98
+ block
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ register_plugin(:_symbol_class_matchers, SymbolClassMatchers_)
105
+ end
106
+ end
107
+
@@ -68,7 +68,7 @@ class Roda
68
68
 
69
69
  # Eagerly load all hash branches when freezing the application.
70
70
  def freeze
71
- opts.delete(:autoload_hash_branch_files).each{|file| require file}
71
+ opts.delete(:autoload_hash_branch_files).each{|file| require file} unless opts.frozen?
72
72
  super
73
73
  end
74
74
  end
@@ -54,7 +54,7 @@ class Roda
54
54
 
55
55
  # Eagerly load all autoloaded named routes when freezing the application.
56
56
  def freeze
57
- opts.delete(:autoload_named_route_files).each{|file| require file}
57
+ opts.delete(:autoload_named_route_files).each{|file| require file} unless opts.frozen?
58
58
  super
59
59
  end
60
60
  end
@@ -16,10 +16,11 @@ class Roda
16
16
  # to wrap template blocks with arbitrary output and then inject the
17
17
  # wrapped output into the template.
18
18
  #
19
- # If the output buffer object responds to +capture+ (e.g. when
20
- # +erubi/capture_block+ is being used as the template engine),
21
- # this will call +capture+ on the output buffer object, instead
22
- # of setting the output buffer object temporarily to a new object.
19
+ # If the output buffer object responds to +capture+ and is not
20
+ # an instance of String (e.g. when +erubi/capture_block+ is being
21
+ # used as the template engine), this will call +capture+ on the
22
+ # output buffer object, instead of setting the output buffer object
23
+ # temporarily to a new object.
23
24
  module CaptureERB
24
25
  def self.load_dependencies(app)
25
26
  app.plugin :render
@@ -34,7 +35,7 @@ class Roda
34
35
  outvar = render_opts[:template_opts][:outvar]
35
36
  buf_was = instance_variable_get(outvar)
36
37
 
37
- if buf_was.respond_to?(:capture)
38
+ if buf_was.respond_to?(:capture) && !buf_was.instance_of?(String)
38
39
  buf_was.capture(&block)
39
40
  else
40
41
  begin
@@ -12,11 +12,10 @@ class Roda
12
12
  # # ...
13
13
  # end
14
14
  #
15
- # You can register a Date class matcher for that regexp (note that
16
- # the block must return an array):
15
+ # You can register a Date class matcher for that regexp:
17
16
  #
18
17
  # class_matcher(Date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
19
- # [Date.new(y.to_i, m.to_i, d.to_i)]
18
+ # Date.new(y.to_i, m.to_i, d.to_i)
20
19
  # end
21
20
  #
22
21
  # And then use the Date class as a matcher, and it will yield a Date object:
@@ -26,7 +25,8 @@ class Roda
26
25
  # end
27
26
  #
28
27
  # This is useful to DRY up code if you are using the same type of pattern and
29
- # type conversion in multiple places in your application.
28
+ # type conversion in multiple places in your application. You can have the
29
+ # block return an array to yield multiple captures.
30
30
  #
31
31
  # If you have a segment match the passed regexp, but decide during block
32
32
  # processing that you do not want to treat it as a match, you can have the
@@ -37,22 +37,99 @@ class Roda
37
37
  # y = y.to_i
38
38
  # m = m.to_i
39
39
  # d = d.to_i
40
- # [Date.new(y, m, d)] if Date.valid_date?(y, m, d)
40
+ # Date.new(y, m, d) if Date.valid_date?(y, m, d)
41
41
  # end
42
42
  #
43
+ # The second argument to class_matcher can be a class already registered
44
+ # as a class matcher. This can DRY up code that wants a conversion
45
+ # performed by an existing class matcher:
46
+ #
47
+ # class_matcher Employee, Integer do |id|
48
+ # Employee[id]
49
+ # end
50
+ #
51
+ # With the above example, the Integer matcher performs the conversion to
52
+ # integer, so +id+ is yielded as an integer. The block then looks up the
53
+ # employee with that id. If there is no employee with that id, then
54
+ # the Employee matcher will not match.
55
+ #
56
+ # If using the symbol_matchers plugin, you can provide a recognized symbol
57
+ # matcher as the second argument to class_matcher, and it will work in
58
+ # a similar manner:
59
+ #
60
+ # symbol_matcher(:employee_id, /E-(\d{6})/) do |employee_id|
61
+ # employee_id.to_i
62
+ # end
63
+ # class_matcher Employee, :employee_id do |id|
64
+ # Employee[id]
65
+ # end
66
+ #
67
+ # Blocks passed to the class_matchers plugin are evaluated in route
68
+ # block context.
69
+ #
43
70
  # This plugin does not work with the params_capturing plugin, as it does not
44
71
  # offer the ability to associate block arguments with named keys.
45
72
  module ClassMatchers
73
+ def self.load_dependencies(app)
74
+ app.plugin :_symbol_class_matchers
75
+ end
76
+
77
+ def self.configure(app)
78
+ app.opts[:class_matchers] ||= {
79
+ Integer=>[/(\d{1,100})/, /\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer].freeze,
80
+ String=>[/([^\/]+)/, nil, nil].freeze
81
+ }
82
+ end
83
+
46
84
  module ClassMethods
47
- # Set the regexp to use for the given class. The block given will be
48
- # called with all matched values from the regexp, and should return an
49
- # array with the captures to yield to the match block.
50
- def class_matcher(klass, re, &block)
51
- meth = :"_match_class_#{klass}"
52
- self::RodaRequest.class_eval do
53
- consume_re = consume_pattern(re)
54
- define_method(meth){consume(consume_re, &block)}
55
- private meth
85
+ # Set the matcher and block to use for the given class.
86
+ # The matcher can be a regexp, registered class matcher, or registered symbol
87
+ # matcher (if using the symbol_matchers plugin).
88
+ #
89
+ # If providing a regexp, the block given will be called with all regexp captures.
90
+ # If providing a registered class or symbol, the block will be called with the
91
+ # captures returned by the block for the registered class or symbol, or the regexp
92
+ # captures if no block was registered with the class or symbol. In either case,
93
+ # if a block is given, it should return an array with the captures to yield to
94
+ # the match block.
95
+ def class_matcher(klass, matcher, &block)
96
+ _symbol_class_matcher(Class, klass, matcher, block) do |meth, (_, regexp, convert_meth)|
97
+ if regexp
98
+ define_method(meth){consume(regexp, convert_meth)}
99
+ else
100
+ define_method(meth){_consume_segment(convert_meth)}
101
+ end
102
+ end
103
+ end
104
+
105
+ # Freeze the class_matchers hash when freezing the app.
106
+ def freeze
107
+ opts[:class_matchers].freeze
108
+ super
109
+ end
110
+ end
111
+
112
+ module RequestMethods
113
+ # Use faster approach for segment matching. This is used for
114
+ # matchers based on the String class matcher, and avoids the
115
+ # use of regular expressions for scanning.
116
+ def _consume_segment(convert_meth)
117
+ rp = @remaining_path
118
+ if _match_class_String
119
+ if convert_meth
120
+ if captures = scope.send(convert_meth, @captures.pop)
121
+ if captures.is_a?(Array)
122
+ @captures.concat(captures)
123
+ else
124
+ @captures << captures
125
+ end
126
+ else
127
+ @remaining_path = rp
128
+ nil
129
+ end
130
+ else
131
+ true
132
+ end
56
133
  end
57
134
  end
58
135
  end
@@ -0,0 +1,67 @@
1
+ # frozen-string-literal: true
2
+
3
+ class Roda
4
+ module RodaPlugins
5
+ # The conditional_sessions plugin loads the sessions plugin. However,
6
+ # it only allows sessions if the block passed to the plugin returns
7
+ # truthy. The block is evaluated in request context. This is designed for
8
+ # use in applications that want to use sessions for some requests,
9
+ # and want to be sure that sessions are not used for other requests.
10
+ # For example, if you want to make sure that sessions are not used for
11
+ # requests with paths starting with /static, you could do:
12
+ #
13
+ # plugin :conditional_sessions, secret: ENV["SECRET"] do
14
+ # !path_info.start_with?('/static')
15
+ # end
16
+ #
17
+ # The the request session, session_created_at, and session_updated_at methods
18
+ # raise a RodaError exception when sessions are not allowed. The request
19
+ # persist_session and route scope clear_session methods do nothing when
20
+ # sessions are not allowed.
21
+ module ConditionalSessions
22
+ # Pass all options to the sessions block, and use the block to define
23
+ # a request method for whether sessions are allowed.
24
+ def self.load_dependencies(app, opts=OPTS, &block)
25
+ app.plugin :sessions, opts
26
+ app::RodaRequest.class_eval do
27
+ define_method(:use_sessions?, &block)
28
+ alias use_sessions? use_sessions?
29
+ end
30
+ end
31
+
32
+ module InstanceMethods
33
+ # Do nothing if not using sessions.
34
+ def clear_session
35
+ super if @_request.use_sessions?
36
+ end
37
+ end
38
+
39
+ module RequestMethods
40
+ # Raise RodaError if not using sessions.
41
+ def session
42
+ raise RodaError, "session called on request not using sessions" unless use_sessions?
43
+ super
44
+ end
45
+
46
+ # Raise RodaError if not using sessions.
47
+ def session_created_at
48
+ raise RodaError, "session_created_at called on request not using sessions" unless use_sessions?
49
+ super
50
+ end
51
+
52
+ # Raise RodaError if not using sessions.
53
+ def session_updated_at
54
+ raise RodaError, "session_updated_at called on request not using sessions" unless use_sessions?
55
+ super
56
+ end
57
+
58
+ # Do nothing if not using sessions.
59
+ def persist_session(headers, session)
60
+ super if use_sessions?
61
+ end
62
+ end
63
+ end
64
+
65
+ register_plugin(:conditional_sessions, ConditionalSessions)
66
+ end
67
+ end
@@ -92,7 +92,10 @@ class Roda
92
92
  # content_security_policy.get_script_src
93
93
  # # => [:self, :unsafe_eval, 'example.com', [:nonce, 'foobarbaz']]
94
94
  #
95
- # The clear method can be used to remove all settings from the policy.
95
+ # The clear method can be used to remove all settings from the policy. Empty policies
96
+ # do not set any headers. You can use +response.skip_content_security_policy!+ to skip
97
+ # setting a policy. This is faster than calling +content_security_policy.clear+, since
98
+ # it does not duplicate the default policy.
96
99
  #
97
100
  # The following methods to set boolean settings are also defined:
98
101
  #
@@ -304,12 +307,19 @@ class Roda
304
307
  @content_security_policy ||= roda_class.opts[:content_security_policy].dup
305
308
  end
306
309
 
310
+ # Do not set a content security policy header for this response.
311
+ def skip_content_security_policy!
312
+ @skip_content_security_policy = true
313
+ end
314
+
307
315
  private
308
316
 
309
317
  # Set the appropriate content security policy header.
310
318
  def set_default_headers
311
319
  super
312
- (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
320
+ unless @skip_content_security_policy
321
+ (@content_security_policy || roda_class.opts[:content_security_policy]).set_header(headers)
322
+ end
313
323
  end
314
324
  end
315
325
  end
@@ -4,8 +4,7 @@
4
4
  class Roda
5
5
  module RodaPlugins
6
6
  # The early_hints plugin allows sending 103 Early Hints responses
7
- # using the rack.early_hints environment variable. Currently, this
8
- # is only supported by puma 3.11+, and on other servers this is a no-op.
7
+ # using the rack.early_hints environment variable.
9
8
  # Early hints allow clients to preload necessary files before receiving
10
9
  # the response.
11
10
  module EarlyHints
@@ -99,7 +99,10 @@ class Roda
99
99
  # permissions_policy.get_fullscreen
100
100
  # # => [:self, "https://example.com", "https://*.example.com"]
101
101
  #
102
- # The clear method can be used to remove all settings from the policy.
102
+ # The clear method can be used to remove all settings from the policy. Empty policies
103
+ # do not set any headers. You can use +response.skip_permissions_policy!+ to skip
104
+ # setting a policy. This is faster than calling +permissions_policy.clear+, since
105
+ # it does not duplicate the default policy.
103
106
  module PermissionsPolicy
104
107
  SUPPORTED_SETTINGS = %w'
105
108
  accelerometer
@@ -311,12 +314,19 @@ class Roda
311
314
  @permissions_policy ||= roda_class.opts[:permissions_policy].dup
312
315
  end
313
316
 
317
+ # Do not set a permissions policy header for this response.
318
+ def skip_permissions_policy!
319
+ @skip_permissions_policy = true
320
+ end
321
+
314
322
  private
315
323
 
316
324
  # Set the appropriate permissions policy header.
317
325
  def set_default_headers
318
326
  super
319
- (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
327
+ unless @skip_permissions_policy
328
+ (@permissions_policy || roda_class.opts[:permissions_policy]).set_header(headers)
329
+ end
320
330
  end
321
331
  end
322
332
  end
@@ -21,6 +21,10 @@ class Roda
21
21
  #
22
22
  # r.is "foo", String
23
23
  # r.is "foo", :bar
24
+ #
25
+ # If used with the symbol_matchers plugin, this plugin respects the regexps
26
+ # for the registered symbols, but it does not perform the conversions, the
27
+ # captures for the regexp are used directly as the captures for the match method.
24
28
  module PlaceholderStringMatchers
25
29
  def self.load_dependencies(app)
26
30
  app.plugin :_symbol_regexp_matchers
@@ -42,12 +42,12 @@ class Roda
42
42
  # end
43
43
  module Public
44
44
  SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
45
- PARSER = URI::DEFAULT_PARSER
46
45
  RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
47
46
  ENCODING_MAP = {:zstd=>'zstd', :brotli=>'br', :gzip=>'gzip'}.freeze
48
47
  ENCODING_EXTENSIONS = {'br'=>'.br', 'gzip'=>'.gz', 'zstd'=>'.zst'}.freeze
49
48
 
50
49
  # :nocov:
50
+ PARSER = defined?(::URI::RFC2396_PARSER) ? ::URI::RFC2396_PARSER : ::URI::DEFAULT_PARSER
51
51
  MATCH_METHOD = RUBY_VERSION >= '2.4' ? :match? : :match
52
52
  # :nocov:
53
53
 
@@ -48,7 +48,7 @@ class Roda
48
48
  #
49
49
  # :allowed_paths :: Set the template paths to allow. Attempts to render paths outside
50
50
  # of these paths will raise an error. Defaults to the +:views+ directory.
51
- # :cache :: nil/false to explicitly disable premanent template caching. By default, permanent
51
+ # :cache :: nil/false to explicitly disable permanent template caching. By default, permanent
52
52
  # template caching is disabled by default if RACK_ENV is development. When permanent
53
53
  # template caching is disabled, for templates with paths in the file system, the
54
54
  # modification time of the file will be checked on every render, and if it has changed,
@@ -23,7 +23,7 @@ class Roda
23
23
  #
24
24
  # :d :: <tt>/(\d+)/</tt>, a decimal segment
25
25
  # :rest :: <tt>/(.*)/</tt>, all remaining characters, if any
26
- # :w :: <tt>/(\w+)/</tt>, a alphanumeric segment
26
+ # :w :: <tt>/(\w+)/</tt>, an alphanumeric segment
27
27
  #
28
28
  # If the placeholder_string_matchers plugin is loaded, this feature also applies to
29
29
  # placeholders in strings, so the following:
@@ -39,11 +39,10 @@ class Roda
39
39
  # be loaded first.
40
40
  #
41
41
  # You can provide a block when calling +symbol_matcher+, and it will be called
42
- # for all matches to allow for type conversion. The block must return an
43
- # array:
42
+ # for all matches to allow for type conversion:
44
43
  #
45
44
  # symbol_matcher(:date, /(\d\d\d\d)-(\d\d)-(\d\d)/) do |y, m, d|
46
- # [Date.new(y.to_i, m.to_i, d.to_i)]
45
+ # Date.new(y.to_i, m.to_i, d.to_i)
47
46
  # end
48
47
  #
49
48
  # route do |r|
@@ -61,29 +60,80 @@ class Roda
61
60
  # y = y.to_i
62
61
  # m = m.to_i
63
62
  # d = d.to_i
64
- # [Date.new(y, m, d)] if Date.valid_date?(y, m, d)
63
+ # Date.new(y, m, d) if Date.valid_date?(y, m, d)
65
64
  # end
66
65
  #
67
- # However, if providing a block to the symbol_matchers plugin, the symbol may
68
- # not work with the params_capturing plugin.
66
+ # You can have the block return an array to yield multiple captures.
67
+ #
68
+ # The second argument to symbol_matcher can be a symbol already registered
69
+ # as a symbol matcher. This can DRY up code that wants a conversion
70
+ # performed by an existing class matcher or to use the same regexp:
71
+ #
72
+ # symbol_matcher :employee_id, :d do |id|
73
+ # id.to_i
74
+ # end
75
+ # symbol_matcher :employee, :employee_id do |id|
76
+ # Employee[id]
77
+ # end
78
+ #
79
+ # With the above example, the :d matcher matches only decimal strings, but
80
+ # yields them as string. The registered :employee_id matcher converts the
81
+ # decimal string to an integer. The registered :employee matcher builds
82
+ # on that and uses the integer to lookup the related employee. If there is
83
+ # no employee with that id, then the :employee matcher will not match.
84
+ #
85
+ # If using the class_matchers plugin, you can provide a recognized class
86
+ # matcher as the second argument to symbol_matcher, and it will work in
87
+ # a similar manner:
88
+ #
89
+ # symbol_matcher :employee, Integer do |id|
90
+ # Employee[id]
91
+ # end
92
+ #
93
+ # Blocks passed to the symbol matchers plugin are evaluated in route
94
+ # block context.
95
+ #
96
+ # If providing a block to the symbol_matchers plugin, the symbol may
97
+ # not work with the params_capturing plugin. Note that the use of
98
+ # symbol matchers inside strings when using the placeholder_string_matchers
99
+ # plugin only uses the regexp, it does not respect the conversion blocks
100
+ # registered with the symbols.
69
101
  module SymbolMatchers
70
102
  def self.load_dependencies(app)
71
103
  app.plugin :_symbol_regexp_matchers
104
+ app.plugin :_symbol_class_matchers
72
105
  end
73
106
 
74
107
  def self.configure(app)
108
+ app.opts[:symbol_matchers] ||= {}
75
109
  app.symbol_matcher(:d, /(\d+)/)
76
110
  app.symbol_matcher(:w, /(\w+)/)
77
111
  app.symbol_matcher(:rest, /(.*)/)
78
112
  end
79
113
 
80
114
  module ClassMethods
81
- # Set the regexp to use for the given symbol, instead of the default.
82
- def symbol_matcher(s, re, &block)
83
- meth = :"match_symbol_#{s}"
84
- array = [re, block].freeze
85
- self::RodaRequest.send(:define_method, meth){array}
86
- self::RodaRequest.send(:private, meth)
115
+ # Set the matcher and block to use for the given class.
116
+ # The matcher can be a regexp, registered symbol matcher, or registered class
117
+ # matcher (if using the class_matchers plugin).
118
+ #
119
+ # If providing a regexp, the block given will be called with all regexp captures.
120
+ # If providing a registered symbol or class, the block will be called with the
121
+ # captures returned by the block for the registered symbol or class, or the regexp
122
+ # captures if no block was registered with the symbol or class. In either case,
123
+ # if a block is given, it should return an array with the captures to yield to
124
+ # the match block.
125
+ def symbol_matcher(s, matcher, &block)
126
+ _symbol_class_matcher(Symbol, s, matcher, block) do |meth, array|
127
+ define_method(meth){array}
128
+ end
129
+
130
+ nil
131
+ end
132
+
133
+ # Freeze the class_matchers hash when freezing the app.
134
+ def freeze
135
+ opts[:symbol_matchers].freeze
136
+ super
87
137
  end
88
138
  end
89
139
 
@@ -97,8 +147,13 @@ class Roda
97
147
  meth = :"match_symbol_#{s}"
98
148
  if respond_to?(meth, true)
99
149
  # Allow calling private match methods
100
- re, block = send(meth)
101
- consume(self.class.cached_matcher(re){re}, &block)
150
+ _, re, convert_meth = send(meth)
151
+ if re
152
+ consume(re, convert_meth)
153
+ else
154
+ # defined in class_matchers plugin
155
+ _consume_segment(convert_meth)
156
+ end
102
157
  else
103
158
  super
104
159
  end
data/lib/roda/request.rb CHANGED
@@ -443,16 +443,7 @@ class Roda
443
443
  # Match integer segment of up to 100 decimal characters, and yield resulting value as an
444
444
  # integer.
445
445
  def _match_class_Integer
446
- consume(/\A\/(\d{1,100})(?=\/|\z)/) do |i|
447
- if i = _match_class_convert_Integer(i)
448
- [i]
449
- end
450
- end
451
- end
452
-
453
- # Convert the segment matched by the Integer matcher to an integer.
454
- def _match_class_convert_Integer(value)
455
- value.to_i
446
+ consume(/\A\/(\d{1,100})(?=\/|\z)/, :_convert_class_Integer)
456
447
  end
457
448
 
458
449
  # Match only if all of the arguments in the given array match.
@@ -555,14 +546,26 @@ class Roda
555
546
  # match, returns false without changes. Otherwise, modifies
556
547
  # SCRIPT_NAME to include the matched path, removes the matched
557
548
  # path from PATH_INFO, and updates captures with any regex captures.
558
- def consume(pattern)
549
+ def consume(pattern, meth=nil)
559
550
  if matchdata = pattern.match(@remaining_path)
560
551
  captures = matchdata.captures
561
- if defined?(yield)
552
+
553
+ if meth
554
+ return unless captures = scope.send(meth, *captures)
555
+ # :nocov:
556
+ elsif defined?(yield)
557
+ # RODA4: Remove
562
558
  return unless captures = yield(*captures)
559
+ # :nocov:
563
560
  end
561
+
564
562
  @remaining_path = matchdata.post_match
565
- @captures.concat(captures)
563
+
564
+ if captures.is_a?(Array)
565
+ @captures.concat(captures)
566
+ else
567
+ @captures << captures
568
+ end
566
569
  end
567
570
  end
568
571
 
data/lib/roda/version.rb CHANGED
@@ -4,7 +4,7 @@ class Roda
4
4
  RodaMajorVersion = 3
5
5
 
6
6
  # The minor version of Roda, updated for new feature releases of Roda.
7
- RodaMinorVersion = 84
7
+ RodaMinorVersion = 86
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
data/lib/roda.rb CHANGED
@@ -570,6 +570,13 @@ WARNING
570
570
  def session
571
571
  @_request.session
572
572
  end
573
+
574
+ private
575
+
576
+ # Convert the segment matched by the Integer matcher to an integer.
577
+ def _convert_class_Integer(value)
578
+ value.to_i
579
+ end
573
580
  end
574
581
  end
575
582
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: roda
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.84.0
4
+ version: 3.86.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-09-12 00:00:00.000000000 Z
11
+ date: 2024-11-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -167,6 +167,7 @@ files:
167
167
  - lib/roda/plugins/_base64.rb
168
168
  - lib/roda/plugins/_before_hook.rb
169
169
  - lib/roda/plugins/_optimized_matching.rb
170
+ - lib/roda/plugins/_symbol_class_matchers.rb
170
171
  - lib/roda/plugins/_symbol_regexp_matchers.rb
171
172
  - lib/roda/plugins/additional_render_engines.rb
172
173
  - lib/roda/plugins/additional_view_directories.rb
@@ -185,6 +186,7 @@ files:
185
186
  - lib/roda/plugins/class_level_routing.rb
186
187
  - lib/roda/plugins/class_matchers.rb
187
188
  - lib/roda/plugins/common_logger.rb
189
+ - lib/roda/plugins/conditional_sessions.rb
188
190
  - lib/roda/plugins/content_for.rb
189
191
  - lib/roda/plugins/content_security_policy.rb
190
192
  - lib/roda/plugins/cookie_flags.rb
@@ -325,7 +327,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
325
327
  - !ruby/object:Gem::Version
326
328
  version: '0'
327
329
  requirements: []
328
- rubygems_version: 3.5.16
330
+ rubygems_version: 3.5.22
329
331
  signing_key:
330
332
  specification_version: 4
331
333
  summary: Routing tree web toolkit