roda-cj 0.9.6 → 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.
- 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'
|