roda 1.2.0 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +42 -0
  3. data/README.rdoc +73 -144
  4. data/doc/conventions.rdoc +10 -8
  5. data/doc/release_notes/1.3.0.txt +109 -0
  6. data/lib/roda.rb +67 -100
  7. data/lib/roda/plugins/assets.rb +4 -4
  8. data/lib/roda/plugins/chunked.rb +4 -1
  9. data/lib/roda/plugins/class_level_routing.rb +7 -1
  10. data/lib/roda/plugins/cookies.rb +34 -0
  11. data/lib/roda/plugins/default_headers.rb +7 -6
  12. data/lib/roda/plugins/delegate.rb +8 -1
  13. data/lib/roda/plugins/delete_empty_headers.rb +33 -0
  14. data/lib/roda/plugins/delete_nil_headers.rb +34 -0
  15. data/lib/roda/plugins/environments.rb +12 -4
  16. data/lib/roda/plugins/error_email.rb +6 -1
  17. data/lib/roda/plugins/error_handler.rb +7 -4
  18. data/lib/roda/plugins/hash_matcher.rb +32 -0
  19. data/lib/roda/plugins/header_matchers.rb +12 -2
  20. data/lib/roda/plugins/json.rb +9 -6
  21. data/lib/roda/plugins/module_include.rb +92 -0
  22. data/lib/roda/plugins/multi_route.rb +7 -0
  23. data/lib/roda/plugins/multi_run.rb +11 -5
  24. data/lib/roda/plugins/named_templates.rb +7 -1
  25. data/lib/roda/plugins/not_found.rb +6 -0
  26. data/lib/roda/plugins/param_matchers.rb +43 -0
  27. data/lib/roda/plugins/path_matchers.rb +51 -0
  28. data/lib/roda/plugins/render.rb +32 -14
  29. data/lib/roda/plugins/static_path_info.rb +10 -3
  30. data/lib/roda/plugins/symbol_matchers.rb +1 -1
  31. data/lib/roda/version.rb +13 -1
  32. data/spec/freeze_spec.rb +28 -0
  33. data/spec/plugin/class_level_routing_spec.rb +26 -0
  34. data/spec/plugin/content_for_spec.rb +1 -2
  35. data/spec/plugin/cookies_spec.rb +25 -0
  36. data/spec/plugin/default_headers_spec.rb +4 -7
  37. data/spec/plugin/delegate_spec.rb +4 -1
  38. data/spec/plugin/delete_empty_headers_spec.rb +15 -0
  39. data/spec/plugin/error_handler_spec.rb +31 -0
  40. data/spec/plugin/hash_matcher_spec.rb +27 -0
  41. data/spec/plugin/header_matchers_spec.rb +15 -0
  42. data/spec/plugin/json_spec.rb +1 -2
  43. data/spec/plugin/mailer_spec.rb +2 -2
  44. data/spec/plugin/module_include_spec.rb +31 -0
  45. data/spec/plugin/multi_route_spec.rb +14 -0
  46. data/spec/plugin/multi_run_spec.rb +41 -0
  47. data/spec/plugin/named_templates_spec.rb +25 -0
  48. data/spec/plugin/not_found_spec.rb +29 -0
  49. data/spec/plugin/param_matchers_spec.rb +37 -0
  50. data/spec/plugin/path_matchers_spec.rb +42 -0
  51. data/spec/plugin/render_spec.rb +33 -8
  52. data/spec/plugin/static_path_info_spec.rb +6 -0
  53. data/spec/plugin/view_subdirs_spec.rb +1 -2
  54. data/spec/response_spec.rb +12 -0
  55. data/spec/spec_helper.rb +2 -0
  56. data/spec/version_spec.rb +8 -2
  57. 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.instance_eval do
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
- attr_reader :multi_run_apps
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
- @multi_run_apps[prefix.to_s] = app
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
@@ -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
- # :opts :: The tilt options used when rendering templates, defaults to
40
- # <tt>{:outvar=>'@_out_buf', :default_encoding=>Encoding.default_external}</tt>.
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!(opts)
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[:opts] ||= (opts[:opts] || {}).dup
107
- opts[:opts][:outvar] ||= '@_out_buf'
108
- if RUBY_VERSION >= "1.9" && !opts[:opts].has_key?(:default_encoding)
109
- opts[:opts][:default_encoding] = Encoding.default_external
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
- opts[:opts][:engine_class] = ErubisEscaping::Eruby
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[:opts] = opts[:opts].dup
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[:opts]
141
- template_opts = template_opts.merge(opts[:opts]) if opts[:opts]
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[: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
- e[SCRIPT_NAME] += e[PATH_INFO].chomp(path)
40
- e[PATH_INFO] = path
41
- super
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
- request_module{define_method(:"match_symbol_#{s}"){re}}
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
- RodaVersion = '1.2.0'.freeze
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
@@ -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
@@ -8,8 +8,7 @@ else
8
8
  describe "content_for plugin" do
9
9
  before do
10
10
  app(:bare) do
11
- plugin :render
12
- render_opts[:views] = "./spec/views"
11
+ plugin :render, :views=>'./spec/views'
13
12
  plugin :content_for
14
13
 
15
14
  route do |r|
@@ -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 at a later point" do
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['Content-Type'] = 'text/json'
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([])
@@ -7,9 +7,12 @@ describe "delegate plugin" do
7
7
  request_delegate :root
8
8
  response_delegate :headers
9
9
 
10
+ def self.a; 'foo'; end
11
+ class_delegate :a
12
+
10
13
  route do
11
14
  root do
12
- headers['Content-Type'] = 'foo'
15
+ headers['Content-Type'] = a
13
16
  end
14
17
  end
15
18
  end