roda 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +42 -0
- data/README.rdoc +73 -144
- data/doc/conventions.rdoc +10 -8
- data/doc/release_notes/1.3.0.txt +109 -0
- data/lib/roda.rb +67 -100
- data/lib/roda/plugins/assets.rb +4 -4
- data/lib/roda/plugins/chunked.rb +4 -1
- data/lib/roda/plugins/class_level_routing.rb +7 -1
- data/lib/roda/plugins/cookies.rb +34 -0
- data/lib/roda/plugins/default_headers.rb +7 -6
- data/lib/roda/plugins/delegate.rb +8 -1
- data/lib/roda/plugins/delete_empty_headers.rb +33 -0
- data/lib/roda/plugins/delete_nil_headers.rb +34 -0
- data/lib/roda/plugins/environments.rb +12 -4
- data/lib/roda/plugins/error_email.rb +6 -1
- data/lib/roda/plugins/error_handler.rb +7 -4
- data/lib/roda/plugins/hash_matcher.rb +32 -0
- data/lib/roda/plugins/header_matchers.rb +12 -2
- data/lib/roda/plugins/json.rb +9 -6
- data/lib/roda/plugins/module_include.rb +92 -0
- data/lib/roda/plugins/multi_route.rb +7 -0
- data/lib/roda/plugins/multi_run.rb +11 -5
- data/lib/roda/plugins/named_templates.rb +7 -1
- data/lib/roda/plugins/not_found.rb +6 -0
- data/lib/roda/plugins/param_matchers.rb +43 -0
- data/lib/roda/plugins/path_matchers.rb +51 -0
- data/lib/roda/plugins/render.rb +32 -14
- data/lib/roda/plugins/static_path_info.rb +10 -3
- data/lib/roda/plugins/symbol_matchers.rb +1 -1
- data/lib/roda/version.rb +13 -1
- data/spec/freeze_spec.rb +28 -0
- data/spec/plugin/class_level_routing_spec.rb +26 -0
- data/spec/plugin/content_for_spec.rb +1 -2
- data/spec/plugin/cookies_spec.rb +25 -0
- data/spec/plugin/default_headers_spec.rb +4 -7
- data/spec/plugin/delegate_spec.rb +4 -1
- data/spec/plugin/delete_empty_headers_spec.rb +15 -0
- data/spec/plugin/error_handler_spec.rb +31 -0
- data/spec/plugin/hash_matcher_spec.rb +27 -0
- data/spec/plugin/header_matchers_spec.rb +15 -0
- data/spec/plugin/json_spec.rb +1 -2
- data/spec/plugin/mailer_spec.rb +2 -2
- data/spec/plugin/module_include_spec.rb +31 -0
- data/spec/plugin/multi_route_spec.rb +14 -0
- data/spec/plugin/multi_run_spec.rb +41 -0
- data/spec/plugin/named_templates_spec.rb +25 -0
- data/spec/plugin/not_found_spec.rb +29 -0
- data/spec/plugin/param_matchers_spec.rb +37 -0
- data/spec/plugin/path_matchers_spec.rb +42 -0
- data/spec/plugin/render_spec.rb +33 -8
- data/spec/plugin/static_path_info_spec.rb +6 -0
- data/spec/plugin/view_subdirs_spec.rb +1 -2
- data/spec/response_spec.rb +12 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/version_spec.rb +8 -2
- metadata +19 -3
@@ -34,20 +34,26 @@ class Roda
|
|
34
34
|
module MultiRun
|
35
35
|
# Initialize the storage for the dispatched applications
|
36
36
|
def self.configure(app)
|
37
|
-
app.
|
38
|
-
@multi_run_apps ||= {}
|
39
|
-
end
|
37
|
+
app.opts[:multi_run_apps] ||= {}
|
40
38
|
end
|
41
39
|
|
42
40
|
module ClassMethods
|
41
|
+
# Freeze the multi_run apps so that there can be no thread safety issues at runtime.
|
42
|
+
def freeze
|
43
|
+
opts[:multi_run_apps].freeze
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
43
47
|
# Hash storing rack applications to dispatch to, keyed by the prefix
|
44
48
|
# for the application.
|
45
|
-
|
49
|
+
def multi_run_apps
|
50
|
+
opts[:multi_run_apps]
|
51
|
+
end
|
46
52
|
|
47
53
|
# Add a rack application to dispatch to for the given prefix when
|
48
54
|
# r.multi_run is called.
|
49
55
|
def run(prefix, app)
|
50
|
-
|
56
|
+
multi_run_apps[prefix.to_s] = app
|
51
57
|
self::RodaRequest.refresh_multi_run_regexp!
|
52
58
|
end
|
53
59
|
end
|
@@ -58,9 +58,15 @@ class Roda
|
|
58
58
|
end
|
59
59
|
|
60
60
|
module ClassMethods
|
61
|
+
# Freeze the named templates so that there can be no thread safety issues at runtime.
|
62
|
+
def freeze
|
63
|
+
opts[:named_templates].freeze
|
64
|
+
super
|
65
|
+
end
|
66
|
+
|
61
67
|
# Store a new template block and options for the given template name.
|
62
68
|
def template(name, options=nil, &block)
|
63
|
-
opts[:named_templates][name.to_s] = [options, block]
|
69
|
+
opts[:named_templates][name.to_s] = [options, block].freeze
|
64
70
|
nil
|
65
71
|
end
|
66
72
|
end
|
@@ -18,6 +18,11 @@ class Roda
|
|
18
18
|
# not_found do
|
19
19
|
# "Where did it go?"
|
20
20
|
# end
|
21
|
+
#
|
22
|
+
# Before not_found is called, any existing headers on the response
|
23
|
+
# will be cleared. So if you want to be sure the headers are set
|
24
|
+
# even in a not_found block, you need to reset them in the
|
25
|
+
# not_found block.
|
21
26
|
module NotFound
|
22
27
|
# If a block is given, install the block as the not_found handler.
|
23
28
|
def self.configure(app, &block)
|
@@ -43,6 +48,7 @@ class Roda
|
|
43
48
|
result = super
|
44
49
|
|
45
50
|
if result[0] == 404 && (v = result[2]).is_a?(Array) && v.empty?
|
51
|
+
@_response.headers.clear
|
46
52
|
super{not_found}
|
47
53
|
else
|
48
54
|
result
|
@@ -0,0 +1,43 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The param_matchers plugin adds hash matchers that operate
|
4
|
+
# on the request's params.
|
5
|
+
#
|
6
|
+
# It adds a :param matcher for matching on any param with the
|
7
|
+
# same name, yielding the value of the param.
|
8
|
+
#
|
9
|
+
# r.on :param=>'foo' do |value|
|
10
|
+
# # Matches '?foo=bar', '?foo='
|
11
|
+
# # Doesn't match '?bar=foo'
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# It adds a :param! matcher for matching on any non-empty param
|
15
|
+
# with the same name, yielding the value of the param
|
16
|
+
#
|
17
|
+
# r.on :param!=>'foo' do |value|
|
18
|
+
# # Matches '?foo=bar'
|
19
|
+
# # Doesn't match '?foo=', '?bar=foo'
|
20
|
+
# end
|
21
|
+
module ParamMatchers
|
22
|
+
module RequestMethods
|
23
|
+
# Match the given parameter if present, even if the parameter is empty.
|
24
|
+
# Adds any match to the captures.
|
25
|
+
def match_param(key)
|
26
|
+
if v = self[key]
|
27
|
+
@captures << v
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Match the given parameter if present and not empty.
|
32
|
+
# Adds any match to the captures.
|
33
|
+
def match_param!(key)
|
34
|
+
if (v = self[key]) && !v.empty?
|
35
|
+
@captures << v
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
register_plugin(:param_matchers, ParamMatchers)
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The path_matchers plugin adds hash matchers that operate
|
4
|
+
# on the request's path.
|
5
|
+
#
|
6
|
+
# It adds a :prefix matcher for matching on the path's prefix,
|
7
|
+
# yielding the rest of the matched segment:
|
8
|
+
#
|
9
|
+
# r.on :prefix=>'foo' do |suffix|
|
10
|
+
# # Matches '/foo-bar', yielding '-bar'
|
11
|
+
# # Does not match bar-foo
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# It adds a :suffix matcher for matching on the path's suffix,
|
15
|
+
# yielding the part of the segment before the suffix:
|
16
|
+
#
|
17
|
+
# r.on :suffix=>'bar' do |prefix|
|
18
|
+
# # Matches '/foo-bar', yielding 'foo-'
|
19
|
+
# # Does not match bar-foo
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# It adds an :extension matcher for matching on the given file extension,
|
23
|
+
# yielding the part of the segment before the extension:
|
24
|
+
#
|
25
|
+
# r.on :extension=>'bar' do |reset|
|
26
|
+
# # Matches '/foo.bar', yielding 'foo'
|
27
|
+
# # Does not match bar.foo
|
28
|
+
# end
|
29
|
+
module PathMatchers
|
30
|
+
module RequestMethods
|
31
|
+
# Match when the current segment ends with the given extension.
|
32
|
+
# request path end with the extension.
|
33
|
+
def match_extension(ext)
|
34
|
+
match_suffix(".#{ext}")
|
35
|
+
end
|
36
|
+
|
37
|
+
# Match when the current path segment starts with the given prefix.
|
38
|
+
def match_prefix(prefix)
|
39
|
+
consume(self.class.cached_matcher([:prefix, prefix]){/#{prefix}([^\\\/]+)/})
|
40
|
+
end
|
41
|
+
|
42
|
+
# Match when the current path segment ends with the given suffix.
|
43
|
+
def match_suffix(suffix)
|
44
|
+
consume(self.class.cached_matcher([:suffix, suffix]){/([^\\\/]+)#{suffix}/})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
register_plugin(:path_matchers, PathMatchers)
|
50
|
+
end
|
51
|
+
end
|
data/lib/roda/plugins/render.rb
CHANGED
@@ -36,8 +36,8 @@ class Roda
|
|
36
36
|
# :layout :: The base name of the layout file, defaults to 'layout'.
|
37
37
|
# :layout_opts :: The options to use when rendering the layout, if different
|
38
38
|
# from the default options.
|
39
|
-
# :
|
40
|
-
#
|
39
|
+
# :template_opts :: The tilt options used when rendering templates, defaults to
|
40
|
+
# <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
|
41
41
|
# :views :: The directory holding the view files, defaults to 'views' in the
|
42
42
|
# current directory.
|
43
43
|
#
|
@@ -86,11 +86,16 @@ class Roda
|
|
86
86
|
# Setup default rendering options. See Render for details.
|
87
87
|
def self.configure(app, opts=OPTS)
|
88
88
|
if app.opts[:render]
|
89
|
-
app.opts[:render].merge
|
89
|
+
app.opts[:render] = app.opts[:render].merge(opts)
|
90
90
|
else
|
91
91
|
app.opts[:render] = opts.dup
|
92
92
|
end
|
93
93
|
|
94
|
+
if opts[:opts] && !opts[:template_opts]
|
95
|
+
RodaPlugins.deprecate("The render plugin :opts option is deprecated and will be removed in Roda 2. Switch to using the :template_opts option")
|
96
|
+
app.opts[:render][:template_opts] = opts[:opts]
|
97
|
+
end
|
98
|
+
|
94
99
|
opts = app.opts[:render]
|
95
100
|
opts[:engine] ||= "erb"
|
96
101
|
opts[:ext] = nil unless opts.has_key?(:ext)
|
@@ -103,15 +108,18 @@ class Roda
|
|
103
108
|
opts[:layout_opts] = opts[:layout_opts].merge(layout)
|
104
109
|
end
|
105
110
|
|
106
|
-
opts[:
|
107
|
-
|
108
|
-
if RUBY_VERSION >= "1.9" && !
|
109
|
-
|
111
|
+
template_opts = opts[:template_opts] = (opts[:template_opts] || {}).dup
|
112
|
+
template_opts[:outvar] ||= '@_out_buf'
|
113
|
+
if RUBY_VERSION >= "1.9" && !template_opts.has_key?(:default_encoding)
|
114
|
+
template_opts[:default_encoding] = Encoding.default_external
|
110
115
|
end
|
111
116
|
if opts[:escape]
|
112
|
-
|
117
|
+
template_opts[:engine_class] = ErubisEscaping::Eruby
|
113
118
|
end
|
114
119
|
opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, ENV['RACK_ENV'] != 'development')
|
120
|
+
opts.extend(RodaDeprecateMutation)
|
121
|
+
opts[:layout_opts].extend(RodaDeprecateMutation)
|
122
|
+
opts[:template_opts].extend(RodaDeprecateMutation)
|
115
123
|
end
|
116
124
|
|
117
125
|
module ClassMethods
|
@@ -120,10 +128,11 @@ class Roda
|
|
120
128
|
# affecting the parent class.
|
121
129
|
def inherited(subclass)
|
122
130
|
super
|
123
|
-
opts = subclass.opts[:render]
|
124
|
-
opts[:layout_opts] = opts[:layout_opts].dup
|
125
|
-
opts[:
|
131
|
+
opts = subclass.opts[:render].dup
|
132
|
+
opts[:layout_opts] = opts[:layout_opts].dup.extend(RodaDeprecateMutation)
|
133
|
+
opts[:template_opts] = opts[:template_opts].dup.extend(RodaDeprecateMutation)
|
126
134
|
opts[:cache] = thread_safe_cache if opts[:cache]
|
135
|
+
subclass.opts[:render] = opts.extend(RodaDeprecateMutation)
|
127
136
|
end
|
128
137
|
|
129
138
|
# Return the render options for this class.
|
@@ -137,8 +146,13 @@ class Roda
|
|
137
146
|
def render(template, opts = OPTS, &block)
|
138
147
|
opts = find_template(parse_template_opts(template, opts))
|
139
148
|
cached_template(opts) do
|
140
|
-
template_opts = render_opts[:
|
141
|
-
|
149
|
+
template_opts = render_opts[:template_opts]
|
150
|
+
current_template_opts = opts[:template_opts]
|
151
|
+
if opts[:opts] && !current_template_opts
|
152
|
+
RodaPlugins.deprecate("The render method :opts option is deprecated and will be removed in Roda 2. Switch to using the :template_opts option")
|
153
|
+
current_template_opts = opts[:opts]
|
154
|
+
end
|
155
|
+
template_opts = template_opts.merge(current_template_opts) if current_template_opts
|
142
156
|
opts[:template_class].new(opts[:path], 1, template_opts, &opts[:template_block])
|
143
157
|
end.render(self, (opts[:locals]||OPTS), &block)
|
144
158
|
end
|
@@ -199,7 +213,11 @@ class Roda
|
|
199
213
|
end
|
200
214
|
|
201
215
|
if render_opts[:cache]
|
202
|
-
template_opts = opts[:
|
216
|
+
template_opts = opts[:template_opts]
|
217
|
+
if opts[:opts] && !template_opts
|
218
|
+
RodaPlugins.deprecate("The render method :opts option is deprecated and will be removed in Roda 2. Switch to using the :template_opts option")
|
219
|
+
template_opts = opts[:opts]
|
220
|
+
end
|
203
221
|
template_block = opts[:template_block] if !content
|
204
222
|
|
205
223
|
key = if template_class || template_opts || template_block
|
@@ -36,9 +36,16 @@ class Roda
|
|
36
36
|
def run(_)
|
37
37
|
e = @env
|
38
38
|
path = @remaining_path
|
39
|
-
|
40
|
-
|
41
|
-
|
39
|
+
begin
|
40
|
+
script_name = e[SCRIPT_NAME]
|
41
|
+
path_info = e[PATH_INFO]
|
42
|
+
e[SCRIPT_NAME] += path_info.chomp(path)
|
43
|
+
e[PATH_INFO] = path
|
44
|
+
super
|
45
|
+
ensure
|
46
|
+
e[SCRIPT_NAME] = script_name
|
47
|
+
e[PATH_INFO] = path_info
|
48
|
+
end
|
42
49
|
end
|
43
50
|
|
44
51
|
private
|
@@ -54,7 +54,7 @@ class Roda
|
|
54
54
|
module ClassMethods
|
55
55
|
# Set the regexp to use for the given symbol, instead of the default.
|
56
56
|
def symbol_matcher(s, re)
|
57
|
-
|
57
|
+
self::RodaRequest.send(:define_method, :"match_symbol_#{s}"){re}
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
data/lib/roda/version.rb
CHANGED
@@ -1,3 +1,15 @@
|
|
1
1
|
class Roda
|
2
|
-
|
2
|
+
# The major version of Roda, updated only for major changes that are
|
3
|
+
# likely to require modification to Roda apps.
|
4
|
+
RodaMajorVersion = 1
|
5
|
+
|
6
|
+
# The minor version of Roda, updated for new feature releases of Roda.
|
7
|
+
RodaMinorVersion = 3
|
8
|
+
|
9
|
+
# The patch version of Roda, updated only for bug fixes from the last
|
10
|
+
# feature release.
|
11
|
+
RodaPatchVersion = 0
|
12
|
+
|
13
|
+
# The full version of Roda as a string.
|
14
|
+
RodaVersion = "#{RodaMajorVersion}.#{RodaMinorVersion}.#{RodaPatchVersion}".freeze
|
3
15
|
end
|
data/spec/freeze_spec.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
describe "Roda.freeze" do
|
4
|
+
before do
|
5
|
+
app{}.freeze
|
6
|
+
end
|
7
|
+
|
8
|
+
it "should make opts not be modifiable after calling finalize!" do
|
9
|
+
proc{app.opts[:foo] = 'bar'}.should raise_error
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should make use and route raise errors" do
|
13
|
+
proc{app.use Class.new}.should raise_error
|
14
|
+
proc{app.route{}}.should raise_error
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should make plugin raise errors" do
|
18
|
+
proc{app.plugin Module.new}.should raise_error
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should make subclassing raise errors" do
|
22
|
+
proc{Class.new(app)}.should raise_error
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should freeze app" do
|
26
|
+
app.frozen?.should == true
|
27
|
+
end
|
28
|
+
end
|
@@ -135,4 +135,30 @@ describe "class_level_routing plugin" do
|
|
135
135
|
status("/asdfa/asdf").should == 404
|
136
136
|
body("/asdfa/asdf").should == "nf"
|
137
137
|
end
|
138
|
+
|
139
|
+
it "works when freezing the app" do
|
140
|
+
app.freeze
|
141
|
+
body.should == 'root'
|
142
|
+
body('/foo').should == 'foo'
|
143
|
+
body('/foo/bar').should == 'foobar'
|
144
|
+
body('/dgo').should == 'bazgetgo'
|
145
|
+
body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
|
146
|
+
body('/bar').should == "x-get-bar"
|
147
|
+
body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
|
148
|
+
body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
|
149
|
+
body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
|
150
|
+
body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
|
151
|
+
body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
|
152
|
+
body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
|
153
|
+
body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
|
154
|
+
if ::Rack::Request.method_defined?("link?")
|
155
|
+
body('/bar', 'REQUEST_METHOD'=>'LINK').should == "x-link-bar"
|
156
|
+
body('/bar', 'REQUEST_METHOD'=>'UNLINK').should == "x-unlink-bar"
|
157
|
+
end
|
158
|
+
|
159
|
+
status.should == 200
|
160
|
+
status("/asdfa/asdf").should == 404
|
161
|
+
|
162
|
+
proc{app.on{}}.should raise_error
|
163
|
+
end
|
138
164
|
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
describe "cookies plugin" do
|
4
|
+
it "should set cookies on response" do
|
5
|
+
app(:cookies) do |r|
|
6
|
+
response.set_cookie("foo", "bar")
|
7
|
+
response.set_cookie("bar", "baz")
|
8
|
+
"Hello"
|
9
|
+
end
|
10
|
+
|
11
|
+
header('Set-Cookie').should == "foo=bar\nbar=baz"
|
12
|
+
body.should == 'Hello'
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should delete cookies on response" do
|
16
|
+
app(:cookies) do |r|
|
17
|
+
response.set_cookie("foo", "bar")
|
18
|
+
response.delete_cookie("foo")
|
19
|
+
"Hello"
|
20
|
+
end
|
21
|
+
|
22
|
+
header('Set-Cookie').should =~ /foo=; (max-age=0; )?expires=Thu, 01[ -]Jan[ -]1970 00:00:00 (-0000|GMT)/
|
23
|
+
body.should == 'Hello'
|
24
|
+
end
|
25
|
+
end
|
@@ -30,11 +30,10 @@ describe "default_headers plugin" do
|
|
30
30
|
req[1].should == h
|
31
31
|
end
|
32
32
|
|
33
|
-
it "should allow modifying the default headers
|
33
|
+
it "should allow modifying the default headers by reloading the plugin" do
|
34
34
|
app(:bare) do
|
35
|
-
plugin :default_headers
|
36
|
-
default_headers
|
37
|
-
default_headers['Foo'] = 'baz'
|
35
|
+
plugin :default_headers, 'Content-Type' => 'text/json'
|
36
|
+
plugin :default_headers, 'Foo' => 'baz'
|
38
37
|
|
39
38
|
route do |r|
|
40
39
|
r.halt response.finish_with_body([])
|
@@ -48,9 +47,7 @@ describe "default_headers plugin" do
|
|
48
47
|
h = {'Content-Type'=>'text/json', 'Foo'=>'bar'}
|
49
48
|
|
50
49
|
app(:bare) do
|
51
|
-
plugin :default_headers
|
52
|
-
default_headers['Content-Type'] = 'text/json'
|
53
|
-
default_headers['Foo'] = 'bar'
|
50
|
+
plugin :default_headers, h
|
54
51
|
|
55
52
|
route do |r|
|
56
53
|
r.halt response.finish_with_body([])
|