roda 1.1.0 → 1.2.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 +70 -0
- data/README.rdoc +261 -302
- data/Rakefile +1 -1
- data/doc/release_notes/1.2.0.txt +406 -0
- data/lib/roda.rb +206 -124
- data/lib/roda/plugins/all_verbs.rb +11 -10
- data/lib/roda/plugins/assets.rb +5 -5
- data/lib/roda/plugins/backtracking_array.rb +12 -5
- data/lib/roda/plugins/caching.rb +10 -8
- data/lib/roda/plugins/class_level_routing.rb +94 -0
- data/lib/roda/plugins/content_for.rb +6 -0
- data/lib/roda/plugins/default_headers.rb +4 -11
- data/lib/roda/plugins/delay_build.rb +42 -0
- data/lib/roda/plugins/delegate.rb +64 -0
- data/lib/roda/plugins/drop_body.rb +33 -0
- data/lib/roda/plugins/empty_root.rb +48 -0
- data/lib/roda/plugins/environments.rb +68 -0
- data/lib/roda/plugins/error_email.rb +1 -2
- data/lib/roda/plugins/error_handler.rb +1 -1
- data/lib/roda/plugins/halt.rb +7 -5
- data/lib/roda/plugins/head.rb +4 -2
- data/lib/roda/plugins/header_matchers.rb +17 -9
- data/lib/roda/plugins/hooks.rb +16 -32
- data/lib/roda/plugins/json.rb +4 -10
- data/lib/roda/plugins/mailer.rb +233 -0
- data/lib/roda/plugins/match_affix.rb +48 -0
- data/lib/roda/plugins/multi_route.rb +9 -11
- data/lib/roda/plugins/multi_run.rb +81 -0
- data/lib/roda/plugins/named_templates.rb +93 -0
- data/lib/roda/plugins/not_allowed.rb +43 -48
- data/lib/roda/plugins/path.rb +63 -2
- data/lib/roda/plugins/render.rb +79 -48
- data/lib/roda/plugins/render_each.rb +6 -0
- data/lib/roda/plugins/sinatra_helpers.rb +523 -0
- data/lib/roda/plugins/slash_path_empty.rb +25 -0
- data/lib/roda/plugins/static_path_info.rb +64 -0
- data/lib/roda/plugins/streaming.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +12 -8
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +33 -0
- data/spec/plugin/backtracking_array_spec.rb +24 -18
- data/spec/plugin/class_level_routing_spec.rb +138 -0
- data/spec/plugin/delay_build_spec.rb +23 -0
- data/spec/plugin/delegate_spec.rb +20 -0
- data/spec/plugin/drop_body_spec.rb +20 -0
- data/spec/plugin/empty_root_spec.rb +14 -0
- data/spec/plugin/environments_spec.rb +31 -0
- data/spec/plugin/h_spec.rb +1 -3
- data/spec/plugin/header_matchers_spec.rb +14 -0
- data/spec/plugin/hooks_spec.rb +3 -5
- data/spec/plugin/mailer_spec.rb +191 -0
- data/spec/plugin/match_affix_spec.rb +22 -0
- data/spec/plugin/multi_run_spec.rb +31 -0
- data/spec/plugin/named_templates_spec.rb +65 -0
- data/spec/plugin/path_spec.rb +66 -2
- data/spec/plugin/render_spec.rb +46 -1
- data/spec/plugin/sinatra_helpers_spec.rb +534 -0
- data/spec/plugin/slash_path_empty_spec.rb +22 -0
- data/spec/plugin/static_path_info_spec.rb +50 -0
- data/spec/request_spec.rb +23 -0
- data/spec/response_spec.rb +12 -1
- metadata +48 -6
@@ -0,0 +1,25 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The slash_path_empty plugin considers "/" as an empty path,
|
4
|
+
# in addition to the default of "" being considered an empty
|
5
|
+
# path. This makes it so +r.is+ without an argument will match
|
6
|
+
# a path of "/", and +r.is+ and verb methods such as +r.get+ and
|
7
|
+
# +r.post+ will match if the path is "/" after the arguments
|
8
|
+
# are processed. This can make it easier to handle applications
|
9
|
+
# where a trailing "/" in the path should be ignored.
|
10
|
+
module SlashPathEmpty
|
11
|
+
SLASH = "/".freeze
|
12
|
+
|
13
|
+
module RequestMethods
|
14
|
+
private
|
15
|
+
|
16
|
+
# Consider the path empty if it is "/".
|
17
|
+
def empty_path?
|
18
|
+
super || remaining_path == SLASH
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
register_plugin(:slash_path_empty, SlashPathEmpty)
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The static_path_info plugin changes Roda's behavior so that the
|
4
|
+
# SCRIPT_NAME/PATH_INFO environment settings are not modified
|
5
|
+
# while the request is beind routed, improving performance. If
|
6
|
+
# you have any helpers that operate on PATH_INFO or SCRIPT_NAME,
|
7
|
+
# their behavior will not change depending on where they are
|
8
|
+
# called in the routing tree.
|
9
|
+
#
|
10
|
+
# This still updates SCRIPT_NAME/PATH_INFO before dispatching to
|
11
|
+
# another rack app via +r.run+.
|
12
|
+
module StaticPathInfo
|
13
|
+
module RequestMethods
|
14
|
+
PATH_INFO = "PATH_INFO".freeze
|
15
|
+
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
16
|
+
|
17
|
+
# The current path to match requests against. This is initialized
|
18
|
+
# to PATH_INFO when the request is created.
|
19
|
+
attr_reader :remaining_path
|
20
|
+
|
21
|
+
# Set remaining_path when initializing.
|
22
|
+
def initialize(*)
|
23
|
+
super
|
24
|
+
@remaining_path = @env[PATH_INFO]
|
25
|
+
end
|
26
|
+
|
27
|
+
# The already matched part of the path, including the original SCRIPT_NAME.
|
28
|
+
def matched_path
|
29
|
+
e = @env
|
30
|
+
e[SCRIPT_NAME] + e[PATH_INFO].chomp(@remaining_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Update SCRIPT_NAME/PATH_INFO based on the current remaining_path
|
34
|
+
# before dispatching to another rack app, so the app still works as
|
35
|
+
# a URL mapper.
|
36
|
+
def run(_)
|
37
|
+
e = @env
|
38
|
+
path = @remaining_path
|
39
|
+
e[SCRIPT_NAME] += e[PATH_INFO].chomp(path)
|
40
|
+
e[PATH_INFO] = path
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Update remaining_path with the remaining characters
|
47
|
+
def update_remaining_path(remaining)
|
48
|
+
@remaining_path = remaining
|
49
|
+
end
|
50
|
+
|
51
|
+
# Yield to the block, restoring the remaining_path before
|
52
|
+
# the method returns.
|
53
|
+
def keep_remaining_path
|
54
|
+
path = @remaining_path
|
55
|
+
yield
|
56
|
+
ensure
|
57
|
+
@remaining_path = path
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
register_plugin(:static_path_info, StaticPathInfo)
|
63
|
+
end
|
64
|
+
end
|
@@ -23,15 +23,18 @@ class Roda
|
|
23
23
|
# end
|
24
24
|
# end
|
25
25
|
#
|
26
|
-
# This plugin should be loaded after the render plugin, since
|
27
|
-
# it works by overriding parts of the render plugin.
|
28
|
-
#
|
29
26
|
# Note that when a view subdirectory is set, the layout will
|
30
27
|
# also be looked up in the subdirectory unless it contains
|
31
28
|
# a slash. So if you want to use a view subdirectory for
|
32
29
|
# templates but have a shared layout, you should make sure your
|
33
30
|
# layout contains a slash, similar to the example above.
|
34
31
|
module ViewSubdirs
|
32
|
+
# Load the render plugin before this plugin, since this plugin
|
33
|
+
# works by overriding a method in the render plugin.
|
34
|
+
def self.load_dependencies(app)
|
35
|
+
app.plugin :render
|
36
|
+
end
|
37
|
+
|
35
38
|
module InstanceMethods
|
36
39
|
# Set the view subdirectory to use. This can be set to nil
|
37
40
|
# to not use a view subdirectory.
|
@@ -44,12 +47,13 @@ class Roda
|
|
44
47
|
# Override the template name to use the view subdirectory if the
|
45
48
|
# there is a view subdirectory and the template name does not
|
46
49
|
# contain a slash.
|
47
|
-
def
|
48
|
-
|
49
|
-
if (v = @_view_subdir) &&
|
50
|
-
|
50
|
+
def template_name(opts)
|
51
|
+
name = super
|
52
|
+
if (v = @_view_subdir) && name !~ /\//
|
53
|
+
"#{v}/#{name}"
|
54
|
+
else
|
55
|
+
name
|
51
56
|
end
|
52
|
-
super
|
53
57
|
end
|
54
58
|
end
|
55
59
|
end
|
data/lib/roda/version.rb
CHANGED
data/spec/integration_spec.rb
CHANGED
@@ -35,6 +35,25 @@ describe "integration" do
|
|
35
35
|
body('/hello').should == 'D First Second Block'
|
36
36
|
end
|
37
37
|
|
38
|
+
it "should clear middleware when clear_middleware! is called" do
|
39
|
+
c = @c
|
40
|
+
app(:bare) do
|
41
|
+
use c, "First", "Second" do
|
42
|
+
"Block"
|
43
|
+
end
|
44
|
+
|
45
|
+
route do |r|
|
46
|
+
r.get "hello" do
|
47
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
clear_middleware!
|
52
|
+
end
|
53
|
+
|
54
|
+
body('/hello').should == 'D '
|
55
|
+
end
|
56
|
+
|
38
57
|
it "should support adding middleware using use after route block setup" do
|
39
58
|
c = @c
|
40
59
|
app(:bare) do
|
@@ -64,6 +83,20 @@ describe "integration" do
|
|
64
83
|
body('/hello').should == 'D 1 2 3'
|
65
84
|
end
|
66
85
|
|
86
|
+
it "should not inherit middleware in subclass if inhert_middleware = false" do
|
87
|
+
c = @c
|
88
|
+
app(:bare){use(c, '1', '2'){"3"}}
|
89
|
+
@app.inherit_middleware = false
|
90
|
+
@app = Class.new(@app)
|
91
|
+
@app.route do |r|
|
92
|
+
r.get "hello" do
|
93
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
body('/hello').should == 'D '
|
98
|
+
end
|
99
|
+
|
67
100
|
it "should inherit route in subclass" do
|
68
101
|
c = @c
|
69
102
|
app(:bare) do
|
@@ -16,23 +16,29 @@ describe "backtracking_array plugin" do
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
19
|
+
tests = lambda do
|
20
|
+
status.should == 404
|
21
|
+
|
22
|
+
body("/a").should == 'a'
|
23
|
+
body("/a/b").should == 'a/b'
|
24
|
+
status("/a/b/").should == 404
|
25
|
+
|
26
|
+
body("/c/d").should == 'c-d'
|
27
|
+
body("/c/e").should == 'c-e'
|
28
|
+
body("/c/d/d").should == 'c/d-d'
|
29
|
+
body("/c/d/e").should == 'c/d-e'
|
30
|
+
status("/c/d/").should == 404
|
31
|
+
|
32
|
+
body("/f").should == 'f'
|
33
|
+
body("/f/g").should == 'f/g'
|
34
|
+
body("/g").should == 'g'
|
35
|
+
body("/g/h").should == 'g/h'
|
36
|
+
status("/f/g/").should == 404
|
37
|
+
status("/g/h/").should == 404
|
38
|
+
end
|
39
|
+
|
40
|
+
tests.call
|
41
|
+
app.plugin(:static_path_info)
|
42
|
+
tests.call
|
37
43
|
end
|
38
44
|
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "class_level_routing plugin" do
|
4
|
+
before do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :class_level_routing
|
7
|
+
plugin :all_verbs
|
8
|
+
|
9
|
+
root do
|
10
|
+
'root'
|
11
|
+
end
|
12
|
+
|
13
|
+
on "foo" do
|
14
|
+
request.get "bar" do
|
15
|
+
"foobar"
|
16
|
+
end
|
17
|
+
|
18
|
+
"foo"
|
19
|
+
end
|
20
|
+
|
21
|
+
is "d:d" do |x|
|
22
|
+
request.get do
|
23
|
+
"bazget#{x}"
|
24
|
+
end
|
25
|
+
|
26
|
+
request.post do
|
27
|
+
"bazpost#{x}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
meths = %w'get post delete head options patch put trace'
|
32
|
+
meths.concat(%w'link unlink') if ::Rack::Request.method_defined?("link?")
|
33
|
+
meths.each do |meth|
|
34
|
+
send(meth, :d) do |m|
|
35
|
+
"x-#{meth}-#{m}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
it "adds class methods for setting up routes" do
|
42
|
+
body.should == 'root'
|
43
|
+
body('/foo').should == 'foo'
|
44
|
+
body('/foo/bar').should == 'foobar'
|
45
|
+
body('/dgo').should == 'bazgetgo'
|
46
|
+
body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
|
47
|
+
body('/bar').should == "x-get-bar"
|
48
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
|
49
|
+
body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
|
50
|
+
body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
|
51
|
+
body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
|
52
|
+
body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
|
53
|
+
body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
|
54
|
+
body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
|
55
|
+
if ::Rack::Request.method_defined?("link?")
|
56
|
+
body('/bar', 'REQUEST_METHOD'=>'LINK').should == "x-link-bar"
|
57
|
+
body('/bar', 'REQUEST_METHOD'=>'UNLINK').should == "x-unlink-bar"
|
58
|
+
end
|
59
|
+
|
60
|
+
status.should == 200
|
61
|
+
status("/asdfa/asdf").should == 404
|
62
|
+
|
63
|
+
@app = Class.new(app)
|
64
|
+
body.should == 'root'
|
65
|
+
body('/foo').should == 'foo'
|
66
|
+
body('/foo/bar').should == 'foobar'
|
67
|
+
body('/dgo').should == 'bazgetgo'
|
68
|
+
body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
|
69
|
+
body('/bar').should == "x-get-bar"
|
70
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
|
71
|
+
body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
|
72
|
+
body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
|
73
|
+
body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
|
74
|
+
body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
|
75
|
+
body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
|
76
|
+
body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
|
77
|
+
end
|
78
|
+
|
79
|
+
it "only calls class level routes if routing tree doesn't handle request" do
|
80
|
+
app.route do |r|
|
81
|
+
r.root do
|
82
|
+
'iroot'
|
83
|
+
end
|
84
|
+
|
85
|
+
r.get 'foo' do
|
86
|
+
'ifoo'
|
87
|
+
end
|
88
|
+
|
89
|
+
r.on 'bar' do
|
90
|
+
r.get true do
|
91
|
+
response.status = 404
|
92
|
+
''
|
93
|
+
end
|
94
|
+
r.post true do
|
95
|
+
'ibar'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
body.should == 'iroot'
|
101
|
+
body('/foo').should == 'ifoo'
|
102
|
+
body('/foo/bar').should == 'foobar'
|
103
|
+
body('/dgo').should == 'bazgetgo'
|
104
|
+
body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
|
105
|
+
body('/bar').should == ""
|
106
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').should == "ibar"
|
107
|
+
body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
|
108
|
+
body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
|
109
|
+
body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
|
110
|
+
body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
|
111
|
+
body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
|
112
|
+
body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
|
113
|
+
end
|
114
|
+
|
115
|
+
it "works with the not_found plugin if loaded before" do
|
116
|
+
app.plugin :not_found do
|
117
|
+
"nf"
|
118
|
+
end
|
119
|
+
|
120
|
+
body.should == 'root'
|
121
|
+
body('/foo').should == 'foo'
|
122
|
+
body('/foo/bar').should == 'foobar'
|
123
|
+
body('/dgo').should == 'bazgetgo'
|
124
|
+
body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
|
125
|
+
body('/bar').should == "x-get-bar"
|
126
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
|
127
|
+
body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
|
128
|
+
body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
|
129
|
+
body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
|
130
|
+
body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
|
131
|
+
body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
|
132
|
+
body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
|
133
|
+
|
134
|
+
status.should == 200
|
135
|
+
status("/asdfa/asdf").should == 404
|
136
|
+
body("/asdfa/asdf").should == "nf"
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "delay_build plugin" do
|
4
|
+
it "does not build rack app until app is called" do
|
5
|
+
app(:delay_build){"a"}
|
6
|
+
app.instance_variable_get(:@app).should == nil
|
7
|
+
body.should == "a"
|
8
|
+
app.instance_variable_get(:@app).should_not == nil
|
9
|
+
end
|
10
|
+
|
11
|
+
it "only rebuilds the app if build! is called" do
|
12
|
+
app(:delay_build){"a"}
|
13
|
+
body.should == "a"
|
14
|
+
c = Class.new do
|
15
|
+
def initialize(_) end
|
16
|
+
def call(_) [200, {}, ["b"]] end
|
17
|
+
end
|
18
|
+
app.use c
|
19
|
+
body.should == "a"
|
20
|
+
app.build!
|
21
|
+
body.should == "b"
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "delegate plugin" do
|
4
|
+
it "adds request_delegate and response_delegate class methods for delegating" do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :delegate
|
7
|
+
request_delegate :root
|
8
|
+
response_delegate :headers
|
9
|
+
|
10
|
+
route do
|
11
|
+
root do
|
12
|
+
headers['Content-Type'] = 'foo'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
header('Content-Type').should == 'foo'
|
18
|
+
status('/foo').should == 404
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "drop_body plugin" do
|
4
|
+
it "automatically drops body and Content-Type/Content-Length headers for responses without a body" do
|
5
|
+
app(:drop_body) do |r|
|
6
|
+
response.status = r.path.to_i
|
7
|
+
response.write('a')
|
8
|
+
end
|
9
|
+
|
10
|
+
[101, 102, 204, 205, 304].each do |i|
|
11
|
+
body(i.to_s).should == ''
|
12
|
+
header('Content-Type', i.to_s).should == nil
|
13
|
+
header('Content-Length', i.to_s).should == nil
|
14
|
+
end
|
15
|
+
|
16
|
+
body('200').should == 'a'
|
17
|
+
header('Content-Type', '200').should == 'text/html'
|
18
|
+
header('Content-Length', '200').should == '1'
|
19
|
+
end
|
20
|
+
end
|