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