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.
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