roda 3.83.0 → 3.85.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (104) hide show
  1. checksums.yaml +4 -4
  2. data/lib/roda/plugins/Integer_matcher_max.rb +9 -8
  3. data/lib/roda/plugins/_optimized_matching.rb +2 -2
  4. data/lib/roda/plugins/_symbol_class_matchers.rb +107 -0
  5. data/lib/roda/plugins/capture_erb.rb +6 -5
  6. data/lib/roda/plugins/class_matchers.rb +91 -14
  7. data/lib/roda/plugins/hsts.rb +35 -0
  8. data/lib/roda/plugins/placeholder_string_matchers.rb +4 -0
  9. data/lib/roda/plugins/public.rb +1 -1
  10. data/lib/roda/plugins/render.rb +1 -1
  11. data/lib/roda/plugins/symbol_matchers.rb +70 -15
  12. data/lib/roda/request.rb +16 -13
  13. data/lib/roda/response.rb +1 -1
  14. data/lib/roda/version.rb +1 -1
  15. data/lib/roda.rb +7 -0
  16. metadata +5 -179
  17. data/CHANGELOG +0 -691
  18. data/README.rdoc +0 -1136
  19. data/doc/conventions.rdoc +0 -177
  20. data/doc/release_notes/3.0.0.txt +0 -84
  21. data/doc/release_notes/3.1.0.txt +0 -24
  22. data/doc/release_notes/3.10.0.txt +0 -132
  23. data/doc/release_notes/3.11.0.txt +0 -54
  24. data/doc/release_notes/3.12.0.txt +0 -19
  25. data/doc/release_notes/3.13.0.txt +0 -38
  26. data/doc/release_notes/3.14.0.txt +0 -36
  27. data/doc/release_notes/3.14.1.txt +0 -43
  28. data/doc/release_notes/3.15.0.txt +0 -21
  29. data/doc/release_notes/3.16.0.txt +0 -52
  30. data/doc/release_notes/3.17.0.txt +0 -62
  31. data/doc/release_notes/3.18.0.txt +0 -170
  32. data/doc/release_notes/3.19.0.txt +0 -229
  33. data/doc/release_notes/3.2.0.txt +0 -22
  34. data/doc/release_notes/3.20.0.txt +0 -7
  35. data/doc/release_notes/3.21.0.txt +0 -5
  36. data/doc/release_notes/3.22.0.txt +0 -24
  37. data/doc/release_notes/3.23.0.txt +0 -28
  38. data/doc/release_notes/3.24.0.txt +0 -14
  39. data/doc/release_notes/3.25.0.txt +0 -12
  40. data/doc/release_notes/3.26.0.txt +0 -15
  41. data/doc/release_notes/3.27.0.txt +0 -15
  42. data/doc/release_notes/3.28.0.txt +0 -13
  43. data/doc/release_notes/3.29.0.txt +0 -15
  44. data/doc/release_notes/3.3.0.txt +0 -291
  45. data/doc/release_notes/3.30.0.txt +0 -14
  46. data/doc/release_notes/3.31.0.txt +0 -11
  47. data/doc/release_notes/3.32.0.txt +0 -42
  48. data/doc/release_notes/3.33.0.txt +0 -8
  49. data/doc/release_notes/3.34.0.txt +0 -17
  50. data/doc/release_notes/3.35.0.txt +0 -12
  51. data/doc/release_notes/3.36.0.txt +0 -17
  52. data/doc/release_notes/3.37.0.txt +0 -42
  53. data/doc/release_notes/3.38.0.txt +0 -5
  54. data/doc/release_notes/3.39.0.txt +0 -16
  55. data/doc/release_notes/3.4.0.txt +0 -24
  56. data/doc/release_notes/3.40.0.txt +0 -24
  57. data/doc/release_notes/3.41.0.txt +0 -9
  58. data/doc/release_notes/3.42.0.txt +0 -21
  59. data/doc/release_notes/3.43.0.txt +0 -34
  60. data/doc/release_notes/3.44.0.txt +0 -23
  61. data/doc/release_notes/3.45.0.txt +0 -22
  62. data/doc/release_notes/3.46.0.txt +0 -19
  63. data/doc/release_notes/3.47.0.txt +0 -13
  64. data/doc/release_notes/3.48.0.txt +0 -10
  65. data/doc/release_notes/3.49.0.txt +0 -18
  66. data/doc/release_notes/3.5.0.txt +0 -31
  67. data/doc/release_notes/3.50.0.txt +0 -21
  68. data/doc/release_notes/3.51.0.txt +0 -20
  69. data/doc/release_notes/3.52.0.txt +0 -20
  70. data/doc/release_notes/3.53.0.txt +0 -14
  71. data/doc/release_notes/3.54.0.txt +0 -48
  72. data/doc/release_notes/3.55.0.txt +0 -12
  73. data/doc/release_notes/3.56.0.txt +0 -33
  74. data/doc/release_notes/3.57.0.txt +0 -34
  75. data/doc/release_notes/3.58.0.txt +0 -16
  76. data/doc/release_notes/3.59.0.txt +0 -17
  77. data/doc/release_notes/3.6.0.txt +0 -21
  78. data/doc/release_notes/3.60.0.txt +0 -56
  79. data/doc/release_notes/3.61.0.txt +0 -24
  80. data/doc/release_notes/3.62.0.txt +0 -41
  81. data/doc/release_notes/3.63.0.txt +0 -36
  82. data/doc/release_notes/3.64.0.txt +0 -26
  83. data/doc/release_notes/3.65.0.txt +0 -12
  84. data/doc/release_notes/3.66.0.txt +0 -23
  85. data/doc/release_notes/3.67.0.txt +0 -25
  86. data/doc/release_notes/3.68.0.txt +0 -21
  87. data/doc/release_notes/3.69.0.txt +0 -33
  88. data/doc/release_notes/3.7.0.txt +0 -123
  89. data/doc/release_notes/3.70.0.txt +0 -19
  90. data/doc/release_notes/3.71.0.txt +0 -33
  91. data/doc/release_notes/3.72.0.txt +0 -48
  92. data/doc/release_notes/3.73.0.txt +0 -33
  93. data/doc/release_notes/3.74.0.txt +0 -28
  94. data/doc/release_notes/3.75.0.txt +0 -19
  95. data/doc/release_notes/3.76.0.txt +0 -18
  96. data/doc/release_notes/3.77.0.txt +0 -8
  97. data/doc/release_notes/3.78.0.txt +0 -99
  98. data/doc/release_notes/3.79.0.txt +0 -148
  99. data/doc/release_notes/3.8.0.txt +0 -27
  100. data/doc/release_notes/3.80.0.txt +0 -31
  101. data/doc/release_notes/3.81.0.txt +0 -24
  102. data/doc/release_notes/3.82.0.txt +0 -43
  103. data/doc/release_notes/3.83.0.txt +0 -6
  104. data/doc/release_notes/3.9.0.txt +0 -67
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8f3ad50e6de6bdfa7e2019cbcbe4a07b920a23adb30645616340b8bc007951a1
4
- data.tar.gz: b5bd2835bb0f5286fb92555c602949482c4730b3497de4506a841a6ab3e80718
3
+ metadata.gz: a862ed0414bf90081e744ed7c50ac19806f141ebeae8c699d5f96b9a4e718e10
4
+ data.tar.gz: b1fcd622255d24044ba7f26c2c3f35e5fc8ea7f30b2ba577bec5fe306ca75c76
5
5
  SHA512:
6
- metadata.gz: 8e6937b542ae838a0b4e79171936fc3cd937faf127900b34265ff9e0923144bf5cf25879fe0103b43b51fb9c940e1dfc6c058f91eb02bab23177369e3b0424f7
7
- data.tar.gz: d7515ac6becbb47900713e5349b52eb41e284391c5a75743b4e3adcb3609a8ad09a20b8e38e7d85ddb59c52681396a9ec2b1b82b3264752731837b873907835d
6
+ metadata.gz: '086d21f564d66b14fafabbb99c6d670559c12fed91105c1acb9e5e9147623bf15de0730d35f2b5bd098a6e26aba0d6a73497a914ee23d97d8cb58dbbf0f6dd5b'
7
+ data.tar.gz: 713dee3419ce7bf52742d7da6be4243c393b393aeb5762dcd0dc61a58d691e586226636b5df687e56e0dd280a89930a18b1e32f3c9b499516afc461cf92fcf4c
@@ -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
+
@@ -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,35 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The hsts plugin allows for easily configuring an appropriate
7
+ # Strict-Transport-Security response header for the application:
8
+ #
9
+ # plugin :hsts
10
+ # # Strict-Transport-Security: max-age=63072000; includeSubDomains
11
+ #
12
+ # plugin :hsts, preload: true
13
+ # # Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
14
+ #
15
+ # plugin :hsts, max_age: 31536000, subdomains: false
16
+ # # Strict-Transport-Security: max-age=31536000
17
+ module Hsts
18
+ # Ensure default_headers plugin is loaded first
19
+ def self.load_dependencies(app, opts=OPTS)
20
+ app.plugin :default_headers
21
+ end
22
+
23
+ # Configure the Strict-Transport-Security header. Options:
24
+ # :max_age :: Set max-age in seconds (default is 63072000, two years)
25
+ # :preload :: Set preload, so the domain can be included in HSTS preload lists
26
+ # :subdomains :: Set to false to not set includeSubDomains. By default,
27
+ # includeSubDomains is set to enforce HTTPS for subdomains.
28
+ def self.configure(app, opts=OPTS)
29
+ app.plugin :default_headers, RodaResponseHeaders::STRICT_TRANSPORT_SECURITY => "max-age=#{opts[:max_age]||63072000}#{'; includeSubDomains' unless opts[:subdomains] == false}#{'; preload' if opts[:preload]}".freeze
30
+ end
31
+ end
32
+
33
+ register_plugin(:hsts, Hsts)
34
+ end
35
+ 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/response.rb CHANGED
@@ -15,7 +15,7 @@ class Roda
15
15
  %w'Allow Cache-Control Content-Disposition Content-Encoding Content-Length
16
16
  Content-Security-Policy Content-Security-Policy-Report-Only Content-Type
17
17
  ETag Expires Last-Modified Link Location Set-Cookie Transfer-Encoding Vary
18
- Permissions-Policy Permissions-Policy-Report-Only'.
18
+ Permissions-Policy Permissions-Policy-Report-Only Strict-Transport-Security'.
19
19
  each do |value|
20
20
  value = value.downcase if downcase
21
21
  const_set(value.gsub('-', '_').upcase!.to_sym, value.freeze)
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 = 83
7
+ RodaMinorVersion = 85
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