roda 0.9.0 → 1.0.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +362 -167
  4. data/Rakefile +2 -2
  5. data/doc/release_notes/1.0.0.txt +329 -0
  6. data/lib/roda.rb +553 -180
  7. data/lib/roda/plugins/_erubis_escaping.rb +28 -0
  8. data/lib/roda/plugins/all_verbs.rb +7 -9
  9. data/lib/roda/plugins/backtracking_array.rb +92 -0
  10. data/lib/roda/plugins/content_for.rb +46 -0
  11. data/lib/roda/plugins/csrf.rb +60 -0
  12. data/lib/roda/plugins/flash.rb +53 -7
  13. data/lib/roda/plugins/halt.rb +8 -14
  14. data/lib/roda/plugins/head.rb +56 -0
  15. data/lib/roda/plugins/header_matchers.rb +2 -2
  16. data/lib/roda/plugins/json.rb +84 -0
  17. data/lib/roda/plugins/multi_route.rb +50 -10
  18. data/lib/roda/plugins/not_allowed.rb +140 -0
  19. data/lib/roda/plugins/pass.rb +13 -6
  20. data/lib/roda/plugins/per_thread_caching.rb +70 -0
  21. data/lib/roda/plugins/render.rb +20 -33
  22. data/lib/roda/plugins/render_each.rb +61 -0
  23. data/lib/roda/plugins/symbol_matchers.rb +79 -0
  24. data/lib/roda/plugins/symbol_views.rb +40 -0
  25. data/lib/roda/plugins/view_subdirs.rb +53 -0
  26. data/lib/roda/version.rb +3 -0
  27. data/spec/matchers_spec.rb +61 -5
  28. data/spec/plugin/_erubis_escaping_spec.rb +29 -0
  29. data/spec/plugin/backtracking_array_spec.rb +38 -0
  30. data/spec/plugin/content_for_spec.rb +34 -0
  31. data/spec/plugin/csrf_spec.rb +49 -0
  32. data/spec/plugin/flash_spec.rb +69 -5
  33. data/spec/plugin/head_spec.rb +35 -0
  34. data/spec/plugin/json_spec.rb +50 -0
  35. data/spec/plugin/multi_route_spec.rb +22 -6
  36. data/spec/plugin/not_allowed_spec.rb +55 -0
  37. data/spec/plugin/pass_spec.rb +8 -2
  38. data/spec/plugin/per_thread_caching_spec.rb +28 -0
  39. data/spec/plugin/render_each_spec.rb +30 -0
  40. data/spec/plugin/render_spec.rb +7 -1
  41. data/spec/plugin/symbol_matchers_spec.rb +68 -0
  42. data/spec/plugin/symbol_views_spec.rb +32 -0
  43. data/spec/plugin/view_subdirs_spec.rb +45 -0
  44. data/spec/plugin_spec.rb +11 -1
  45. data/spec/redirect_spec.rb +21 -4
  46. data/spec/request_spec.rb +9 -0
  47. metadata +49 -5
