roda 3.56.0 → 3.59.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: 25ca2a1c88af9f7305cdcdb21e3b5983f879386f07c296d7ea0e8f863fddfeac
4
- data.tar.gz: 24d9dbfeaa438c859b2b3fcecd2170d55e9d4335b1b57ca06c140b900d4a8283
3
+ metadata.gz: 963e2d9ac538dbe97835ef65e2e2ba4184838664696bb084ce423a85ef23c205
4
+ data.tar.gz: d47a7f9c86357e99b0c9f470b6c33a32962df55189303020982af55d70907c7b
5
5
  SHA512:
6
- metadata.gz: 8c1b58f0a4ac491533eebb08e83c5801d095d94dbf4270d5834fb3511c82fd305244c0c02e23ba51dba1479e81b361faf556ad85698ea77ebd8a130f471b9527
7
- data.tar.gz: b18e88e98b3c42a83f0e66891e48b03b9b641ecaf5963e5e5fd71d086c92176cefd5642464c790ed5edde30459c4a1d78b62864b1f14cd38f3bea1c9b6cefa30
6
+ metadata.gz: c84cb0ae8c66b537c0a7d32d83d9c5a639439b15fbc650d3569efab5e134e0883ba06183c7f17bfb379f4018060c7d276a5016431ae3e21d05c2db3801a85762
7
+ data.tar.gz: 0c9495ec5fe24b774512f6495f97f5bb4cf8189b964f21a04c5e880d4a536ade2e045c7131f0a54a7c1cd203fe142d53c6d7cde4cf223a8e8be4cb5c5475fede
data/CHANGELOG CHANGED
@@ -1,3 +1,27 @@
1
+ = 3.59.0 (2022-08-12)
2
+
3
+ * Add additional_render_engines plugin, for considering multiple render engines for templates (jeremyevans)
4
+
5
+ * Fix typo in private method name in delete_empty_headers plugin (mculpt) (#279)
6
+
7
+ = 3.58.0 (2022-07-13)
8
+
9
+ * Add filter_common_logger plugin for skipping the logging of certain requests when using the common_logger plugin (jeremyevans)
10
+
11
+ * Make exception_page plugin use Exception#detailed_message on Ruby 3.2+ (jeremyevans)
12
+
13
+ * Make heartbeat plugin compatible with recent changes in the rack master branch (jeremyevans)
14
+
15
+ = 3.57.0 (2022-06-14)
16
+
17
+ * Make static_routing plugin depend on the hash_paths instead of the hash_routes plugin (jeremyevans)
18
+
19
+ * Split hash_branches and hash_paths plugins from hash_routes plugin (jeremyevans)
20
+
21
+ * Hex escape unprintable characters in common_logger plugin output (jeremyevans)
22
+
23
+ * Add hash_branch_view_subdir plugin for automatically appending a view subdirectory on a successful hash branch (jeremyevans)
24
+
1
25
  = 3.56.0 (2022-05-13)
2
26
 
3
27
  * Make status_303 plugin use 303 responses for HTTP/2 and higher versions (jeremyevans)
data/doc/conventions.rdoc CHANGED
@@ -86,17 +86,17 @@ Large applications generally need more structure:
86
86
 
87
87
  For larger apps, the +Rakefile+, +assets/+, +migrate+, +models.rb+, +models/+, +public/+, remain the same.
88
88
 
89
- +app_name.rb+ should use the +hash_routes+ and +view_options+ plugin, or the +multi_run+ plugin.
90
- The routes used by the +hash_routes+ or +multi_run+ should be stored in routing files in the +routes/+
89
+ +app_name.rb+ should use the +hash_branch_view_subdir+ plugin (which builds on the +hash_branches+ and
90
+ +view_options+ plugin), or the +multi_run+ plugin.
91
+ The routes used by the +hash_branches+ or +multi_run+ should be stored in routing files in the +routes/+
91
92
  directory, with one file per prefix.
92
93
 
93
94
  For specs/tests, you should have +spec/models/+ and +spec/web/+, with one file per model in +spec/models/+
94
95
  and one file per prefix in +spec/web/+. Substitute +spec+ with +test+ if that is what you are using as the
95
96
  name of the directory.
96
97
 
97
- You should have a separate view subdirectory per prefix. If you are using +hash_routes+ and +view_options+ plugins,
98
- use +set_view_subdir+ in your routing files to specify the subdirectory to use, so it doesn't need to be
99
- specified on every call to view.
98
+ You should have a separate view subdirectory per prefix. With the +hash_branch_view_subdir+, the application
99
+ will automatically set a separate view subdirectory per routing tree branch.
100
100
 
101
101
  +helpers/+ should be used to store helper methods for your application, that you call in your routing files
102
102
  and views. In a small application, these methods should just be specified in +app_name.rb+
@@ -104,7 +104,7 @@ and views. In a small application, these methods should just be specified in +a
104
104
  === Really Large Applications
105
105
 
106
106
  For very large applications, it's expected that there will be deviations from these conventions. However,
107
- it is recommended to use the +hash_routes+ or +multi_run+ plugins to organize your application, and have
107
+ it is recommended to use the +hash_branch_view_subdir+ or +multi_run+ plugins to organize your application, and have
108
108
  subdirectories in the +routes/+ directory, and nested subdirectories in the +views/+ directory.
109
109
 
110
110
  == Roda Application File Layout
@@ -156,19 +156,22 @@ For larger applications, there are some slight changes to the Roda application f
156
156
 
157
157
  plugin :render, escape: true, layout: './layout'
158
158
  plugin :assets
159
- plugin :view_options
160
- plugin :hash_routes
159
+ plugin :hash_branch_view_subdir
161
160
  Dir['routes/*.rb'].each{|f| require_relative f}
162
161
 
163
162
  route do |r|
164
- r.hash_routes
163
+ r.hash_branches('')
164
+
165
+ r.root do
166
+ # ...
167
+ end
165
168
  end
166
169
 
167
170
  Dir['helpers/*.rb'].each{|f| require_relative f}
168
171
  end
169
172
 
170
- After loading the +view_options+ and +hash_routes+ plugin, you require all of your
173
+ After loading the +hash_branch_view_subdir+ plugin, you require all of your
171
174
  routing files. Inside your route block, instead of defining your routes, you just call
172
- +r.hash_routes+, which will dispatch to all of your routing files. After your route
175
+ +r.hash_branches+, which will dispatch to all of your routing files. After your route
173
176
  block, you require all of your helper files containing the instance methods for your
174
177
  route block or views, instead of defining the methods directly.
@@ -0,0 +1,34 @@
1
+ = New Features
2
+
3
+ * hash_branches and hash_paths plugins have been split off from the
4
+ hash_routes plugin, allowing you to use only those parts instead
5
+ of all of hash_routes.
6
+
7
+ The hash_branches plugin supports the hash_branch class method
8
+ and r.hash_branches routing method.
9
+
10
+ The hash_paths plugin supports the hash_path class method and
11
+ r.hash_paths routing method.
12
+
13
+ The hash_routes plugin functions as it did previously by
14
+ requiring the hash_branches and hash_paths plugins. It adds
15
+ the hash_routes DSL and r.hash_routes routing method.
16
+
17
+ * A hash_branch_view_subdir has been added. It builds on the
18
+ view_options plugin and new hash_branches plugin, automatically
19
+ appending a view subdirectory for each successful hash branch.
20
+ This can DRY up code that uses a separate view subdirectory for
21
+ each branch.
22
+
23
+ = Other Improvements
24
+
25
+ * Unprintable characters are now hex escaped in the output of the
26
+ common_logger plugin. This can protect users who use software
27
+ that respects shell escape sequences to view the logs.
28
+
29
+ = Backwards Compatibility
30
+
31
+ * The static_routing plugin now depends on the hash_paths plugin
32
+ instead of the hash_routes plugin, so you will need to update
33
+ your application to explicitly load the hash_routes plugin if
34
+ you were relying on static_routing to implicitly load it.
@@ -0,0 +1,16 @@
1
+ = New Features
2
+
3
+ * A filter_common_logger plugin has been added, allowing you to skip
4
+ logging of certain requests in the common_logger plugin. This
5
+ allows you to only log requests for certain paths, or only log
6
+ requests for certain types of responses.
7
+
8
+ = Other Improvements
9
+
10
+ * The heartbeat plugin is now compatible with recent changes in the
11
+ rack master branch (what will be rack 3).
12
+
13
+ * The exception_page plugin will now use Exception#detailed_message
14
+ on Ruby 3.2+, preserving the did_you_mean and error_highlight
15
+ information. Additionally, the display of exception messages
16
+ has been improved.
@@ -0,0 +1,17 @@
1
+ = New Features
2
+
3
+ * An additional_render_engines plugin has been added, for considering
4
+ multiple render engines for templates. If the template path does not
5
+ exist for the default render engine, then each additional render
6
+ engine will be checked, returning the first path that exists:
7
+
8
+ plugin :additional_render_engines, ['haml', 'str']
9
+
10
+ This is similar to the additional_view_directories plugin added in
11
+ 3.53.0. Both plugins can be used if you want to consider multiple
12
+ view directories and multiple render engines.
13
+
14
+ = Other Improvements
15
+
16
+ * A typo in a private method name in the delete_empty_headers plugin
17
+ has been fixed.
@@ -0,0 +1,61 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The additional_render_engines plugin allows for specifying additional render
7
+ # engines to consider for templates. When rendering a template, it will
8
+ # first try the default template engine specified in the render plugin. If the
9
+ # template file to be rendered does not exist, it will try each additional render
10
+ # engine specified in this plugin, in order, using the path to the first
11
+ # template file that exists in the file system. If no such path is found, it
12
+ # uses the default path specified by the render plugin.
13
+ #
14
+ # Example:
15
+ #
16
+ # plugin :render # default engine is 'erb'
17
+ # plugin :additional_render_engines, ['haml', 'str']
18
+ #
19
+ # route do |r|
20
+ # # Will check the following in order, using path for first
21
+ # # template file that exists:
22
+ # # * views/t.erb
23
+ # # * views/t.haml
24
+ # # * views/t.str
25
+ # render :t
26
+ # end
27
+ module AdditionalRenderEngines
28
+ def self.load_dependencies(app, render_engines)
29
+ app.plugin :render
30
+ end
31
+
32
+ # Set the additional render engines to consider.
33
+ def self.configure(app, render_engines)
34
+ app.opts[:additional_render_engines] = render_engines.dup.freeze
35
+ end
36
+
37
+ module InstanceMethods
38
+ private
39
+
40
+ # If the template path does not exist, try looking for the template
41
+ # using each of the render engines, in order, returning
42
+ # the first path that exists. If no template path exists for the
43
+ # default any or any additional engines, return the original path.
44
+ def template_path(opts)
45
+ orig_path = super
46
+
47
+ unless File.file?(orig_path)
48
+ self.opts[:additional_render_engines].each do |engine|
49
+ path = super(opts.merge(:engine=>engine))
50
+ return path if File.file?(path)
51
+ end
52
+ end
53
+
54
+ orig_path
55
+ end
56
+ end
57
+ end
58
+
59
+ register_plugin(:additional_render_engines, AdditionalRenderEngines)
60
+ end
61
+ end
@@ -147,7 +147,7 @@ class Roda
147
147
  # If you have the yuicompressor gem installed and working, it will be used
148
148
  # automatically to compress your javascript and css assets. For javascript
149
149
  # assets, if yuicompressor is not available, the plugin will check for
150
- # closure_compiler, uglifier, and minjs and use the first one that works.
150
+ # closure-compiler, uglifier, and minjs and use the first one that works.
151
151
  # If no compressors are available, the assets will just be concatenated
152
152
  # together and not compressed during compilation. You can use the
153
153
  # :css_compressor and :js_compressor options to specify the compressor to use.
@@ -20,6 +20,9 @@ class Roda
20
20
  # plugin :common_logger, Logger.new('filename')
21
21
  # plugin :common_logger, Logger.new('filename'), method: :debug
22
22
  module CommonLogger
23
+ MUTATE_LINE = RUBY_VERSION < '2.3' || RUBY_VERSION >= '3'
24
+ private_constant :MUTATE_LINE
25
+
23
26
  def self.configure(app, logger=nil, opts=OPTS)
24
27
  app.opts[:common_logger] = logger || app.opts[:common_logger] || $stderr
25
28
  app.opts[:common_logger_meth] = app.opts[:common_logger].method(opts.fetch(:method){logger.respond_to?(:write) ? :write : :<<})
@@ -53,7 +56,15 @@ class Roda
53
56
 
54
57
  env = @_request.env
55
58
 
56
- opts[:common_logger_meth].call("#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n")
59
+ line = "#{env['HTTP_X_FORWARDED_FOR'] || env["REMOTE_ADDR"] || "-"} - #{env["REMOTE_USER"] || "-"} [#{Time.now.strftime("%d/%b/%Y:%H:%M:%S %z")}] \"#{env["REQUEST_METHOD"]} #{env["SCRIPT_NAME"]}#{env["PATH_INFO"]}#{"?#{env["QUERY_STRING"]}" if ((qs = env["QUERY_STRING"]) && !qs.empty?)} #{@_request.http_version}\" #{result[0]} #{((length = result[1]['Content-Length']) && (length unless length == '0')) || '-'} #{elapsed_time}\n"
60
+ if MUTATE_LINE
61
+ line.gsub!(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
62
+ # :nocov:
63
+ else
64
+ line = line.gsub(/[^[:print:]\n]/){|c| sprintf("\\x%x", c.ord)}
65
+ # :nocov:
66
+ end
67
+ opts[:common_logger_meth].call(line)
57
68
  end
58
69
 
59
70
  # Create timer instance used for timing
@@ -13,18 +13,18 @@ class Roda
13
13
  module ResponseMethods
14
14
  # Delete any empty headers when calling finish
15
15
  def finish
16
- delelete_empty_headers(super)
16
+ delete_empty_headers(super)
17
17
  end
18
18
 
19
19
  # Delete any empty headers when calling finish_with_body
20
20
  def finish_with_body(_)
21
- delelete_empty_headers(super)
21
+ delete_empty_headers(super)
22
22
  end
23
23
 
24
24
  private
25
25
 
26
26
  # Delete any empty headers from response
27
- def delelete_empty_headers(res)
27
+ def delete_empty_headers(res)
28
28
  res[1].delete_if{|_, v| v.is_a?(String) && v.empty?}
29
29
  res
30
30
  end
@@ -127,7 +127,7 @@ div.context ol.context-line li span { float: right; }
127
127
  div.commands { margin-left: 40px; }
128
128
  div.commands a { color:black; text-decoration:none; }
129
129
  #summary { background: #ffc; }
130
- #summary h2 { font-weight: normal; color: #666; }
130
+ #summary h2 { font-weight: normal; color: #666; font-family: monospace; white-space: pre-wrap;}
131
131
  #summary ul#quicklinks { list-style-type: none; margin-bottom: 2em; }
132
132
  #summary ul#quicklinks li { float: left; padding: 0 1em; }
133
133
  #summary ul#quicklinks>li+li { border-left: 1px #666 solid; }
@@ -196,12 +196,13 @@ END
196
196
  # Designed to be used with the +json+ exception, which will
197
197
  # automatically convert the hash to JSON format.
198
198
  def exception_page(exception, opts=OPTS)
199
+ message = exception_page_exception_message(exception)
199
200
  if opts[:json]
200
201
  @_response['Content-Type'] = "application/json"
201
202
  {
202
203
  "exception"=>{
203
204
  "class"=>exception.class.to_s,
204
- "message"=>exception.message.to_s,
205
+ "message"=>message,
205
206
  "backtrace"=>exception.backtrace.map(&:to_s)
206
207
  }
207
208
  }
@@ -319,7 +320,7 @@ END
319
320
 
320
321
  <div id="summary">
321
322
  <h1>#{h exception.class} at #{h r.path}</h1>
322
- <h2>#{h exception.message}</h2>
323
+ <h2>#{h message}</h2>
323
324
  <table><tr>
324
325
  <th>Ruby</th>
325
326
  <td>
@@ -394,7 +395,22 @@ END1
394
395
  END
395
396
  else
396
397
  @_response['Content-Type'] = "text/plain"
397
- "#{exception.class}: #{exception.message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
398
+ "#{exception.class}: #{message}\n#{exception.backtrace.map{|l| "\t#{l}"}.join("\n")}"
399
+ end
400
+ end
401
+
402
+ private
403
+
404
+ # :nocov:
405
+ if RUBY_VERSION >= '3.2'
406
+ def exception_page_exception_message(exception)
407
+ exception.detailed_message(highlight: false).to_s
408
+ end
409
+ # :nocov:
410
+ else
411
+ # Return message to use for exception.
412
+ def exception_page_exception_message(exception)
413
+ exception.message.to_s
398
414
  end
399
415
  end
400
416
  end
@@ -0,0 +1,46 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The skip_common_logger plugin allows for skipping common_logger logging
7
+ # of some requests. You pass a block when loading the plugin, and the
8
+ # block will be called before logging each request. The block should return
9
+ # whether the request should be logged.
10
+ #
11
+ # Example:
12
+ #
13
+ # # Only log server errors
14
+ # plugin :filter_common_logger do |result|
15
+ # result[0] >= 500
16
+ # end
17
+ #
18
+ # # Don't log requests to certain paths
19
+ # plugin :filter_common_logger do |_|
20
+ # # Block is called in the same context as the route block
21
+ # !request.path.start_with?('/admin/')
22
+ # end
23
+ module FilterCommonLogger
24
+ def self.load_dependencies(app)
25
+ app.plugin :common_logger
26
+ end
27
+
28
+ def self.configure(app, &block)
29
+ app.send(:define_method, :_common_log_request?, &block)
30
+ app.send(:private, :_common_log_request?)
31
+ app.send(:alias_method, :_common_log_request?, :_common_log_request?)
32
+ end
33
+
34
+ module InstanceMethods
35
+ private
36
+
37
+ # Log request/response information in common log format to logger.
38
+ def _roda_after_90__common_logger(result)
39
+ super if result && _common_log_request?(result)
40
+ end
41
+ end
42
+ end
43
+
44
+ register_plugin(:filter_common_logger, FilterCommonLogger)
45
+ end
46
+ end
@@ -0,0 +1,76 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The hash_branch_view_subdir plugin builds on the hash_branches and view_options
7
+ # plugins, automatically appending a view subdirectory for any matching hash branch
8
+ # taken. In cases where you are using a separate view subdirectory per hash branch,
9
+ # this can result in DRYer code. Example:
10
+ #
11
+ # plugin :hash_branch_view_subdir
12
+ #
13
+ # route do |r|
14
+ # r.hash_branches
15
+ # end
16
+ #
17
+ # hash_branch 'foo' do |r|
18
+ # # view subdirectory here is 'foo'
19
+ # r.hash_branches('foo')
20
+ # end
21
+ #
22
+ # hash_branch 'foo', 'bar' do |r|
23
+ # # view subdirectory here is 'foo/bar'
24
+ # end
25
+ module HashBranchViewSubdir
26
+ def self.load_dependencies(app)
27
+ app.plugin :hash_branches
28
+ app.plugin :view_options
29
+ end
30
+
31
+ def self.configure(app)
32
+ app.opts[:hash_branch_view_subdir_methods] ||= {}
33
+ end
34
+
35
+ module ClassMethods
36
+ # Freeze the hash_branch_view_subdir metadata when freezing the app.
37
+ def freeze
38
+ opts[:hash_branch_view_subdir_methods].freeze.each_value(&:freeze)
39
+ super
40
+ end
41
+
42
+ # Duplicate hash_branch_view_subdir metadata in subclass.
43
+ def inherited(subclass)
44
+ super
45
+
46
+ h = subclass.opts[:hash_branch_view_subdir_methods]
47
+ opts[:hash_branch_view_subdir_methods].each do |namespace, routes|
48
+ h[namespace] = routes.dup
49
+ end
50
+ end
51
+
52
+ # Automatically append a view subdirectory for a successful hash_branch route,
53
+ # by modifying the generated method to append the view subdirectory before
54
+ # dispatching to the original block.
55
+ def hash_branch(namespace='', segment, &block)
56
+ meths = opts[:hash_branch_view_subdir_methods][namespace] ||= {}
57
+
58
+ if block
59
+ meth = meths[segment] = define_roda_method(meths[segment] || "_hash_branch_view_subdir_#{namespace}_#{segment}", 1, &convert_route_block(block))
60
+ super do |*_|
61
+ append_view_subdir(segment)
62
+ send(meth, @_request)
63
+ end
64
+ else
65
+ if meth = meths.delete(segment)
66
+ remove_method(meth)
67
+ end
68
+ super
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ register_plugin(:hash_branch_view_subdir, HashBranchViewSubdir)
75
+ end
76
+ end
@@ -0,0 +1,145 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The hash_branches plugin allows for O(1) dispatch to multiple route tree branches,
7
+ # based on the next segment in the remaining path:
8
+ #
9
+ # class App < Roda
10
+ # plugin :hash_branches
11
+ #
12
+ # hash_branch("a") do |r|
13
+ # # /a branch
14
+ # end
15
+ #
16
+ # hash_branch("b") do |r|
17
+ # # /b branch
18
+ # end
19
+ #
20
+ # route do |r|
21
+ # r.hash_branches
22
+ # end
23
+ # end
24
+ #
25
+ # With the above routing tree, the +r.hash_branches+ call in the main routing tree
26
+ # will dispatch requests for the +/a+ and +/b+ branches of the tree to the appropriate
27
+ # routing blocks.
28
+ #
29
+ # In this example, the hash branches for +/a+ and +/b+ are in the same file, but in larger
30
+ # applications, they are usually stored in separate files. This allows for easily splitting
31
+ # up the routing tree into a separate file per branch.
32
+ #
33
+ # The +hash_branch+ method supports namespaces, which allow for dispatching to sub-branches
34
+ # any level of the routing tree, fully supporting the needs of applications with large and
35
+ # deep routing branches:
36
+ #
37
+ # class App < Roda
38
+ # plugin :hash_branches
39
+ #
40
+ # # Only one argument used, so the namespace defaults to '', and the argument
41
+ # # specifies the route name
42
+ # hash_branch("a") do |r|
43
+ # # No argument given, so uses the already matched path as the namespace,
44
+ # # which is '/a' in this case.
45
+ # r.hash_branches
46
+ # end
47
+ #
48
+ # hash_branch("b") do |r|
49
+ # # uses :b as the namespace when looking up routes, as that was explicitly specified
50
+ # r.hash_branches(:b)
51
+ # end
52
+ #
53
+ # # Two arguments used, so first specifies the namespace and the second specifies
54
+ # # the branch name
55
+ # hash_branch("/a", "b") do |r|
56
+ # # /a/b path
57
+ # end
58
+ #
59
+ # hash_branch("/a", "c") do |r|
60
+ # # /a/c path
61
+ # end
62
+ #
63
+ # hash_branch(:b, "b") do |r|
64
+ # # /b/b path
65
+ # end
66
+ #
67
+ # hash_branch(:b, "c") do |r|
68
+ # # /b/c path
69
+ # end
70
+ #
71
+ # route do |r|
72
+ # # No argument given, so uses '' as the namespace, as no part of the path has
73
+ # # been matched yet
74
+ # r.hash_branches
75
+ # end
76
+ # end
77
+ #
78
+ # With the above routing tree, requests for the +/a+ and +/b+ branches will be
79
+ # dispatched to the appropriate +hash_branch+ block. Those blocks will the dispatch
80
+ # to the remaining +hash_branch+ blocks, with the +/a+ branch using the implicit namespace of
81
+ # +/a+, and the +/b+ branch using the explicit namespace of +:b+.
82
+ #
83
+ # It is best for performance to explicitly specify the namespace when calling
84
+ # +r.hash_branches+.
85
+ module HashBranches
86
+ def self.configure(app)
87
+ app.opts[:hash_branches] ||= {}
88
+ end
89
+
90
+ module ClassMethods
91
+ # Freeze the hash_branches metadata when freezing the app.
92
+ def freeze
93
+ opts[:hash_branches].freeze.each_value(&:freeze)
94
+ super
95
+ end
96
+
97
+ # Duplicate hash_branches metadata in subclass.
98
+ def inherited(subclass)
99
+ super
100
+
101
+ h = subclass.opts[:hash_branches]
102
+ opts[:hash_branches].each do |namespace, routes|
103
+ h[namespace] = routes.dup
104
+ end
105
+ end
106
+
107
+ # Add branch handler for the given namespace and segment. If called without
108
+ # a block, removes the existing branch handler if it exists.
109
+ def hash_branch(namespace='', segment, &block)
110
+ segment = "/#{segment}"
111
+ routes = opts[:hash_branches][namespace] ||= {}
112
+ if block
113
+ routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
114
+ elsif meth = routes.delete(segment)
115
+ remove_method(meth)
116
+ end
117
+ end
118
+ end
119
+
120
+ module RequestMethods
121
+ # Checks the matching hash_branch namespace for a branch matching the next
122
+ # segment in the remaining path, and dispatch to that block if there is one.
123
+ def hash_branches(namespace=matched_path)
124
+ rp = @remaining_path
125
+
126
+ return unless rp.getbyte(0) == 47 # "/"
127
+
128
+ if routes = roda_class.opts[:hash_branches][namespace]
129
+ if segment_end = rp.index('/', 1)
130
+ if meth = routes[rp[0, segment_end]]
131
+ @remaining_path = rp[segment_end, 100000000]
132
+ always{scope.send(meth, self)}
133
+ end
134
+ elsif meth = routes[rp]
135
+ @remaining_path = ''
136
+ always{scope.send(meth, self)}
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ register_plugin(:hash_branches, HashBranches)
144
+ end
145
+ end
@@ -0,0 +1,128 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
5
+ module RodaPlugins
6
+ # The hash_paths plugin allows for O(1) dispatch to multiple routes at any point
7
+ # in the routing tree. It is useful when you have a large number of specific routes
8
+ # to dispatch to at any point in the routing tree.
9
+ #
10
+ # You configure the hash paths to dispatch to using the +hash_path+ class method,
11
+ # specifying the remaining path, with a block to handle that path. Then you dispatch
12
+ # to the configured paths using +r.hash_paths+:
13
+ #
14
+ # class App < Roda
15
+ # plugin :hash_paths
16
+ #
17
+ # hash_path("/a") do |r|
18
+ # # /a path
19
+ # end
20
+ #
21
+ # hash_path("/a/b") do |r|
22
+ # # /a/b path
23
+ # end
24
+ #
25
+ # route do |r|
26
+ # r.hash_paths
27
+ # end
28
+ # end
29
+ #
30
+ # With the above routing tree, the +r.hash_paths+ call will dispatch requests for the +/a+ and
31
+ # +/a/b+ request paths.
32
+ #
33
+ # The +hash_path+ class method supports namespaces, which allows +r.hash_paths+ to be used at
34
+ # any level of the routing tree. Here is an example that uses namespaces for sub-branches:
35
+ #
36
+ # class App < Roda
37
+ # plugin :hash_paths
38
+ #
39
+ # # Two arguments provided, so first argument is the namespace
40
+ # hash_path("/a", "/b") do |r|
41
+ # # /a/b path
42
+ # end
43
+ #
44
+ # hash_path("/a", "/c") do |r|
45
+ # # /a/c path
46
+ # end
47
+ #
48
+ # hash_path(:b, "/b") do |r|
49
+ # # /b/b path
50
+ # end
51
+ #
52
+ # hash_path(:b, "/c") do |r|
53
+ # # /b/c path
54
+ # end
55
+ #
56
+ # route do |r|
57
+ # r.on 'a' do
58
+ # # No argument given, so uses the already matched path as the namespace,
59
+ # # which is '/a' in this case.
60
+ # r.hash_paths
61
+ # end
62
+ #
63
+ # r.on 'b' do
64
+ # # uses :b as the namespace when looking up routes, as that was explicitly specified
65
+ # r.hash_paths(:b)
66
+ # end
67
+ # end
68
+ # end
69
+ #
70
+ # With the above routing tree, requests for the +/a+ branch will be handled by the first
71
+ # +r.hash_paths+ call, and requests for the +/b+ branch will be handled by the second
72
+ # +r.hash_paths+ call. Those will dispatch to the configured hash paths for the +/a+ and
73
+ # +:b+ namespaces.
74
+ #
75
+ # It is best for performance to explicitly specify the namespace when calling
76
+ # +r.hash_paths+.
77
+ module HashPaths
78
+ def self.configure(app)
79
+ app.opts[:hash_paths] ||= {}
80
+ end
81
+
82
+ module ClassMethods
83
+ # Freeze the hash_paths metadata when freezing the app.
84
+ def freeze
85
+ opts[:hash_paths].freeze.each_value(&:freeze)
86
+ super
87
+ end
88
+
89
+ # Duplicate hash_paths metadata in subclass.
90
+ def inherited(subclass)
91
+ super
92
+
93
+ h = subclass.opts[:hash_paths]
94
+ opts[:hash_paths].each do |namespace, routes|
95
+ h[namespace] = routes.dup
96
+ end
97
+ end
98
+
99
+ # Add path handler for the given namespace and path. When the
100
+ # r.hash_paths method is called, checks the matching namespace
101
+ # for the full remaining path, and dispatch to that block if
102
+ # there is one. If called without a block, removes the existing
103
+ # path handler if it exists.
104
+ def hash_path(namespace='', path, &block)
105
+ routes = opts[:hash_paths][namespace] ||= {}
106
+ if block
107
+ routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block))
108
+ elsif meth = routes.delete(path)
109
+ remove_method(meth)
110
+ end
111
+ end
112
+ end
113
+
114
+ module RequestMethods
115
+ # Checks the matching hash_path namespace for a branch matching the
116
+ # remaining path, and dispatch to that block if there is one.
117
+ def hash_paths(namespace=matched_path)
118
+ if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path])
119
+ @remaining_path = ''
120
+ always{scope.send(meth, self)}
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ register_plugin(:hash_paths, HashPaths)
127
+ end
128
+ end
@@ -3,58 +3,9 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The hash_routes plugin combines the O(1) dispatching speed of the static_routing plugin with
7
- # the flexibility of the multi_route plugin. For any point in the routing tree,
8
- # it allows you dispatch to multiple routes where the next segment or the remaining path
9
- # is a static string.
10
- #
11
- # For a basic replacement of the multi_route plugin, you can replace class level
12
- # <tt>route('segment')</tt> calls with <tt>hash_branch('segment')</tt>:
13
- #
14
- # class App < Roda
15
- # plugin :hash_routes
16
- #
17
- # hash_branch("a") do |r|
18
- # # /a branch
19
- # end
20
- #
21
- # hash_branch("b") do |r|
22
- # # /b branch
23
- # end
24
- #
25
- # route do |r|
26
- # r.hash_branches
27
- # end
28
- # end
29
- #
30
- # With the above routing tree, the +r.hash_branches+ call in the main routing tree,
31
- # will dispatch requests for the +/a+ and +/b+ branches of the tree to the appropriate
32
- # routing blocks.
33
- #
34
- # In addition to supporting routing via the next segment, you can also support similar
35
- # routing for entire remaining path using the +hash_path+ class method:
36
- #
37
- # class App < Roda
38
- # plugin :hash_routes
39
- #
40
- # hash_path("/a") do |r|
41
- # # /a path
42
- # end
43
- #
44
- # hash_path("/a/b") do |r|
45
- # # /a/b path
46
- # end
47
- #
48
- # route do |r|
49
- # r.hash_paths
50
- # end
51
- # end
52
- #
53
- # With the above routing tree, the +r.hash_paths+ call will dispatch requests for the +/a+ and
54
- # +/a/b+ request paths.
55
- #
56
- # You can combine the two approaches, and use +r.hash_routes+ to first try routing the
57
- # full path, and then try routing the next segment:
6
+ # The hash_routes plugin builds on top of the hash_branches and hash_paths plugins, and adds
7
+ # a DSL for configuring hash branches and paths. It also adds an +r.hash_routes+ method for
8
+ # first attempting dispatch to the configured hash_paths, then to the configured hash_branches:
58
9
  #
