roda 3.59.0 → 3.61.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: 963e2d9ac538dbe97835ef65e2e2ba4184838664696bb084ce423a85ef23c205
4
- data.tar.gz: d47a7f9c86357e99b0c9f470b6c33a32962df55189303020982af55d70907c7b
3
+ metadata.gz: 337e7cc074ffe0c300a4dbf9a14f72552caa5b9446fdf0ca04b9f22d9117c9c7
4
+ data.tar.gz: 53a8b4ec124df8ef0efdc7efcc7bec3e0e7ea7d13fa60fb5d6adf06f50ed081e
5
5
  SHA512:
6
- metadata.gz: c84cb0ae8c66b537c0a7d32d83d9c5a639439b15fbc650d3569efab5e134e0883ba06183c7f17bfb379f4018060c7d276a5016431ae3e21d05c2db3801a85762
7
- data.tar.gz: 0c9495ec5fe24b774512f6495f97f5bb4cf8189b964f21a04c5e880d4a536ade2e045c7131f0a54a7c1cd203fe142d53c6d7cde4cf223a8e8be4cb5c5475fede
6
+ metadata.gz: 78671d90145cf431a7deff333e6b75ad681252cae159b5ff09dfaba70d4b3fc17ec1f7362c52454c3f8e94e2343bd4b4d419a5c23738a6b525e9fdd79dc8846c
7
+ data.tar.gz: 55b5bc0be878d58d66e0834110b47313f522e1f109b95c23348b2cb09430b01383595197db8f8db6587f10d3d9e4a7a353a2d70fb1ed4a099573309a5aabcae6
data/CHANGELOG CHANGED
@@ -1,3 +1,13 @@
1
+ = 3.61.0 (2022-10-12)
2
+
3
+ * Make Integer matcher limit integer segments to 100 characters by default (jeremyevans)
4
+
5
+ * Limit input bytesize by default for integer, float, and date/time typecasts in typecast_params (jeremyevans)
6
+
7
+ = 3.60.0 (2022-09-13)
8
+
9
+ * Add link_to plugin with link_to method for creating HTML links (jeremyevans)
10
+
1
11
  = 3.59.0 (2022-08-12)
2
12
 
3
13
  * Add additional_render_engines plugin, for considering multiple render engines for templates (jeremyevans)
