roda 1.0.0 → 1.1.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 +34 -0
- data/README.rdoc +18 -13
- data/Rakefile +8 -0
- data/doc/conventions.rdoc +163 -0
- data/doc/release_notes/1.1.0.txt +226 -0
- data/lib/roda.rb +51 -22
- data/lib/roda/plugins/assets.rb +613 -0
- data/lib/roda/plugins/caching.rb +215 -0
- data/lib/roda/plugins/chunked.rb +278 -0
- data/lib/roda/plugins/error_email.rb +112 -0
- data/lib/roda/plugins/flash.rb +3 -3
- data/lib/roda/plugins/hooks.rb +1 -1
- data/lib/roda/plugins/indifferent_params.rb +3 -3
- data/lib/roda/plugins/middleware.rb +3 -8
- data/lib/roda/plugins/multi_route.rb +110 -18
- data/lib/roda/plugins/not_allowed.rb +3 -3
- data/lib/roda/plugins/path.rb +38 -0
- data/lib/roda/plugins/render.rb +18 -16
- data/lib/roda/plugins/render_each.rb +0 -2
- data/lib/roda/plugins/streaming.rb +1 -2
- data/lib/roda/plugins/view_subdirs.rb +7 -1
- data/lib/roda/version.rb +1 -1
- data/spec/assets/css/app.scss +1 -0
- data/spec/assets/css/no_access.css +1 -0
- data/spec/assets/css/raw.css +1 -0
- data/spec/assets/js/head/app.js +1 -0
- data/spec/integration_spec.rb +95 -3
- data/spec/matchers_spec.rb +2 -2
- data/spec/plugin/assets_spec.rb +413 -0
- data/spec/plugin/caching_spec.rb +335 -0
- data/spec/plugin/chunked_spec.rb +182 -0
- data/spec/plugin/default_headers_spec.rb +6 -5
- data/spec/plugin/error_email_spec.rb +76 -0
- data/spec/plugin/multi_route_spec.rb +120 -0
- data/spec/plugin/not_allowed_spec.rb +14 -3
- data/spec/plugin/path_spec.rb +29 -0
- data/spec/plugin/render_each_spec.rb +6 -1
- data/spec/plugin/symbol_matchers_spec.rb +7 -2
- data/spec/request_spec.rb +10 -0
- data/spec/response_spec.rb +47 -0
- data/spec/views/about.erb +1 -0
- data/spec/views/about.str +1 -0
- data/spec/views/content-yield.erb +1 -0
- data/spec/views/home.erb +2 -0
- data/spec/views/home.str +2 -0
- data/spec/views/layout-alternative.erb +2 -0
- data/spec/views/layout-yield.erb +3 -0
- data/spec/views/layout.erb +2 -0
- data/spec/views/layout.str +2 -0
- metadata +57 -2
@@ -7,6 +7,7 @@ describe "default_headers plugin" do
|
|
7
7
|
app(:bare) do
|
8
8
|
plugin :default_headers, h
|
9
9
|
route do |r|
|
10
|
+
r.halt response.finish_with_body([])
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
@@ -22,6 +23,7 @@ describe "default_headers plugin" do
|
|
22
23
|
plugin :default_headers
|
23
24
|
|
24
25
|
route do |r|
|
26
|
+
r.halt response.finish_with_body([])
|
25
27
|
end
|
26
28
|
end
|
27
29
|
|
@@ -29,18 +31,17 @@ describe "default_headers plugin" do
|
|
29
31
|
end
|
30
32
|
|
31
33
|
it "should allow modifying the default headers at a later point" do
|
32
|
-
h = {'Content-Type'=>'text/json', 'Foo'=>'bar'}
|
33
|
-
|
34
34
|
app(:bare) do
|
35
35
|
plugin :default_headers
|
36
36
|
default_headers['Content-Type'] = 'text/json'
|
37
|
-
default_headers['Foo'] = '
|
37
|
+
default_headers['Foo'] = 'baz'
|
38
38
|
|
39
39
|
route do |r|
|
40
|
+
r.halt response.finish_with_body([])
|
40
41
|
end
|
41
42
|
end
|
42
43
|
|
43
|
-
req[1].should ==
|
44
|
+
req[1].should == {'Content-Type'=>'text/json', 'Foo'=>'baz'}
|
44
45
|
end
|
45
46
|
|
46
47
|
it "should work correctly in subclasses" do
|
@@ -52,11 +53,11 @@ describe "default_headers plugin" do
|
|
52
53
|
default_headers['Foo'] = 'bar'
|
53
54
|
|
54
55
|
route do |r|
|
56
|
+
r.halt response.finish_with_body([])
|
55
57
|
end
|
56
58
|
end
|
57
59
|
|
58
60
|
@app = Class.new(@app)
|
59
|
-
@app.route{}
|
60
61
|
|
61
62
|
req[1].should == h
|
62
63
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "error_email plugin" do
|
4
|
+
def app(opts={})
|
5
|
+
@emails = emails = [] unless defined?(@emails)
|
6
|
+
@app ||= super(:bare) do
|
7
|
+
plugin :error_email, {:to=>'t', :from=>'f', :emailer=>lambda{|h| emails << h}}.merge(opts)
|
8
|
+
|
9
|
+
route do |r|
|
10
|
+
raise ArgumentError rescue error_email($!)
|
11
|
+
'e'
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def email
|
17
|
+
@emails.last
|
18
|
+
end
|
19
|
+
|
20
|
+
it "adds error_email method for emailing exceptions" do
|
21
|
+
app
|
22
|
+
body('rack.input'=>StringIO.new, 'QUERY_STRING'=>'b=c', 'rack.session'=>{'d'=>'e'}).should == 'e'
|
23
|
+
email[:to].should == 't'
|
24
|
+
email[:from].should == 'f'
|
25
|
+
email[:host].should == 'localhost'
|
26
|
+
email[:message].should =~ /^Subject: ArgumentError/
|
27
|
+
email[:message].should =~ /^Backtrace:$.+^ENV:$.+^"rack\.input" => .+^Params:$\s+^"b" => "c"$\s+^Session:$\s+^"d" => "e"$/m
|
28
|
+
end
|
29
|
+
|
30
|
+
it "uses :host option" do
|
31
|
+
app(:host=>'foo.bar.com')
|
32
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
33
|
+
email[:host].should == 'foo.bar.com'
|
34
|
+
end
|
35
|
+
|
36
|
+
it "handles error messages with new lines" do
|
37
|
+
app.route do |r|
|
38
|
+
raise "foo\nbar\nbaz" rescue error_email($!)
|
39
|
+
'e'
|
40
|
+
end
|
41
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
42
|
+
email[:message].should =~ %r{From: f\r\nSubject: RuntimeError: foo\r\n bar\r\n baz\r\nTo: t\r\n\r\n}
|
43
|
+
end
|
44
|
+
|
45
|
+
it "adds :prefix option to subject line" do
|
46
|
+
app(:prefix=>'TEST ')
|
47
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
48
|
+
email[:message].should =~ /^Subject: TEST ArgumentError/
|
49
|
+
end
|
50
|
+
|
51
|
+
it "uses :headers option for additional headers" do
|
52
|
+
app(:headers=>{'Foo'=>'Bar', 'Baz'=>'Quux'})
|
53
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
54
|
+
email[:message].should =~ /^Foo: Bar/
|
55
|
+
email[:message].should =~ /^Baz: Quux/
|
56
|
+
end
|
57
|
+
|
58
|
+
it "requires the :to and :from options" do
|
59
|
+
proc{app :from=>nil}.should raise_error(Roda::RodaError)
|
60
|
+
proc{app :to=>nil}.should raise_error(Roda::RodaError)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "works correctly in subclasses" do
|
64
|
+
@app = Class.new(app)
|
65
|
+
@app.route do |r|
|
66
|
+
raise ArgumentError rescue error_email($!)
|
67
|
+
'e'
|
68
|
+
end
|
69
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
70
|
+
email[:to].should == 't'
|
71
|
+
email[:from].should == 'f'
|
72
|
+
email[:host].should == 'localhost'
|
73
|
+
email[:message].should =~ /^Subject: ArgumentError/
|
74
|
+
email[:message].should =~ /Backtrace.*ENV/m
|
75
|
+
end
|
76
|
+
end
|
@@ -13,6 +13,8 @@ describe "multi_route plugin" do
|
|
13
13
|
r.is "a" do
|
14
14
|
"geta"
|
15
15
|
end
|
16
|
+
|
17
|
+
"getd"
|
16
18
|
end
|
17
19
|
|
18
20
|
route("post") do |r|
|
@@ -23,6 +25,14 @@ describe "multi_route plugin" do
|
|
23
25
|
r.is "a" do
|
24
26
|
"posta"
|
25
27
|
end
|
28
|
+
|
29
|
+
"postd"
|
30
|
+
end
|
31
|
+
|
32
|
+
route(:p) do |r|
|
33
|
+
r.is do
|
34
|
+
'p'
|
35
|
+
end
|
26
36
|
end
|
27
37
|
|
28
38
|
route do |r|
|
@@ -30,6 +40,10 @@ describe "multi_route plugin" do
|
|
30
40
|
r.multi_route do
|
31
41
|
"foo"
|
32
42
|
end
|
43
|
+
|
44
|
+
r.on "p" do
|
45
|
+
r.route(:p)
|
46
|
+
end
|
33
47
|
end
|
34
48
|
|
35
49
|
r.get do
|
@@ -71,6 +85,29 @@ describe "multi_route plugin" do
|
|
71
85
|
body('/foo/post/b').should == 'foo'
|
72
86
|
end
|
73
87
|
|
88
|
+
it "does not have multi_route match non-String named routes" do
|
89
|
+
body('/foo/p').should == 'p'
|
90
|
+
status('/foo/p/2').should == 404
|
91
|
+
end
|
92
|
+
|
93
|
+
it "has multi_route pick up routes newly added" do
|
94
|
+
body('/foo/get/').should == 'get'
|
95
|
+
status('/foo/delete').should == 404
|
96
|
+
app.route('delete'){|r| r.on{'delete'}}
|
97
|
+
body('/foo/delete').should == 'delete'
|
98
|
+
end
|
99
|
+
|
100
|
+
it "makes multi_route match longest route if multiple routes have the same prefix" do
|
101
|
+
app.route("post/a"){|r| r.on{"pa2"}}
|
102
|
+
app.route("get/a"){|r| r.on{"ga2"}}
|
103
|
+
status('/foo').should == 404
|
104
|
+
body('/foo/get/').should == 'get'
|
105
|
+
body('/foo/get/a').should == 'ga2'
|
106
|
+
body('/foo/post/').should == 'post'
|
107
|
+
body('/foo/post/a').should == 'pa2'
|
108
|
+
body('/foo/post/b').should == 'foo'
|
109
|
+
end
|
110
|
+
|
74
111
|
it "handles loading the plugin multiple times correctly" do
|
75
112
|
app.plugin :multi_route
|
76
113
|
body.should == 'get'
|
@@ -111,4 +148,87 @@ describe "multi_route plugin" do
|
|
111
148
|
status('/c').should == 404
|
112
149
|
status('/c', 'REQUEST_METHOD'=>'POST').should == 404
|
113
150
|
end
|
151
|
+
|
152
|
+
it "uses the named route return value in multi_route if no block is given" do
|
153
|
+
app.route{|r| r.multi_route}
|
154
|
+
body('/get').should == 'getd'
|
155
|
+
body('/post').should == 'postd'
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
describe "multi_route plugin" do
|
160
|
+
before do
|
161
|
+
app(:bare) do
|
162
|
+
plugin :multi_route
|
163
|
+
|
164
|
+
route("foo", "foo") do |r|
|
165
|
+
"#{@p}ff"
|
166
|
+
end
|
167
|
+
|
168
|
+
route("bar", "foo") do |r|
|
169
|
+
"#{@p}fb"
|
170
|
+
end
|
171
|
+
|
172
|
+
route("foo", "bar") do |r|
|
173
|
+
"#{@p}bf"
|
174
|
+
end
|
175
|
+
|
176
|
+
route("bar", "bar") do |r|
|
177
|
+
"#{@p}bb"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
it "handles namespaces in r.route" do
|
183
|
+
app.route("foo") do |r|
|
184
|
+
@p = 'f'
|
185
|
+
r.on("foo"){r.route("foo", "foo")}
|
186
|
+
r.on("bar"){r.route("bar", "foo")}
|
187
|
+
@p
|
188
|
+
end
|
189
|
+
|
190
|
+
app.route("bar") do |r|
|
191
|
+
@p = 'b'
|
192
|
+
r.on("foo"){r.route("foo", "bar")}
|
193
|
+
r.on("bar"){r.route("bar", "bar")}
|
194
|
+
@p
|
195
|
+
end
|
196
|
+
|
197
|
+
app.route do |r|
|
198
|
+
r.on("foo"){r.route("foo")}
|
199
|
+
r.on("bar"){r.route("bar")}
|
200
|
+
end
|
201
|
+
|
202
|
+
body('/foo').should == 'f'
|
203
|
+
body('/foo/foo').should == 'fff'
|
204
|
+
body('/foo/bar').should == 'ffb'
|
205
|
+
body('/bar').should == 'b'
|
206
|
+
body('/bar/foo').should == 'bbf'
|
207
|
+
body('/bar/bar').should == 'bbb'
|
208
|
+
end
|
209
|
+
|
210
|
+
it "handles namespaces in r.multi_route" do
|
211
|
+
app.route("foo") do |r|
|
212
|
+
@p = 'f'
|
213
|
+
r.multi_route("foo")
|
214
|
+
@p
|
215
|
+
end
|
216
|
+
|
217
|
+
app.route("bar") do |r|
|
218
|
+
@p = 'b'
|
219
|
+
r.multi_route("bar")
|
220
|
+
@p
|
221
|
+
end
|
222
|
+
|
223
|
+
app.route do |r|
|
224
|
+
r.multi_route
|
225
|
+
end
|
226
|
+
|
227
|
+
body('/foo').should == 'f'
|
228
|
+
body('/foo/foo').should == 'fff'
|
229
|
+
body('/foo/bar').should == 'ffb'
|
230
|
+
body('/bar').should == 'b'
|
231
|
+
body('/bar/foo').should == 'bbf'
|
232
|
+
body('/bar/bar').should == 'bbb'
|
233
|
+
end
|
114
234
|
end
|
@@ -19,14 +19,22 @@ describe "not_allowed plugin" do
|
|
19
19
|
"c"
|
20
20
|
end
|
21
21
|
|
22
|
+
r.on "q" do
|
23
|
+
r.is do
|
24
|
+
r.get do
|
25
|
+
"q"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
22
30
|
r.get do
|
23
31
|
r.is 'b' do
|
24
32
|
'b'
|
25
33
|
end
|
26
|
-
r.is
|
34
|
+
r.is(/(d)/) do |s|
|
27
35
|
s
|
28
36
|
end
|
29
|
-
r.get
|
37
|
+
r.get(/(e)/) do |s|
|
30
38
|
s
|
31
39
|
end
|
32
40
|
end
|
@@ -43,7 +51,10 @@ describe "not_allowed plugin" do
|
|
43
51
|
status('/d', 'REQUEST_METHOD'=>'POST').should == 404
|
44
52
|
|
45
53
|
body('/e').should == 'e'
|
46
|
-
status('/
|
54
|
+
status('/e', 'REQUEST_METHOD'=>'POST').should == 404
|
55
|
+
|
56
|
+
body('/q').should == 'q'
|
57
|
+
status('/q', 'REQUEST_METHOD'=>'POST').should == 405
|
47
58
|
|
48
59
|
body('/c').should == 'cg'
|
49
60
|
body('/c').should == 'cg'
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "path plugin" do
|
4
|
+
it "adds path method for defining named paths" do
|
5
|
+
app(:bare) do
|
6
|
+
plugin :path
|
7
|
+
path :foo, "/foo"
|
8
|
+
path :bar do |o|
|
9
|
+
"/bar/#{o}"
|
10
|
+
end
|
11
|
+
|
12
|
+
route do |r|
|
13
|
+
"#{foo_path}#{bar_path('a')}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
body.should == '/foo/bar/a'
|
18
|
+
end
|
19
|
+
|
20
|
+
it "raises if both path and block are given" do
|
21
|
+
app.plugin :path
|
22
|
+
proc{app.path(:foo, '/foo'){}}.should raise_error(Roda::RodaError)
|
23
|
+
end
|
24
|
+
|
25
|
+
it "raises if neither path nor block are given" do
|
26
|
+
app.plugin :path
|
27
|
+
proc{app.path(:foo)}.should raise_error(Roda::RodaError)
|
28
|
+
end
|
29
|
+
end
|
@@ -5,7 +5,7 @@ describe "render_each plugin" do
|
|
5
5
|
app(:bare) do
|
6
6
|
plugin :render_each
|
7
7
|
def render(t, opts)
|
8
|
-
"r#{t}#{opts[:locals][:foo] if opts[:locals]}#{opts[:bar]} "
|
8
|
+
"r#{t}#{opts[:locals][:foo] if opts[:locals]}#{opts[:bar]}#{opts[:locals][:bar] if opts[:locals]} "
|
9
9
|
end
|
10
10
|
|
11
11
|
route do |r|
|
@@ -20,11 +20,16 @@ describe "render_each plugin" do
|
|
20
20
|
r.is 'b' do
|
21
21
|
render_each([1,2,3], :bar, :local=>nil)
|
22
22
|
end
|
23
|
+
|
24
|
+
r.is 'c' do
|
25
|
+
render_each([1,2,3], :bar, :locals=>{:foo=>4})
|
26
|
+
end
|
23
27
|
end
|
24
28
|
end
|
25
29
|
|
26
30
|
body.should == 'rfoo1 rfoo2 rfoo3 '
|
27
31
|
body("/a").should == 'rbar14 rbar24 rbar34 '
|
28
32
|
body("/b").should == 'rbar rbar rbar '
|
33
|
+
body("/c").should == 'rbar41 rbar42 rbar43 '
|
29
34
|
end
|
30
35
|
end
|
@@ -23,12 +23,16 @@ describe "symbol_matchers plugin" do
|
|
23
23
|
"format#{f.inspect}"
|
24
24
|
end
|
25
25
|
|
26
|
+
r.is "thing/:thing" do |d|
|
27
|
+
"thing#{d}"
|
28
|
+
end
|
29
|
+
|
26
30
|
r.is :f do |f|
|
27
31
|
"f#{f}"
|
28
32
|
end
|
29
33
|
|
30
|
-
r.is 'q:rest' do |
|
31
|
-
"rest#{
|
34
|
+
r.is 'q:rest' do |rest|
|
35
|
+
"rest#{rest}"
|
32
36
|
end
|
33
37
|
|
34
38
|
r.is :w do |w|
|
@@ -64,5 +68,6 @@ describe "symbol_matchers plugin" do
|
|
64
68
|
status("/1/f/a").should == 404
|
65
69
|
body("/qa/b/c/d//f/g").should == 'resta/b/c/d//f/g'
|
66
70
|
body('/q').should == 'rest'
|
71
|
+
body('/thing/q').should == 'thingq'
|
67
72
|
end
|
68
73
|
end
|
data/spec/request_spec.rb
CHANGED
@@ -62,3 +62,13 @@ describe "request.inspect" do
|
|
62
62
|
body('REQUEST_METHOD'=>'POST').should == "#<Foo::RodaRequest POST />"
|
63
63
|
end
|
64
64
|
end
|
65
|
+
|
66
|
+
describe "TERM.inspect" do
|
67
|
+
it "should return TERM" do
|
68
|
+
app do |r|
|
69
|
+
r.class::TERM.inspect
|
70
|
+
end
|
71
|
+
|
72
|
+
body.should == "TERM"
|
73
|
+
end
|
74
|
+
end
|
data/spec/response_spec.rb
CHANGED
@@ -67,6 +67,16 @@ describe "response #finish" do
|
|
67
67
|
body.should == 'a200text/html1'
|
68
68
|
end
|
69
69
|
|
70
|
+
it "should set Content-Length header" do
|
71
|
+
app do |r|
|
72
|
+
response.write 'a'
|
73
|
+
response['Content-Length'].should == nil
|
74
|
+
throw :halt, response.finish
|
75
|
+
end
|
76
|
+
|
77
|
+
header('Content-Length').should == '1'
|
78
|
+
end
|
79
|
+
|
70
80
|
it "should not overwrite existing status" do
|
71
81
|
app do |r|
|
72
82
|
response.status = 500
|
@@ -78,6 +88,43 @@ describe "response #finish" do
|
|
78
88
|
end
|
79
89
|
end
|
80
90
|
|
91
|
+
describe "response #finish_with_body" do
|
92
|
+
it "should use given body" do
|
93
|
+
app do |r|
|
94
|
+
throw :halt, response.finish_with_body(['123'])
|
95
|
+
end
|
96
|
+
|
97
|
+
body.should == '123'
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should set status to 200 if status has not been set" do
|
101
|
+
app do |r|
|
102
|
+
throw :halt, response.finish_with_body([])
|
103
|
+
end
|
104
|
+
|
105
|
+
status.should == 200
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should not set Content-Length header" do
|
109
|
+
app do |r|
|
110
|
+
response.write 'a'
|
111
|
+
response['Content-Length'].should == nil
|
112
|
+
throw :halt, response.finish_with_body(['123'])
|
113
|
+
end
|
114
|
+
|
115
|
+
header('Content-Length').should == nil
|
116
|
+
end
|
117
|
+
|
118
|
+
it "should not overwrite existing status" do
|
119
|
+
app do |r|
|
120
|
+
response.status = 500
|
121
|
+
throw :halt, response.finish_with_body(['123'])
|
122
|
+
end
|
123
|
+
|
124
|
+
status.should == 500
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
81
128
|
describe "response #redirect" do
|
82
129
|
it "should set location and status" do
|
83
130
|
app do |r|
|