59
10
  # class App < Roda
60
11
  # plugin :hash_routes
@@ -84,60 +35,14 @@ class Roda
84
35
  # +hash_path+ block. Other requests for the +/a+ branch, and all requests for the +/b+
85
36
  # branch will be routed to the appropriate +hash_branch+ block.
86
37
  #
87
- # Both +hash_branch+ and +hash_path+ support namespaces, which allows them to be used at
88
- # any level of the routing tree. Here is an example that uses namespaces for sub-branches:
89
- #
90
- # class App < Roda
91
- # plugin :hash_routes
92
- #
93
- # # Only one argument used, so the namespace defaults to '', and the argument
94
- # # specifies the route name
95
- # hash_branch("a") do |r|
96
- # # uses '/a' as the namespace when looking up routes,
97
- # # as that part of the path has been routed now
98
- # r.hash_routes
99
- # end
100
- #
101
- # # Two arguments used, so first specifies the namespace and the second specifies
102
- # # the route name
103
- # hash_branch('', "b") do |r|
104
- # # uses :b as the namespace when looking up routes, as that was explicitly specified
105
- # r.hash_routes(:b)
106
- # end
107
- #
108
- # hash_path("/a", "/b") do |r|
109
- # # /a/b path
110
- # end
111
- #
112
- # hash_path("/a", "/c") do |r|
113
- # # /a/c path
114
- # end
115
- #
116
- # hash_path(:b, "/b") do |r|
117
- # # /b/b path
118
- # end
119
- #
120
- # hash_path(:b, "/c") do |r|
121
- # # /b/c path
122
- # end
123
- #
124
- # route do |r|
125
- # # uses '' as the namespace, as no part of the path has been routed yet
126
- # r.hash_branches
127
- # end
128
- # end
129
- #
130
- # With the above routing tree, requests for the +/a+ and +/b+ branches will be
131
- # dispatched to the appropriate +hash_branch+ block. Those blocks will the dispatch
132
- # to the +hash_path+ blocks, with the +/a+ branch using the implicit namespace of
133
- # +/a+, and the +/b+ branch using the explicit namespace of +:b+. In general, it
134
- # is best for performance to explicitly specify the namespace when calling
135
- # +r.hash_branches+, +r.hash_paths+, and +r.hash_routes+.
38
+ # It is best for performance to explicitly specify the namespace when calling
39
+ # +r.hash_routes+.
136
40
  #
