roda 3.18.0 → 3.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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