data/README.rdoc CHANGED
@@ -1,9 +1,35 @@
1
- = Roda
2
-
3
- Roda is a routing tree web toolkit, designed for building fast and
4
- maintainable web applications in ruby.
5
-
6
- = Installation
1
+ rdoc-image:https://roda.jeremyevans.net/images/roda-logo.svg
2
+
3
+ A routing tree web toolkit, designed for building fast and maintainable web applications in Ruby.
4
+
5
+ == Table of contents
6
+
7
+ - {Installation}[#label-Installation]
8
+ - {Resources}[#label-Resources]
9
+ - {Goals}[#label-Goals]
10
+ - {Usage}[#label-Usage]
11
+ - {Running the application}[#label-Running+the+Application]
12
+ - {The routing tree}[#label-The+Routing+Tree]
13
+ - {Matchers}[#label-Matchers]
14
+ - {Optional segments}[#label-Optional+segments]
15
+ - {Match/Route Block Return Values}[#label-Match-2FRoute+Block+Return+Values]
16
+ - {Status codes}[#label-Status+Codes]
17
+ - {Verb methods}[#label-Verb+Methods]
18
+ - {Root method}[#label-Root+Method]
19
+ - {Request and Response}[#label-Request+and+Response]
20
+ - {Pollution}[#label-Pollution]
21
+ - {Composition}[#label-Composition]
22
+ - {Testing}[#label-Testing]
23
+ - {Settings}[#label-Settings]
24
+ - {Rendering}[#label-Rendering]
25
+ - {Security}[#label-Security]
26
+ - {Code Reloading}[#label-Code+Reloading]
27
+ - {Plugins}[#label-Plugins]
28
+ - {No introspection}[#label-No+Introspection]
29
+ - {Inspiration}[#label-Inspiration]
30
+ - {Ruby Support Policy}[#label-Ruby+Support+Policy]
31
+
32
+ == Installation
7
33
 
8
34
  $ gem install roda
9
35
 
@@ -140,13 +166,11 @@ for every request.
140
166
  == Running the Application
141
167
 
142
168
  Running a Roda application is similar to running any other rack-based application
143
- that uses a +config.ru+ file. You can start a basic server using +rackup+:
169
+ that uses a +config.ru+ file. You can start a basic server using +rackup+, +puma+,
170
+ +unicorn+, +passenger+, or any other webserver that can handle +config.ru+ files:
144
171
 
145
172
  $ rackup
146
173
 
147
- Ruby web servers such as Unicorn and Puma also ship with their own programs
148
- that you can use to run a Roda application.
149
-
150
174
  == The Routing Tree
151
175
 
152
176
  Roda is called a routing tree web toolkit because the way most sites are structured,
@@ -454,7 +478,7 @@ shared branch:
454
478
 
455
479
  This works well for many cases, but there are also cases where you really want to
456
480
  treat it as one route with an optional segment. One simple way to do that is to
457
- use a parameter instead of an optional segment (e.g. +/items/123?opt=456+).
481
+ use a parameter instead of an optional segment (e.g. <tt>/items/123?opt=456</tt>).
458
482
 
459
483
  r.is "items", Integer do |item_id|
460
484
  optional_data = r.params['opt'].to_s
@@ -647,14 +671,14 @@ If you have a lot of rack applications that you want to dispatch to, and
647
671
  which one to dispatch to is based on the request path prefix, look into the
648
672
  +multi_run+ plugin.
649
673
 
650
- === hash_routes plugin
674
+ === hash_branches plugin
651
675
 
652
676
  If you are just looking to split up the main route block up by branches,
653
- you should use the +hash_routes+ plugin,
677
+ you should use the +hash_branches+ plugin,
654
678
  which keeps the current scope of the +route+ block:
655
679
 
656
680
  class App < Roda
657
- plugin :hash_routes
681
+ plugin :hash_branches
658
682
 
659
683
  hash_branch "api" do |r|
660
684
  r.is do
@@ -663,7 +687,7 @@ which keeps the current scope of the +route+ block:
663
687
  end
664
688
 
665
689
  route do |r|
666
- r.hash_routes
690
+ r.hash_branches
667
691
  end
668
692
  end
669
693
 
@@ -1120,3 +1144,4 @@ MIT
1120
1144
  == Maintainer
1121
1145
 
1122
1146
  Jeremy Evans <code@jeremyevans.net>
1147
+
@@ -0,0 +1,56 @@
1
+ = New Features
2
+
3
+ * A link_to plugin has been added with a link_to method for
4
+ creating HTML links.
5
+
6
+ The simplest usage of link_to is passing the body and the location
7
+ to link to as strings:
8
+
9
+ # Instance level
10
+ link_to("body", "/path")
11
+ # => "<a href=\"/path\">body</a>"
12
+
13
+ The link_to plugin depends on the path plugin, and allows you to
14
+ pass symbols for named paths:
15
+
16
+ # Class level
17
+ path :foo, "/path/to/too"
18
+
19
+ # Instance level
20
+ link_to("body", :foo)
21
+ # => "<a href=\"/path/to/foo\">body</a>"
22
+
23
+ It also allows you to pass instances of classes that you have
24
+ registered with the path plugin:
25
+
26
+ # Class level
27
+ A = Struct.new(:id)
28
+ path A do
29
+ "/path/to/a/#{id}"
30
+ end
31
+
32
+ # Instance level
33
+ link_to("body", A.new(1))
34
+ # => "<a href=\"/path/to/a/1\">body</a>"
35
+
36
+ To set additional HTML attributes on the tag, you can pass them as
37
+ an options hash:
38
+
39
+ link_to("body", "/path", foo: "bar")
40
+ # => "<a href=\"/path\" foo=\"bar\">body</a>"
41
+
42
+ If the body is nil, it will be set to the same as the path:
43
+
44
+ link_to(nil, "/path")
45
+ # => "<a href=\"/path\">/path</a>"
46
+
47
+ The plugin will automatically HTML escape the path and any HTML
48
+ attribute values, using the h plugin:
49
+
50
+ link_to("body", "/path?a=1&b=2", foo: '"bar"')
51
+ # => "<a href=\"/path?a=1&amp;b=2\" foo=\"&quot;bar&quot;\">body</a>"
52
+
53
+ = Other Improvements
54
+
55
+ * Coverage testing has been expanded to multiple rack versions, instead
56
+ of just the current rack release.
@@ -0,0 +1,24 @@
1
+ = Improvements
2
+
3
+ * The typecast_params plugin now limits input bytesize for integer,
4
+ float, and date/time typecasts. If the input is over the allowed
5
+ bytesize, typecasting will fail. This prevents issues with trying
6
+ to typecast arbitrarily large input.
7
+
8
+ * The default Integer class matcher now limits integer segments to
9
+ 100 characters by default, also to prevent issues with typecasting
10
+ arbitrarily large input. Segments larger than 100 characters will
11
+ no longer be matched by the Integer class matcher.
12
+
13
+ = Backwards Compatibility
14
+
15
+ * If the input bytesize limits in the typecast_params plugin cause
16
+ issues in your application, you can use the :skip_bytesize_checking
17
+ option when loading the plugin to disable the checks.
18
+
19
+ * If the default Integer class matcher limit causes problems in your
20
+ application, you can use the class_matchers plugin to override the
21
+ matcher to not use a limit:
22
+
23
+ plugin :class_matchers
24
+ class_matcher(Integer, /(\d+)/){|a| [a.to_i]}
@@ -52,7 +52,7 @@ class Roda
52
52
  end
53
53
  end
54
54
  elsif matcher == Integer
55
- if matchdata = /\A\/(\d+)(?=\/|\z)/.match(@remaining_path)
55
+ if matchdata = /\A\/(\d{1,100})(?=\/|\z)/.match(@remaining_path)
56
56
  @remaining_path = matchdata.post_match
57
57
  always{yield(matchdata[1].to_i)}
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+)\z/.match(@remaining_path)
154
+ if matchdata = /\A\/(\d{1,100})\z/.match(@remaining_path)
155
155
  @remaining_path = ''
156
156
  always{yield(matchdata[1].to_i)}
157
157
  end
@@ -34,9 +34,7 @@ class Roda
34
34
  module AllVerbs
35
35
  module RequestMethods
36
36
  %w'delete head options link patch put trace unlink'.each do |verb|
37
- # :nocov:
38
37
  if ::Rack::Request.method_defined?("#{verb}?")
39
- # :nocov:
40
38
  class_eval(<<-END, __FILE__, __LINE__+1)
41
39
  def #{verb}(*args, &block)
42
40
  _verb(args, &block) if #{verb}?
@@ -11,7 +11,7 @@ class Roda
11
11
  # * Doesn't include middleware timing
12
12
  # * Doesn't proxy the body
13
13
  # * Doesn't support different capitalization of the Content-Length response header
14
- # * Logs to $stderr instead of env['rack.errors'] if explicit logger not passed
14
+ # * Logs to +$stderr+ instead of <tt>env['rack.errors']</tt> if explicit logger not passed
15
15
  #
16
16
  # Example:
17
17
  #
@@ -1,8 +1,6 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- # :nocov:
4
3
  raise LoadError, "disallow_file_uploads plugin not supported on Rack <1.6" if Rack.release < '1.6'
5
- # :nocov:
6
4
 
7
5
  #
8
6
  class Roda
@@ -52,14 +52,12 @@ class Roda
52
52
  end
53
53
 
54
54
  class Params < Rack::QueryParser::Params
55
- # :nocov:
56
- if Rack.release >= '2.3'
55
+ if Rack.release >= '3'
57
56
  def initialize
58
57
  @size = 0
59
58
  @params = Hash.new(&INDIFFERENT_PROC)
60
59
  end
61
60
  else
62
- # :nocov:
63
61
  def initialize(limit = Rack::Utils.key_space_limit)
64
62
  @limit = limit
65
63
  @size = 0
@@ -71,9 +69,7 @@ class Roda
71
69
  end
72
70
 
73
71
  module RequestMethods
74
- # :nocov:
75
- query_parser = Rack.release >= '2.3' ? QueryParser.new(QueryParser::Params, 32) : QueryParser.new(QueryParser::Params, 65536, 32)
76
- # :nocov:
72
+ query_parser = Rack.release >= '3' ? QueryParser.new(QueryParser::Params, 32) : QueryParser.new(QueryParser::Params, 65536, 32)
77
73
  QUERY_PARSER = Rack::Utils.default_query_parser = query_parser
78
74
 
79
75
  private
@@ -89,7 +85,6 @@ class Roda
89
85
  end
90
86
  end
91
87
  else
92
- # :nocov:
93
88
  module InstanceMethods
94
89
  # A copy of the request params that will automatically
95
90
  # convert symbols to strings.
@@ -115,7 +110,6 @@ class Roda
115
110
  end
116
111
  end
117
112
  end
118
- # :nocov:
119
113
  end
120
114
  end
121
115
 
@@ -86,12 +86,10 @@ class Roda
86
86
 
87
87
 
88
88
  # Rack 3 dropped requirement that input be rewindable
89
- if Rack.release >= '2.3'
90
- # :nocov:
89
+ if Rack.release >= '3'
91
90
  def _read_json_input(input)
92
91
  input.read
93
92
  end
94
- # :nocov:
95
93
  else
96
94
  def _read_json_input(input)
97
95
  input.rewind
@@ -0,0 +1,83 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The link_to plugin adds the +link_to+ instance method, which can be used for constructing
7
+ # HTML links (+a+ tag with +href+ attribute).
8
+ #
9
+ # The simplest usage of +link_to+ is passing the body and the location to link to as strings:
10
+ #
11
+ # link_to("body", "/path")
12
+ # # => "<a href=\"/path\">body</a>"
13
+ #
14
+ # The link_to plugin depends on the path plugin, and allows you to pass symbols for named paths:
15
+ #
16
+ # # Class level
17
+ # path :foo, "/path/to/too"
18
+ #
19
+ # # Instance level
20
+ # link_to("body", :foo)
21
+ # # => "<a href=\"/path/to/foo\">body</a>"
22
+ #
23
+ # It also allows you to pass instances of classes that you have registered with the path plugin:
24
+ #
25
+ # # Class level
26
+ # A = Struct.new(:id)
27
+ # path A do
28
+ # "/path/to/a/#{id}"
29
+ # end
30
+ #
31
+ # # Instance level
32
+ # link_to("body", A.new(1))
33
+ # # => "<a href=\"/path/to/a/1\">body</a>"
34
+ #
35
+ # To set additional HTML attributes on the +a+ tag, you can pass them as an options hash:
36
+ #
37
+ # link_to("body", "/path", foo: "bar")
38
+ # # => "<a href=\"/path\" foo=\"bar\">body</a>"
39
+ #
40
+ # If the body is nil, it will be set to the same as the path:
41
+ #
42
+ # link_to(nil, "/path")
43
+ # # => "<a href=\"/path\">/path</a>"
44
+ #
45
+ # The plugin will automatically HTML escape the path and any HTML attribute values, using the h plugin:
46
+ #
47
+ # link_to("body", "/path?a=1&b=2", foo: '"bar"')
48
+ # # => "<a href=\"/path?a=1&amp;b=2\" foo=\"&quot;bar&quot;\">body</a>"
49
+ module LinkTo
50
+ def self.load_dependencies(app)
51
+ app.plugin :h
52
+ app.plugin :path
53
+ end
54
+
55
+ module InstanceMethods
56
+ # Return a string with an HTML +a+ tag with an +href+ attribute. See LinkTo
57
+ # module documentation for details.
58
+ def link_to(body, href, attributes=OPTS)
59
+ case href
60
+ when Symbol
61
+ href = public_send(:"#{href}_path")
62
+ when String
63
+ # nothing
64
+ else
65
+ href = path(href)
66
+ end
67
+
68
+ href = h(href)
69
+
70
+ body = href if body.nil?
71
+
72
+ buf = String.new << "<a href=\"#{href}\""
73
+ attributes.each do |k, v|
74
+ buf << " " << k.to_s << "=\"" << h(v) << "\""
75
+ end
76
+ buf << ">" << body << "</a>"
77
+ end
78
+ end
79
+ end
80
+
81
+ register_plugin(:link_to, LinkTo)
82
+ end
83
+ end
@@ -3,9 +3,7 @@
3
3
  begin
4
4
  require 'rack/files'
5
5
  rescue LoadError
6
- # :nocov:
7
6
  require 'rack/file'
8
- # :nocov:
9
7
  end
10
8
 
11
9
  #
@@ -66,9 +64,7 @@ class Roda
66
64
  # end
67
65
  # end
68
66
  module MultiPublic
69
- # :nocov:
70
67
  RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
71
- # :nocov:
72
68
 
73
69
  def self.load_dependencies(app, _, opts=OPTS)
74
70
  app.plugin(:public, opts)
@@ -116,9 +116,7 @@ class Roda
116
116
  # arguments, record the verb used. If given an argument, add an is
117
117
  # check with the arguments.
118
118
  %w'get post delete head options link patch put trace unlink'.each do |verb|
119
- # :nocov:
120
119
  if ::Rack::Request.method_defined?("#{verb}?")
121
- # :nocov:
122
120
  class_eval(<<-END, __FILE__, __LINE__+1)
123
121
  def #{verb}(*args, &block)
124
122
  if (empty = args.empty?) && @_is_verbs
@@ -5,9 +5,7 @@ require 'uri'
5
5
  begin
6
6
  require 'rack/files'
7
7
  rescue LoadError
8
- # :nocov:
9
8
  require 'rack/file'
10
- # :nocov:
11
9
  end
12
10
 
13
11
  #
@@ -45,9 +43,7 @@ class Roda
45
43
  module Public
46
44
  SPLIT = Regexp.union(*[File::SEPARATOR, File::ALT_SEPARATOR].compact)
47
45
  PARSER = URI::DEFAULT_PARSER
48
- # :nocov:
49
46
  RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
50
- # :nocov:
51
47
 
52
48
  # Use options given to setup a Rack::File instance for serving files. Options:
53
49
  # :default_mime :: The default mime type to use if the mime type is not recognized.
@@ -142,13 +138,11 @@ class Roda
142
138
  server.serving(self, path)
143
139
  end
144
140
  else
145
- # :nocov:
146
141
  def public_serve(server, path)
147
142
  server = server.dup
148
143
  server.path = path
149
144
  server.serving(env)
150
145
  end
151
- # :nocov:
152
146
  end
153
147
  end
154
148
  end
@@ -214,18 +214,18 @@ class Roda
214
214
  tilt_compiled_method_support = defined?(Tilt::VERSION) && Tilt::VERSION >= '1.2' &&
215
215
  ([1, -2].include?(((compiled_method_arity = Tilt::Template.instance_method(:compiled_method).arity) rescue false)))
216
216
  NO_CACHE = {:cache=>false}.freeze
217
- COMPILED_METHOD_SUPPORT = RUBY_VERSION >= '2.3' && tilt_compiled_method_support
217
+ COMPILED_METHOD_SUPPORT = RUBY_VERSION >= '2.3' && tilt_compiled_method_support && ENV['RODA_RENDER_COMPILED_METHOD_SUPPORT'] != 'no'
218
218
 
219
219
  if compiled_method_arity == -2
220
220
  def self.tilt_template_compiled_method(template, locals_keys, scope_class)
221
221
  template.send(:compiled_method, locals_keys, scope_class)
222
222
  end
223
+ # :nocov:
223
224
  else
224
- # :nocov:
225
225
  def self.tilt_template_compiled_method(template, locals_keys, scope_class)
226
226
  template.send(:compiled_method, locals_keys)
227
227
  end
228
- # :nocov:
228
+ # :nocov:
229
229
  end
230
230
 
231
231
  # Setup default rendering options. See Render for details.
@@ -366,9 +366,7 @@ class Roda
366
366
  false
367
367
  end
368
368
 
369
- # :nocov:
370
369
  if COMPILED_METHOD_SUPPORT
371
- # :nocov:
372
370
  # Compile a method in the given module with the given name that will
373
371
  # call the compiled template method, updating the compiled template method
374
372
  def define_compiled_method(roda_class, method_name, locals_keys=EMPTY_ARRAY)
@@ -412,9 +410,7 @@ class Roda
412
410
  end
413
411
 
414
412
  module ClassMethods
415
- # :nocov:
416
413
  if COMPILED_METHOD_SUPPORT
417
- # :nocov:
418
414
  # If using compiled methods and there is an optimized layout, speed up
419
415
  # access to the layout method to improve the performance of view.
420
416
  def freeze
@@ -437,9 +433,7 @@ class Roda
437
433
  def inherited(subclass)
438
434
  super
439
435
  opts = subclass.opts[:render] = subclass.opts[:render].dup
440
- # :nocov:
441
436
  if COMPILED_METHOD_SUPPORT
442
- # :nocov:
443
437
  opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
444
438
  end
445
439
  opts[:cache] = opts[:cache].dup
@@ -459,9 +453,7 @@ class Roda
459
453
  instance = allocate
460
454
  instance.send(:retrieve_template, instance.send(:view_layout_opts, OPTS))
461
455
 
462
- # :nocov:
463
456
  if COMPILED_METHOD_SUPPORT
464
- # :nocov:
465
457
  if (layout_template = render_opts[:optimize_layout]) && !opts[:render][:optimized_layout_method_created]
466
458
  instance.send(:retrieve_template, :template=>layout_template, :cache_key=>nil, :template_method_cache_key => :_roda_layout)
467
459
  layout_method = opts[:render][:template_method_cache][:_roda_layout]
@@ -610,7 +602,6 @@ class Roda
610
602
  end
611
603
  end
612
604
  else
613
- # :nocov:
614
605
  def _cached_template_method(_)
615
606
  nil
616
607
  end
@@ -630,7 +621,6 @@ class Roda
630
621
  def _optimized_view_content(template)
631
622
  nil
632
623
  end
633
- # :nocov:
634
624
  end
635
625
 
636
626
 
@@ -109,11 +109,9 @@ class Roda
109
109
  end.join
110
110
  end
111
111
  else
112
- # :nocov:
113
112
  def _cached_render_each_template_method(template)
114
113
  nil
115
114
  end
116
- # :nocov:
117
115
  end
118
116
  end
119
117
  end
@@ -43,9 +43,7 @@ class Roda
43
43
  module InstanceMethods
44
44
  private
45
45
 
46
- # :nocov:
47
46
  if Render::COMPILED_METHOD_SUPPORT
48
- # :nocov:
49
47
  # Disable use of cached templates, since it assumes a render/view call with no
50
48
  # options will have no locals.
51
49
  def _cached_template_method(template)
@@ -4,9 +4,7 @@ require 'rack/mime'
4
4
  begin
5
5
  require 'rack/files'
6
6
  rescue LoadError
7
- # :nocov:
8
7
  require 'rack/file'
9
- # :nocov:
10
8
  end
11
9
 
12
10
 
@@ -225,9 +223,7 @@ class Roda
225
223
  ISO88591_ENCODING = Encoding.find('ISO-8859-1')
226
224
  BINARY_ENCODING = Encoding.find('BINARY')
227
225
 
228
- # :nocov:
229
226
  RACK_FILES = defined?(Rack::Files) ? Rack::Files : Rack::File
230
- # :nocov:
231
227
 
232
228
  # Depend on the status_303 plugin.
233
229
  def self.load_dependencies(app, _opts = nil)
@@ -351,10 +347,8 @@ class Roda
351
347
  s, h, b = if Rack.release > '2'
352
348
  file.serving(self, path)
353
349
  else
354
- # :nocov:
355
350
  file.path = path
356
351
  file.serving(@env)
357
- # :nocov:
358
352
  end
359
353
 
360
354
  res.status = opts[:status] || s
@@ -45,11 +45,9 @@ class Roda
45
45
  when nil, false
46
46
  CLEAR_HEADERS
47
47
  when Array
48
- # :nocov:
49
- if Rack.release >= '2.3'
48
+ if Rack.release >= '3'
50
49
  keep_headers = keep_headers.map(&:downcase)
51
50
  end
52
- # :nocov:
53
51
  lambda{|headers| headers.delete_if{|k,_| !keep_headers.include?(k)}}
54
52
  else
55
53
  raise RodaError, "Invalid :keep_headers option"
@@ -5,34 +5,26 @@ require 'time'
5
5
 
6
6
  class Roda
7
7
  module RodaPlugins
8
- # The typecast_params plugin allows for the simple type conversion for
9
- # submitted parameters. Submitted parameters should be considered
10
- # untrusted input, and in standard use with browsers, parameters are
11
- # submitted as strings (or a hash/array containing strings). In most
12
- # cases it makes sense to explicitly convert the parameter to the
8
+ # The typecast_params plugin allows for type conversion of submitted parameters.
9
+ # Submitted parameters should be considered untrusted input, and in standard use
10
+ # with browsers, parameters are # submitted as strings (or a hash/array containing
11
+ # strings). In most # cases it makes sense to explicitly convert the parameter to the
13
12
  # desired type. While this can be done via manual conversion:
14
13
  #
15
- # key = request.params['key'].to_i
16
- # key = nil unless key > 0
14
+ # val = request.params['key'].to_i
15
+ # val = nil unless val > 0
17
16
  #
18
17
  # the typecast_params plugin adds a friendlier interface:
19
18
  #
20
- # key = typecast_params.pos_int('key')
19
+ # val = typecast_params.pos_int('key')
21
20
  #
22
- # As +typecast_params+ is a fairly long method name, you may want to
23
- # consider aliasing it to something more terse in your application,
24
- # such as +tp+.
25
- #
26
- # One advantage of using typecast_params is that access or conversion
27
- # errors are raised as a specific exception class
28
- # (+Roda::RodaPlugins::TypecastParams::Error+). This allows you to handle
29
- # this specific exception class globally and return an appropriate 4xx
30
- # response to the client. You can use the Error#param_name and Error#reason
31
- # methods to get more information about the error.
21
+ # As +typecast_params+ is a fairly long method name, and may be a method you call
22
+ # frequently, you may want to consider aliasing it to something more terse in your
23
+ # application, such as +tp+.
32
24
  #
33
25
  # typecast_params offers support for default values:
34
26
  #
35
- # key = typecast_params.pos_int('key', 1)
27
+ # val = typecast_params.pos_int('key', 1)
36
28
  #
37
29
  # The default value is only used if no value has been submitted for the parameter,
38
30
  # or if the conversion of the value results in +nil+. Handling defaults for parameter
@@ -43,35 +35,41 @@ class Roda
43
35
  # In many cases, parameters should be required, and if they aren't submitted, that
44
36
  # should be considered an error. typecast_params handles this with ! methods:
45
37
  #
46
- # key = typecast_params.pos_int!('key')
38
+ # val = typecast_params.pos_int!('key')
47
39
  #
48
40
  # These ! methods raise an error instead of returning +nil+, and do not allow defaults.
49
41
  #
42
+ # The errors raised by this plugin use a specific exception class,
43
+ # +Roda::RodaPlugins::TypecastParams::Error+. This allows you to handle
44
+ # this specific exception class globally and return an appropriate 4xx
45
+ # response to the client. You can use the Error#param_name and Error#reason
46
+ # methods to get more information about the error.
47
+ #
50
48
  # To make it easy to handle cases where many parameters need the same conversion
51
49
  # done, you can pass an array of keys to a conversion method, and it will return an array
52
50
  # of converted values:
53
51
  #
54
- # key1, key2 = typecast_params.pos_int(['key1', 'key2'])
52
+ # val1, val2 = typecast_params.pos_int(['key1', 'key2'])
55
53
  #
56
54
  # This is equivalent to:
57
55
  #
58
- # key1 = typecast_params.pos_int('key1')
59
- # key2 = typecast_params.pos_int('key2')
56
+ # val1 = typecast_params.pos_int('key1')
57
+ # val2 = typecast_params.pos_int('key2')
60
58
  #
61
59
  # The ! methods also support arrays, ensuring that all parameters have a value:
62
60
  #
63
- # key1, key2 = typecast_params.pos_int!(['key1', 'key2'])
61
+ # val1, val2 = typecast_params.pos_int!(['key1', 'key2'])
64
62
  #
65
63
  # For handling of array parameters, where all entries in the array use the
66
64
  # same conversion, there is an +array+ method which takes the type as the first argument
67
65
  # and the keys to convert as the second argument:
68
66
  #
69
- # keys = typecast_params.array(:pos_int, 'keys')
67
+ # vals = typecast_params.array(:pos_int, 'keys')
70
68
  #
71
69
  # If you want to ensure that all entries in the array are converted successfully and that
72
70
  # there is a value for the array itself, you can use +array!+:
73
71
  #
74
- # keys = typecast_params.array!(:pos_int, 'keys')
72
+ # vals = typecast_params.array!(:pos_int, 'keys')
75
73
  #
76
74
  # This will raise an exception if any of the values in the array for parameter +keys+ cannot
77
75
  # be converted to integer.
@@ -79,8 +77,8 @@ class Roda
79
77
  # Both +array+ and +array!+ support default values which are used if no value is present
80
78
  # for the parameter:
81
79
  #
82
- # keys = typecast_params.array(:pos_int, 'keys', [])
83
- # keys = typecast_params.array!(:pos_int, 'keys', [])
80
+ # vals1 = typecast_params.array(:pos_int, 'keys1', [])
81
+ # vals2 = typecast_params.array!(:pos_int, 'keys2', [])
84
82
  #
85
83
  # You can also pass an array of keys to +array+ or +array!+, if you would like to perform
86
84
  # the same conversion on multiple arrays:
@@ -88,7 +86,7 @@ class Roda
88
86
  # foo_ids, bar_ids = typecast_params.array!(:pos_int, ['foo_ids', 'bar_ids'])
89
87
  #
90
88
  # The previous examples have shown use of the +pos_int+ method, which uses +to_i+ to convert the
91
- # value to an integer, but returns nil if the resulting integer is not positive. Unless you need
89
+ # value to an integer, but returns +nil+ if the resulting integer is not positive. Unless you need
92
90
  # to handle negative numbers, it is recommended to use +pos_int+ instead of +int+ as +int+ will
93
91
  # convert invalid values to 0 (since that is how <tt>String#to_i</tt> works).
94
92
  #
@@ -224,10 +222,10 @@ class Roda
224
222
  # # }
225
223
  #
226
224
  # Using the +:symbolize+ option makes it simpler to transition from untrusted external
227
- # data (string keys), to trusted data that can be used internally (trusted in the sense that
228
- # the expected types are used).
225
+ # data (string keys), to semitrusted data that can be used internally (trusted in the sense that
226
+ # the expected types are used, not that you trust the values).
229
227
  #
230
- # Note that if there are multiple conversion Error raised inside a +convert!+ or +convert_each!+
228
+ # Note that if there are multiple conversion errors raised inside a +convert!+ or +convert_each!+
231
229
  # block, they are recorded and a single TypecastParams::Error instance is raised after
232
230
  # processing the block. TypecastParams::Error#param_names can be called on the exception to
233
231
  # get an array of all parameter names with conversion issues, and TypecastParams::Error#all_errors
@@ -245,14 +243,18 @@ class Roda
245
243
  # specific to the Roda application. You can add support for custom types by passing a block
246
244
  # when loading the typecast_params plugin. This block is executed in the context of the
247
245
  # subclass, and calling +handle_type+ in the block can be used to add conversion methods.
248
- # +handle_type+ accepts a type name and the block used to convert the type:
246
+ # +handle_type+ accepts a type name, an options hash, and the block used to convert the type.
247
+ # The only currently supported option is +:max_input_bytesize+, specifying the maximum bytesize of
248
+ # string input. You can also override the max input bytesize of an existing type using the
249
+ # +max_input_bytesize+ method.
249
250
  #
250
251
  # plugin :typecast_params do
251
- # handle_type(:album) do |value|
252
+ # handle_type(:album, max_input_bytesize: 100) do |value|
252
253
  # if id = convert_pos_int(val)
253
254
  # Album[id]
254
255
  # end
255
256
  # end
257
+ # max_input_bytesize(:date, 256)
256
258
  # end
257
259
  #
258
260
  # By default, the typecast_params conversion procs are passed the parameter value directly
@@ -260,10 +262,18 @@ class Roda
260
262
  # strip leading and trailing whitespace from parameter string values before processing, which
261
263
  # you can do by passing the <tt>strip: :all</tt> option when loading the plugin.
262
264
  #
263
- # By default, the typecast_params conversion procs check that null bytes are not allowed
264
- # in param string values. This check for null bytes occurs prior to any type conversion.
265
+ # By default, the typecasting methods for some types check whether the bytesize of input
266
+ # strings is over the maximum expected values, and raise an error in such cases. The input
267
+ # bytesize is checked prior to any type conversion. If you would like to skip this check
268
+ # and allow any bytesize when doing type conversion for param string values, you can do so by
269
+ # passing the # <tt>:skip_bytesize_checking</tt> option when loading the plugin. By default,
270
+ # there is an 100 byte limit on integer input, an 1000 byte input on float input, and a 128
271
+ # byte limit on date/time input.
272
+ #
273
+ # By default, the typecasting methods check whether input strings have null bytes, and raise
274
+ # an error in such cases. This check for null bytes occurs prior to any type conversion.
265
275
  # If you would like to skip this check and allow null bytes in param string values,
266
- # you can do by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
276
+ # you can do so by passing the <tt>:allow_null_bytes</tt> option when loading the plugin.
267
277
  #
268
278
  # You can use the :date_parse_input_handler option to specify custom handling of date
269
279
  # parsing input. Modern versions of Ruby and the date gem internally raise if the input to
@@ -282,6 +292,11 @@ class Roda
282
292
  # string.b[0, 128]
283
293
  # }
284
294
  #
295
+ # The +date_parse_input_handler+ is only called if the value is under the max input
296
+ # bytesize, so you may need to call +max_input_bytesize+ for the +:date+, +:time+, and
297
+ # +:datetime+ methods to override the max input bytesize if you want to use this option
298
+ # for input strings over 128 bytes.
299
+ #
285
300
  # By design, typecast_params only deals with string keys, it is not possible to use
286
301
  # symbol keys as arguments to the conversion methods and have them converted.
287
302
  module TypecastParams
@@ -409,6 +424,14 @@ class Roda
409
424
  end
410
425
  end
411
426
 
427
+ module SkipBytesizeChecking
428
+ private
429
+
430
+ # Do not check max input bytesize
431
+ def check_allowed_bytesize(v, max)
432
+ end
433
+ end
434
+
412
435
  # Class handling conversion of submitted parameters to desired types.
413
436
  class Params
414
437
  # Handle conversions for the given type using the given block.
@@ -422,23 +445,29 @@ class Roda
422
445
  # This method is used to define all type conversions, even the built
423
446
  # in ones. It can be called in subclasses to setup subclass-specific
424
447
  # types.
425
- def self.handle_type(type, &block)
448
+ def self.handle_type(type, opts=OPTS, &block)
426
449
  convert_meth = :"convert_#{type}"
427
450
  define_method(convert_meth, &block)
428
451
 
452
+ max_input_bytesize = opts[:max_input_bytesize]
453
+ max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
454
+ define_method(max_input_bytesize_meth){max_input_bytesize}
455
+
429
456
  convert_array_meth = :"_convert_array_#{type}"
430
457
  define_method(convert_array_meth) do |v|
431
458
  raise Error, "expected array but received #{v.inspect}" unless v.is_a?(Array)
432
459
  v.map! do |val|
460
+ check_allowed_bytesize(val, send(max_input_bytesize_meth))
433
461
  check_null_byte(val)
434
462
  send(convert_meth, val)
435
463
  end
436
464
  end
437
465
 
438
- private convert_meth, convert_array_meth
466
+ private convert_meth, convert_array_meth, max_input_bytesize_meth
467
+ alias_method max_input_bytesize_meth, max_input_bytesize_meth
439
468
 
440
469
  define_method(type) do |key, default=nil|
441
- process_arg(convert_meth, key, default) if require_hash!
470
+ process_arg(convert_meth, key, default, send(max_input_bytesize_meth)) if require_hash!
442
471
  end
443
472
 
444
473
  define_method(:"#{type}!") do |key|
@@ -446,6 +475,15 @@ class Roda
446
475
  end
447
476
  end
448
477
 
478
+ # Override the maximum input bytesize for the given type. This is mostly useful
479
+ # for overriding the sizes for the default input types.
480
+ def self.max_input_bytesize(type, bytesize)
481
+ max_input_bytesize_meth = :"_max_input_bytesize_for_#{type}"
482
+ define_method(max_input_bytesize_meth){bytesize}
483
+ private max_input_bytesize_meth
484
+ alias_method max_input_bytesize_meth, max_input_bytesize_meth
485
+ end
486
+
449
487
  # Create a new instance with the given object and nesting level.
450
488
  # +obj+ should be an array or hash, and +nesting+ should be an
451
489
  # array. Designed for internal use, should not be called by
@@ -485,17 +523,17 @@ class Roda
485
523
  end
486
524
  end
487
525
 
488
- handle_type(:int) do |v|
526
+ handle_type(:int, :max_input_bytesize=>100) do |v|
489
527
  string_or_numeric!(v) && v.to_i
490
528
  end
491
529
 
492
- handle_type(:pos_int) do |v|
530
+ handle_type(:pos_int, :max_input_bytesize=>100) do |v|
493
531
  if (v = convert_int(v)) && v > 0
494
532
  v
495
533
  end
496
534
  end
497
535
 
498
- handle_type(:Integer) do |v|
536
+ handle_type(:Integer, :max_input_bytesize=>100) do |v|
499
537
  if string_or_numeric!(v)
500
538
  case v
501
539
  when String
@@ -510,11 +548,11 @@ class Roda
510
548
  end
511
549
  end
512
550
 
513
- handle_type(:float) do |v|
551
+ handle_type(:float, :max_input_bytesize=>1000) do |v|
514
552
  string_or_numeric!(v) && v.to_f
515
553
  end
516
554
 
517
- handle_type(:Float) do |v|
555
+ handle_type(:Float, :max_input_bytesize=>1000) do |v|
518
556
  string_or_numeric!(v) && ::Kernel::Float(v)
519
557
  end
520
558
 
@@ -523,15 +561,15 @@ class Roda
523
561
  v
524
562
  end
525
563
 
526
- handle_type(:date) do |v|
564
+ handle_type(:date, :max_input_bytesize=>128) do |v|
527
565
  parse!(::Date, v)
528
566
  end
529
567
 
530
- handle_type(:time) do |v|
568
+ handle_type(:time, :max_input_bytesize=>128) do |v|
531
569
  parse!(::Time, v)
532
570
  end
533
571
 
534
- handle_type(:datetime) do |v|
572
+ handle_type(:datetime, :max_input_bytesize=>128) do |v|
535
573
  parse!(::DateTime, v)
536
574
  end
537
575
 
@@ -712,7 +750,7 @@ class Roda
712
750
  def array(type, key, default=nil)
713
751
  meth = :"_convert_array_#{type}"
714
752
  raise ProgrammerError, "no typecast_params type registered for #{type.inspect}" unless respond_to?(meth, true)
715
- process_arg(meth, key, default) if require_hash!
753
+ process_arg(meth, key, default, send(:"_max_input_bytesize_for_#{type}")) if require_hash!
716
754
  end
717
755
 
718
756
  # Call +array+ with the +type+, +key+, and +default+, but if the return value is nil or any value in
@@ -945,10 +983,10 @@ class Roda
945
983
 
946
984
  # If +key+ is not an array, convert the value at the given +key+ using the +meth+ method and +default+
947
985
  # value. If +key+ is an array, return an array with the conversion done for each respective member of +key+.
948
- def process_arg(meth, key, default)
986
+ def process_arg(meth, key, default, max_input_bytesize=nil)
949
987
  case key
950
988
  when String
951
- v = process(meth, key, default)
989
+ v = process(meth, key, default, max_input_bytesize)
952
990
 
953
991
  if @capture
954
992
  key = key.to_sym if symbolize?
@@ -961,13 +999,20 @@ class Roda
961
999
  when Array
962
1000
  key.map do |k|
963
1001
  raise ProgrammerError, "non-String element in array argument passed to typecast_params: #{k.inspect}" unless k.is_a?(String)
964
- process_arg(meth, k, default)
1002
+ process_arg(meth, k, default, max_input_bytesize)
965
1003
  end
966
1004
  else
967
1005
  raise ProgrammerError, "Unsupported argument for typecast_params conversion method: #{key.inspect}"
968
1006
  end
969
1007
  end
970
1008
 
1009
+ # Raise an Error if the value is a string with bytesize over max (if max is given)
1010
+ def check_allowed_bytesize(v, max)
1011
+ if max && v.is_a?(String) && v.bytesize > max
1012
+ handle_error(nil, :too_long, "string parameter is too long for type", true)
1013
+ end
1014
+ end
1015
+
971
1016
  # Raise an Error if the value is a string containing a null byte.
972
1017
  def check_null_byte(v)
973
1018
  if v.is_a?(String) && v.index("\0")
@@ -977,10 +1022,11 @@ class Roda
977
1022
 
978
1023
  # Get the value of +key+ for the object, and convert it to the expected type using +meth+.
979
1024
  # If the value either before or after conversion is nil, return the +default+ value.
980
- def process(meth, key, default)
1025
+ def process(meth, key, default, max_input_bytesize=nil)
981
1026
  v = param_value(key)
982
1027
 
983
1028
  unless v.nil?
1029
+ check_allowed_bytesize(v, max_input_bytesize)
984
1030
  check_null_byte(v)
985
1031
  v = send(meth, v)
986
1032
  end
@@ -1049,6 +1095,9 @@ class Roda
1049
1095
  if opts[:allow_null_bytes]
1050
1096
  app::TypecastParams.send(:include, AllowNullByte)
1051
1097
  end
1098
+ if opts[:skip_bytesize_checking]
1099
+ app::TypecastParams.send(:include, SkipBytesizeChecking)
1100
+ end
1052
1101
  if opts[:date_parse_input_handler]
1053
1102
  app::TypecastParams.class_eval do
1054
1103
  include DateParseInputHandler
@@ -126,9 +126,7 @@ class Roda
126
126
 
127
127
  private
128
128
 
129
- # :nocov:
130
129
  if Render::COMPILED_METHOD_SUPPORT
131
- # :nocov:
132
130
  # Return nil if using custom view or layout options.
133
131
  # If using a view subdir, prefix the template key with the subdir.
134
132
  def _cached_template_method_key(template)
data/lib/roda/request.rb CHANGED
@@ -1,18 +1,16 @@
1
1
  # frozen-string-literal: true
2
2
 
3
- # :nocov:
4
3
  begin
5
4
  require "rack/version"
6
5
  rescue LoadError
7
6
  require "rack"
8
7
  else
9
- if Rack.release >= '2.3'
8
+ if Rack.release >= '3'
10
9
  require "rack/request"
11
10
  else
12
11
  require "rack"
13
12
  end
14
13
  end
15
- # :nocov:
16
14
 
17
15
  require_relative "cache"
18
16
 
@@ -129,8 +127,7 @@ class Roda
129
127
  "#<#{self.class.inspect} #{@env["REQUEST_METHOD"]} #{path}>"
130
128
  end
131
129
 
132
- # :nocov:
133
- if Rack.release >= '2.3'
130
+ if Rack.release >= '3'
134
131
  def http_version
135
132
  # Prefer SERVER_PROTOCOL as it is required in Rack 3.
136
133
  # Still fall back to HTTP_VERSION if SERVER_PROTOCOL
@@ -139,7 +136,6 @@ class Roda
139
136
  @env['SERVER_PROTOCOL'] || @env['HTTP_VERSION']
140
137
  end
141
138
  else
142
- # :nocov:
143
139
  # What HTTP version the request was submitted with.
144
140
  def http_version
145
141
  # Prefer HTTP_VERSION as it is backwards compatible
@@ -444,10 +440,10 @@ class Roda
444
440
  hash.all?{|k,v| send("match_#{k}", v)}
445
441
  end
446
442
 
447
- # Match integer segment, and yield resulting value as an
443
+ # Match integer segment of up to 100 decimal characters, and yield resulting value as an
448
444
  # integer.
449
445
  def _match_class_Integer
450
- consume(/\A\/(\d+)(?=\/|\z)/){|i| [i.to_i]}
446
+ consume(/\A\/(\d{1,100})(?=\/|\z)/){|i| [i.to_i]}
451
447
  end
452
448
 
453
449
  # Match only if all of the arguments in the given array match.
data/lib/roda/response.rb CHANGED
@@ -42,7 +42,6 @@ class Roda
42
42
  # code for non-empty responses and a 404 code for empty responses.
43
43
  attr_accessor :status
44
44
 
45
- # :nocov:
46
45
  if defined?(Rack::Headers) && Rack::Headers.is_a?(Class)
47
46
  # Set the default headers when creating a response.
48
47
  def initialize
@@ -51,7 +50,6 @@ class Roda
51
50
  @length = 0
52
51
  end
53
52
  else
54
- # :nocov:
55
53
  # Set the default headers when creating a response.
56
54
  def initialize
57
55
  @headers = {}
@@ -173,7 +171,6 @@ class Roda
173
171
 
174
172
  private
175
173
 
176
- # :nocov:
177
174
  if Rack.release < '2.0.2'
178
175
  # Don't use a content length for empty 205 responses on
179
176
  # rack 1, as it violates Rack::Lint in that version.
@@ -181,7 +178,6 @@ class Roda
181
178
  headers.delete("Content-Type")
182
179
  headers.delete("Content-Length")
183
180
  end
184
- # :nocov:
185
181
  else
186
182
  # Set the content length for empty 205 responses to 0
187
183
  def empty_205_headers(headers)
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 = 59
7
+ RodaMinorVersion = 61
8
8
 
9
9
  # The patch version of Roda, updated only for bug fixes from the last
10
10
  # feature release.
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.59.0
4
+ version: 3.61.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jeremy Evans
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-08-12 00:00:00.000000000 Z
11
+ date: 2022-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -164,7 +164,7 @@ dependencies:
164
164
  - - ">="
165
165
  - !ruby/object:Gem::Version
166
166
  version: '0'
167
- description:
167
+ description:
168
168
  email:
169
169
  - code@jeremyevans.net
170
170
  executables: []
@@ -232,6 +232,8 @@ extra_rdoc_files:
232
232
  - doc/release_notes/3.58.0.txt
233
233
  - doc/release_notes/3.59.0.txt
234
234
  - doc/release_notes/3.6.0.txt
235
+ - doc/release_notes/3.60.0.txt
236
+ - doc/release_notes/3.61.0.txt
235
237
  - doc/release_notes/3.7.0.txt
236
238
  - doc/release_notes/3.8.0.txt
237
239
  - doc/release_notes/3.9.0.txt
@@ -298,6 +300,8 @@ files:
298
300
  - doc/release_notes/3.58.0.txt
299
301
  - doc/release_notes/3.59.0.txt
300
302
  - doc/release_notes/3.6.0.txt
303
+ - doc/release_notes/3.60.0.txt
304
+ - doc/release_notes/3.61.0.txt
301
305
  - doc/release_notes/3.7.0.txt
302
306
  - doc/release_notes/3.8.0.txt
303
307
  - doc/release_notes/3.9.0.txt
@@ -359,6 +363,7 @@ files:
359
363
  - lib/roda/plugins/inject_erb.rb
360
364
  - lib/roda/plugins/json.rb
361
365
  - lib/roda/plugins/json_parser.rb
366
+ - lib/roda/plugins/link_to.rb
362
367
  - lib/roda/plugins/mail_processor.rb
363
368
  - lib/roda/plugins/mailer.rb
364
369
  - lib/roda/plugins/match_affix.rb
@@ -433,7 +438,7 @@ metadata:
433
438
  documentation_uri: https://roda.jeremyevans.net/documentation.html
434
439
  mailing_list_uri: https://github.com/jeremyevans/roda/discussions
435
440
  source_code_uri: https://github.com/jeremyevans/roda
436
- post_install_message:
441
+ post_install_message:
437
442
  rdoc_options: []
438
443
  require_paths:
439
444
  - lib
@@ -449,7 +454,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
449
454
  version: '0'
450
455
  requirements: []
451
456
  rubygems_version: 3.3.7
452
- signing_key:
457
+ signing_key:
453
458
  specification_version: 4
454
459
  summary: Routing tree web toolkit
455
460
  test_files: []