@@ -0,0 +1,38 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "backtracking_array plugin" do
4
+ it "backtracks to next entry in array if later matcher fails" do
5
+ app(:backtracking_array) do |r|
6
+ r.is %w'a a/b' do |id|
7
+ id
8
+ end
9
+
10
+ r.is %w'c c/d', %w'd e' do |a, b|
11
+ "#{a}-#{b}"
12
+ end
13
+
14
+ r.is [%w'f f/g', %w'g g/h'] do |id|
15
+ id
16
+ end
17
+ end
18
+
19
+ status.should == 404
20
+
21
+ body("/a").should == 'a'
22
+ body("/a/b").should == 'a/b'
23
+ status("/a/b/").should == 404
24
+
25
+ body("/c/d").should == 'c-d'
26
+ body("/c/e").should == 'c-e'
27
+ body("/c/d/d").should == 'c/d-d'
28
+ body("/c/d/e").should == 'c/d-e'
29
+ status("/c/d/").should == 404
30
+
31
+ body("/f").should == 'f'
32
+ body("/f/g").should == 'f/g'
33
+ body("/g").should == 'g'
34
+ body("/g/h").should == 'g/h'
35
+ status("/f/g/").should == 404
36
+ status("/g/h/").should == 404
37
+ end
38
+ end
@@ -0,0 +1,34 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ begin
4
+ require 'tilt/erb'
5
+ rescue LoadError
6
+ warn "tilt not installed, skipping content_for plugin test"
7
+ else
8
+ describe "content_for plugin" do
9
+ before do
10
+ app(:bare) do
11
+ plugin :render
12
+ render_opts[:views] = "./spec/views"
13
+ plugin :content_for
14
+
15
+ route do |r|
16
+ r.root do
17
+ view(:inline=>"<% content_for :foo do %>foo<% end %>bar", :layout=>{:inline=>'<%= yield %> <%= content_for(:foo) %>'})
18
+ end
19
+ r.get 'a' do
20
+ view(:inline=>"bar", :layout=>{:inline=>'<%= content_for(:foo) %> <%= yield %>'})
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ it "should be able to set content in template and get that content in the layout" do
27
+ body.strip.should == "bar foo"
28
+ end
29
+
30
+ it "should work if content is not set by the template" do
31
+ body('/a').strip.should == "bar"
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,49 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ begin
4
+ require 'rack/csrf'
5
+ rescue LoadError
6
+ warn "rack_csrf not installed, skipping csrf plugin test"
7
+ else
8
+ describe "csrf plugin" do
9
+ it "adds csrf protection and csrf helper methods" do
10
+ app(:bare) do
11
+ use Rack::Session::Cookie, :secret=>'1'
12
+ plugin :csrf, :skip=>['POST:/foo']
13
+
14
+ route do |r|
15
+ r.get do
16
+ response['TAG'] = csrf_tag
17
+ response['METATAG'] = csrf_metatag
18
+ response['TOKEN'] = csrf_token
19
+ response['FIELD'] = csrf_field
20
+ response['HEADER'] = csrf_header
21
+ 'g'
22
+ end
23
+ r.post 'foo' do
24
+ 'bar'
25
+ end
26
+ r.post do
27
+ 'p'
28
+ end
29
+ end
30
+ end
31
+
32
+ io = StringIO.new
33
+ status('REQUEST_METHOD'=>'POST', 'rack.input'=>io).should == 403
34
+ body('/foo', 'REQUEST_METHOD'=>'POST', 'rack.input'=>io).should == 'bar'
35
+
36
+ env = proc{|h| h['Set-Cookie'] ? {'HTTP_COOKIE'=>h['Set-Cookie'].sub("; path=/; HttpOnly", '')} : {}}
37
+ s, h, b = req
38
+ s.should == 200
39
+ field = h['FIELD']
40
+ token = Regexp.escape(h['TOKEN'])
41
+ h['TAG'].should =~ /\A<input type="hidden" name="#{field}" value="#{token}" \/>\z/
42
+ h['METATAG'].should =~ /\A<meta name="#{field}" content="#{token}" \/>\z/
43
+ b.should == ['g']
44
+ s, _, b = req('/', env[h].merge('REQUEST_METHOD'=>'POST', 'rack.input'=>io, "HTTP_#{h['HEADER']}"=>h['TOKEN']))
45
+ s.should == 200
46
+ b.should == ['p']
47
+ end
48
+ end
49
+ end
@@ -1,10 +1,5 @@
1
1
  require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
2
 
3
- begin
4
- require 'sinatra/flash/hash'
5
- rescue LoadError
6
- warn "sinatra-flash not installed, skipping flash plugin test"
7
- else
8
3
  describe "flash plugin" do
9
4
  it "flash.now[] sets flash for current page" do
10
5
  app(:bare) do
@@ -56,4 +51,73 @@ describe "flash plugin" do
56
51
  b.join.should == 'bb'
57
52
  end
58
53
  end
