roda 3.32.0 → 3.37.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: 2a342192f0ab697bea9c4e5b55669644f06fe16e161539b1efc3d10f3c6b9947
4
- data.tar.gz: cf01944fe14494efd606cd892cbbbae5b5128f5a1d83c5e411bd4c64cc6e4321
3
+ metadata.gz: de8c769572875e477058b48557b22861b9de731fa3b00b20a3022a67c929609d
4
+ data.tar.gz: 5cacc0d7b988bd3dc5bafbc30fcac145f6f67ddcc0e7809e1fc9cd60d8a16273
5
5
  SHA512:
6
- metadata.gz: e5de72f1385942855bd2c8e1c0f6be5187430d7d83112d7400bc573f045456a445166edfdbb6a7f70779c7b008745e8063bd44274b0111dec95643f87cfe12fe
7
- data.tar.gz: b69e2fb41045d19c1f9e79dd2d1a87ca691fcd8de975bd34e37456cd6c5183cd2a6ad6bb59a07594527588d2903ef1bceea5f356a809aef03c01e5f64415246b
6
+ metadata.gz: 1bf7df28d82f31820cd19c29b5a3f0f3afbbae2c41cf05974b16698c7ffb1725e924491874ddbb3f355a88990d4aab6d5b5b38f2d9c7612a548bfe0e3701ac2e
7
+ data.tar.gz: 32fca636d6324d29958cbcdf5c949c413eaeda7b74a701032d5766a60ec6b3f2db7ec262268dac697c3b3f5f9b183e127ddf265c7ba5015578969bc7f8be02a2
data/CHANGELOG CHANGED
@@ -1,3 +1,35 @@
1
+ = 3.37.0 (2020-10-16)
2
+
3
+ * Add custom_matchers plugin, for supporting arbitrary objects as matchers (jeremyevans)
4
+
5
+ = 3.36.0 (2020-09-14)
6
+
7
+ * Add multi_public plugin, for serving files from multiple public directories (jeremyevans)
8
+
9
+ * Support report-to directive in the content_security_policy plugin (jeremyevans)
10
+
11
+ * Add Vary response header when using type_routing plugin with Accept request header to prevent caching issues (jeremyevans)
12
+
13
+ = 3.35.0 (2020-08-14)
14
+
15
+ * Add r plugin for r method for accessing request, useful when r local variable is not in scope (jeremyevans)
16
+
17
+ * Warn when loading a plugin with arguments or a block if the plugin does not accept arguments or block (jeremyevans)
18
+
19
+ = 3.34.0 (2020-07-14)
20
+
21
+ * Remove unnecessary conditionals (jeremyevans)
22
+
23
+ * Allow loading the match_affix plugin with a single argument (jeremyevans)
24
+
25
+ * Do not include pre/post context sections if empty in the exception_page plugin (jeremyevans)
26
+
27
+ = 3.33.0 (2020-06-16)
28
+
29
+ * Add :brotli option to public plugin to supplement it to serve brotli-compressed files like :gzip does for gzipped files (hmdne) (#194)
30
+
31
+ * Add url method to path plugin, similar to path but returning the entire URL (jeremyevans)
32
+
1
33
  = 3.32.0 (2020-05-15)
2
34
 
3
35
  * Make :dependencies option in assets plugin work correctly with render plugin template caching (jeremyevans) (#191)
@@ -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
@@ -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.
@@ -0,0 +1,42 @@
1
+ = New Features
2
+
3
+ * A custom_matchers plugin has been added, which allows using
4
+ arbitrary objects as matchers, as long as the matcher has been
5
+ registered. You can register matchers using the custom_matcher
6
+ class method, which takes the class of the matcher, and a block
7
+ which is yielded the matcher object. The block should return
8
+ nil or false if the matcher doesn't match, and any other value
9
+ if the matcher does match. Example:
10
+
11
+ plugin :custom_matchers
12
+ method_segment = Struct.new(:request_method, :next_segment)
13
+ custom_matcher(method_segment) do |matcher|
14
+ # self is the request instance ("r" yielded in the route block below)
15
+ if matcher.request_method == self.request_method
16
+ match(matcher.next_segment)
17
+ end
18
+ end
19
+
20
+ get_foo = method_segment.new('GET', 'foo')
21
+ post_any = method_segment.new('POST', String)
22
+ route do |r|
23
+ r.on('baz') do
24
+ r.on(get_foo) do
25
+ # GET method, /baz/foo prefix
26
+ end
27
+
28
+ r.is(post_any) do |seg|
29
+ # for POST /baz/bar, seg is "bar"
30
+ end
31
+ end
32
+
33
+ r.on('quux') do
34
+ r.is(get_foo) do
35
+ # GET method, /quux/foo route
36
+ end
37
+
38
+ r.on(post_any) do |seg|
39
+ # for POST /quux/xyz, seg is "xyz"
40
+ end
41
+ end
42
+ end
@@ -272,6 +272,12 @@ class Roda
272
272
  raise RodaError, "Cannot add a plugin to a frozen Roda class" if frozen?
273
273
  plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
274
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
+
275
281
  plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
276
282
  include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
277
283
  extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
@@ -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}?
@@ -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
@@ -0,0 +1,87 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The custom_matchers plugin supports using arbitrary objects
7
+ # as matchers, as long as the application has been configured
8
+ # to accept such objects.
9
+ #
10
+ # After loading the plugin, support for custom matchers can be
11
+ # configured using the +custom_matcher+ class method. This
12
+ # method is generally passed the class of the object you want
13
+ # to use as a custom matcher, as well as a block. The block
14
+ # will be called in the context of the request instance
15
+ # with the specific matcher used in the match method.
16
+ #
17
+ # Blocks can append to the captures in order to yield the appropriate
18
+ # values to match blocks, or call request methods that append to the
19
+ # captures.
20
+ #
21
+ # Example:
22
+ #
23
+ # plugin :custom_matchers
24
+ # method_segment = Struct.new(:request_method, :next_segment)
25
+ # custom_matcher(method_segment) do |matcher|
26
+ # # self is the request instance ("r" yielded in the route block below)
27
+ # if matcher.request_method == self.request_method
28
+ # match(matcher.next_segment)
29
+ # end
30
+ # end
31
+ #
32
+ # get_foo = method_segment.new('GET', 'foo')
33
+ # post_any = method_segment.new('POST', String)
34
+ # route do |r|
35
+ # r.on('baz') do
36
+ # r.on(get_foo) do
37
+ # # GET method, /baz/foo prefix
38
+ # end
39
+ #
40
+ # r.is(post_any) do |seg|
41
+ # # for POST /baz/bar, seg is "bar"
42
+ # end
43
+ # end
44
+ #
45
+ # r.on('quux') do
46
+ # r.is(get_foo) do
47
+ # # GET method, /quux/foo route
48
+ # end
49
+ #
50
+ # r.on(post_any) do |seg|
51
+ # # for POST /quux/xyz, seg is "xyz"
52
+ # end
53
+ # end
54
+ # end
55
+ module CustomMatchers
56
+ def self.configure(app)
57
+ app.opts[:custom_matchers] ||= OPTS
58
+ end
59
+
60
+ module ClassMethods
61
+ def custom_matcher(match_class, &block)
62
+ custom_matchers = Hash[opts[:custom_matchers]]
63
+ meth = custom_matchers[match_class] = custom_matchers[match_class] || :"_custom_matcher_#{match_class}"
64
+ opts[:custom_matchers] = custom_matchers.freeze
65
+ self::RodaRequest.send(:define_method, meth, &block)
66
+ nil
67
+ end
68
+ end
69
+
70
+ module RequestMethods
71
+ # Try custom matchers before calling super
72
+ def unsupported_matcher(matcher)
73
+ roda_class.opts[:custom_matchers].each do |match_class, meth|
74
+ if match_class === matcher
75
+ return send(meth, matcher)
76
+ end
77
+ end
78
+
79
+ super
80
+ end
81
+ end
82
+ end
83
+
84
+ register_plugin(:custom_matchers, CustomMatchers)
85
+ end
86
+ end
87
+
@@ -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
 
@@ -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
@@ -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
 
@@ -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
@@ -287,7 +287,9 @@ class Roda
287
287
  false
288
288
  end
289
289
 
290
+ # :nocov:
290
291
  if COMPILED_METHOD_SUPPORT
292
+ # :nocov:
291
293
  # Compile a method in the given module with the given name that will
292
294
  # call the compiled template method, updating the compiled template method
293
295
  def define_compiled_method(roda_class, method_name, locals_keys=EMPTY_ARRAY)
@@ -337,7 +339,9 @@ class Roda
337
339
  def inherited(subclass)
338
340
  super
339
341
  opts = subclass.opts[:render] = subclass.opts[:render].dup
342
+ # :nocov:
340
343
  if COMPILED_METHOD_SUPPORT
344
+ # :nocov:
341
345
  opts[:template_method_cache] = (opts[:cache_class] || RodaCache).new
342
346
  end
343
347
  opts[:cache] = opts[:cache].dup
@@ -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
@@ -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
@@ -609,10 +609,9 @@ class Roda
609
609
  when nil
610
610
  keys = (0...@obj.length)
611
611
 
612
- valid = case @obj
613
- when Array
612
+ valid = if @obj.is_a?(Array)
614
613
  true
615
- when Hash
614
+ else
616
615
  keys = keys.map(&:to_s)
617
616
  keys.all?{|k| @obj.has_key?(k)}
618
617
  end
@@ -742,9 +741,7 @@ class Roda
742
741
  return
743
742
  end
744
743
 
745
- if v = self[key]
746
- v.subkey(keys, do_raise)
747
- end
744
+ self[key].subkey(keys, do_raise)
748
745
  rescue => e
749
746
  handle_error(key, reason, e)
750
747
  end
@@ -808,10 +805,10 @@ class Roda
808
805
  @nested_params = nil
809
806
 
810
807
  if capturing_started
811
- # Unset capturing if capturing was not already started.
808
+ # Unset capturing if capturing was already started.
812
809
  @capture = nil
813
810
  else
814
- # If capturing was already started, update cached nested params
811
+ # If capturing was not already started, update cached nested params
815
812
  # before resetting symbolize setting.
816
813
  @nested_params = nested_params
817
814
  end
@@ -878,7 +875,7 @@ class Roda
878
875
  def handle_error(key, reason, e, do_raise=false)
879
876
  case e
880
877
  when String
881
- handle_error(key, reason, Error.new(e), do_raise=false)
878
+ handle_error(key, reason, Error.new(e), do_raise)
882
879
  when Error, ArgumentError
883
880
  if @capture && (le = @capture.last) && le == e
884
881
  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 = 32
7
+ RodaMinorVersion = 37
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.32.0
4
+ version: 3.37.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-05-15 00:00:00.000000000 Z
11
+ date: 2020-10-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -208,6 +208,11 @@ extra_rdoc_files:
208
208
  - doc/release_notes/3.30.0.txt
209
209
  - doc/release_notes/3.31.0.txt
210
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
215
+ - doc/release_notes/3.37.0.txt
211
216
  files:
212
217
  - CHANGELOG
213
218
  - MIT-LICENSE
@@ -241,6 +246,11 @@ files:
241
246
  - doc/release_notes/3.30.0.txt
242
247
  - doc/release_notes/3.31.0.txt
243
248
  - doc/release_notes/3.32.0.txt
249
+ - doc/release_notes/3.33.0.txt
250
+ - doc/release_notes/3.34.0.txt
251
+ - doc/release_notes/3.35.0.txt
252
+ - doc/release_notes/3.36.0.txt
253
+ - doc/release_notes/3.37.0.txt
244
254
  - doc/release_notes/3.4.0.txt
245
255
  - doc/release_notes/3.5.0.txt
246
256
  - doc/release_notes/3.6.0.txt
@@ -267,6 +277,7 @@ files:
267
277
  - lib/roda/plugins/content_security_policy.rb
268
278
  - lib/roda/plugins/cookies.rb
269
279
  - lib/roda/plugins/csrf.rb
280
+ - lib/roda/plugins/custom_matchers.rb
270
281
  - lib/roda/plugins/default_headers.rb
271
282
  - lib/roda/plugins/default_status.rb
272
283
  - lib/roda/plugins/delay_build.rb
@@ -301,6 +312,7 @@ files:
301
312
  - lib/roda/plugins/middleware.rb
302
313
  - lib/roda/plugins/middleware_stack.rb
303
314
  - lib/roda/plugins/module_include.rb
315
+ - lib/roda/plugins/multi_public.rb
304
316
  - lib/roda/plugins/multi_route.rb
305
317
  - lib/roda/plugins/multi_run.rb
306
318
  - lib/roda/plugins/multi_view.rb
@@ -320,6 +332,7 @@ files:
320
332
  - lib/roda/plugins/placeholder_string_matchers.rb
321
333
  - lib/roda/plugins/precompile_templates.rb
322
334
  - lib/roda/plugins/public.rb
335
+ - lib/roda/plugins/r.rb
323
336
  - lib/roda/plugins/relative_path.rb
324
337
  - lib/roda/plugins/render.rb
325
338
  - lib/roda/plugins/render_each.rb
@@ -377,7 +390,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
377
390
  - !ruby/object:Gem::Version
378
391
  version: '0'
379
392
  requirements: []
380
- rubygems_version: 3.1.2
393
+ rubygems_version: 3.1.4
381
394
  signing_key:
382
395
  specification_version: 4
383
396
  summary: Routing tree web toolkit