137
41
  # Because specifying routes explicitly using the +hash_branch+ and +hash_path+
138
42
  # class methods can get repetitive, the hash_routes plugin offers a DSL for DRYing
139
- # the code up. This DSL is used by calling the +hash_routes+ class method. Below
140
- # is a translation of the previous example to using the +hash_routes+ DSL:
43
+ # the code up. This DSL is used by calling the +hash_routes+ class method. The
44
+ # DSL used tries to mirror the standard Roda DSL, but it is not a normal routing
45
+ # tree (it's not possible to execute arbitrary code between branches during routing).
141
46
  #
142
47
  # class App < Roda
143
48
  # plugin :hash_routes
@@ -264,9 +169,12 @@ class Roda
264
169
  # * views
265
170
  # * all verb methods (get, post, etc.)
266
171
  module HashRoutes
172
+ def self.load_dependencies(app)
173
+ app.plugin :hash_branches
174
+ app.plugin :hash_paths
175
+ end
176
+
267
177
  def self.configure(app)
268
- app.opts[:hash_branches] ||= {}
269
- app.opts[:hash_paths] ||= {}
270
178
  app.opts[:hash_routes_methods] ||= {}
271
179
  end
272
180
 
@@ -359,24 +267,10 @@ class Roda
359
267
  module ClassMethods