54
+
55
+ describe "FlashHash" do
56
+ before do
57
+ @h = Roda::RodaPlugins::Flash::FlashHash.new
58
+ end
59
+
60
+ it ".new should accept nil for empty hash" do
61
+ @h = Roda::RodaPlugins::Flash::FlashHash.new(nil)
62
+ @h.now.should == {}
63
+ @h.next.should == {}
64
+ end
65
+
66
+ it ".new should accept a hash" do
67
+ @h = Roda::RodaPlugins::Flash::FlashHash.new(1=>2)
68
+ @h.now.should == {1=>2}
69
+ @h.next.should == {}
70
+ end
71
+
72
+ it "#[]= assigns to next flash" do
73
+ @h[1] = 2
74
+ @h.now.should == {}
75
+ @h.next.should == {1=>2}
76
+ end
77
+
78
+ it "#discard removes given key from next hash" do
79
+ @h[1] = 2
80
+ @h[nil] = 3
81
+ @h.next.should == {1=>2, nil=>3}
82
+ @h.discard(nil)
83
+ @h.next.should == {1=>2}
84
+ @h.discard(1)
85
+ @h.next.should == {}
86
+ end
87
+
88
+ it "#discard removes all entries from next hash with no arguments" do
89
+ @h[1] = 2
90
+ @h[nil] = 3
91
+ @h.next.should == {1=>2, nil=>3}
92
+ @h.discard
93
+ @h.next.should == {}
94
+ end
95
+
96
+ it "#keep copies entry for key from current hash to next hash" do
97
+ @h.now[1] = 2
98
+ @h.now[nil] = 3
99
+ @h.next.should == {}
100
+ @h.keep(nil)
101
+ @h.next.should == {nil=>3}
102
+ @h.keep(1)
103
+ @h.next.should == {1=>2, nil=>3}
104
+ end
105
+
106
+ it "#keep copies all entries from current hash to next hash" do
107
+ @h.now[1] = 2
108
+ @h.now[nil] = 3
109
+ @h.next.should == {}
110
+ @h.keep
111
+ @h.next.should == {1=>2, nil=>3}
112
+ end
113
+
114
+ it "#sweep replaces current hash with next hash" do
115
+ @h[1] = 2
116
+ @h[nil] = 3
117
+ @h.next.should == {1=>2, nil=>3}
118
+ @h.now.should == {}
119
+ @h.sweep.should == {1=>2, nil=>3}
120
+ @h.next.should == {}
121
+ @h.now.should == {1=>2, nil=>3}
122
+ end
59
123
  end
@@ -0,0 +1,35 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "head plugin" do
4
+ it "considers HEAD requests as GET requests which return no body" do
5
+ app(:head) do |r|
6
+ r.root do
7
+ 'root'
8
+ end
9
+
10
+ r.get 'a' do
11
+ 'a'
12
+ end
13
+
14
+ r.is 'b', :method=>[:get, :post] do
15
+ 'b'
16
+ end
17
+ end
18
+
19
+ s, h, b = req
20
+ s.should == 200
21
+ h['Content-Length'].should == '4'
22
+ b.should == ['root']
23
+
24
+ s, h, b = req('REQUEST_METHOD' => 'HEAD')
25
+ s.should == 200
26
+ h['Content-Length'].should == '4'
27
+ b.should == []
28
+
29
+ body('/a').should == 'a'
30
+ status('/a', 'REQUEST_METHOD' => 'HEAD').should == 200
31
+
32
+ body('/b').should == 'b'
33
+ status('/b', 'REQUEST_METHOD' => 'HEAD').should == 200
34
+ end
35
+ end
@@ -0,0 +1,50 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "json plugin" do
4
+ before do
5
+ c = Class.new do
6
+ def to_json
7
+ '[1]'
8
+ end
9
+ end
10
+
11
+ app(:bare) do
12
+ plugin :json
13
+ json_result_classes << c
14
+
15
+ route do |r|
16
+ r.is 'array' do
17
+ [1, 2, 3]
18
+ end
19
+
20
+ r.is "hash" do
21
+ {'a'=>'b'}
22
+ end
23
+
24
+ r.is 'c' do
25
+ c.new
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ it "should use a json content type for a json response" do
32
+ header('Content-Type', "/array").should == 'application/json'
33
+ header('Content-Type', "/hash").should == 'application/json'
34
+ header('Content-Type', "/c").should == 'application/json'
35
+ header('Content-Type').should == 'text/html'
36
+ end
37
+
38
+ it "should convert objects to json" do
39
+ body('/array').gsub(/\s/, '').should == '[1,2,3]'
40
+ body('/hash').gsub(/\s/, '').should == '{"a":"b"}'
41
+ body('/c').should == '[1]'
42
+ body.should == ''
43
+ end
44
+
45
+ it "should work when subclassing" do
46
+ @app = Class.new(app)
47
+ app.route{[1]}
48
+ body.should == '[1]'
49
+ end
50
+ end
@@ -5,7 +5,7 @@ describe "multi_route plugin" do
5
5
  app(:bare) do
6
6
  plugin :multi_route
7
7
 
8
- route(:get) do |r|
8
+ route("get") do |r|
9
9
  r.is "" do
10
10
  "get"
11
11
  end
@@ -15,7 +15,7 @@ describe "multi_route plugin" do
15
15
  end
16
16
  end
17
17
 
18
- route(:post) do |r|
18
+ route("post") do |r|
19
19
  r.is "" do
20
20
  "post"
21
21
  end
@@ -26,15 +26,22 @@ describe "multi_route plugin" do
26
26
  end
