roda 2.17.0 → 2.18.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.
- checksums.yaml +4 -4
- data/CHANGELOG +12 -0
- data/doc/release_notes/2.18.0.txt +69 -0
- data/lib/roda.rb +7 -1
- data/lib/roda/plugins/assets.rb +70 -42
- data/lib/roda/plugins/assets_preloading.rb +91 -0
- data/lib/roda/plugins/head.rb +11 -0
- data/lib/roda/plugins/not_allowed.rb +1 -1
- data/lib/roda/plugins/public.rb +3 -1
- data/lib/roda/plugins/static_routing.rb +134 -0
- data/lib/roda/plugins/type_routing.rb +6 -6
- data/lib/roda/version.rb +1 -1
- data/spec/matchers_spec.rb +5 -0
- data/spec/plugin/assets_preloading_spec.rb +82 -0
- data/spec/plugin/assets_spec.rb +26 -0
- data/spec/plugin/not_allowed_spec.rb +3 -0
- data/spec/plugin/static_routing_spec.rb +108 -0
- data/spec/plugin/type_routing_spec.rb +29 -0
- data/spec/request_spec.rb +12 -0
- metadata +8 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b5e850d352a98dff085d020e04fa52c55894edaf
|
4
|
+
data.tar.gz: 0fb277bfbfd64699d15d6e56325464e5ba3cb5cb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: a779f1e2cb695dab260f457798582014dd7bee889304de79cea0c05c43f3252b350cddb33e9d17d601f6844eaedc241290b48a2daf7ede4a89133113a2c2f353
|
7
|
+
data.tar.gz: 8594f7058bd36a6cfba5cd47831b0e5267927f09e740c4c2e3b14f237a98432aaa5dde2c15b5a9d9ecb4944de9d35125adc548382383e366795ca8d4678dff1c
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
= 2.18.0 (2016-09-13)
|
2
|
+
|
3
|
+
* Add assets_preloading plugin, for creating link tags or Link header for preloading assets (celsworth, jeremyevans) (#98)
|
4
|
+
|
5
|
+
* Add assets_paths method to assets plugin, for just the paths to the assets, instead of the full tags (celsworth) (#96)
|
6
|
+
|
7
|
+
* Make type_routing plugin work correctly with public plugin (celsworth, jeremyevans) (#95)
|
8
|
+
|
9
|
+
* Add static_routing plugin for 3-4x increase in performance for large numbers of static routes (jeremyevans)
|
10
|
+
|
11
|
+
* Make head plugin work with not_allowed plugin if loaded after (jeremyevans) (#92)
|
12
|
+
|
1
13
|
= 2.17.0 (2016-08-13)
|
2
14
|
|
3
15
|
* Add :postprocessor option to assets plugin, for postprocessing assets (e.g. autoprefixing CSS) (celsworth) (#86)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
= New Plugins
|
2
|
+
|
3
|
+
* A static_routing plugin has been added, which can give a 3-4x
|
4
|
+
increase in performance for large number of static routes, and
|
5
|
+
makes routing O(1) for static routes. Static routes are routes
|
6
|
+
that match full paths, with no placeholders, and are checked before
|
7
|
+
using the normal routing tree.
|
8
|
+
|
9
|
+
Static routes are defined via class-level static_* routing methods.
|
10
|
+
There is a static_* routing method for each HTTP verb (e.g.
|
11
|
+
static_get), as well as a static_route method, which will work
|
12
|
+
for any HTTP verb, with the verb-specific method taking priority.
|
13
|
+
By using static_route, you can get significantly faster performance
|
14
|
+
while retaining some of the benefits of Roda's routing tree design
|
15
|
+
(simple shared logic with verb specific behavior). Example:
|
16
|
+
|
17
|
+
plugin :static_routing
|
18
|
+
|
19
|
+
static_route '/foo' do |r|
|
20
|
+
@var = :foo
|
21
|
+
|
22
|
+
r.get do
|
23
|
+
'Not actually reached'
|
24
|
+
end
|
25
|
+
|
26
|
+
r.post{'static POST /#{@var}'}
|
27
|
+
end
|
28
|
+
|
29
|
+
static_get '/foo' do |r|
|
30
|
+
'static GET /foo'
|
31
|
+
end
|
32
|
+
|
33
|
+
route do |r|
|
34
|
+
'Not a static route'
|
35
|
+
end
|
36
|
+
|
37
|
+
Because static routing routes on the full path instead of by
|
38
|
+
path segment, the methods takes the full path as a string,
|
39
|
+
including the leading slash.
|
40
|
+
|
41
|
+
* An assets_preloading plugin has been added, which makes it simple
|
42
|
+
to generate HTML link tags or a Link header value to tell the
|
43
|
+
browser to preload assets for performance reasons.
|
44
|
+
|
45
|
+
# In routes, using the Link header:
|
46
|
+
response.headers['Link'] = preload_assets_link_header(:css)
|
47
|
+
|
48
|
+
# In templates, using a link tag:
|
49
|
+
<%= preload_assets_link_tags(:css) %>
|
50
|
+
|
51
|
+
= New Features
|
52
|
+
|
53
|
+
* RodaRequest#real_remaining_path has been added. This is designed
|
54
|
+
to be overridden by plugins that modify remaining_path for internal
|
55
|
+
routing purposes. RodaRequest#run now uses real_remaining_path
|
56
|
+
when passing requests to other rack applications.
|
57
|
+
|
58
|
+
* An assets_paths method has been added to the assets plugin. This
|
59
|
+
is similar to the assets method, but it returns an array of paths
|
60
|
+
to the assets, instead of a HTML link/script tag.
|
61
|
+
|
62
|
+
= Other Improvements
|
63
|
+
|
64
|
+
* The public plugin now works correctly when used with the
|
65
|
+
type_routing plugin, for paths ending in extensions that
|
66
|
+
type_routing is configured to handle.
|
67
|
+
|
68
|
+
* The head plugin now works with the not_allowed plugin if it is
|
69
|
+
loaded after the not_allowed plugin.
|
data/lib/roda.rb
CHANGED
@@ -509,6 +509,12 @@ class Roda
|
|
509
509
|
# The current path to match requests against.
|
510
510
|
attr_reader :remaining_path
|
511
511
|
|
512
|
+
# An alias of remaining_path. If a plugin changes remaining_path then
|
513
|
+
# it should override this method to return the untouched original.
|
514
|
+
def real_remaining_path
|
515
|
+
remaining_path
|
516
|
+
end
|
517
|
+
|
512
518
|
# Match POST requests. If no arguments are provided, matches all POST
|
513
519
|
# requests, otherwise, matches only POST requests where the arguments
|
514
520
|
# given fully consume the path.
|
@@ -625,7 +631,7 @@ class Roda
|
|
625
631
|
# a URL mapper.
|
626
632
|
def run(app)
|
627
633
|
e = @env
|
628
|
-
path =
|
634
|
+
path = real_remaining_path
|
629
635
|
sn = SCRIPT_NAME
|
630
636
|
pi = PATH_INFO
|
631
637
|
script_name = e[sn]
|
data/lib/roda/plugins/assets.rb
CHANGED
@@ -63,6 +63,21 @@ class Roda
|
|
63
63
|
# if it set it will automatically prefix the path with the SCRIPT_NAME for
|
64
64
|
# the request.
|
65
65
|
#
|
66
|
+
# == Asset Paths
|
67
|
+
#
|
68
|
+
# If you just want the paths rather than the full tags, you can use
|
69
|
+
# assets_paths instead. This will return an array of the sources that
|
70
|
+
# the assets function would have put into tags:
|
71
|
+
#
|
72
|
+
# assets_paths(:css)
|
73
|
+
# # => ["/assets/css/foo.css", "/assets/css/app.css"]
|
74
|
+
#
|
75
|
+
# If compilation is turned on, it will return the path to the compiled
|
76
|
+
# asset:
|
77
|
+
#
|
78
|
+
# assets_paths(:css)
|
79
|
+
# # => ["/assets/app.5e7b06baa1a514d8473b0eca514b806c201073b9.css"]
|
80
|
+
#
|
66
81
|
# == Asset Groups
|
67
82
|
#
|
68
83
|
# The asset plugin supports groups for the cases where you have different
|
@@ -182,7 +197,20 @@ class Roda
|
|
182
197
|
# and it will run in compiled mode, assuming that the compiled asset files
|
183
198
|
# already exist.
|
184
199
|
#
|
185
|
-
# ====
|
200
|
+
# ==== On Heroku
|
201
|
+
#
|
202
|
+
# Heroku supports precompiling the assets when using Roda. You just need to
|
203
|
+
# add an assets:precompile task, similar to this:
|
204
|
+
#
|
205
|
+
# namespace :assets do
|
206
|
+
# desc "Precompile the assets"
|
207
|
+
# task :precompile do
|
208
|
+
# require './app'
|
209
|
+
# App.compile_assets
|
210
|
+
# end
|
211
|
+
# end
|
212
|
+
#
|
213
|
+
# == Postprocessing
|
186
214
|
#
|
187
215
|
# If you pass a callable object to the :postprocessor option, it will be called
|
188
216
|
# before an asset is served.
|
@@ -202,19 +230,6 @@ class Roda
|
|
202
230
|
# end
|
203
231
|
# }
|
204
232
|
#
|
205
|
-
# ==== On Heroku
|
206
|
-
#
|
207
|
-
# Heroku supports precompiling the assets when using Roda. You just need to
|
208
|
-
# add an assets:precompile task, similar to this:
|
209
|
-
#
|
210
|
-
# namespace :assets do
|
211
|
-
# desc "Precompile the assets"
|
212
|
-
# task :precompile do
|
213
|
-
# require './app'
|
214
|
-
# App.compile_assets
|
215
|
-
# end
|
216
|
-
# end
|
217
|
-
#
|
218
233
|
# == External Assets/Assets from Gems
|
219
234
|
#
|
220
235
|
# The assets plugin only supports loading assets files underneath the assets
|
@@ -586,39 +601,16 @@ class Roda
|
|
586
601
|
end
|
587
602
|
|
588
603
|
module InstanceMethods
|
589
|
-
# Return
|
590
|
-
#
|
591
|
-
|
592
|
-
#
|
593
|
-
# To return the tags for a specific asset group, use an array for
|
594
|
-
# the type, such as [:css, :frontend].
|
595
|
-
#
|
596
|
-
# When the assets are not compiled, this will result in a separate
|
597
|
-
# tag for each asset file. When the assets are compiled, this will
|
598
|
-
# result in a single tag to the compiled asset file.
|
599
|
-
def assets(type, attrs = nil)
|
604
|
+
# Return an array of paths for the given asset type and optionally
|
605
|
+
# asset group. See the assets function documentation for details.
|
606
|
+
def assets_paths(type)
|
600
607
|
o = self.class.assets_opts
|
601
608
|
type, *dirs = type if type.is_a?(Array)
|
602
609
|
stype = type.to_s
|
603
610
|
ru = Rack::Utils
|
604
611
|
|
605
|
-
attrs = if attrs
|
606
|
-
attrs.map{|k,v| "#{k}=\"#{ru.escape_html(v.to_s)}\""}.join(SPACE)
|
607
|
-
else
|
608
|
-
EMPTY_STRING
|
609
|
-
end
|
610
|
-
|
611
|
-
if type == :js
|
612
|
-
tag_start = "<script type=\"text/javascript\" #{attrs} src=\""
|
613
|
-
tag_end = JS_END
|
614
|
-
else
|
615
|
-
tag_start = "<link rel=\"stylesheet\" #{attrs} href=\""
|
616
|
-
tag_end = CSS_END
|
617
|
-
end
|
618
|
-
|
619
612
|
url_prefix = request.script_name if self.class.opts[:add_script_name]
|
620
613
|
|
621
|
-
# Create a tag for each individual file
|
622
614
|
if compiled = o[:compiled]
|
623
615
|
asset_host = o[:compiled_asset_host]
|
624
616
|
if dirs && !dirs.empty?
|
@@ -636,7 +628,9 @@ class Roda
|
|
636
628
|
integrity = "\" integrity=\"#{algo}-#{ru.escape_html([[hash].pack('H*')].pack('m').tr("\n", EMPTY_STRING))}"
|
637
629
|
end
|
638
630
|
|
639
|
-
"#{
|
631
|
+
[ "#{asset_host}#{url_prefix}/#{o[:"compiled_#{stype}_prefix"]}.#{ukey}.#{stype}#{integrity}" ]
|
632
|
+
else
|
633
|
+
[]
|
640
634
|
end
|
641
635
|
else
|
642
636
|
asset_dir = o[type]
|
@@ -644,8 +638,42 @@ class Roda
|
|
644
638
|
dirs.each{|f| asset_dir = asset_dir[f]}
|
645
639
|
prefix = "#{dirs.join(SLASH)}/" if o[:group_subdirs]
|
646
640
|
end
|
647
|
-
Array(asset_dir).map{|f| "#{
|
641
|
+
Array(asset_dir).map{|f| "#{url_prefix}/#{o[:"#{stype}_prefix"]}#{prefix}#{f}#{o[:"#{stype}_suffix"]}"}
|
642
|
+
end
|
643
|
+
end
|
644
|
+
|
645
|
+
# Return a string containing html tags for the given asset type.
|
646
|
+
# This will use a script tag for the :js type and a link tag for
|
647
|
+
# the :css type.
|
648
|
+
#
|
649
|
+
# To return the tags for a specific asset group, use an array for
|
650
|
+
# the type, such as [:css, :frontend].
|
651
|
+
#
|
652
|
+
# When the assets are not compiled, this will result in a separate
|
653
|
+
# tag for each asset file. When the assets are compiled, this will
|
654
|
+
# result in a single tag to the compiled asset file.
|
655
|
+
def assets(type, attrs = nil)
|
656
|
+
ru = Rack::Utils
|
657
|
+
attrs = if attrs
|
658
|
+
attrs.map{|k,v| "#{k}=\"#{ru.escape_html(v.to_s)}\""}.join(SPACE)
|
659
|
+
else
|
660
|
+
EMPTY_STRING
|
648
661
|
end
|
662
|
+
|
663
|
+
ltype = if type.is_a?(Array)
|
664
|
+
type[0]
|
665
|
+
else
|
666
|
+
type
|
667
|
+
end
|
668
|
+
if ltype == :js
|
669
|
+
tag_start = "<script type=\"text/javascript\" #{attrs} src=\""
|
670
|
+
tag_end = JS_END
|
671
|
+
else
|
672
|
+
tag_start = "<link rel=\"stylesheet\" #{attrs} href=\""
|
673
|
+
tag_end = CSS_END
|
674
|
+
end
|
675
|
+
|
676
|
+
assets_paths(type).map{|p| "#{tag_start}#{p}#{tag_end}"}.join(NEWLINE)
|
649
677
|
end
|
650
678
|
|
651
679
|
# Render the asset with the given filename. When assets are compiled,
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The assets_preloading plugin generates html tags or a header value
|
7
|
+
# to facilitate browser preloading of your assets. This allows
|
8
|
+
# compatible browsers to fetch assets before they are required,
|
9
|
+
# streamlining page rendering.
|
10
|
+
#
|
11
|
+
# For a list of compatible browsers, see
|
12
|
+
# http://caniuse.com/#search=link-rel-preload
|
13
|
+
#
|
14
|
+
# The plugin provides two functions - preload_assets_link_header and
|
15
|
+
# preload_assets_link_tags. The resulting preloading should be
|
16
|
+
# identical, it is up to you which system you prefer.
|
17
|
+
#
|
18
|
+
# preload_assets_link_header returns a string suitable for populating
|
19
|
+
# the response Link header:
|
20
|
+
#
|
21
|
+
# response.headers['Link'] = preload_assets_link_header(:css)
|
22
|
+
# # Link header will now contain something like:
|
23
|
+
# # </assets/app.css>;rel="preload";as="style"
|
24
|
+
#
|
25
|
+
# preload_assets_link_tags returns a string to drop into your
|
26
|
+
# templates containing link tags:
|
27
|
+
#
|
28
|
+
# preload_assets_link_tags(:css)
|
29
|
+
# # returns <link href="/assets/app.css" rel="preload" as="style">
|
30
|
+
#
|
31
|
+
# Note that these link tags are different to the usual asset
|
32
|
+
# declarations in markup; this will only instruct a compatible browser
|
33
|
+
# to fetch the file and cache it for later; the browser will not parse
|
34
|
+
# the asset until it encounters a traditional link or script tag.
|
35
|
+
#
|
36
|
+
# You must still setup and link to your assets as you did previously.
|
37
|
+
#
|
38
|
+
# Both functions can be passed any combination of asset types or
|
39
|
+
# asset groups, as multiple arguments:
|
40
|
+
#
|
41
|
+
# # generate tags for css assets and the app js asset group
|
42
|
+
# preload_assets_link_tags(:css, [:js, :app], [:js, :bar])
|
43
|
+
#
|
44
|
+
# # generate Link header for css assets and js asset groups app and bar
|
45
|
+
# preload_assets_link_header(:css, [:js, :app])
|
46
|
+
#
|
47
|
+
module AssetsPreloading
|
48
|
+
TYPE_AS = {
|
49
|
+
:css => 'style'.freeze,
|
50
|
+
:js => 'script'.freeze,
|
51
|
+
}.freeze
|
52
|
+
COMMA = ",".freeze
|
53
|
+
NEWLINE= "\n".freeze
|
54
|
+
|
55
|
+
# Depend on the assets plugin, as we'll be calling some functions in it.
|
56
|
+
def self.load_dependencies(app)
|
57
|
+
app.plugin :assets
|
58
|
+
end
|
59
|
+
|
60
|
+
module InstanceMethods
|
61
|
+
# Return a string of <link> tags for the given asset
|
62
|
+
# types/groups.
|
63
|
+
def preload_assets_link_tags(*args)
|
64
|
+
_preload_assets_array(args).map{|path, as| "<link href=\"#{path}\" rel=\"preload\" as=\"#{as}\">"}.join(NEWLINE)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Return a string suitable for a Link header for the
|
68
|
+
# given asset types/groups.
|
69
|
+
def preload_assets_link_header(*args)
|
70
|
+
_preload_assets_array(args).map{|path, as| "<#{path}>;rel=preload;as=#{as}"}.join(COMMA)
|
71
|
+
end
|
72
|
+
|
73
|
+
private
|
74
|
+
|
75
|
+
# Return an array of paths/as pairs for the given asset
|
76
|
+
# types and/or groups.
|
77
|
+
def _preload_assets_array(assets)
|
78
|
+
assets.map do |type|
|
79
|
+
paths = assets_paths(type)
|
80
|
+
type = type[0] if type.is_a?(Array)
|
81
|
+
as = TYPE_AS[type]
|
82
|
+
|
83
|
+
paths.map{|path| [path, as]}
|
84
|
+
end.flatten(1)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
register_plugin(:assets_preloading, AssetsPreloading)
|
90
|
+
end
|
91
|
+
end
|
data/lib/roda/plugins/head.rb
CHANGED
@@ -26,6 +26,10 @@ class Roda
|
|
26
26
|
# HEAD requests for +/+, +/a+, and +/b+ will all return 200 status
|
27
27
|
# with an empty body.
|
28
28
|
#
|
29
|
+
# This plugin also works with the not_allowed plugin if it is loaded
|
30
|
+
# after the not_allowed plugin. In that case, if GET is one of Allow
|
31
|
+
# header options, then HEAD will be as well.
|
32
|
+
#
|
29
33
|
# NOTE: if you have a public facing website it is recommended that
|
30
34
|
# you enable this plugin. Search engines and other bots may send a
|
31
35
|
# HEAD request prior to crawling a page with a GET request. Without
|
@@ -59,6 +63,13 @@ class Roda
|
|
59
63
|
def match_method(method)
|
60
64
|
super || (!method.is_a?(Array) && head? && method.to_s.upcase == 'GET')
|
61
65
|
end
|
66
|
+
|
67
|
+
# Work with the not_allowed plugin so that if GET is one
|
68
|
+
# of the Allow header options, then HEAD is as well.
|
69
|
+
def method_not_allowed(verbs)
|
70
|
+
verbs = verbs.sub('GET', 'HEAD, GET')
|
71
|
+
super
|
72
|
+
end
|
62
73
|
end
|
63
74
|
end
|
64
75
|
|
@@ -102,7 +102,7 @@ class Roda
|
|
102
102
|
|
103
103
|
# Setup methods for all verbs. If inside an is block and not given
|
104
104
|
# arguments, record the verb used. If given an argument, add an is
|
105
|
-
# check with the
|
105
|
+
# check with the arguments.
|
106
106
|
%w'get post delete head options link patch put trace unlink'.each do |verb|
|
107
107
|
if ::Rack::Request.method_defined?("#{verb}?")
|
108
108
|
class_eval(<<-END, __FILE__, __LINE__+1)
|
data/lib/roda/plugins/public.rb
CHANGED
@@ -41,7 +41,7 @@ class Roda
|
|
41
41
|
# Serve files from the public directory if the file exists and this is a GET request.
|
42
42
|
def public
|
43
43
|
if is_get?
|
44
|
-
path = PARSER.unescape(
|
44
|
+
path = PARSER.unescape(real_remaining_path)
|
45
45
|
return if path.include?(NULL_BYTE)
|
46
46
|
|
47
47
|
roda_opts = roda_class.opts
|
@@ -89,7 +89,9 @@ class Roda
|
|
89
89
|
def public_file_readable?(path)
|
90
90
|
::File.file?(path) && ::File.readable?(path)
|
91
91
|
rescue SystemCallError
|
92
|
+
# :nocov:
|
92
93
|
false
|
94
|
+
# :nocov:
|
93
95
|
end
|
94
96
|
|
95
97
|
if ::Rack.release > '2'
|
@@ -0,0 +1,134 @@
|
|
1
|
+
# frozen-string-literal: true
|
2
|
+
|
3
|
+
#
|
4
|
+
class Roda
|
5
|
+
module RodaPlugins
|
6
|
+
# The static_routing plugin adds static_* routing class methods for handling
|
7
|
+
# static routes (i.e. routes with static paths, no placeholders). These
|
8
|
+
# routes are processed before the normal routing tree and designed for
|
9
|
+
# maximum performance. This can be substantially faster than Roda's normal
|
10
|
+
# tree based routing if you have large numbers of static routes, about 3-4x
|
11
|
+
# for 100-10000 static routes. Example:
|
12
|
+
#
|
13
|
+
# plugin :static_routing
|
14
|
+
#
|
15
|
+
# static_route '/foo' do |r|
|
16
|
+
# @var = :foo
|
17
|
+
#
|
18
|
+
# r.get do
|
19
|
+
# 'Not actually reached'
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# r.post{'static POST /#{@var}'}
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# static_get '/foo' do |r|
|
26
|
+
# 'static GET /foo'
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# route do |r|
|
30
|
+
# 'Not a static route'
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# A few things to note in the above example. First, unlike most other
|
34
|
+
# routing methods in Roda, these take the full path of the request, and only
|
35
|
+
# match if r.path_info matches exactly. This is why you need to include the
|
36
|
+
# leading slash in the path argument.
|
37
|
+
#
|
38
|
+
# Second, the static_* routing methods only take a single string argument for
|
39
|
+
# the path, they do not acccept other options, and do not handle placeholders
|
40
|
+
# in strings. For any routes needing placeholders, you should use Roda's
|
41
|
+
# routing tree.
|
42
|
+
#
|
43
|
+
# There are separate static_* methods for each type of request method, and these
|
44
|
+
# request method specific routes are tried first. There is also a static_route
|
45
|
+
# method that will match regardless of the request method, if there is no
|
46
|
+
# matching request methods specific route. This is why the static_get
|
47
|
+
# method call takes precedence over the static_route method call for /foo.
|
48
|
+
# As shown above, you can use Roda's routing tree methods inside the
|
49
|
+
# static_route block to have shared behavior for different request methods,
|
50
|
+
# while still having handling the request methods differently.
|
51
|
+
module StaticRouting
|
52
|
+
def self.configure(app)
|
53
|
+
app.opts[:static_routes] = {}
|
54
|
+
end
|
55
|
+
|
56
|
+
module ClassMethods
|
57
|
+
# Freeze the static route metadata when freezing the app.
|
58
|
+
def freeze
|
59
|
+
super
|
60
|
+
opts[:static_routes].freeze
|
61
|
+
opts[:static_routes].each_value(&:freeze)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Duplicate static route metadata in subclass.
|
65
|
+
def inherited(subclass)
|
66
|
+
super
|
67
|
+
static_routes = subclass.opts[:static_routes]
|
68
|
+
opts[:static_routes].each do |k, v|
|
69
|
+
static_routes[k] = v.dup
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add a static route for any request method. These are
|
74
|
+
# tried after the request method specific static routes (e.g.
|
75
|
+
# static_get), but allow you to use Roda's routing tree
|
76
|
+
# methods inside the route for handling shared behavior while
|
77
|
+
# still allowing request method specific handling.
|
78
|
+
def static_route(path, &block)
|
79
|
+
add_static_route(nil, path, &block)
|
80
|
+
end
|
81
|
+
|
82
|
+
# Return the static route for the given request method and path.
|
83
|
+
def static_route_for(method, path)
|
84
|
+
if h = opts[:static_routes][path]
|
85
|
+
h[method] || h[nil]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
[:get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
|
90
|
+
request_method = meth.to_s.upcase
|
91
|
+
define_method("static_#{meth}") do |path, &block|
|
92
|
+
add_static_route(request_method, path, &block)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
# Add a static route for the given method.
|
99
|
+
def add_static_route(method, path, &block)
|
100
|
+
(opts[:static_routes][path] ||= {})[method] = block
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
module InstanceMethods
|
105
|
+
# If there is a static routing method for the given path, call it
|
106
|
+
# instead having the routing tree handle the request.
|
107
|
+
def call
|
108
|
+
r = @_request
|
109
|
+
path_info = r.env['PATH_INFO']
|
110
|
+
if route = self.class.static_route_for(r.request_method, r.path_info)
|
111
|
+
catch(:halt){r.static_route(&route)}
|
112
|
+
else
|
113
|
+
super
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
module RequestMethods
|
119
|
+
# Assume that this request matches a static route, setting
|
120
|
+
# the remaining path to the emptry string and passing
|
121
|
+
# control to the given block.
|
122
|
+
def static_route(&block)
|
123
|
+
@remaining_path = ''
|
124
|
+
|
125
|
+
always do
|
126
|
+
scope.instance_exec(self, &block)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
register_plugin(:static_routing, StaticRouting)
|
133
|
+
end
|
134
|
+
end
|
@@ -159,14 +159,14 @@ class Roda
|
|
159
159
|
@requested_type ||= opts[:default_type]
|
160
160
|
end
|
161
161
|
|
162
|
-
#
|
163
|
-
#
|
164
|
-
|
165
|
-
def run(_)
|
162
|
+
# Append the type routing extension back to the path if it was
|
163
|
+
# removed before routing.
|
164
|
+
def real_remaining_path
|
166
165
|
if defined?(@type_routing_extension)
|
167
|
-
|
166
|
+
"#{super}.#{@type_routing_extension}"
|
167
|
+
else
|
168
|
+
super
|
168
169
|
end
|
169
|
-
super
|
170
170
|
end
|
171
171
|
|
172
172
|
private
|
data/lib/roda/version.rb
CHANGED
data/spec/matchers_spec.rb
CHANGED
@@ -131,10 +131,15 @@ describe "matchers" do
|
|
131
131
|
r.on "posts/:id" do |id|
|
132
132
|
id
|
133
133
|
end
|
134
|
+
|
135
|
+
r.on "responses-:id" do |id|
|
136
|
+
id
|
137
|
+
end
|
134
138
|
end
|
135
139
|
|
136
140
|
body('/posts/123').must_equal '123'
|
137
141
|
status('/post/123').must_equal 404
|
142
|
+
body('/responses-123').must_equal '123'
|
138
143
|
end
|
139
144
|
|
140
145
|
it "should handle multiple params in single string" do
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "assets_preloading plugin" do
|
4
|
+
before do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :assets, {
|
7
|
+
:css => ['app.scss'],
|
8
|
+
:js => { :head => ['app.js'] },
|
9
|
+
:path => 'spec/assets',
|
10
|
+
:public => 'spec',
|
11
|
+
}
|
12
|
+
plugin :assets_preloading
|
13
|
+
|
14
|
+
route do |r|
|
15
|
+
r.is 'header-css' do
|
16
|
+
preload_assets_link_header :css
|
17
|
+
end
|
18
|
+
r.is 'header-js' do
|
19
|
+
preload_assets_link_header [:js, :head]
|
20
|
+
end
|
21
|
+
r.is 'header-multiple' do
|
22
|
+
preload_assets_link_header :css, [:js, :head]
|
23
|
+
end
|
24
|
+
|
25
|
+
r.is 'tags-css' do
|
26
|
+
preload_assets_link_tags :css
|
27
|
+
end
|
28
|
+
r.is 'tags-js' do
|
29
|
+
preload_assets_link_tags [:js, :head]
|
30
|
+
end
|
31
|
+
r.is 'tags-multiple' do
|
32
|
+
preload_assets_link_tags :css, [:js, :head]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it "preload_assets_link_header returns a well-formed header" do
|
39
|
+
html = body('/header-multiple')
|
40
|
+
|
41
|
+
assets = html.split(',')
|
42
|
+
assets.count.must_equal 2
|
43
|
+
|
44
|
+
assets.each do |asset|
|
45
|
+
parts = asset.split(';')
|
46
|
+
parts.count.must_equal 3
|
47
|
+
|
48
|
+
parts[0].scan(/^<.*>$/).count.must_equal 1
|
49
|
+
parts.select{|c| c == 'rel=preload' }.count.must_equal 1
|
50
|
+
parts.select{|c| c.match(/^as=\w+$/) }.count.must_equal 1
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
it "preload_assets_link_header returns the correct 'as' for an asset type" do
|
55
|
+
html = body('/header-css')
|
56
|
+
html.scan('as=style').count.must_equal 1
|
57
|
+
|
58
|
+
html = body('/header-js')
|
59
|
+
html.scan('as=script').count.must_equal 1
|
60
|
+
end
|
61
|
+
|
62
|
+
it "preload_assets_link_tags returns well-formed tags" do
|
63
|
+
html = body('/tags-multiple')
|
64
|
+
|
65
|
+
tags = html.scan(/<link.+?>/)
|
66
|
+
tags.count.must_equal 2
|
67
|
+
|
68
|
+
tags.each do |tag|
|
69
|
+
tag.scan(' rel="preload"').count.must_equal 1
|
70
|
+
tag.scan(/ href=".+"/).count.must_equal 1
|
71
|
+
tag.scan(/ as=".+"/).count.must_equal 1
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
it "preload_assets_link_tags returns the correct 'as' for an asset type" do
|
76
|
+
html = body('/tags-css')
|
77
|
+
html.scan('as="style"').count.must_equal 1
|
78
|
+
|
79
|
+
html = body('/tags-js')
|
80
|
+
html.scan('as="script"').count.must_equal 1
|
81
|
+
end
|
82
|
+
end
|
data/spec/plugin/assets_spec.rb
CHANGED
@@ -43,6 +43,15 @@ if run_tests
|
|
43
43
|
r.is 'test' do
|
44
44
|
"#{assets(:css)}\n#{assets([:js, :head])}"
|
45
45
|
end
|
46
|
+
|
47
|
+
r.is 'paths_test' do
|
48
|
+
css_paths = assets_paths(:css)
|
49
|
+
js_paths = assets_paths([:js, :head])
|
50
|
+
empty_paths = assets_paths(:empty)
|
51
|
+
{ 'css' => css_paths, 'js' => js_paths, 'empty' => empty_paths }.map do |k, a|
|
52
|
+
"#{k}:#{a.class}:#{a.length}:#{a.join(',')}"
|
53
|
+
end.join("\n")
|
54
|
+
end
|
46
55
|
end
|
47
56
|
end
|
48
57
|
end
|
@@ -139,6 +148,23 @@ if run_tests
|
|
139
148
|
app.assets_opts.values_at(*keys).must_equal [{'Content-Type'=>"C", 'A'=>'B1', 'C'=>'D', 'E'=>'G'}, {'Content-Type'=>"C", 'A'=>'B', 'E'=>'F1'}, {'a'=>'b', 'c'=>'d1'}]
|
140
149
|
end
|
141
150
|
|
151
|
+
it 'assets_paths should return arrays of source paths' do
|
152
|
+
html = body('/paths_test')
|
153
|
+
html.scan('css:Array:2:/assets/css/app.scss,/assets/css/raw.css').length.must_equal 1
|
154
|
+
html.scan('js:Array:1:/assets/js/head/app.js').length.must_equal 1
|
155
|
+
html.scan('empty:Array:0').length.must_equal 1
|
156
|
+
end
|
157
|
+
|
158
|
+
it 'assets_paths should return the compiled path in an array' do
|
159
|
+
app.compile_assets
|
160
|
+
html = body('/paths_test')
|
161
|
+
css_hash = app.assets_opts[:compiled]['css']
|
162
|
+
js_hash = app.assets_opts[:compiled]['js.head']
|
163
|
+
html.scan("css:Array:1:/assets/app.#{css_hash}.css").length.must_equal 1
|
164
|
+
html.scan("js:Array:1:/assets/app.head.#{js_hash}.js").length.must_equal 1
|
165
|
+
html.scan('empty:Array:0').length.must_equal 1
|
166
|
+
end
|
167
|
+
|
142
168
|
it 'should handle rendering assets, linking to them, and accepting requests for them when not compiling' do
|
143
169
|
html = body('/test')
|
144
170
|
html.scan(/<link/).length.must_equal 2
|
@@ -62,5 +62,8 @@ describe "not_allowed plugin" do
|
|
62
62
|
body('/c', 'REQUEST_METHOD'=>'PATCH').must_equal 'c'
|
63
63
|
status('/c', 'REQUEST_METHOD'=>'PATCH').must_equal 405
|
64
64
|
header('Allow', '/c', 'REQUEST_METHOD'=>'PATCH').must_equal 'GET, POST'
|
65
|
+
|
66
|
+
@app.plugin :head
|
67
|
+
header('Allow', '/c', 'REQUEST_METHOD'=>'PATCH').must_equal 'HEAD, GET, POST'
|
65
68
|
end
|
66
69
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "static_routing plugin" do
|
4
|
+
it "adds support for static routes that are taken before normal routes" do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :static_routing
|
7
|
+
static_route "/foo" do |r|
|
8
|
+
"#{r.path}:#{r.remaining_path}"
|
9
|
+
end
|
10
|
+
static_route "/bar" do |r|
|
11
|
+
r.get{"GET:#{r.path}:#{r.remaining_path}"}
|
12
|
+
r.post{"POST:#{r.path}:#{r.remaining_path}"}
|
13
|
+
end
|
14
|
+
static_get "/bar" do |r|
|
15
|
+
r.get{"GET2:#{r.path}:#{r.remaining_path}"}
|
16
|
+
end
|
17
|
+
static_route "/quux" do |r|
|
18
|
+
r.halt [500, {}, []]
|
19
|
+
end
|
20
|
+
|
21
|
+
route do |r|
|
22
|
+
r.on 'foo' do
|
23
|
+
r.get true do
|
24
|
+
'foo1'
|
25
|
+
end
|
26
|
+
r.root do
|
27
|
+
'foo2'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
r.get 'baz' do
|
32
|
+
'baz'
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
2.times do
|
38
|
+
body('/foo').must_equal '/foo:'
|
39
|
+
body('/foo/').must_equal 'foo2'
|
40
|
+
body('/bar').must_equal 'GET2:/bar:'
|
41
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').must_equal 'POST:/bar:'
|
42
|
+
status('/bar', 'REQUEST_METHOD'=>'PATCH').must_equal 404
|
43
|
+
body('/baz').must_equal 'baz'
|
44
|
+
status('/quux').must_equal 500
|
45
|
+
@app = Class.new(@app)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it "does not allow placeholders in static routes" do
|
50
|
+
app(:bare) do
|
51
|
+
plugin :static_routing
|
52
|
+
static_route "/:foo" do |r|
|
53
|
+
"#{r.path}:#{r.remaining_path}"
|
54
|
+
end
|
55
|
+
|
56
|
+
route{}
|
57
|
+
end
|
58
|
+
body('/:foo').must_equal '/:foo:'
|
59
|
+
status('/a').must_equal 404
|
60
|
+
end
|
61
|
+
|
62
|
+
it "duplicates data structures in subclasses" do
|
63
|
+
app(:bare) do
|
64
|
+
plugin :static_routing
|
65
|
+
static_route "/foo" do |r|
|
66
|
+
'foo'
|
67
|
+
end
|
68
|
+
|
69
|
+
route{}
|
70
|
+
end
|
71
|
+
|
72
|
+
old_app = @app
|
73
|
+
@app = Class.new(old_app)
|
74
|
+
old_app.static_route '/bar' do |r|
|
75
|
+
'bar1'
|
76
|
+
end
|
77
|
+
old_app.static_get '/foo' do |r|
|
78
|
+
'foop'
|
79
|
+
end
|
80
|
+
@app.static_route '/bar' do |r|
|
81
|
+
'bar2'
|
82
|
+
end
|
83
|
+
|
84
|
+
body('/foo').must_equal 'foo'
|
85
|
+
body('/bar').must_equal 'bar2'
|
86
|
+
body('/foo', 'REQUEST_METHOD'=>'POST').must_equal 'foo'
|
87
|
+
@app = old_app
|
88
|
+
body('/foo').must_equal 'foop'
|
89
|
+
body('/bar').must_equal 'bar1'
|
90
|
+
body('/foo', 'REQUEST_METHOD'=>'POST').must_equal 'foo'
|
91
|
+
end
|
92
|
+
|
93
|
+
it "freezes static routes when app is frozen" do
|
94
|
+
app(:bare) do
|
95
|
+
plugin :static_routing
|
96
|
+
static_route "/foo"
|
97
|
+
freeze
|
98
|
+
|
99
|
+
proc do
|
100
|
+
static_get "/foo"
|
101
|
+
end.must_raise
|
102
|
+
|
103
|
+
proc do
|
104
|
+
static_route "/bar"
|
105
|
+
end.must_raise
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -58,6 +58,7 @@ describe "type_routing plugin" do
|
|
58
58
|
r.run(sup_app)
|
59
59
|
end
|
60
60
|
|
61
|
+
body('/a', 'HTTP_ACCEPT' => 'text/html').must_equal 'HTML: html'
|
61
62
|
body('/a.json', 'HTTP_ACCEPT' => 'text/html').must_equal 'JSON: json'
|
62
63
|
body('/a.xml', 'HTTP_ACCEPT' => 'application/json').must_equal 'XML: xml'
|
63
64
|
body('/a.html', 'HTTP_ACCEPT' => 'application/xml').must_equal 'HTML: html'
|
@@ -264,4 +265,32 @@ describe "type_routing plugin" do
|
|
264
265
|
body('/a.html').must_equal 'HTML'
|
265
266
|
body('/a.json').must_equal 'JSON'
|
266
267
|
end
|
268
|
+
|
269
|
+
it "removes the handled part from r.remaining_path" do
|
270
|
+
app(:bare) do
|
271
|
+
plugin :type_routing
|
272
|
+
|
273
|
+
route do |r|
|
274
|
+
r.is 'a' do
|
275
|
+
r.html{ r.remaining_path }
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
body('/a.html').must_equal ''
|
281
|
+
end
|
282
|
+
|
283
|
+
it "overrides r.real_remaining_path correctly" do
|
284
|
+
app(:bare) do
|
285
|
+
plugin :type_routing
|
286
|
+
|
287
|
+
route do |r|
|
288
|
+
r.is 'a' do
|
289
|
+
r.html{ r.real_remaining_path }
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
body('/a.html').must_equal '.html'
|
295
|
+
end
|
267
296
|
end
|
data/spec/request_spec.rb
CHANGED
@@ -12,6 +12,18 @@ describe "request.path, .remaining_path, and .matched_path" do
|
|
12
12
|
end
|
13
13
|
end
|
14
14
|
|
15
|
+
describe "request.real_remaining_path" do
|
16
|
+
it "should be an alias of remaining_path" do
|
17
|
+
app do |r|
|
18
|
+
r.on "foo" do
|
19
|
+
"#{r.remaining_path}:#{r.real_remaining_path}"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
body("/foo/bar").must_equal "/bar:/bar"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
15
27
|
describe "request.halt" do
|
16
28
|
it "should return rack response as argument given it as argument" do
|
17
29
|
app do |r|
|
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: 2.
|
4
|
+
version: 2.18.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: 2016-
|
11
|
+
date: 2016-09-13 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rack
|
@@ -183,6 +183,7 @@ extra_rdoc_files:
|
|
183
183
|
- doc/release_notes/2.15.0.txt
|
184
184
|
- doc/release_notes/2.16.0.txt
|
185
185
|
- doc/release_notes/2.17.0.txt
|
186
|
+
- doc/release_notes/2.18.0.txt
|
186
187
|
files:
|
187
188
|
- CHANGELOG
|
188
189
|
- MIT-LICENSE
|
@@ -203,6 +204,7 @@ files:
|
|
203
204
|
- doc/release_notes/2.15.0.txt
|
204
205
|
- doc/release_notes/2.16.0.txt
|
205
206
|
- doc/release_notes/2.17.0.txt
|
207
|
+
- doc/release_notes/2.18.0.txt
|
206
208
|
- doc/release_notes/2.2.0.txt
|
207
209
|
- doc/release_notes/2.3.0.txt
|
208
210
|
- doc/release_notes/2.4.0.txt
|
@@ -216,6 +218,7 @@ files:
|
|
216
218
|
- lib/roda/plugins/_erubis_escaping.rb
|
217
219
|
- lib/roda/plugins/all_verbs.rb
|
218
220
|
- lib/roda/plugins/assets.rb
|
221
|
+
- lib/roda/plugins/assets_preloading.rb
|
219
222
|
- lib/roda/plugins/backtracking_array.rb
|
220
223
|
- lib/roda/plugins/caching.rb
|
221
224
|
- lib/roda/plugins/chunked.rb
|
@@ -277,6 +280,7 @@ files:
|
|
277
280
|
- lib/roda/plugins/slash_path_empty.rb
|
278
281
|
- lib/roda/plugins/static.rb
|
279
282
|
- lib/roda/plugins/static_path_info.rb
|
283
|
+
- lib/roda/plugins/static_routing.rb
|
280
284
|
- lib/roda/plugins/status_handler.rb
|
281
285
|
- lib/roda/plugins/streaming.rb
|
282
286
|
- lib/roda/plugins/symbol_matchers.rb
|
@@ -301,6 +305,7 @@ files:
|
|
301
305
|
- spec/opts_spec.rb
|
302
306
|
- spec/plugin/_erubis_escaping_spec.rb
|
303
307
|
- spec/plugin/all_verbs_spec.rb
|
308
|
+
- spec/plugin/assets_preloading_spec.rb
|
304
309
|
- spec/plugin/assets_spec.rb
|
305
310
|
- spec/plugin/backtracking_array_spec.rb
|
306
311
|
- spec/plugin/caching_spec.rb
|
@@ -361,6 +366,7 @@ files:
|
|
361
366
|
- spec/plugin/shared_vars_spec.rb
|
362
367
|
- spec/plugin/sinatra_helpers_spec.rb
|
363
368
|
- spec/plugin/slash_path_empty_spec.rb
|
369
|
+
- spec/plugin/static_routing_spec.rb
|
364
370
|
- spec/plugin/static_spec.rb
|
365
371
|
- spec/plugin/status_handler_spec.rb
|
366
372
|
- spec/plugin/streaming_spec.rb
|