roda 3.18.0 → 3.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -298,7 +298,9 @@ class Roda
298
298
  # :timestamp_paths :: Include the timestamp of assets in asset paths in non-compiled mode. Doing this can
299
299
  # slow down development requests due to additional requests to get last modified times,
300
300
  # put it will make sure the paths change in development when there are modifications,
301
- # which can fix issues when using a caching proxy in non-compiled mode.
301
+ # which can fix issues when using a caching proxy in non-compiled mode. This can also
302
+ # be specified as a string to use that string to separate the timestamp from the asset.
303
+ # By default, <tt>/</tt> is used as the separator if timestamp paths are enabled.
302
304
  module Assets
303
305
  DEFAULTS = {
304
306
  :compiled_name => 'app'.freeze,
@@ -375,6 +377,10 @@ class Roda
375
377
  app.plugin :early_hints
376
378
  end
377
379
 
380
+ if opts[:timestamp_paths] && !opts[:timestamp_paths].is_a?(String)
381
+ opts[:timestamp_paths] = '/'
382
+ end
383
+
378
384
  DEFAULTS.each do |k, v|
379
385
  opts[k] = v unless opts.has_key?(k)
380
386
  end
@@ -629,9 +635,9 @@ class Roda
629
635
  prefix = "#{dirs.join('/')}/" if o[:group_subdirs]
630
636
  end
631
637
  Array(asset_dir).map do |f|
632
- if o[:timestamp_paths]
638
+ if ts = o[:timestamp_paths]
633
639
  mtime = asset_last_modified(File.join(o[:"#{stype}_path"], *[prefix, f].compact))
634
- mtime = "#{sprintf("%i%06i", mtime.to_i, mtime.usec)}/"
640
+ mtime = "#{sprintf("%i%06i", mtime.to_i, mtime.usec)}#{ts}"
635
641
  end
636
642
  "#{url_prefix}/#{o[:"#{stype}_prefix"]}#{mtime}#{prefix}#{f}#{o[:"#{stype}_suffix"]}"
637
643
  end
@@ -784,7 +790,8 @@ class Roda
784
790
  /#{o[:"compiled_#{type}_prefix"]}(#{Regexp.union(assets)})/
785
791
  else
786
792
  assets = unnest_assets_hash(o[type])
787
- /#{o[:"#{type}_prefix"]}#{"\\d+\/" if o[:timestamp_paths]}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
793
+ ts = o[:timestamp_paths]
794
+ /#{o[:"#{type}_prefix"]}#{"\\d+#{ts}" if ts}(#{Regexp.union(assets.uniq)})#{o[:"#{type}_suffix"]}/
788
795
  end
789
796
  end
790
797
 
@@ -3,43 +3,16 @@
3
3
  #
4
4
  class Roda
5
5
  module RodaPlugins
6
- # The delay_build plugin does not build the rack app until
7
- # Roda.app is called, and only rebuilds the rack app if Roda.build!
8
- # is called. This differs from Roda's default behavior, which
9
- # rebuilds the rack app every time the route block changes and
10
- # every time middleware is added if a route block has already
11
- # been defined.
12
- #
13
- # If you are loading hundreds of middleware after a
14
- # route block has already been defined, this can fix a possible
15
- # performance issue, turning an O(n^2) calculation into an
16
- # O(n) calculation, where n is the number of middleware used.
17
6
  module DelayBuild
18
7
  module ClassMethods
19
- # If the app is not been defined yet, build the app.
20
- def app
21
- @app || build!
22
- end
23
-
24
- # Rebuild the application.
8
+ # No-op for backwards compatibility
25
9
  def build!
26
- @build_app = true
27
- build_rack_app
28
- @app
29
- ensure
30
- @build_app = false
31
- end
32
-
33
- private
34
-
35
- # Do not build the rack app automatically, wait for an
36
- # explicit call to build!.
37
- def build_rack_app
38
- super if @build_app
39
10
  end
40
11
  end
41
12
  end
42
13
 
14
+ # RODA4: Remove plugin
15
+ # Only available for backwards compatibility, no longer needed
43
16
  register_plugin(:delay_build, DelayBuild)
44
17
  end
45
18
  end
@@ -37,7 +37,7 @@ class Roda
37
37
  # the remaining path is +/+.
38
38
  def root(&block)
39
39
  super
40
- if remaining_path == '' && is_get?
40
+ if remaining_path.empty? && is_get?
41
41
  always(&block)
42
42
  end
43
43
  end
@@ -0,0 +1,455 @@
1
+ # frozen-string-literal: true
2
+
3
+ #
4
+ class Roda
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:
58
+ #
59
+ # class App < Roda
60
+ # plugin :hash_routes
61
+ #
62
+ # hash_branch("a") do |r|
63
+ # # /a branch
64
+ # end
65
+ #
66
+ # hash_branch("b") do |r|
67
+ # # /b branch
68
+ # end
69
+ #
70
+ # hash_path("/a") do |r|
71
+ # # /a path
72
+ # end
73
+ #
74
+ # hash_path("/a/b") do |r|
75
+ # # /a/b path
76
+ # end
77
+ #
78
+ # route do |r|
79
+ # r.hash_routes
80
+ # end
81
+ # end
82
+ #
83
+ # With the above routing tree, requests for +/a+ and +/a/b+ will be routed to the appropriate
84
+ # +hash_path+ block. Other requests for the +/a+ branch, and all requests for the +/b+
85
+ # branch will be routed to the appropriate +hash_branch+ block.
86
+ #
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+.
136
+ #
137
+ # Because specifying routes explicitly using the +hash_branch+ and +hash_path+
138
+ # 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:
141
+ #
142
+ # class App < Roda
143
+ # plugin :hash_routes
144
+ #
145
+ # # No block argument is used, DSL evaluates block using instance_exec
146
+ # hash_routes "" do
147
+ # # on method is used for routing to next segment,
148
+ # # for similarity to standard Roda
149
+ # on "a" do |r|
150
+ # r.hash_routes '/a'
151
+ # end
152
+ #
153
+ # on "b" do |r|
154
+ # r.hash_routes(:b)
155
+ # end
156
+ # end
157
+ #
158
+ # # Block argument is used, block is yielded DSL instance
159
+ # hash_routes "/a" do |hr|
160
+ # # is method is used for routing to the remaining path,
161
+ # # for similarity to standard Roda
162
+ # hr.is "b" do |r|
163
+ # # /a/b path
164
+ # end
165
+ #
166
+ # hr.is "c" do |r|
167
+ # # /a/c path
168
+ # end
169
+ # end
170
+ #
171
+ # hash_routes :b do
172
+ # is "b" do |r|
173
+ # # /b/b path
174
+ # end
175
+ #
176
+ # is "c" do |r|
177
+ # # /b/c path
178
+ # end
179
+ # end
180
+ #
181
+ # route do |r|
182
+ # # No change here, DSL only makes setup DRYer
183
+ # r.hash_branches
184
+ # end
185
+ # end
186
+ #
187
+ # The +hash_routes+ DSL also offers some additional features to handle additional
188
+ # cases. It supports verb methods, such as +get+ and +post+, which operate like
189
+ # +is+, but are only called if the verb matches (and are not yielded the request).
190
+ # It supports a +view+ method for routes that only render views, as well as a
191
+ # +views+ method for setting up routes for multiple views in a single call, which
192
+ # is a good replacement for the +multi_view+ plugin.
193
+ # +is+, +view+, and the verb methods can use a value of +true+ for the empty
194
+ # remaining path (as the empty string specifies the <tt>"/"</tt> remaining path).
195
+ # It also supports a +dispatch_from+ method, allowing you to setup dispatching to
196
+ # current group of routes from a higher-level namespace.
197
+ # The +hash_routes+ class method will return the DSL instance, so you are not
198
+ # limited to using it with a block.
199
+ #
200
+ # Here's the above example modified to use some of these features:
201
+ #
202
+ # class App < Roda
203
+ # plugin :hash_routes
204
+ #
205
+ # hash_routes "/a" do
206
+ # # Dispatch requests for the /a branch from the empty (default) routing
207
+ # # namespace to this namespace
208
+ # dispatch_from "a"
209
+ #
210
+ # # Handle GET /a path, render "a" template, returning 404 for non-GET requests
211
+ # view true, "a"
212
+ #
213
+ # # Handle /a/b path, returning 404 for non-GET requests
214
+ # get "b" do
215
+ # # GET /a/b path
216
+ # end
217
+ #
218
+ # # Handle /a/c path, returning 404 for non-POST requests
219
+ # post "c" do
220
+ # # POST /a/c path
221
+ # end
222
+ # end
223
+ #
224
+ # bhr = hash_routes(:b)
225
+ #
226
+ # # Dispatch requests for the /b branch from the empty routing to this namespace,
227
+ # # but first check routes in the :b_preauth namespace. If there is no
228
+ # # matching route in the :b_preauth namespace, call the check_authenticated!
229
+ # # method before dispatching to any of the routes in this namespace
230
+ # bhr.dispatch_from "", "b" do |r|
231
+ # r.hash_routes :b_preauth
232
+ # check_authenticated!
233
+ # end
234
+ #
235
+ # bhr.is true do |r|
236
+ # # /b path
237
+ # end
238
+ #
239
+ # bhr.is "" do |r|
240
+ # # /b/ path
241
+ # end
242
+ #
243
+ # # GET /b/d path, render 'd2' template, returning 404 for non-GET requests
244
+ # bhr.views 'd', 'd2'
245
+ #
246
+ # # GET /b/e path, render 'e' template, returning 404 for non-GET requests
247
+ # # GET /b/f path, render 'f' template, returning 404 for non-GET requests
248
+ # bhr.views %w'e f'
249
+ #
250
+ # route do |r|
251
+ # r.hash_branches
252
+ # end
253
+ # end
254
+ #
255
+ # The +view+ and +views+ method depend on the render plugin being loaded, but this
256
+ # plugin does not load the render plugin. You must load the render plugin separately
257
+ # if you want to use the +view+ and +views+ methods.
258
+ #
259
+ # Certain parts of the +hash_routes+ DSL support do not work with the
260
+ # route_block_args plugin, as doing so would reduce performance. These are:
261
+ #
262
+ # * dispatch_from
263
+ # * view
264
+ # * views
265
+ # * all verb methods (get, post, etc.)
266
+ module HashRoutes
267
+ def self.configure(app)
268
+ app.opts[:hash_branches] ||= {}
269
+ app.opts[:hash_paths] ||= {}
270
+ app.opts[:hash_routes_methods] ||= {}
271
+ end
272
+
273
+ # Internal class handling the internals of the +hash_routes+ class method blocks.
274
+ class DSL
275
+ def initialize(roda, namespace)
276
+ @roda = roda
277
+ @namespace = namespace
278
+ end
279
+
280
+ # Setup the given branch in the given namespace to dispatch to routes in this
281
+ # namespace. If a block is given, call the block with the request before
282
+ # dispatching to routes in this namespace.
283
+ def dispatch_from(namespace='', branch, &block)
284
+ ns = @namespace
285
+ if block
286
+ meth_hash = @roda.opts[:hash_routes_methods]
287
+ key = [:dispatch_from, namespace, branch].freeze
288
+ meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_dispatch_from_#{namespace}_#{branch}", 1, &block)
289
+ @roda.hash_branch(namespace, branch) do |r|
290
+ send(meth, r)
291
+ r.hash_routes(ns)
292
+ end
293
+ else
294
+ @roda.hash_branch(namespace, branch) do |r|
295
+ r.hash_routes(ns)
296
+ end
297
+ end
298
+ end
299
+
300
+ # Use the segment to setup a branch in the current namespace.
301
+ def on(segment, &block)
302
+ @roda.hash_branch(@namespace, segment, &block)
303
+ end
304
+
305
+ # Use the segment to setup a path in the current namespace.
306
+ # If path is given as a string, it is prefixed with a slash.
307
+ # If path is +true+, the empty string is used as the path.
308
+ def is(path, &block)
309
+ path = path == true ? "" : "/#{path}"
310
+ @roda.hash_path(@namespace, path, &block)
311
+ end
312
+
313
+ # Use the segment to setup a path in the current namespace that
314
+ # will render the view with the given name if the GET method is
315
+ # used, and will return a 404 if another request method is used.
316
+ # If path is given as a string, it is prefixed with a slash.
317
+ # If path is +true+, the empty string is used as the path.
318
+ def view(path, template)
319
+ path = path == true ? "" : "/#{path}"
320
+ @roda.hash_path(@namespace, path) do |r|
321
+ r.get do
322
+ view(template)
323
+ end
324
+ end
325
+ end
326
+
327
+ # For each template in the array of templates, setup a path in
328
+ # the current namespace for the template using the same name
329
+ # as the template.
330
+ def views(templates)
331
+ templates.each do |template|
332
+ view(template, template)
333
+ end
334
+ end
335
+
336
+ [:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
337
+ define_method(meth) do |path, &block|
338
+ verb(meth, path, &block)
339
+ end
340
+ end
341
+
342
+ private
343
+
344
+ # Setup a path in the current namespace for the given request method verb.
345
+ # Returns 404 for requests for the path with a different request method.
346
+ def verb(verb, path, &block)
347
+ path = path == true ? "" : "/#{path}"
348
+ meth_hash = @roda.opts[:hash_routes_methods]
349
+ key = [@namespace, path].freeze
350
+ meth = meth_hash[key] = @roda.define_roda_method(meth_hash[key] || "hash_routes_#{@namespace}_#{path}", 0, &block)
351
+ @roda.hash_path(@namespace, path) do |r|
352
+ r.send(verb) do
353
+ send(meth)
354
+ end
355
+ end
356
+ end
357
+ end
358
+
359
+ module ClassMethods
360
+ # Freeze the hash_routes metadata when freezing the app.
361
+ def freeze
362
+ opts[:hash_branches].freeze.each_value(&:freeze)
363
+ opts[:hash_paths].freeze.each_value(&:freeze)
364
+ opts[:hash_routes_methods].freeze
365
+ super
366
+ end
367
+
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
+ # Invoke the DSL for configuring hash routes, see DSL for methods inside the
381
+ # block. If the block accepts an argument, yield the DSL instance. If the
382
+ # block does not accept an argument, instance_exec the block in the context
383
+ # of the DSL instance.
384
+ def hash_routes(namespace='', &block)
385
+ dsl = DSL.new(self, namespace)
386
+ if block
387
+ if block.arity == 1
388
+ yield dsl
389
+ else
390
+ dsl.instance_exec(&block)
391
+ end
392
+ end
393
+
394
+ dsl
395
+ end
396
+
397
+ # Add branch handler for the given namespace and segment.
398
+ def hash_branch(namespace='', segment, &block)
399
+ segment = "/#{segment}"
400
+ routes = opts[:hash_branches][namespace] ||= {}
401
+ routes[segment] = define_roda_method(routes[segment] || "hash_branch_#{namespace}_#{segment}", 1, &convert_route_block(block))
402
+ end
403
+
404
+ # Add path handler for the given namespace and path. When the
405
+ # r.hash_paths method is called, checks the matching namespace
406
+ # for the full remaining path, and dispatch to that block if
407
+ # there is one.
408
+ def hash_path(namespace='', path, &block)
409
+ routes = opts[:hash_paths][namespace] ||= {}
410
+ routes[path] = define_roda_method(routes[path] || "hash_path_#{namespace}_#{path}", 1, &convert_route_block(block))
411
+ end
412
+ end
413
+
414
+ module RequestMethods
415
+ # Checks the matching hash_branch namespace for a branch matching the next
416
+ # segment in the remaining path, and dispatch to that block if there is one.
417
+ def hash_branches(namespace=matched_path)
418
+ rp = @remaining_path
419
+
420
+ return unless rp.getbyte(0) == 47 # "/"
421
+
422
+ if routes = roda_class.opts[:hash_branches][namespace]
423
+ if segment_end = rp.index('/', 1)
424
+ if meth = routes[rp[0, segment_end]]
425
+ @remaining_path = rp[segment_end, 100000000]
426
+ always{scope.send(meth, self)}
427
+ end
428
+ elsif meth = routes[rp]
429
+ @remaining_path = ''
430
+ always{scope.send(meth, self)}
431
+ end
432
+ end
433
+ end
434
+
435
+ # Checks the matching hash_path namespace for a branch matching the
436
+ # remaining path, and dispatch to that block if there is one.
437
+ def hash_paths(namespace=matched_path)
438
+ if (routes = roda_class.opts[:hash_paths][namespace]) && (meth = routes[@remaining_path])
439
+ @remaining_path = ''
440
+ always{scope.send(meth, self)}
441
+ end
442
+ end
443
+
444
+ # Check for matches in both the hash_path and hash_branch namespaces for
445
+ # a matching remaining path or next segment in the remaining path, respectively.
446
+ def hash_routes(namespace=matched_path)
447
+ hash_paths(namespace)
448
+ hash_branches(namespace)
449
+ end
450
+ end
451
+ end
452
+
453
+ register_plugin(:hash_routes, HashRoutes)
454
+ end
455
+ end