27
27
 
28
28
  route do |r|
29
+ r.on 'foo' do
30
+ r.multi_route do
31
+ "foo"
32
+ end
33
+ end
34
+
29
35
  r.get do
30
- route(:get)
36
+ r.route("get")
31
37
 
32
38
  r.is "b" do
33
39
  "getb"
34
40
  end
35
41
  end
42
+
36
43
  r.post do
37
- route(:post)
44
+ r.route("post")
38
45
 
39
46
  r.is "b" do
40
47
  "postb"
@@ -55,6 +62,15 @@ describe "multi_route plugin" do
55
62
  status('/c', 'REQUEST_METHOD'=>'POST').should == 404
56
63
  end
57
64
 
65
+ it "uses multi_route to dispatch to any named route" do
66
+ status('/foo').should == 404
67
+ body('/foo/get/').should == 'get'
68
+ body('/foo/get/a').should == 'geta'
69
+ body('/foo/post/').should == 'post'
70
+ body('/foo/post/a').should == 'posta'
71
+ body('/foo/post/b').should == 'foo'
72
+ end
73
+
58
74
  it "handles loading the plugin multiple times correctly" do
59
75
  app.plugin :multi_route
60
76
  body.should == 'get'
@@ -71,14 +87,14 @@ describe "multi_route plugin" do
71
87
  @app = Class.new(@app)
72
88
  @app.route do |r|
73
89
  r.get do
74
- route(:post)
90
+ r.route("post")
75
91
 
76
92
  r.is "b" do
77
93
  "1b"
78
94
  end
79
95
  end
80
96
  r.post do
81
- route(:get)
97
+ r.route("get")
82
98
 
83
99
  r.is "b" do
84
100
  "2b"
@@ -0,0 +1,55 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "not_allowed plugin" do
4
+ it "skips the current block if pass is called" do
5
+ app(:not_allowed) do |r|
6
+ r.get '' do
7
+ 'a'
8
+ end
9
+
10
+ r.is "c" do
11
+ r.get do
12
+ "cg"
13
+ end
14
+
15
+ r.post do
16
+ "cp"
17
+ end
18
+
19
+ "c"
20
+ end
21
+
22
+ r.get do
23
+ r.is 'b' do
24
+ 'b'
25
+ end
26
+ r.is /(d)/ do |s|
27
+ s
28
+ end
29
+ r.get /(e)/ do |s|
30
+ s
31
+ end
32
+ end
33
+ end
34
+
35
+ body.should == 'a'
36
+ status('REQUEST_METHOD'=>'POST').should == 405
37
+ header('Allow', 'REQUEST_METHOD'=>'POST').should == 'GET'
38
+
39
+ body('/b').should == 'b'
40
+ status('/b', 'REQUEST_METHOD'=>'POST').should == 404
41
+
42
+ body('/d').should == 'd'
43
+ status('/d', 'REQUEST_METHOD'=>'POST').should == 404
44
+
45
+ body('/e').should == 'e'
46
+ status('/d', 'REQUEST_METHOD'=>'POST').should == 404
47
+
48
+ body('/c').should == 'cg'
49
+ body('/c').should == 'cg'
50
+ body('/c', 'REQUEST_METHOD'=>'POST').should == 'cp'
51
+ body('/c', 'REQUEST_METHOD'=>'PATCH').should == 'c'
52
+ status('/c', 'REQUEST_METHOD'=>'PATCH').should == 405
53
+ header('Allow', '/c', 'REQUEST_METHOD'=>'PATCH').should == 'GET, POST'
54
+ end
55
+ end
@@ -1,8 +1,13 @@
1
1
  require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
2
 
3
3
  describe "pass plugin" do
4
- it "executes on no arguments" do
4
+ it "skips the current block if pass is called" do
5
5
  app(:pass) do |r|
6
+ r.root do
7
+ r.pass if env['FOO']
8
+ 'root'
9
+ end
10
+
6
11
  r.on :id do |id|
7
12
  r.pass if id == 'foo'
8
13
  id
@@ -13,11 +18,12 @@ describe "pass plugin" do
13
18
  end
14
19
  end
15
20
 
21
+ body.should == 'root'
22
+ status('FOO'=>true).should == 404
16
23
  body("/a").should == 'a'
17
24
  body("/a/b").should == 'a'
18
25
  body("/foo/a").should == 'fooa'
19
26
  body("/foo/a/b").should == 'fooa'
20
27
  status("/foo").should == 404
21
- status.should == 404
22
28
  end
23
29
  end