360
268
  # Freeze the hash_routes metadata when freezing the app.
361
269
  def freeze
362
- opts[:hash_branches].freeze.each_value(&:freeze)
363
- opts[:hash_paths].freeze.each_value(&:freeze)
364
270
  opts[:hash_routes_methods].freeze
365
271
  super
366
272
  end
367
273
 
368
- # Duplicate hash_routes metadata in subclass.
369
- def inherited(subclass)
370
- super
371
-
372
- [:hash_branches, :hash_paths].each do |k|
373
- h = subclass.opts[k]
374
- opts[k].each do |namespace, routes|
375
- h[namespace] = routes.dup
376
- end
377
- end
378
- end
379
-
380
274
  # Invoke the DSL for configuring hash routes, see DSL for methods inside the
381
275
  # block. If the block accepts an argument, yield the DSL instance. If the
382
276
  # block does not accept an argument, instance_exec the block in the context
@@ -393,66 +287,9 @@ class Roda
393
287
 
394
288
  dsl
395
289
  end
396
-
397
- # Add branch handler for the given namespace and segment. If called without
398
- # a block, removes the existing branch handler if it exists.
399
- def hash_branch(namespace='', segment, &block)
400
- segment = "/#{segment}"
401
- routes = opts[:hash_branches][namespace] ||= {}
402
- if block
403
- routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
404
- elsif meth = routes[segment]
405
- routes.delete(segment)
406
- remove_method(meth)
407
- end
408
- end
409
-
410
- # Add path handler for the given namespace and path. When the
411
- # r.hash_paths method is called, checks the matching namespace
412
- # for the full remaining path, and dispatch to that block if
413
- # there is one. If called without a block, removes the existing
414
- # path handler if it exists.
415
- def hash_path(namespace='', path, &block)
416
- routes = opts[:hash_paths][namespace] ||= {}
417
- if block
418
- routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block))
419
- elsif meth = routes[path]
420
- routes.delete(path)
421
- remove_method(meth)
422
- end
423
- end
424
290
  end
