roda 1.2.0 → 1.3.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.
- 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([])
|