roda 3.54.0 → 3.57.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +32 -0
  3. data/doc/conventions.rdoc +14 -11
  4. data/doc/release_notes/3.55.0.txt +12 -0
  5. data/doc/release_notes/3.56.0.txt +33 -0
  6. data/doc/release_notes/3.57.0.txt +34 -0
  7. data/lib/roda/plugins/chunked.rb +2 -2
  8. data/lib/roda/plugins/common_logger.rb +12 -1
  9. data/lib/roda/plugins/cookies.rb +2 -0
  10. data/lib/roda/plugins/hash_branch_view_subdir.rb +76 -0
  11. data/lib/roda/plugins/hash_branches.rb +145 -0
  12. data/lib/roda/plugins/hash_paths.rb +128 -0
  13. data/lib/roda/plugins/hash_routes.rb +13 -176
  14. data/lib/roda/plugins/json_parser.rb +6 -2
  15. data/lib/roda/plugins/middleware.rb +17 -2
  16. data/lib/roda/plugins/multi_public.rb +8 -0
  17. data/lib/roda/plugins/multi_route.rb +1 -1
  18. data/lib/roda/plugins/multi_view.rb +0 -4
  19. data/lib/roda/plugins/named_routes.rb +1 -2
  20. data/lib/roda/plugins/not_allowed.rb +13 -0
  21. data/lib/roda/plugins/public.rb +8 -0
  22. data/lib/roda/plugins/render.rb +5 -3
  23. data/lib/roda/plugins/route_csrf.rb +1 -0
  24. data/lib/roda/plugins/run_append_slash.rb +1 -1
  25. data/lib/roda/plugins/run_require_slash.rb +46 -0
  26. data/lib/roda/plugins/sessions.rb +1 -0
  27. data/lib/roda/plugins/sinatra_helpers.rb +10 -0
  28. data/lib/roda/plugins/static.rb +2 -0
  29. data/lib/roda/plugins/static_routing.rb +1 -1
  30. data/lib/roda/plugins/status_303.rb +6 -3
  31. data/lib/roda/plugins/status_handler.rb +35 -9
  32. data/lib/roda/plugins/symbol_status.rb +2 -0
  33. data/lib/roda/plugins/unescape_path.rb +2 -0
  34. data/lib/roda/request.rb +35 -1
  35. data/lib/roda/response.rb +5 -0
  36. data/lib/roda/version.rb +1 -1
  37. metadata +30 -6
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '083b83c9271643842b60c7027b16a3ed2068fef54423b3a80dae73ffa991b0cf'
4
- data.tar.gz: c2b9efd6155dd597f13e7167ce6ad6044f8692fb5dafa7994c1527c3a94f1641
3
+ metadata.gz: 578868ccd08ba1fa91302273c92176def99072db735ec119d13cbf49e78a7249
4
+ data.tar.gz: b95b2a33a18a3b135f494ba7f0ffa1639a5fd876b0b199f414826cab7fa75a9d
5
5
  SHA512:
6
- metadata.gz: e33b2bc1b476bb103d380ac0b310e09640a0bdbce160c7307c92fba93b2dbb2f2a149b2ea3e1db65701354ab274d4d5cea03a7d813f9c81b271bb43d31915503
7
- data.tar.gz: 9c3f7d67ed534f769ba453974b2dca02318a3330064d5f350ed6f8e0d25314513d884cddbaa6a0e0b9fae36b962fec659db050ced37e543201d4271099813eda
6
+ metadata.gz: 2b7b769a1653315b1fd9bca1a993ff0e699f2a09fb4db135304d8c7473f2c834c61b91e188005ac029fd85feaca6cc6a106efdd83f0e53771cddd8f3a73204aa
7
+ data.tar.gz: 7bdc602a54f8f5b34a7edc0355b3e6a426b29dd4671c30ef9d7712c757e116c9e6679cd08884f28c7636a80560878284ff4de479f0c479d905948ebfce9b12b3
data/CHANGELOG CHANGED
@@ -1,3 +1,35 @@
1
+ = 3.57.0 (2022-06-14)
2
+
3
+ * Make static_routing plugin depend on the hash_paths instead of the hash_routes plugin (jeremyevans)
4
+
5
+ * Split hash_branches and hash_paths plugins from hash_routes plugin (jeremyevans)
6
+
7
+ * Hex escape unprintable characters in common_logger plugin output (jeremyevans)
8
+
9
+ * Add hash_branch_view_subdir plugin for automatically appending a view subdirectory on a successful hash branch (jeremyevans)
10
+
11
+ = 3.56.0 (2022-05-13)
12
+
13
+ * Make status_303 plugin use 303 responses for HTTP/2 and higher versions (jeremyevans)
14
+
15
+ * Add RodaRequest#http_version for determining the HTTP version in use (jeremyevans)
16
+
17
+ * Do not set a body for 405 responses when using the verb methods in the not_allowed plugin (jeremyevans) (#267)
18
+
19
+ * Support status_handler method :keep_headers option in status_handler plugin (jeremyevans) (#267)
20
+
21
+ * Make not_allowed plugin have r.root return 405 responses for non-GET requests (jeremyevans) (#266)
22
+
23
+ * In Rack 3, only require the parts of rack used by Roda, instead of requiring rack itself and relying on autoload (jeremyevans)
24
+
25
+ * Add run_require_slash plugin, for skipping application dispatch for remaining paths that would violate Rack SPEC (jeremyevans)
26
+
27
+ = 3.55.0 (2022-04-12)
28
+
29
+ * Allow passing blocks to the view method in the render plugin (jeremyevans) (#262)
30
+
31
+ * Add :forward_response_headers middleware plugin option to use app headers as default for response (janko) (#259)
32
+
1
33
  = 3.54.0 (2022-03-14)
2
34
 
3
35
  * Make chunked plugin not use Transfer-Encoding: chunked by default (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,12 @@
1
+ = New Features
2
+
3
+ * A :forward_response_headers option has been added to the middleware
4
+ plugin, which uses the response headers added by the middleware
5
+ as default response headers even if the middleware does not handle
6
+ the response. Response headers set by the underlying application
7
+ take precedence over response headers set by the middleware.
8
+
9
+ * The render plugin view method now accepts a block and will pass the
10
+ block to the underlying render method call. This is useful for
11
+ rendering a template that yields inside of an existing layout.
12
+ Previously, you had to nest render calls to do that.
@@ -0,0 +1,33 @@
1
+ = New Features
2
+
3
+ * RodaRequest#http_version has been added for determining the HTTP
4
+ version the request was submitted with. This will be a string
5
+ such as "HTTP/1.0", "HTTP/1.1", "HTTP/2", etc. This will use the
6
+ SERVER_PROTOCOL and HTTP_VERSION entries from the environment to
7
+ determine which HTTP version is in use.
8
+
9
+ * The status_handler method in the status_handler plugin now supports
10
+ a :keep_headers option. The value for this option should be an
11
+ array of header names to keep. All other headers are removed. The
12
+ default behavior without the option is still to remove all headers.
13
+
14
+ * A run_require_slash plugin has been added, which will skip
15
+ dispatching to another rack application if the remaining path is not
16
+ empty and does not start with a slash.
17
+
18
+ = Other Improvements
19
+
20
+ * The status_303 plugin will use 303 as the default redirect status
21
+ for non-GET requests for HTTP/2 and higher HTTP versions. Previously,
22
+ it only used 303 for HTTP/1.1.
23
+
24
+ * The not_allowed plugin now overrides the r.root method to return
25
+ 405 responses to non-GET requests to the root.
26
+
27
+ * The not_allowed plugin no longer sets the body when returning 405
28
+ responses using methods such as r.get and r.post. Previously, the
29
+ body was unintentionally set to the same value as the Allow header.
30
+
31
+ * When using the Rack master branch (what will become Rack 3), Roda
32
+ only requires the parts of rack that it uses, instead of requiring
33
+ rack and relying on autoload to load the parts of rack in use.
@@ -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.
@@ -215,7 +215,7 @@ class Roda
215
215
  # If chunking by default, call chunked if it hasn't yet been
216
216
  # called and chunking is not specifically disabled.
217
217
  def view(*a)
218
- if opts[:chunk_by_default] && !defined?(@_chunked)
218
+ if opts[:chunk_by_default] && !defined?(@_chunked) && !defined?(yield)
219
219
  chunked(*a)
220
220
  else
221
221
  super
@@ -226,7 +226,7 @@ class Roda
226
226
  # an overview. If a block is given, it is passed to #delay.
227
227
  def chunked(template, opts=OPTS, &block)
228
228
  unless defined?(@_chunked)
229
- @_chunked = !self.opts[:force_chunked_encoding] || env['HTTP_VERSION'] == "HTTP/1.1"
229
+ @_chunked = !self.opts[:force_chunked_encoding] || @_request.http_version == "HTTP/1.1"
230
230
  end
231
231
 
232
232
  if block
@@ -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?)} #{env["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
@@ -1,5 +1,7 @@
1
1
  # frozen-string-literal: true
2
2
 
3
+ require 'rack/utils'
4
+
3
5
  #
4
6
  class Roda
5
7
  module RodaPlugins
@@ -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