425
291
 
426
292
  module RequestMethods
427
- # Checks the matching hash_branch namespace for a branch matching the next
428
- # segment in the remaining path, and dispatch to that block if there is one.
429
- def hash_branches(namespace=matched_path)
430
- rp = @remaining_path
431
-
432
- return unless rp.getbyte(0) == 47 # "/"
433
-
434
- if routes = roda_class.opts[:hash_branches][namespace]
435
- if segment_end = rp.index('/', 1)
436
- if meth = routes[rp[0, segment_end]]
437
- @remaining_path = rp[segment_end, 100000000]
438
- always{scope.send(meth, self)}
439
- end
440
- elsif meth = routes[rp]
441
- @remaining_path = ''
442
- always{scope.send(meth, self)}
443
- end
444
- end
445
- end
446
-
447
- # Checks the matching hash_path namespace for a branch matching the
448
- # remaining path, and dispatch to that block if there is one.
449
- def hash_paths(namespace=matched_path)
450
- if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path])
451
- @remaining_path = ''
452
- always{scope.send(meth, self)}
453
- end
454
- end
455
-
456
293
  # Check for matches in both the hash_path and hash_branch namespaces for
457
294
  # a matching remaining path or next segment in the remaining path, respectively.
458
295
  def hash_routes(namespace=matched_path)
