roda-cj 0.9.6 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +16 -0
- data/README.rdoc +211 -103
- data/Rakefile +1 -1
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +295 -42
- data/lib/roda/plugins/all_verbs.rb +1 -1
- data/lib/roda/plugins/assets.rb +277 -0
- data/lib/roda/plugins/backtracking_array.rb +1 -1
- data/lib/roda/plugins/error_email.rb +110 -0
- data/lib/roda/plugins/multi_route.rb +14 -3
- data/lib/roda/plugins/not_allowed.rb +10 -3
- data/lib/roda/plugins/path.rb +38 -0
- data/lib/roda/plugins/symbol_matchers.rb +1 -1
- data/lib/roda/plugins/view_subdirs.rb +7 -1
- data/lib/roda/version.rb +1 -1
- data/spec/integration_spec.rb +95 -3
- data/spec/plugin/_erubis_escaping_spec.rb +1 -0
- data/spec/plugin/assets_spec.rb +86 -0
- data/spec/plugin/error_email_spec.rb +68 -0
- data/spec/plugin/multi_route_spec.rb +22 -0
- data/spec/plugin/not_allowed_spec.rb +13 -0
- data/spec/plugin/path_spec.rb +29 -0
- metadata +104 -66
- checksums.yaml +0 -7
@@ -88,8 +88,11 @@ class Roda
|
|
88
88
|
always(&block) if #{verb == :get ? :is_get : verb}?
|
89
89
|
else
|
90
90
|
args << ::Roda::RodaPlugins::Base::RequestMethods::TERM
|
91
|
-
if_match(args) do
|
92
|
-
#{verb}
|
91
|
+
if_match(args) do |*args|
|
92
|
+
if #{verb == :get ? :is_get : verb}?
|
93
|
+
block_result(yield(*args))
|
94
|
+
throw :halt, response.finish
|
95
|
+
end
|
93
96
|
response.status = 405
|
94
97
|
response['Allow'] = '#{verb.to_s.upcase}'
|
95
98
|
nil
|
@@ -112,7 +115,11 @@ class Roda
|
|
112
115
|
begin
|
113
116
|
@_is_verbs = []
|
114
117
|
|
115
|
-
ret =
|
118
|
+
ret = if verbs.empty?
|
119
|
+
yield
|
120
|
+
else
|
121
|
+
yield(*captures)
|
122
|
+
end
|
116
123
|
|
117
124
|
unless @_is_verbs.empty?
|
118
125
|
response.status = 405
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The path plugin adds support for named paths. Using the +path+ class method, you can
|
4
|
+
# easily create <tt>*_path</tt> instance methods for each named path. Those instance
|
5
|
+
# methods can then be called if you need to get the path for a form action, link,
|
6
|
+
# redirect, or anything else. Example:
|
7
|
+
#
|
8
|
+
# plugin :path
|
9
|
+
# path :foo, '/foo'
|
10
|
+
# path :bar do |bar|
|
11
|
+
# "/bar/#{bar.id}"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# route do |r|
|
15
|
+
# r.post 'bar' do
|
16
|
+
# bar = Bar.create(r.params['bar'])
|
17
|
+
# r.redirect bar_path(bar)
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
module Path
|
21
|
+
module ClassMethods
|
22
|
+
def path(name, path=nil, &block)
|
23
|
+
raise RodaError, "cannot provide both path and block to Roda.path" if path && block
|
24
|
+
raise RodaError, "must provide either path or block to Roda.path" unless path || block
|
25
|
+
|
26
|
+
if path
|
27
|
+
path = path.dup.freeze
|
28
|
+
block = lambda{path}
|
29
|
+
end
|
30
|
+
|
31
|
+
define_method("#{name}_path", &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
register_plugin(:path, Path)
|
37
|
+
end
|
38
|
+
end
|
@@ -38,7 +38,7 @@ class Roda
|
|
38
38
|
# Note that because of how segment matching works, :format, :opt, and :optd
|
39
39
|
# are only going to work inside of a string, like this:
|
40
40
|
#
|
41
|
-
# r.is "album:opt" do |id|
|
41
|
+
# r.is "album:opt" do |id| end
|
42
42
|
# # matches /album (yielding nil) and /album/foo (yielding "foo")
|
43
43
|
# # does not match /album/ or /album/foo/bar
|
44
44
|
module SymbolMatchers
|
@@ -6,7 +6,7 @@ class Roda
|
|
6
6
|
# use, and template names that do not contain a slash will
|
7
7
|
# automatically use that view subdirectory. Example:
|
8
8
|
#
|
9
|
-
# plugin :render
|
9
|
+
# plugin :render, :layout=>'./layout'
|
10
10
|
# plugin :view_subdirs
|
11
11
|
#
|
12
12
|
# route do |r|
|
@@ -25,6 +25,12 @@ class Roda
|
|
25
25
|
#
|
26
26
|
# This plugin should be loaded after the render plugin, since
|
27
27
|
# it works by overriding parts of the render plugin.
|
28
|
+
#
|
29
|
+
# Note that when a view subdirectory is set, the layout will
|
30
|
+
# also be looked up in the subdirectory unless it contains
|
31
|
+
# a slash. So if you want to use a view subdirectory for
|
32
|
+
# templates but have a shared layout, you should make sure your
|
33
|
+
# layout contains a slash, similar to the example above.
|
28
34
|
module ViewSubdirs
|
29
35
|
module InstanceMethods
|
30
36
|
# Set the view subdirectory to use. This can be set to nil
|
data/lib/roda/version.rb
CHANGED
data/spec/integration_spec.rb
CHANGED
@@ -18,7 +18,7 @@ describe "integration" do
|
|
18
18
|
|
19
19
|
end
|
20
20
|
|
21
|
-
it "should setup middleware using use
|
21
|
+
it "should setup middleware using use" do
|
22
22
|
c = @c
|
23
23
|
app(:bare) do
|
24
24
|
use c, "First", "Second" do
|
@@ -35,7 +35,24 @@ describe "integration" do
|
|
35
35
|
body('/hello').should == 'D First Second Block'
|
36
36
|
end
|
37
37
|
|
38
|
-
it "should
|
38
|
+
it "should support adding middleware using use after route block setup" do
|
39
|
+
c = @c
|
40
|
+
app(:bare) do
|
41
|
+
route do |r|
|
42
|
+
r.get "hello" do
|
43
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
use c, "First", "Second" do
|
48
|
+
"Block"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
body('/hello').should == 'D First Second Block'
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should inherit middleware in subclass" do
|
39
56
|
c = @c
|
40
57
|
@app = Class.new(app(:bare){use(c, '1', '2'){"3"}})
|
41
58
|
@app.route do |r|
|
@@ -47,7 +64,55 @@ describe "integration" do
|
|
47
64
|
body('/hello').should == 'D 1 2 3'
|
48
65
|
end
|
49
66
|
|
50
|
-
it "should
|
67
|
+
it "should inherit route in subclass" do
|
68
|
+
c = @c
|
69
|
+
app(:bare) do
|
70
|
+
use(c, '1', '2'){"3"}
|
71
|
+
route do |r|
|
72
|
+
r.get "hello" do
|
73
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@app = Class.new(app)
|
78
|
+
|
79
|
+
body('/hello').should == 'D 1 2 3'
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should use instance of subclass when inheriting routes" do
|
83
|
+
c = @c
|
84
|
+
obj = nil
|
85
|
+
app(:bare) do
|
86
|
+
use(c, '1', '2'){"3"}
|
87
|
+
route do |r|
|
88
|
+
r.get "hello" do
|
89
|
+
obj = self
|
90
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
@app = Class.new(app)
|
95
|
+
|
96
|
+
body('/hello').should == 'D 1 2 3'
|
97
|
+
obj.should be_a_kind_of(@app)
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should handle middleware added to subclass using superclass route" do
|
101
|
+
c = @c
|
102
|
+
app(:bare) do
|
103
|
+
route do |r|
|
104
|
+
r.get "hello" do
|
105
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
@app = Class.new(app)
|
110
|
+
@app.use(c, '1', '2'){"3"}
|
111
|
+
|
112
|
+
body('/hello').should == 'D 1 2 3'
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should not have future middleware additions to superclass affect subclass" do
|
51
116
|
c = @c
|
52
117
|
a = app
|
53
118
|
@app = Class.new(a)
|
@@ -60,4 +125,31 @@ describe "integration" do
|
|
60
125
|
|
61
126
|
body('/hello').should == 'D '
|
62
127
|
end
|
128
|
+
|
129
|
+
it "should not have future middleware additions to subclass affect superclass" do
|
130
|
+
c = @c
|
131
|
+
a = app do |r|
|
132
|
+
r.get "hello" do
|
133
|
+
"D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
@app = Class.new(a)
|
137
|
+
@app.use(c, '1', '2'){"3"}
|
138
|
+
@app = a
|
139
|
+
|
140
|
+
body('/hello').should == 'D '
|
141
|
+
end
|
142
|
+
|
143
|
+
it "should have app return the rack application to call" do
|
144
|
+
app(:bare){}.app.should == nil
|
145
|
+
app.route{|r|}
|
146
|
+
app.app.should be_a_kind_of(Proc)
|
147
|
+
c = Class.new{def initialize(app) @app = app end; def call(env) @app.call(env) end}
|
148
|
+
app.use c
|
149
|
+
app.app.should be_a_kind_of(c)
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should have route_block return the route block" do
|
153
|
+
app{|r| 1}.route_block.call(nil).should == 1
|
154
|
+
end
|
63
155
|
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'tilt'
|
5
|
+
require 'tilt/sass'
|
6
|
+
require 'tilt/coffee'
|
7
|
+
rescue LoadError
|
8
|
+
warn 'tilt not installed, skipping assets plugin test'
|
9
|
+
else
|
10
|
+
describe 'assets plugin' do
|
11
|
+
before do
|
12
|
+
app(:bare) do
|
13
|
+
plugin(:assets, {
|
14
|
+
path: './spec/dummy/assets',
|
15
|
+
css_engine: 'scss',
|
16
|
+
js_engine: 'coffee',
|
17
|
+
headers: {
|
18
|
+
"Cache-Control" => 'public, max-age=2592000, no-transform',
|
19
|
+
'Connection' => 'keep-alive',
|
20
|
+
'Age' => '25637',
|
21
|
+
'Strict-Transport-Security' => 'max-age=31536000',
|
22
|
+
'Content-Disposition' => 'inline'
|
23
|
+
}
|
24
|
+
})
|
25
|
+
|
26
|
+
assets_opts[:css] = ['app', '../raw.css']
|
27
|
+
assets_opts[:js] = { head: ['app'] }
|
28
|
+
|
29
|
+
route do |r|
|
30
|
+
r.assets
|
31
|
+
|
32
|
+
r.is 'test' do
|
33
|
+
response.write assets :css
|
34
|
+
response.write assets [:js, :head]
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'should contain proper configuration' do
|
41
|
+
app.assets_opts[:path].should == './spec/dummy/assets'
|
42
|
+
app.assets_opts[:css].should include('app')
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should serve proper assets' do
|
46
|
+
body('/assets/css/app.css').should include('color: red')
|
47
|
+
body('/assets/css/%242E%242E/raw.css').should include('color: blue')
|
48
|
+
body('/assets/js/head/app.js').should include('console.log')
|
49
|
+
body('/assets/css/http://google.com').should include('google.com')
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'should contain proper assets html tags' do
|
53
|
+
html = body '/test'
|
54
|
+
html.scan(/<link/).length.should eq 2
|
55
|
+
html.scan(/<script/).length.should eq 1
|
56
|
+
html.should include('link')
|
57
|
+
html.should include('script')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should only show one link when concat/compile is true' do
|
61
|
+
app.assets_opts[:concat] = true
|
62
|
+
html = body '/test'
|
63
|
+
html.scan(/<link/).length.should eq 1
|
64
|
+
|
65
|
+
app.assets_opts[:compiled] = true
|
66
|
+
html = body '/test'
|
67
|
+
html.scan(/<link/).length.should eq 1
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'should join all files when concat is true' do
|
71
|
+
app.assets_opts[:concat] = true
|
72
|
+
path = app.assets_opts[:concat_name] + '/css/123'
|
73
|
+
css = body("/assets/css/#{path}.css")
|
74
|
+
css.should include('color: red')
|
75
|
+
css.should include('color: blue')
|
76
|
+
end
|
77
|
+
|
78
|
+
it 'should grab compiled files' do
|
79
|
+
app.compile_assets
|
80
|
+
app.assets_opts[:compiled] = true
|
81
|
+
path = app.assets_opts[:compiled_name] + '/js-head/123'
|
82
|
+
js = body("/assets/js/#{path}.js")
|
83
|
+
js.should include('console.log')
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,68 @@
|
|
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).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/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 "adds :prefix option to subject line" do
|
37
|
+
app(:prefix=>'TEST ')
|
38
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
39
|
+
email[:message].should =~ /^Subject: TEST ArgumentError/
|
40
|
+
end
|
41
|
+
|
42
|
+
it "uses :headers option for additional headers" do
|
43
|
+
app(:headers=>{'Foo'=>'Bar', 'Baz'=>'Quux'})
|
44
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
45
|
+
email[:message].should =~ /^Foo: Bar/
|
46
|
+
email[:message].should =~ /^Baz: Quux/
|
47
|
+
end
|
48
|
+
|
49
|
+
it "requires the :to and :from options" do
|
50
|
+
proc{app :from=>nil}.should raise_error(Roda::RodaError)
|
51
|
+
proc{app :to=>nil}.should raise_error(Roda::RodaError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "works correctly in subclasses" do
|
55
|
+
@app = Class.new(app)
|
56
|
+
@app.route do |r|
|
57
|
+
raise ArgumentError rescue error_email($!)
|
58
|
+
'e'
|
59
|
+
end
|
60
|
+
body('rack.input'=>StringIO.new).should == 'e'
|
61
|
+
email[:to].should == 't'
|
62
|
+
email[:from].should == 'f'
|
63
|
+
email[:host].should == 'localhost'
|
64
|
+
email[:message].should =~ /^Subject: ArgumentError/
|
65
|
+
email[:message].should =~ /Backtrace.*ENV/m
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
@@ -25,11 +25,21 @@ describe "multi_route plugin" do
|
|
25
25
|
end
|
26
26
|
end
|
27
27
|
|
28
|
+
route(:p) do |r|
|
29
|
+
r.is do
|
30
|
+
'p'
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
28
34
|
route do |r|
|
29
35
|
r.on 'foo' do
|
30
36
|
r.multi_route do
|
31
37
|
"foo"
|
32
38
|
end
|
39
|
+
|
40
|
+
r.on "p" do
|
41
|
+
r.route(:p)
|
42
|
+
end
|
33
43
|
end
|
34
44
|
|
35
45
|
r.get do
|
@@ -71,6 +81,18 @@ describe "multi_route plugin" do
|
|
71
81
|
body('/foo/post/b').should == 'foo'
|
72
82
|
end
|
73
83
|
|
84
|
+
it "does not have multi_route match non-String named routes" do
|
85
|
+
body('/foo/p').should == 'p'
|
86
|
+
status('/foo/p/2').should == 404
|
87
|
+
end
|
88
|
+
|
89
|
+
it "Can have multi_route pick up routes newly added" do
|
90
|
+
body('/foo/get/').should == 'get'
|
91
|
+
status('/foo/delete').should == 404
|
92
|
+
app.route('delete'){|r| r.on{'delete'}}
|
93
|
+
body('/foo/delete').should == 'delete'
|
94
|
+
end
|
95
|
+
|
74
96
|
it "handles loading the plugin multiple times correctly" do
|
75
97
|
app.plugin :multi_route
|
76
98
|
body.should == 'get'
|
@@ -23,6 +23,12 @@ describe "not_allowed plugin" do
|
|
23
23
|
r.is 'b' do
|
24
24
|
'b'
|
25
25
|
end
|
26
|
+
r.is /(d)/ do |s|
|
27
|
+
s
|
28
|
+
end
|
29
|
+
r.get /(e)/ do |s|
|
30
|
+
s
|
31
|
+
end
|
26
32
|
end
|
27
33
|
end
|
28
34
|
|
@@ -33,6 +39,13 @@ describe "not_allowed plugin" do
|
|
33
39
|
body('/b').should == 'b'
|
34
40
|
status('/b', 'REQUEST_METHOD'=>'POST').should == 404
|
35
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'
|
36
49
|
body('/c').should == 'cg'
|
37
50
|
body('/c', 'REQUEST_METHOD'=>'POST').should == 'cp'
|
38
51
|
body('/c', 'REQUEST_METHOD'=>'PATCH').should == 'c'
|