roda 3.31.0 → 3.36.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +38 -0
  3. data/README.rdoc +4 -0
  4. data/doc/conventions.rdoc +17 -8
  5. data/doc/release_notes/3.31.0.txt +1 -1
  6. data/doc/release_notes/3.32.0.txt +42 -0
  7. data/doc/release_notes/3.33.0.txt +8 -0
  8. data/doc/release_notes/3.34.0.txt +17 -0
  9. data/doc/release_notes/3.35.0.txt +12 -0
  10. data/doc/release_notes/3.36.0.txt +17 -0
  11. data/lib/roda.rb +8 -0
  12. data/lib/roda/plugins/_symbol_regexp_matchers.rb +2 -0
  13. data/lib/roda/plugins/all_verbs.rb +2 -0
  14. data/lib/roda/plugins/assets.rb +8 -3
  15. data/lib/roda/plugins/backtracking_array.rb +1 -3
  16. data/lib/roda/plugins/class_level_routing.rb +0 -1
  17. data/lib/roda/plugins/content_security_policy.rb +5 -3
  18. data/lib/roda/plugins/default_headers.rb +2 -0
  19. data/lib/roda/plugins/disallow_file_uploads.rb +2 -0
  20. data/lib/roda/plugins/exception_page.rb +15 -5
  21. data/lib/roda/plugins/hash_matcher.rb +1 -1
  22. data/lib/roda/plugins/indifferent_params.rb +2 -0
  23. data/lib/roda/plugins/match_affix.rb +1 -1
  24. data/lib/roda/plugins/multi_public.rb +87 -0
  25. data/lib/roda/plugins/not_allowed.rb +2 -0
  26. data/lib/roda/plugins/partials.rb +33 -6
  27. data/lib/roda/plugins/path.rb +21 -11
  28. data/lib/roda/plugins/placeholder_string_matchers.rb +2 -0
  29. data/lib/roda/plugins/public.rb +38 -30
  30. data/lib/roda/plugins/r.rb +35 -0
  31. data/lib/roda/plugins/render.rb +21 -5
  32. data/lib/roda/plugins/render_each.rb +11 -3
  33. data/lib/roda/plugins/render_locals.rb +2 -0
  34. data/lib/roda/plugins/sinatra_helpers.rb +4 -4
  35. data/lib/roda/plugins/symbol_matchers.rb +2 -2
  36. data/lib/roda/plugins/type_routing.rb +6 -1
  37. data/lib/roda/plugins/typecast_params.rb +4 -6
  38. data/lib/roda/plugins/view_options.rb +2 -0
  39. data/lib/roda/request.rb +2 -4
  40. data/lib/roda/version.rb +1 -1
  41. metadata +14 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d6d28c0f8b5197ab3d2a74726150921a435ccdcd8f18c6eb2c9bbe4d1cb2a14
4
- data.tar.gz: f1464284c0cb20638bc7575769271c6ea39642ff3b1842fad33b92759db915cd
3
+ metadata.gz: abd198c51a38d272c8601fb367c12b9ad244f8383bcfb2f0583e7c7e23744eca
4
+ data.tar.gz: 75cee1c957032abd4144f01fbd720794d2f66e7422514fa6e58b8bd5f8087b6e
5
5
  SHA512:
6
- metadata.gz: 4fca0abe3d0072e0a3e7201a089fd708610933ce4d461496bb0c13cbf7d84d2d10010c4007c460953972070d782742dba4d88799fd608f3df1ef7cca58cecef1
7
- data.tar.gz: 189a647f809aa732baebc225ea313c59e543d0d0935c8008a85084694161753e0939a7cac4c1f9d010e03785ec792d49b6c3e5643925066e1cda12d0b7701237
6
+ metadata.gz: e3b7fed2ed29b869b205114d3bf644bb81f1f8d3a267682976aa4eef019e9ca015ade6526d75a99d5f6f1128991f70ba692e1c527acdc62874fbd39e49c24dba
7
+ data.tar.gz: 341da7749e2432b1a3139f29b09f6a219480bb6984a74e121fc2b30bc4f7c4f7c6e51b1be01fb836ef85399cef1cfd15249495d335c80b44eed75a1a657960c7
data/CHANGELOG CHANGED
@@ -1,3 +1,41 @@
1
+ = 3.36.0 (2020-09-14)
2
+
3
+ * Add multi_public plugin, for serving files from multiple public directories (jeremyevans)
4
+
5
+ * Support report-to directive in the content_security_policy plugin (jeremyevans)
6
+
7
+ * Add Vary response header when using type_routing plugin with Accept request header to prevent caching issues (jeremyevans)
8
+
9
+ = 3.35.0 (2020-08-14)
10
+
11
+ * Add r plugin for r method for accessing request, useful when r local variable is not in scope (jeremyevans)
12
+
13
+ * Warn when loading a plugin with arguments or a block if the plugin does not accept arguments or block (jeremyevans)
14
+
15
+ = 3.34.0 (2020-07-14)
16
+
17
+ * Remove unnecessary conditionals (jeremyevans)
18
+
19
+ * Allow loading the match_affix plugin with a single argument (jeremyevans)
20
+
21
+ * Do not include pre/post context sections if empty in the exception_page plugin (jeremyevans)
22
+
23
+ = 3.33.0 (2020-06-16)
24
+
25
+ * Add :brotli option to public plugin to supplement it to serve brotli-compressed files like :gzip does for gzipped files (hmdne) (#194)
26
+
27
+ * Add url method to path plugin, similar to path but returning the entire URL (jeremyevans)
28
+
29
+ = 3.32.0 (2020-05-15)
30
+
31
+ * Make :dependencies option in assets plugin work correctly with render plugin template caching (jeremyevans) (#191)
32
+
33
+ * Support render method :dependencies option for specifying which files to check for modification (jgarth, jeremyevans) (#192)
34
+
35
+ * Add each_partial to the partials plugin for rendering a partial for each element in an enumerable (jeremyevans)
36
+
37
+ * Make render_each in render_each plugin handle template names with directories and extensions (jeremyevans)
38
+
1
39
  = 3.31.0 (2020-04-15)
2
40
 
3
41
  * Add :relative option to path method in path plugin, for generating a method returning relative paths (jeremyevans)
@@ -418,6 +418,10 @@ You can provide an array to specify multiple request methods and match on any of
418
418
  {method: :post} # matches POST
419
419
  {method: ['post', 'patch']} # matches POST and PATCH
420
420
 
421
+ === true
422
+
423
+ If +true+ is given directly as a matcher, it always matches.
424
+
421
425
  === false, nil
422
426
 
423
427
  If +false+ or +nil+ is given directly as a matcher, it doesn't match anything.
@@ -15,6 +15,8 @@ For a small application, the following directory layout is recommended:
15
15
  Rakefile
16
16
  app_name.rb
17
17
  assets/
18
+ config.ru
19
+ db.rb
18
20
  migrate/
19
21
  models.rb
20
22
  models/
@@ -25,6 +27,8 @@ For a small application, the following directory layout is recommended:
25
27
  +app_name.rb+ should contain the Roda application, and should reflect the name of your application.
26
28
  So, if your application is named +FooBar+, you should use +foo_bar.rb+.
27
29
 
30
+ +config.ru+ should contain the code the webserver uses to determine which application to run.
31
+
28
32
  +views/+ should contain your template files. This assumes you are using the +render+ plugin
29
33
  and server-side rendering. If you are creating a single page application and just serving
30
34
  JSON, then you won't need a +views+ directory. For small applications, all view files should be
@@ -36,7 +40,11 @@ Again, for pure JSON applications, you won't need a +public+ directory.
36
40
  +assets/+ should contain the source files for your CSS and javascript assets. If you are
37
41
  not using the +assets+ plugin, you won't need an +assets+ directory.
38
42
 
39
- +models.rb+ should contain all code related to your database/ORM. This file should be required
43
+ +db.rb+ should contain the minimum code to setup a database connection, without loading any of
44
+ the applications models. This can be required in cases where you don't want the models loaded,
45
+ such as when running migrations. This file should be required by +models.rb+.
46
+
47
+ +models.rb+ should contain all code related to your ORM. This file should be required
40
48
  by +app_name.rb+. This keeps your model code separate from your web code, making it easier
41
49
  to use outside of your web code. It allows you to get an IRB shell for accessing your models
42
50
  via <tt>irb -r ./models</tt>, without loading the Roda application.
@@ -46,7 +54,7 @@ via <tt>irb -r ./models</tt>, without loading the Roda application.
46
54
  +migrate/+ should create your database migration files, if you are using an ORM that uses
47
55
  migrations.
48
56
 
49
- +spec/+ should contain your specifications/tests. For a small application, it's recommended
57
+ +spec/+ (or +test/+ should contain your specifications/tests. For a small application, it's recommended
50
58
  to a have a single file for your model tests, and a single file for your web/integration tests.
51
59
 
52
60
  +Rakefile+ should contain the rake tasks for the application. The convention is that the
@@ -83,7 +91,8 @@ The routes used by the +hash_routes+ or +multi_run+ should be stored in routing
83
91
  directory, with one file per prefix.
84
92
 
85
93
  For specs/tests, you should have +spec/models/+ and +spec/web/+, with one file per model in +spec/models/+
86
- and one file per prefix in +spec/web/+.
94
+ and one file per prefix in +spec/web/+. Substitute +spec+ with +test+ if that is what you are using as the
95
+ name of the directory.
87
96
 
88
97
  You should have a separate view subdirectory per prefix. If you are using +hash_routes+ and +view_options+ plugins,
89
98
  use +set_view_subdir+ in your routing files to specify the subdirectory to use, so it doesn't need to be
@@ -105,7 +114,7 @@ subdirectories in the +routes/+ directory, and nested subdirectories in the +vie
105
114
  For a small application, the convention in Roda is to layout your Roda application file (+app_name.rb+) like this:
106
115
 
107
116
  require 'roda'
108
- require './models'
117
+ require_relative 'models'
109
118
 
110
119
  class AppName < Roda
111
120
  SOME_CONSTANT = 1
@@ -138,24 +147,24 @@ used in your route block or views.
138
147
  For larger applications, there are some slight changes to the Roda application file layout:
139
148
 
140
149
  require 'roda'
141
- require './models'
150
+ require_relative 'models'
142
151
 
143
152
  class AppName < Roda
144
153
  SOME_CONSTANT = 1
145
154
 
146
155
  use SomeMiddleware
147
156
 
148
- plugin :render, escape: true
157
+ plugin :render, escape: true, layout: './layout'
149
158
  plugin :assets
150
159
  plugin :view_options
151
160
  plugin :hash_routes
152
- Dir['./routes/*.rb'].each{|f| require f}
161
+ Dir['routes/*.rb'].each{|f| require_relative f}
153
162
 
154
163
  route do |r|
155
164
  r.hash_routes
156
165
  end
157
166
 
158
- Dir['./helpers/*.rb'].each{|f| require f}
167
+ Dir['helpers/*.rb'].each{|f| require_relative f}
159
168
  end
160
169
 
161
170
  After loading the +view_options+ and +hash_routes+ plugin, you require all of your
@@ -1,6 +1,6 @@
1
1
  = New Features
2
2
 
3
- * A relative_paths plugin has been added, adding a relative_path
3
+ * A relative_path plugin has been added, adding a relative_path
4
4
  method that will take an absolute path and make it relative to the
5
5
  current request by prepending an appropriate prefix. This is
6
6
  helpful when using Roda as a static site generator to generate a
@@ -0,0 +1,42 @@
1
+ = New Features
2
+
3
+ * render_each in the render_each plugin now automatically handles
4
+ template names with subdirectories and extensions. Previously, these
5
+ caused issues unless the :local option was provided. So now you
6
+ can use:
7
+
8
+ render_each(foos, "items/foo")
9
+
10
+ instead of:
11
+
12
+ render_each(foos, "items/foo", :local=>:foo)
13
+
14
+ * each_partial has been added to the partials plugin. It operates
15
+ similarly to render_each, but uses the convention for partial
16
+ template naming. So this:
17
+
18
+ each_partial(foos, "items/foo")
19
+
20
+ is the same as:
21
+
22
+ render_each(foos, "items/_foo", :local=>:foo)
23
+
24
+ = Other Improvements
25
+
26
+ * The :dependencies option in the assets plugin now works correctly
27
+ with compiled templates in the render plugin in uncached mode
28
+ (the default in development). Previously, modifying a dependency
29
+ file would not result in recompiling the asset template when
30
+ requesting the main file.
31
+
32
+ * Method visibility issues in the following plugins have been fixed:
33
+
34
+ * content_security_policy
35
+ * default_headers
36
+ * indifferent_params
37
+ * placeholder_string_matchers
38
+ * symbol_matchers
39
+
40
+ Previously, these plugins made private methods public by mistake
41
+ when overriding them. Additionally, Roda.freeze no longer changes
42
+ the visibility of the set_default_headers private method.
@@ -0,0 +1,8 @@
1
+ = New Features
2
+
3
+ * The path plugin now supports a url method, allowing for returning
4
+ the entire URL instead of just the path for class-based paths.
5
+
6
+ * The public plugin now supports a :brotli option that will directly
7
+ serve brotli-compressed files (with .br extension) similar to how the
8
+ :gzip option directly serves gzipped files (with the .gz extension).
@@ -0,0 +1,17 @@
1
+ = Improvements
2
+
3
+ * Multiple unneeded conditionals have been removed.
4
+
5
+ * pre_content and post_context sections in backtraces are no longer
6
+ included in the exception_page plugin output if they would be
7
+ empty.
8
+
9
+ * The match_affix plugin can be loaded again with a single argument.
10
+ It was originally designed to accept a single argument, but a bug
11
+ introduced in 2.29.0 made it require two arguments.
12
+
13
+ * Core Roda and all plugins that ship with Roda now have 100% branch
14
+ coverage.
15
+
16
+ * The sinatra_helpers plugin no longer emits statement not reached
17
+ warnings in verbose mode.
@@ -0,0 +1,12 @@
1
+ = New Features
2
+
3
+ * An r plugin has been added. This plugin adds an r method for the
4
+ request, useful for allowing the use of r.halt and r.redirect even
5
+ in methods where the r local variable is not in scope.
6
+
7
+ = Other Improvements
8
+
9
+ * Attempting to load a plugin with an argument or block when the plugin
10
+ does not accept arguments or a block now warns. This is because a
11
+ future update to support a block or an optional argument could break
12
+ the call.
@@ -0,0 +1,17 @@
1
+ = New Features
2
+
3
+ * A multi_public plugin has been added, which allows serving static
4
+ files from multiple separate directories. This is especially
5
+ useful when there are different access control requirements per
6
+ directory.
7
+
8
+ * The content_security_policy now supports a
9
+ content_security_policy.report_to method to set the
10
+ report-to directive.
11
+
12
+ = Other Improvements
13
+
14
+ * When using the type_routing plugin and performing type routing
15
+ using the Accept request header, the Vary response header will be
16
+ added or updated so that http caches do not cache a response for one
17
+ type and serve it for a different type.
@@ -197,6 +197,8 @@ class Roda
197
197
  if instance_method(:set_default_headers).owner == ResponseMethods &&
198
198
  instance_method(:default_headers).owner == ResponseMethods
199
199
 
200
+ private
201
+
200
202
  def set_default_headers
201
203
  @headers['Content-Type'] ||= 'text/html'
202
204
  end
@@ -270,6 +272,12 @@ class Roda
270
272
  raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
271
273
  plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
272
274
  raise RodaError, "Invalid plugin type: #{plugin.class.inspect}" unless plugin.is_a?(Module)
275
+
276
+ if !plugin.respond_to?(:load_dependencies) && !plugin.respond_to?(:configure) && (!args.empty? || block)
277
+ # RODA4: switch from warning to error
278
+ RodaPlugins.warn("Plugin #{plugin} does not accept arguments or a block, but arguments or a block was passed when loading this. This will raise an error in Roda 4.")
279
+ end
280
+
273
281
  plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
274
282
  include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
275
283
  extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
@@ -8,6 +8,8 @@ class Roda
8
8
  # using a regexp.
9
9
  module SymbolRegexpMatchers
10
10
  module RequestMethods
11
+ private
12
+
11
13
  # The regular expression to use for matching symbols. By default, any non-empty
12
14
  # segment matches.
13
15
  def _match_symbol_regexp(s)
@@ -34,7 +34,9 @@ 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:
37
38
  if ::Rack::Request.method_defined?("#{verb}?")
39
+ # :nocov:
38
40
  class_eval(<<-END, __FILE__, __LINE__+1)
39
41
  def #{verb}(*args, &block)
40
42
  _verb(args, &block) if #{verb}?
@@ -405,6 +405,11 @@ class Roda
405
405
  opts[s] ||= {}
406
406
  end
407
407
 
408
+ expanded_deps = opts[:expanded_dependencies] = {}
409
+ opts[:dependencies].each do |file, deps|
410
+ expanded_deps[File.expand_path(file)] = Array(deps)
411
+ end
412
+
408
413
  if headers = opts[:headers]
409
414
  opts[:css_headers] = headers.merge(opts[:css_headers])
410
415
  opts[:js_headers] = headers.merge(opts[:js_headers])
@@ -412,7 +417,7 @@ class Roda
412
417
  opts[:css_headers]['Content-Type'] ||= "text/css; charset=UTF-8".freeze
413
418
  opts[:js_headers]['Content-Type'] ||= "application/javascript; charset=UTF-8".freeze
414
419
 
415
- [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies].each do |s|
420
+ [:css_headers, :js_headers, :css_opts, :js_opts, :dependencies, :expanded_dependencies].each do |s|
416
421
  opts[s].freeze
417
422
  end
418
423
  [:headers, :css, :js].each do |s|
@@ -732,7 +737,7 @@ class Roda
732
737
  content = if file.end_with?(".#{type}")
733
738
  ::File.read(file)
734
739
  else
735
- render_asset_file(file, :template_opts=>o[:"#{type}_opts"])
740
+ render_asset_file(file, :template_opts=>o[:"#{type}_opts"], :dependencies=>o[:expanded_dependencies][file])
736
741
  end
737
742
 
738
743
  o[:postprocessor] ? o[:postprocessor].call(file, type, content) : content
@@ -762,7 +767,7 @@ class Roda
762
767
  # other files, check the modification times of all dependencies and
763
768
  # return the maximum.
764
769
  def asset_last_modified(file)
765
- if deps = self.class.assets_opts[:dependencies][file]
770
+ if deps = self.class.assets_opts[:expanded_dependencies][file]
766
771
  ([file] + Array(deps)).map{|f| ::File.stat(f).mtime}.max
767
772
  else
768
773
  ::File.stat(file).mtime
@@ -33,9 +33,7 @@ class Roda
33
33
  # elements. If the remaining elements could not be
34
34
  # matched, reset the state and continue to the next
35
35
  # entry in the array.
36
- def _match_array(arg, rest=nil)
37
- return super unless rest
38
-
36
+ def _match_array(arg, rest)
39
37
  path = @remaining_path
40
38
  captures = @captures
41
39
  caps = captures.dup
@@ -56,7 +56,6 @@ class Roda
56
56
  # currently have a routing block, setup an empty routing block so that things will still work if
57
57
  # a routing block isn't added.
58
58
  def self.configure(app)
59
- app.route{} unless app.route_block
60
59
  app.opts[:class_level_routes] ||= []
61
60
  end
62
61
 
@@ -56,6 +56,7 @@ class Roda
56
56
  # * media_src
57
57
  # * object_src
58
58
  # * plugin_types
59
+ # * report_to
59
60
  # * report_uri
60
61
  # * require_sri_for
61
62
  # * sandbox
@@ -123,6 +124,7 @@ class Roda
123
124
  media-src
124
125
  object-src
125
126
  plugin-types
127
+ report-to
126
128
  report-uri
127
129
  require-sri-for
128
130
  sandbox
@@ -145,9 +147,7 @@ class Roda
145
147
  # add_* method name adds to the setting value, or clears setting if no values
146
148
  # are given.
147
149
  define_method("add_#{meth}") do |*args|
148
- if args.empty?
149
- @opts[setting]
150
- else
150
+ unless args.empty?
151
151
  @opts[setting] ||= EMPTY_ARRAY
152
152
  @opts[setting] += args
153
153
  @opts[setting].freeze
@@ -304,6 +304,8 @@ class Roda
304
304
  @content_security_policy ||= roda_class.opts[:content_security_policy].dup
305
305
  end
306
306
 
307
+ private
308
+
307
309
  # Set the appropriate content security policy header.
308
310
  def set_default_headers
309
311
  super
@@ -31,6 +31,8 @@ class Roda
31
31
  if owner == Base::ResponseMethods || (owner == response_class && app.opts[:set_default_headers_overridder] == response_class)
32
32
  app.opts[:set_default_headers_overridder] = response_class
33
33
  response_class.class_eval(<<-END, __FILE__, __LINE__+1)
34
+ private
35
+
34
36
  def set_default_headers
35
37
  h = @headers
36
38
  #{headers.map{|k,v| "h[#{k.inspect}] ||= #{v.inspect}"}.join('; ')}
@@ -1,6 +1,8 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ # :nocov:
3
4
  raise LoadError, "disallow_file_uploads plugin not supported on Rack <1.6" if Rack.release < '1.6'
5
+ # :nocov:
4
6
 
5
7
  #
6
8
  class Roda
@@ -252,11 +252,21 @@ END
252
252
  begin
253
253
  lineno -= 1
254
254
  lines = ::File.readlines(filename)
255
- pre_lineno = frame[:pre_context_lineno] = [lineno-context, 0].max
256
- frame[:pre_context] = lines[pre_lineno...lineno]
257
- frame[:context_line] = lines[lineno].chomp
258
- post_lineno = frame[:post_context_lineno] = [lineno+context, lines.size].min
259
- frame[:post_context] = lines[lineno+1..post_lineno]
255
+ if line = lines[lineno]
256
+ pre_lineno = [lineno-context, 0].max
257
+ if (pre_context = lines[pre_lineno...lineno]) && !pre_context.empty?
258
+ frame[:pre_context_lineno] = pre_lineno
259
+ frame[:pre_context] = pre_context
260
+ end
261
+
262
+ post_lineno = [lineno+context, lines.size].min
263
+ if (post_context = lines[lineno+1..post_lineno]) && !post_context.empty?
264
+ frame[:post_context_lineno] = post_lineno
265
+ frame[:post_context] = post_context
266
+ end
267
+
268
+ frame[:context_line] = line.chomp
269
+ end
260
270
  rescue
261
271
  end
262
272
 
@@ -8,7 +8,7 @@ class Roda
8
8
  #
9
9
  # class App < Roda
10
10
  # hash_matcher(:foo) do |v|
11
- # self['foo'] == v
11
+ # params['foo'] == v
12
12
  # end
13
13
  #
14
14
  # route do
@@ -64,6 +64,8 @@ class Roda
64
64
  module RequestMethods
65
65
  QUERY_PARSER = Rack::Utils.default_query_parser = QueryParser.new(QueryParser::Params, 65536, 100)
66
66
 
67
+ private
68
+
67
69
  def query_parser
68
70
  QUERY_PARSER
69
71
  end
@@ -28,7 +28,7 @@ class Roda
28
28
  #
29
29
  # This plugin automatically loads the placeholder_string_matchers plugin.
30
30
  module MatchAffix
31
- def self.load_dependencies(app, _prefix, _suffix)
31
+ def self.load_dependencies(app, _prefix, _suffix=nil)
32
32
  app.plugin :placeholder_string_matchers
33
33
  end
34
34
 
@@ -0,0 +1,87 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The multi_public plugin adds an +r.multi_public+ method that accepts an argument specifying
7
+ # a directory from which to serve static files. It is similar to the public plugin, but
8
+ # allows for multiple separate directories.
9
+ #
10
+ # Here's an example of using the multi_public plugin to serve 3 different types of files
11
+ # from 3 different directories:
12
+ #
13
+ # plugin :multi_public,
14
+ # img: 'static/images',
15
+ # font: 'assets/fonts',
16
+ # form: 'static/forms/pdfs'
17
+ #
18
+ # r.route do
19
+ # r.on "images" do
20
+ # r.multi_public(:img)
21
+ # end
22
+ #
23
+ # r.on "fonts" do
24
+ # r.multi_public(:font)
25
+ # end
26
+ #
27
+ # r.on "forms" do
28
+ # r.multi_public(:form)
29
+ # end
30
+ # end
31
+ #
32
+ # It is possible to simplify the routing tree for this using string keys and an array
33
+ # matcher:
34
+ #
35
+ # plugin :multi_public,
36
+ # 'images' => 'static/images',
37
+ # 'fonts' => 'assets/fonts',
38
+ # 'forms' => 'static/forms/pdfs'
39
+ #
40
+ # r.route do
41
+ # r.on %w"images fonts forms" do |dir|
42
+ # r.multi_public(dir)
43
+ # end
44
+ # end
45
+ #
46
+ # You can provide custom headers and default mime type for each directory using an array
47
+ # of three elements as the value, with the first element being the path, the second
48
+ # being the custom headers, and the third being the default mime type:
49
+ #
50
+ # plugin :multi_public,
51
+ # 'images' => ['static/images', {'Cache-Control'=>'max-age=86400'}, nil],
52
+ # 'fonts' => ['assets/fonts', {'Cache-Control'=>'max-age=31536000'}, 'font/ttf'],
53
+ # 'forms' => ['static/forms/pdfs', nil, 'application/pdf']
54
+ #
55
+ # r.route do
56
+ # r.on %w"images fonts forms" do |dir|
57
+ # r.multi_public(dir)
58
+ # end
59
+ # end
60
+ module MultiPublic
61
+ def self.load_dependencies(app, _, opts=OPTS)
62
+ app.plugin(:public, opts)
63
+ end
64
+
65
+ # Use the given directories to setup servers. Any opts are passed to the public plugin.
66
+ def self.configure(app, directories, _=OPTS)
67
+ roots = app.opts[:multi_public_servers] = (app.opts[:multi_public_servers] || {}).dup
68
+ directories.each do |key, path|
69
+ path, headers, mime = path
70
+ roots[key] = ::Rack::File.new(app.expand_path(path), headers||{}, mime||'text/plain')
71
+ end
72
+ roots.freeze
73
+ end
74
+
75
+ module RequestMethods
76
+ # Serve files from the directory corresponding to the given key if the file exists and
77
+ # this is a GET request.
78
+ def multi_public(key)
79
+ public_serve_with(roda_class.opts[:multi_public_servers].fetch(key))
80
+ end
81
+ end
82
+ end
83
+
84
+ register_plugin(:multi_public, MultiPublic)
85
+ end
86
+ end
87
+
@@ -104,7 +104,9 @@ class Roda
104
104
  # arguments, record the verb used. If given an argument, add an is
105
105
  # check with the arguments.
106
106
  %w'get post delete head options link patch put trace unlink'.each do |verb|
107
+ # :nocov:
107
108
  if ::Rack::Request.method_defined?("#{verb}?")
109
+ # :nocov:
108
110
  class_eval(<<-END, __FILE__, __LINE__+1)
109
111
  def #{verb}(*args, &block)
110
112
  if (empty = args.empty?) && @_is_verbs
@@ -18,27 +18,54 @@ class Roda
18
18
  # render('_test')
19
19
  # render('dir/_test')
20
20
  #
21
- # Note that this plugin automatically loads the :render plugin.
21
+ # To render the same template once for each object in an enumerable,
22
+ # you can use the +render_partials+ method:
23
+ #
24
+ # each_partial([1,2,3], :foo) # uses _foo.erb
25
+ #
26
+ # This is basically equivalent to:
27
+ #
28
+ # render_each([1,2,3], "_foo", local: :foo)
29
+ #
30
+ # This plugin depends on the render and render_each plugins.
22
31
  module Partials
23
- # Depend on the render plugin, since this overrides
24
- # some of its methods.
32
+ # Depend on the render plugin, passing received options to it.
33
+ # Also depend on the render_each plugin.
25
34
  def self.load_dependencies(app, opts=OPTS)
26
35
  app.plugin :render, opts
36
+ app.plugin :render_each
27
37
  end
28
38
 
29
39
  module InstanceMethods
40
+ # For each object in the given enumerable, render the given
41
+ # template (prefixing the template filename with an underscore).
42
+ def each_partial(enum, template, opts=OPTS)
43
+ unless opts.has_key?(:local)
44
+ opts = Hash[opts]
45
+ opts[:local] = render_each_default_local(template)
46
+ end
47
+ render_each(enum, partial_template_name(template.to_s), opts)
48
+ end
49
+
30
50
  # Renders the given template without a layout, but
31
51
  # prefixes the template filename to use with an
32
52
  # underscore.
33
53
  def partial(template, opts=OPTS)
34
54
  opts = parse_template_opts(template, opts)
35
55
  if opts[:template]
36
- template = opts[:template].split('/')
37
- template[-1] = "_#{template[-1]}"
38
- opts[:template] = template.join('/')
56
+ opts[:template] = partial_template_name(opts[:template])
39
57
  end
40
58
  render_template(opts)
41
59
  end
60
+
61
+ private
62
+
63
+ # Prefix the template base filename with an underscore.
64
+ def partial_template_name(template)
65
+ segments = template.split('/')
66
+ segments[-1] = "_#{segments[-1]}"
67
+ segments.join('/')
68
+ end
42
69
  end
43
70
  end
44
71
 
@@ -10,7 +10,8 @@ class Roda
10
10
  #
11
11
  # Additionally, you can call the +path+ class method with a class and a block, and it will register
12
12
  # the class. You can then call the +path+ instance method with an instance of that class, and it will
13
- # execute the block in the context of the route block scope with the arguments provided to path.
13
+ # execute the block in the context of the route block scope with the arguments provided to path. You
14
+ # can call the +url+ instance method with the same arguments as the +path+ method to get the full URL.
14
15
  #
15
16
  # Example:
16
17
  #
@@ -46,11 +47,11 @@ class Roda
46
47
  #
47
48
  # r.post 'quux' do
48
49
  # bar = Quux[1]
49
- # r.redirect path(quux, '/bar') # /quux/1/bar
50
+ # r.redirect url(quux, '/bar') # http://example.com/quux/1/bar
50
51
  # end
51
52
  # end
52
53
  #
53
- # The path method accepts the following options when not called with a class:
54
+ # The path class method accepts the following options when not called with a class:
54
55
  #
55
56
  # :add_script_name :: Prefix the path generated with SCRIPT_NAME. This defaults to the app's
56
57
  # :add_script_name option.
@@ -62,7 +63,7 @@ class Roda
62
63
  # method. If a Symbol or String, uses the value as the url method name.
63
64
  # :url_only :: Do not create a path method, just a url method.
64
65
  #
65
- # Note that if :add_script_name, :url, or :url_only is used, the path method will also create a
66
+ # Note that if :add_script_name, :relative, :url, or :url_only is used, the path method will also create a
66
67
  # <tt>_*_path</tt> private method.
67
68
  module Path
68
69
  DEFAULT_PORTS = {'http' => 80, 'https' => 443}.freeze
@@ -165,14 +166,8 @@ class Roda
165
166
  end
166
167
 
167
168
  url_block = lambda do |*a, &blk|
168
- r = request
169
- scheme = r.scheme
170
- port = r.port
171
- uri = ["#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}"]
172
- uri << request.script_name.to_s if add_script_name
173
169
  # Allow calling private _method to get path
174
- uri << send(_meth, *a, &blk)
175
- File.join(uri)
170
+ "#{_base_url}#{request.script_name if add_script_name}#{send(_meth, *a, &blk)}"
176
171
  end
177
172
 
178
173
  define_method(url_meth, &url_block)
@@ -207,6 +202,21 @@ class Roda
207
202
  path = request.script_name.to_s + path if opts[:add_script_name]
208
203
  path
209
204
  end
205
+
206
+ # Similar to #path, but returns a complete URL.
207
+ def url(*args, &block)
208
+ "#{_base_url}#{path(*args, &block)}"
209
+ end
210
+
211
+ private
212
+
213
+ # The string to prepend to the path to make the path a URL.
214
+ def _base_url
215
+ r = @_request
216
+ scheme = r.scheme
217
+ port = r.port
218
+ "#{scheme}://#{r.host}#{":#{port}" unless DEFAULT_PORTS[scheme] == port}"
219
+ end
210
220
  end
211
221
  end
212
222
 
@@ -27,6 +27,8 @@ class Roda
27
27
  end
28
28
 
29
29
  module RequestMethods
30
+ private
31
+
30
32
  def _match_string(str)
31
33
  if str.index(":")
32
34
  consume(self.class.cached_matcher(str){Regexp.escape(str).gsub(/:(\w+)/){|m| _match_symbol_regexp($1)}})
@@ -42,6 +42,8 @@ class Roda
42
42
  # :default_mime :: The default mime type to use if the mime type is not recognized.
43
43
  # :gzip :: Whether to serve already gzipped files with a .gz extension for clients
44
44
  # supporting gzipped transfer encoding.
45
+ # :brotli :: Whether to serve already brotli-compressed files with a .br extension
46
+ # for clients supporting brotli transfer encoding.
45
47
  # :headers :: A hash of headers to use for statically served files
46
48
  # :root :: Use this option for the root of the public directory (default: "public")
47
49
  def self.configure(app, opts={})
@@ -52,41 +54,13 @@ class Roda
52
54
  end
53
55
  app.opts[:public_server] = ::Rack::File.new(app.opts[:public_root], opts[:headers]||{}, opts[:default_mime] || 'text/plain')
54
56
  app.opts[:public_gzip] = opts[:gzip]
57
+ app.opts[:public_brotli] = opts[:brotli]
55
58
  end
56
59
 
57
60
  module RequestMethods
58
61
  # Serve files from the public directory if the file exists and this is a GET request.
59
62
  def public
60
- if is_get?
61
- path = PARSER.unescape(real_remaining_path)
62
- return if path.include?("\0")
63
-
64
- roda_opts = roda_class.opts
65
- server = roda_opts[:public_server]
66
- path = ::File.join(server.root, *public_path_segments(path))
67
-
68
- if roda_opts[:public_gzip] && env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/
69
- gzip_path = path + '.gz'
70
-
71
- if public_file_readable?(gzip_path)
72
- res = public_serve(server, gzip_path)
73
- headers = res[1]
74
-
75
- unless res[0] == 304
76
- if mime_type = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
77
- headers['Content-Type'] = mime_type
78
- end
79
- headers['Content-Encoding'] = 'gzip'
80
- end
81
-
82
- halt res
83
- end
84
- end
85
-
86
- if public_file_readable?(path)
87
- halt public_serve(server, path)
88
- end
89
- end
63
+ public_serve_with(roda_class.opts[:public_server])
90
64
  end
91
65
 
92
66
  private
@@ -113,6 +87,40 @@ class Roda
113
87
  # :nocov:
114
88
  end
115
89
 
90
+ def public_serve_with(server)
91
+ return unless is_get?
92
+ path = PARSER.unescape(real_remaining_path)
93
+ return if path.include?("\0")
94
+
95
+ roda_opts = roda_class.opts
96
+ path = ::File.join(server.root, *public_path_segments(path))
97
+
98
+ public_serve_compressed(server, path, '.br', 'br') if roda_opts[:public_brotli]
99
+ public_serve_compressed(server, path, '.gz', 'gzip') if roda_opts[:public_gzip]
100
+
101
+ if public_file_readable?(path)
102
+ halt public_serve(server, path)
103
+ end
104
+ end
105
+
106
+ def public_serve_compressed(server, path, suffix, encoding)
107
+ if env['HTTP_ACCEPT_ENCODING'] =~ /\b#{encoding}\b/
108
+ compressed_path = path + suffix
109
+
110
+ if public_file_readable?(compressed_path)
111
+ res = public_serve(server, compressed_path)
112
+ headers = res[1]
113
+
114
+ unless res[0] == 304
115
+ headers['Content-Type'] = ::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
116
+ headers['Content-Encoding'] = encoding
117
+ end
118
+
119
+ halt res
120
+ end
121
+ end
122
+ end
123
+
116
124
  if ::Rack.release > '2'
117
125
  # Serve the given path using the given Rack::File server.
118
126
  def public_serve(server, path)
@@ -0,0 +1,35 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The r plugin adds an +r+ instance method that will return the request.
7
+ # This allows you to use common Roda idioms such as +r.halt+ and
8
+ # +r.redirect+ even when +r+ isn't a local variable in scope. Example:
9
+ #
10
+ # plugin :r
11
+ #
12
+ # def foo
13
+ # r.redirect "/bar"
14
+ # end
15
+ #
16
+ # route do |r|
17
+ # r.get "foo" do
18
+ # foo
19
+ # end
20
+ # r.get "bar" do
21
+ # "bar"
22
+ # end
23
+ # end
24
+ module R
25
+ module InstanceMethods
26
+ # The request object.
27
+ def r
28
+ @_request
29
+ end
30
+ end
31
+ end
32
+
33
+ register_plugin(:r, R)
34
+ end
35
+ end
@@ -241,12 +241,13 @@ class Roda
241
241
  # template file has been modified. This is an internal class and
242
242
  # the API is subject to change at any time.
243
243
  class TemplateMtimeWrapper
244
- def initialize(template_class, path, *template_args)
244
+ def initialize(template_class, path, dependencies, *template_args)
245
245
  @template_class = template_class
246
246
  @path = path
247
247
  @template_args = template_args
248
+ @dependencies = ([path] + Array(dependencies)) if dependencies
248
249
 
249
- @mtime = (File.mtime(path) if File.file?(path))
250
+ @mtime = template_last_modified if File.file?(path)
250
251
  @template = template_class.new(path, *template_args)
251
252
  end
252
253
 
@@ -257,17 +258,28 @@ class Roda
257
258
  @template.render(*args, &block)
258
259
  end
259
260
 
261
+ # Return when the template was last modified. If the template depends on any
262
+ # other files, check the modification times of all dependencies and
263
+ # return the maximum.
264
+ def template_last_modified
265
+ if deps = @dependencies
266
+ deps.map{|f| File.mtime(f)}.max
267
+ else
268
+ File.mtime(@path)
269
+ end
270
+ end
271
+
260
272
  # If the template file has been updated, return true and update
261
273
  # the template object and the modification time. Other return false.
262
274
  def modified?
263
275
  begin
264
- mtime = File.mtime(path = @path)
276
+ mtime = template_last_modified
265
277
  rescue
266
278
  # ignore errors
267
279
  else
268
280
  if mtime != @mtime
269
281
  @mtime = mtime
270
- @template = @template_class.new(path, *@template_args)
282
+ @template = @template_class.new(@path, *@template_args)
271
283
  return true
272
284
  end
273
285
  end
@@ -275,7 +287,9 @@ class Roda
275
287
  false
276
288
  end
277
289
 
290
+ # :nocov:
278
291
  if COMPILED_METHOD_SUPPORT
292
+ # :nocov:
279
293
  # Compile a method in the given module with the given name that will
280
294
  # call the compiled template method, updating the compiled template method
281
295
  def define_compiled_method(roda_class, method_name, locals_keys=EMPTY_ARRAY)
@@ -325,7 +339,9 @@ class Roda
325
339
  def inherited(subclass)
326
340
  super
327
341
  opts = subclass.opts[:render] = subclass.opts[:render].dup
342
+ # :nocov:
328
343
  if COMPILED_METHOD_SUPPORT
344
+ # :nocov:
329
345
  opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
330
346
  end
331
347
  opts[:cache] = opts[:cache].dup
@@ -576,7 +592,7 @@ class Roda
576
592
  !opts[:inline]
577
593
 
578
594
  if render_opts[:check_template_mtime] && !opts[:template_block] && !cache
579
- template = TemplateMtimeWrapper.new(opts[:template_class], opts[:path], 1, template_opts)
595
+ template = TemplateMtimeWrapper.new(opts[:template_class], opts[:path], opts[:dependencies], 1, template_opts)
580
596
 
581
597
  if define_compiled_method
582
598
  method_name = :"_roda_template_#{self.class.object_id}_#{method_cache_key}"
@@ -26,7 +26,9 @@ class Roda
26
26
  #
27
27
  # Will render the +foo+ template, but the local variable used inside
28
28
  # the template will be +bar+. You can use <tt>local: nil</tt> to
29
- # not set a local variable inside the template.
29
+ # not set a local variable inside the template. By default, the
30
+ # local variable name is based on the template name, with any
31
+ # directories and file extensions removed.
30
32
  module RenderEach
31
33
  # Load the render plugin before this plugin, since this plugin
32
34
  # calls the render method.
@@ -45,11 +47,11 @@ class Roda
45
47
  # set a local variable. If not set, uses the template name.
46
48
  def render_each(enum, template, opts=(no_opts = true; optimized_template = _cached_render_each_template_method(template); OPTS))
47
49
  if optimized_template
48
- return _optimized_render_each(enum, optimized_template, template.to_s.to_sym, {})
50
+ return _optimized_render_each(enum, optimized_template, render_each_default_local(template), {})
49
51
  elsif opts.has_key?(:local)
50
52
  as = opts[:local]
51
53
  else
52
- as = template.to_s.to_sym
54
+ as = render_each_default_local(template)
53
55
  if no_opts && optimized_template.nil? && (optimized_template = _optimized_render_method_for_locals(template, (locals = {as=>nil})))
54
56
  return _optimized_render_each(enum, optimized_template, as, locals)
55
57
  end
@@ -77,6 +79,12 @@ class Roda
77
79
 
78
80
  private
79
81
 
82
+ # The default local variable name to use for the template, if the :local option
83
+ # is not used when calling render_each.
84
+ def render_each_default_local(template)
85
+ File.basename(template.to_s).sub(/\..+\z/, '').to_sym
86
+ end
87
+
80
88
  if Render::COMPILED_METHOD_SUPPORT
81
89
  # If compiled method support is enabled in the render plugin, return the
82
90
  # method name to call to render the template. Return false if not given
@@ -43,7 +43,9 @@ class Roda
43
43
  module InstanceMethods
44
44
  private
45
45
 
46
+ # :nocov:
46
47
  if Render::COMPILED_METHOD_SUPPORT
48
+ # :nocov:
47
49
  # Disable use of cached templates, since it assumes a render/view call with no
48
50
  # options will have no locals.
49
51
  def _cached_template_method(template)
@@ -374,7 +374,7 @@ class Roda
374
374
 
375
375
  module ResponseMethods
376
376
  # Set or retrieve the response status code.
377
- def status(value = (return @status; nil))
377
+ def status(value = nil || (return @status))
378
378
  @status = value
379
379
  end
380
380
 
@@ -401,7 +401,7 @@ class Roda
401
401
 
402
402
  # Set multiple response headers with Hash, or return the headers if no
403
403
  # argument is given.
404
- def headers(hash = (return @headers; nil))
404
+ def headers(hash = nil || (return @headers))
405
405
  @headers.merge!(hash)
406
406
  end
407
407
 
@@ -412,7 +412,7 @@ class Roda
412
412
 
413
413
  # Set the Content-Type of the response body given a media type or file
414
414
  # extension. See plugin documentation for options.
415
- def content_type(type = (return @headers["Content-Type"]; nil), opts = OPTS)
415
+ def content_type(type = nil || (return @headers["Content-Type"]), opts = OPTS)
416
416
  unless (mime_type = mime_type(type) || opts[:default])
417
417
  raise RodaError, "Unknown media type: #{type}"
418
418
  end
@@ -478,7 +478,7 @@ class Roda
478
478
  # If a type and value are given, set the value in Rack's MIME registry.
479
479
  # If only a type is given, lookup the type in Rack's MIME registry and
480
480
  # return it.
481
- def mime_type(type=(return; nil), value = nil)
481
+ def mime_type(type=nil || (return), value = nil)
482
482
  return type.to_s if type.to_s.include?('/')
483
483
  type = ".#{type}" unless type.to_s[0] == ?.
484
484
  if value
@@ -6,7 +6,7 @@ class Roda
6
6
  # The symbol_matchers plugin allows you do define custom regexps to use
7
7
  # for specific symbols. For example, if you have a route such as:
8
8
  #
9
- # r.on :username do
9
+ # r.on :username do |username|
10
10
  # # ...
11
11
  # end
12
12
  #
@@ -28,7 +28,7 @@ class Roda
28
28
  # If the placeholder_string_matchers plugin is loaded, this feature also applies to
29
29
  # placeholders in strings, so the following:
30
30
  #
31
- # r.on "users/:username" do
31
+ # r.on "users/:username" do |username|
32
32
  # # ...
33
33
  # end
34
34
  #
@@ -4,7 +4,7 @@
4
4
  class Roda
5
5
  module RodaPlugins
6
6
  # This plugin makes it easier to to respond to specific request data types. User agents can request
7
- # specific data types by either supplying an appropriate +Accept+ header
7
+ # specific data types by either supplying an appropriate +Accept+ request header
8
8
  # or by appending it as file extension to the path.
9
9
  #
10
10
  # Example:
@@ -50,6 +50,10 @@ class Roda
50
50
  # Content-Type header will be set to Roda's default (which you can override via
51
51
  # the default_headers plugin).
52
52
  #
53
+ # If the type routing is based on the +Accept+ request header and not the file extension,
54
+ # then an appropriate +Vary+ header will be set or appended to, so that HTTP caches do
55
+ # not serve the same result for requests with different +Accept+ headers.
56
+ #
53
57
  # To match custom extensions, use the :types option:
54
58
  #
55
59
  # plugin :type_routing, types: {
@@ -195,6 +199,7 @@ class Roda
195
199
  @env['HTTP_ACCEPT'].to_s.split(/\s*,\s*/).map do |part|
196
200
  mime, _= part.split(/\s*;\s*/, 2)
197
201
  if sym = mimes[mime]
202
+ response['Vary'] = (vary = response['Vary']) ? "#{vary}, Accept" : 'Accept'
198
203
  return sym
199
204
  end
200
205
  end
@@ -742,9 +742,7 @@ class Roda
742
742
  return
743
743
  end
744
744
 
745
- if v = self[key]
746
- v.subkey(keys, do_raise)
747
- end
745
+ self[key].subkey(keys, do_raise)
748
746
  rescue => e
749
747
  handle_error(key, reason, e)
750
748
  end
@@ -808,10 +806,10 @@ class Roda
808
806
  @nested_params = nil
809
807
 
810
808
  if capturing_started
811
- # Unset capturing if capturing was not already started.
809
+ # Unset capturing if capturing was already started.
812
810
  @capture = nil
813
811
  else
814
- # If capturing was already started, update cached nested params
812
+ # If capturing was not already started, update cached nested params
815
813
  # before resetting symbolize setting.
816
814
  @nested_params = nested_params
817
815
  end
@@ -878,7 +876,7 @@ class Roda
878
876
  def handle_error(key, reason, e, do_raise=false)
879
877
  case e
880
878
  when String
881
- handle_error(key, reason, Error.new(e), do_raise=false)
879
+ handle_error(key, reason, Error.new(e), do_raise)
882
880
  when Error, ArgumentError
883
881
  if @capture && (le = @capture.last) && le == e
884
882
  raise e if do_raise
@@ -126,7 +126,9 @@ class Roda
126
126
 
127
127
  private
128
128
 
129
+ # :nocov:
129
130
  if Render::COMPILED_METHOD_SUPPORT
131
+ # :nocov:
130
132
  # Return nil if using custom view or layout options.
131
133
  # If using a view subdir, prefix the template key with the subdir.
132
134
  def _cached_template_method_key(template)
@@ -466,10 +466,8 @@ class Roda
466
466
  rp = @remaining_path
467
467
  if rp.getbyte(0) == 47
468
468
  if last = rp.index('/', 1)
469
- if last > 1
470
- @captures << rp[1, last-1]
471
- @remaining_path = rp[last, rp.length]
472
- end
469
+ @captures << rp[1, last-1]
470
+ @remaining_path = rp[last, rp.length]
473
471
  elsif rp.length > 1
474
472
  @captures << rp[1,rp.length]
475
473
  @remaining_path = ""
@@ -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 = 31
7
+ RodaMinorVersion = 36
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.31.0
4
+ version: 3.36.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: 2020-04-15 00:00:00.000000000 Z
11
+ date: 2020-09-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -207,6 +207,11 @@ extra_rdoc_files:
207
207
  - doc/release_notes/3.29.0.txt
208
208
  - doc/release_notes/3.30.0.txt
209
209
  - doc/release_notes/3.31.0.txt
210
+ - doc/release_notes/3.32.0.txt
211
+ - doc/release_notes/3.33.0.txt
212
+ - doc/release_notes/3.34.0.txt
213
+ - doc/release_notes/3.35.0.txt
214
+ - doc/release_notes/3.36.0.txt
210
215
  files:
211
216
  - CHANGELOG
212
217
  - MIT-LICENSE
@@ -239,6 +244,11 @@ files:
239
244
  - doc/release_notes/3.3.0.txt
240
245
  - doc/release_notes/3.30.0.txt
241
246
  - doc/release_notes/3.31.0.txt
247
+ - doc/release_notes/3.32.0.txt
248
+ - doc/release_notes/3.33.0.txt
249
+ - doc/release_notes/3.34.0.txt
250
+ - doc/release_notes/3.35.0.txt
251
+ - doc/release_notes/3.36.0.txt
242
252
  - doc/release_notes/3.4.0.txt
243
253
  - doc/release_notes/3.5.0.txt
244
254
  - doc/release_notes/3.6.0.txt
@@ -299,6 +309,7 @@ files:
299
309
  - lib/roda/plugins/middleware.rb
300
310
  - lib/roda/plugins/middleware_stack.rb
301
311
  - lib/roda/plugins/module_include.rb
312
+ - lib/roda/plugins/multi_public.rb
302
313
  - lib/roda/plugins/multi_route.rb
303
314
  - lib/roda/plugins/multi_run.rb
304
315
  - lib/roda/plugins/multi_view.rb
@@ -318,6 +329,7 @@ files:
318
329
  - lib/roda/plugins/placeholder_string_matchers.rb
319
330
  - lib/roda/plugins/precompile_templates.rb
320
331
  - lib/roda/plugins/public.rb
332
+ - lib/roda/plugins/r.rb
321
333
  - lib/roda/plugins/relative_path.rb
322
334
  - lib/roda/plugins/render.rb
323
335
  - lib/roda/plugins/render_each.rb