@@ -14,13 +14,6 @@ class Roda
14
14
  #
15
15
  # plugin :heartbeat, path: '/status'
16
16
  module Heartbeat
17
- # :nocov:
18
- HEADER_CLASS = (defined?(Rack::Headers) && Rack::Headers.is_a?(Class)) ? Rack::Headers : Hash
19
- # :nocov:
20
- private_constant :HEADER_CLASS
21
-
22
- HEARTBEAT_RESPONSE = [200, {'Content-Type'=>'text/plain'}.freeze, ['OK'.freeze].freeze].freeze
23
-
24
17
  # Set the heartbeat path to the given path.
25
18
  def self.configure(app, opts=OPTS)
26
19
  app.opts[:heartbeat_path] = (opts[:path] || app.opts[:heartbeat_path] || "/heartbeat").dup.freeze
@@ -32,9 +25,11 @@ class Roda
32
25
  # If the request is for a heartbeat path, return the heartbeat response.
33
26
  def _roda_before_20__heartbeat
34
27
  if env['PATH_INFO'] == opts[:heartbeat_path]
35
- response = HEARTBEAT_RESPONSE.dup
36
- response[1] = HEADER_CLASS[response[1]]
37
- throw :halt, response
28
+ response = @_response
29
+ response.status = 200
30
+ response['Content-Type'] = 'text/plain'
31
+ response.write 'OK'
32
+ throw :halt, response.finish
38
33
  end
