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.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +34 -0
  3. data/README.rdoc +18 -13
  4. data/Rakefile +8 -0
  5. data/doc/conventions.rdoc +163 -0
  6. data/doc/release_notes/1.1.0.txt +226 -0
  7. data/lib/roda.rb +51 -22
  8. data/lib/roda/plugins/assets.rb +613 -0
  9. data/lib/roda/plugins/caching.rb +215 -0
  10. data/lib/roda/plugins/chunked.rb +278 -0
  11. data/lib/roda/plugins/error_email.rb +112 -0
  12. data/lib/roda/plugins/flash.rb +3 -3
  13. data/lib/roda/plugins/hooks.rb +1 -1
  14. data/lib/roda/plugins/indifferent_params.rb +3 -3
  15. data/lib/roda/plugins/middleware.rb +3 -8
  16. data/lib/roda/plugins/multi_route.rb +110 -18
  17. data/lib/roda/plugins/not_allowed.rb +3 -3
  18. data/lib/roda/plugins/path.rb +38 -0
  19. data/lib/roda/plugins/render.rb +18 -16
  20. data/lib/roda/plugins/render_each.rb +0 -2
  21. data/lib/roda/plugins/streaming.rb +1 -2
  22. data/lib/roda/plugins/view_subdirs.rb +7 -1
  23. data/lib/roda/version.rb +1 -1
  24. data/spec/assets/css/app.scss +1 -0
  25. data/spec/assets/css/no_access.css +1 -0
  26. data/spec/assets/css/raw.css +1 -0
  27. data/spec/assets/js/head/app.js +1 -0
  28. data/spec/integration_spec.rb +95 -3
  29. data/spec/matchers_spec.rb +2 -2
  30. data/spec/plugin/assets_spec.rb +413 -0
  31. data/spec/plugin/caching_spec.rb +335 -0
  32. data/spec/plugin/chunked_spec.rb +182 -0
  33. data/spec/plugin/default_headers_spec.rb +6 -5
  34. data/spec/plugin/error_email_spec.rb +76 -0
  35. data/spec/plugin/multi_route_spec.rb +120 -0
  36. data/spec/plugin/not_allowed_spec.rb +14 -3
  37. data/spec/plugin/path_spec.rb +29 -0
  38. data/spec/plugin/render_each_spec.rb +6 -1
  39. data/spec/plugin/symbol_matchers_spec.rb +7 -2
  40. data/spec/request_spec.rb +10 -0
  41. data/spec/response_spec.rb +47 -0
  42. data/spec/views/about.erb +1 -0
  43. data/spec/views/about.str +1 -0
  44. data/spec/views/content-yield.erb +1 -0
  45. data/spec/views/home.erb +2 -0
  46. data/spec/views/home.str +2 -0
  47. data/spec/views/layout-alternative.erb +2 -0
  48. data/spec/views/layout-yield.erb +3 -0
  49. data/spec/views/layout.erb +2 -0
  50. data/spec/views/layout.str +2 -0
  51. 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'] = 'bar'
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 == h
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 /(d)/ do |s|
34
+ r.is(/(d)/) do |s|
27
35
  s
28
36
  end
29
- r.get /(e)/ do |s|
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('/d', 'REQUEST_METHOD'=>'POST').should == 404
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 |r|
31
- "rest#{r}"
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
@@ -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|