39
34
  end
40
35
  end
@@ -465,7 +465,11 @@ class Roda
465
465
  def #{field}(address, &block)
466
466
  on(:#{field}=>address, &block)
467
467
  end
468
+ END
469
+
470
+ next if [:rcpt, :text, :body, :subject].include?(field)
468
471
 
472
+ class_eval(<<-END, __FILE__, __LINE__+1)
469
473
  private
470
474
 
471
475
  def match_#{field}(address)
@@ -474,11 +478,6 @@ class Roda
474
478
  END
475
479
  end
476
480
 
477
- undef_method :match_rcpt
478
- undef_method :match_text
479
- undef_method :match_body
480
- undef_method :match_subject
481
-
482
481
  # Same as +header+, but also mark the message as being handled.
483
482
  def handle_header(key, value=nil)
484
483
  header(key, value) do |*args|
@@ -8,7 +8,7 @@ class Roda
8
8
  # which will check # if the first segment in the path matches a named route,
9
9
  # and dispatch to that named route.
10
10
  #
11
- # The hash_routes plugin offers a +r.hash_routes+ method that is similar to
11
+ # The hash_branches plugin offers a +r.hash_branches+ method that is similar to
12
12
  # and performs better than the +r.multi_route+ method, and it is recommended
13
13
  # to consider using that instead of this plugin.
14
14
  #
@@ -16,10 +16,6 @@ class Roda
16
16
  # +multi_view_compile+ class method that will take an array of view template
17
17
  # names and construct a regexp that can be passed to +r.multi_view+.
18
18
  #
19
- # The hash_routes plugin offers a views method that is similar to and performs
20
- # better than the +r.multi_view+ method, and it is recommended to consider
21
- # using that instead of this plugin.
22
- #
23
19
  # Example:
24
20
  #
25
21
  # plugin :multi_view
@@ -145,8 +145,7 @@ class Roda
145
145
  routes = opts[:namespaced_routes][namespace] ||= {}
146
146
  if block
147
147
  routes[name] = define_roda_method(routes[name] || "named_routes_#{namespace}_#{name}", 1, &convert_route_block(block))
148
- elsif meth = routes[name]
149
- routes.delete(name)
148
+ elsif meth = routes.delete(name)
150
149
  remove_method(meth)
151
150
  end
152
151
  else
@@ -50,7 +50,7 @@ class Roda
50
50
  # while still handling the request methods differently.
51
51
  module StaticRouting
52
52
  def self.load_dependencies(app)
53
- app.plugin :hash_routes
53
+ app.plugin :hash_paths
54
54
  end
55
55
 
56
56
  module ClassMethods
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 = 56
7
+ RodaMinorVersion = 59
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.56.0
4
+ version: 3.59.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: 2022-05-13 00:00:00.000000000 Z
11
+ date: 2022-08-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rack
@@ -228,6 +228,9 @@ extra_rdoc_files:
228
228
  - doc/release_notes/3.54.0.txt
229
229
  - doc/release_notes/3.55.0.txt
230
230
  - doc/release_notes/3.56.0.txt
231
+ - doc/release_notes/3.57.0.txt
232
+ - doc/release_notes/3.58.0.txt
233
+ - doc/release_notes/3.59.0.txt
231
234
  - doc/release_notes/3.6.0.txt
232
235
  - doc/release_notes/3.7.0.txt
233
236
  - doc/release_notes/3.8.0.txt
@@ -291,6 +294,9 @@ files:
291
294
  - doc/release_notes/3.54.0.txt
292
295
  - doc/release_notes/3.55.0.txt
293
296
  - doc/release_notes/3.56.0.txt
297
+ - doc/release_notes/3.57.0.txt
298
+ - doc/release_notes/3.58.0.txt
299
+ - doc/release_notes/3.59.0.txt
294
300
  - doc/release_notes/3.6.0.txt
295
301
  - doc/release_notes/3.7.0.txt
296
302
  - doc/release_notes/3.8.0.txt
@@ -302,6 +308,7 @@ files:
302
308
  - lib/roda/plugins/_before_hook.rb
303
309
  - lib/roda/plugins/_optimized_matching.rb
304
310
  - lib/roda/plugins/_symbol_regexp_matchers.rb
311
+ - lib/roda/plugins/additional_render_engines.rb
305
312
  - lib/roda/plugins/additional_view_directories.rb
306
313
  - lib/roda/plugins/all_verbs.rb
307
314
  - lib/roda/plugins/assets.rb
@@ -334,10 +341,14 @@ files:
334
341
  - lib/roda/plugins/error_handler.rb
335
342
  - lib/roda/plugins/error_mail.rb
336
343
  - lib/roda/plugins/exception_page.rb
344
+ - lib/roda/plugins/filter_common_logger.rb
337
345
  - lib/roda/plugins/flash.rb
338
346
  - lib/roda/plugins/h.rb
339
347
  - lib/roda/plugins/halt.rb
348
+ - lib/roda/plugins/hash_branch_view_subdir.rb
349
+ - lib/roda/plugins/hash_branches.rb
340
350
  - lib/roda/plugins/hash_matcher.rb
351
+ - lib/roda/plugins/hash_paths.rb
341
352
  - lib/roda/plugins/hash_routes.rb
342
353
  - lib/roda/plugins/head.rb
343
354
  - lib/roda/plugins/header_matchers.rb