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
@@ -0,0 +1,109 @@
1
+ = Preparation for Roda 2
2
+
3
+ * In Roda 2 (the next version), the PATH_INFO and SCRIPT_NAME
4
+ env variables will not be modified during routing. Instead,
5
+ Roda will use the static_path_info plugin behavior by default.
6
+ Users are strongly encouraged to use the static_path_info
7
+ plugin to make sure their apps will work with Roda 2.
8
+
9
+ * In Roda 2, Roda#initialize will take the env hash, and #call
10
+ will take the route block. The private #_route method will be
11
+ eliminated. This should not affect applications, but it will
12
+ affect plugins that override these methods.
13
+
14
+ * The issues mentioned below should all have deprecation warnings,
15
+ except where noted.
16
+
17
+ == New Plugins to Replace Deprecated Features
18
+
19
+ * RodaResponse#set_cookie and #delete_cookie have been moved to the
20
+ cookies plugin.
21
+
22
+ * Roda.request_module and .response_module have been moved to the
23
+ module_include plugin.
24
+
25
+ * Roda.hash_matcher has been moved to the hash_matcher plugin.
26
+
27
+ * The :extension hash matcher has been moved to the path_machers
28
+ plugin. This plugin also contains new :prefix and :suffix hash
29
+ matchers.
30
+
31
+ * The :param and :param! hash matchers have been moved to the
32
+ param_matchers plugin.
33
+
34
+ == Other Deprecation Issues
35
+
36
+ * The :opts render plugin option and :opts option to render and
37
+ view are now deprecated. Use the :template_opts option instead.
38
+
39
+ * RodaRequest#full_path_info has been deprecated, switch to using
40
+ #path.
41
+
42
+ * Mutating plugin option hashes for the chunked, default_headers,
43
+ error_email, json, and render plugins is now deprecated, these
44
+ option hashes will be frozen in Roda 2.
45
+
46
+ * The :header hash matcher in the header_matchers plugin now
47
+ gives a deprecation warning, because in Roda 2, the matcher will
48
+ yield the value of the header to the block. To get the new
49
+ behavior and silence the deprecation warning, you need to set an
50
+ option:
51
+
52
+ Roda.opts[:match_header_yield] = true
53
+
54
+ * Mutating json_result_classes directly in the json plugin is now
55
+ deprecated. You should now pass a :classes option to the plugin
56
+ specifying the classes to convert, if you want to handle classes
57
+ other than Array and Hash. There will not be a deprecation warning
58
+ if you attempt to mutate json_result_classes.
59
+
60
+ = New Features
61
+
62
+ * The Roda.freeze method now freezes internal datastructures to avoid
63
+ thread safety issues. The plugins that ship with Roda will freeze
64
+ their datastructures when Roda.freeze is called. It is recommended
65
+ that production applications call freeze on the application after
66
+ fully loading it, especially if they are using a threaded webserver
67
+ and a non-MRI ruby.
68
+
69
+ * A delete_empty_headers plugin has been added that automatically
70
+ deletes headers set to the empty string. This makes it simpler to
71
+ delete default headers if they shouldn't be set for a specific
72
+ request.
73
+
74
+ * A class_delegate method has been added to the delegate plugin. This
75
+ makes it easier to create instance methods that call class methods.
76
+
77
+ * Roda::RodaMajorVersion, RodaMinorVersion, and RodaPatchVersion
78
+ constants have been added.
79
+
80
+ = Other Improvements
81
+
82
+ * The error_handler plugin now uses a new response instead of reusing
83
+ the existing response. This fixes cases where changing the
84
+ Content-Type and then raising an exception would result in a error
85
+ page that used the previously set Content-Type.
86
+
87
+ * The not_found plugin now clears previous set headers before calling
88
+ not found. This fixes cases where Content-Type was set previously,
89
+ and also fixes an incorrect Content-Length being used for the
90
+ response.
91
+
92
+ * The static_path_info plugin now restores the original SCRIPT_NAME
93
+ and PATH_INFO before returning from r.run, fixing usage with some
94
+ middleware.
95
+
96
+ * The multi_run plugin now works when subclassing the app.
97
+
98
+ * The default_headers plugin is now faster by skipping an unnecessary
99
+ hash duplication.
100
+
101
+ * A Gemfile has been added to make development slightly easier.
102
+
103
+ = Backwards Compatibility
104
+
105
+ * RodaResponse is no longer a subclass of Rack::Response, it is now a
106
+ subclass of Object. This shouldn't have an effect unless you were
107
+ calling a method on an instance that was defined by Rack::Response
108
+ and not RodaResponse, but most of those methods would have raised
109
+ exceptions.
data/lib/roda.rb CHANGED
@@ -41,7 +41,7 @@ class Roda
41
41
  # Base class used for Roda requests. The instance methods for this
42
42
  # class are added by Roda::RodaPlugins::Base::RequestMethods, the
43
43
  # class methods are added by Roda::RodaPlugins::Base::RequestClassMethods.
44
- class RodaRequest < ::Rack::Request;
44
+ class RodaRequest < ::Rack::Request
45
45
  @roda_class = ::Roda
46
46
  @match_pattern_cache = ::Roda::RodaCache.new
47
47
  end
@@ -49,7 +49,7 @@ class Roda
49
49
  # Base class used for Roda responses. The instance methods for this
50
50
  # class are added by Roda::RodaPlugins::Base::ResponseMethods, the class
51
51
  # methods are added by Roda::RodaPlugins::Base::ResponseClassMethods.
52
- class RodaResponse < ::Rack::Response;
52
+ class RodaResponse
53
53
  @roda_class = ::Roda
54
54
  end
55
55
 
@@ -59,6 +59,18 @@ class Roda
59
59
  @opts = {}
60
60
  @route_block = nil
61
61
 
62
+ module RodaDeprecateMutation
63
+ [:[]=, :clear, :compare_by_identity, :default=, :default_proc=, :delete, :delete_if,
64
+ :keep_if, :merge!, :reject!, :replace, :select!, :shift, :store, :update].each do |m|
65
+ class_eval(<<-END, __FILE__, __LINE__+1)
66
+ def #{m}(*)
67
+ RodaPlugins.deprecate("Mutating this hash (\#{inspect}) via the #{m} method is deprecated, this hash will be frozen in Roda 2.")
68
+ super
69
+ end
70
+ END
71
+ end
72
+ end
73
+
62
74
  # Module in which all Roda plugins should be stored. Also contains logic for
63
75
  # registering and loading plugins.
64
76
  module RodaPlugins
@@ -86,6 +98,12 @@ class Roda
86
98
  @plugins[name] = mod
87
99
  end
88
100
 
101
+ # Emit a deprecation message. By default this just calls warn. You can override this
102
+ # method to log deprecation messages to a file or include backtraces (or something else).
103
+ def self.deprecate(msg)
104
+ warn(msg)
105
+ end
106
+
89
107
  # The base plugin for Roda, implementing all default functionality.
90
108
  # Methods are put into a plugin so future plugins can easily override
91
109
  # them and call super to get the default behavior.
@@ -120,29 +138,28 @@ class Roda
120
138
  build_rack_app
121
139
  end
122
140
 
123
- # Create a match_#{key} method in the request class using the given
124
- # block, so that using a hash key in a request match method will
125
- # call the block. The block should return nil or false to not
126
- # match, and anything else to match.
127
- #
128
- # class App < Roda
129
- # hash_matcher(:foo) do |v|
130
- # self['foo'] == v
131
- # end
141
+ # Freeze the internal state of the class, to avoid thread safety issues at runtime.
142
+ # It's optional to call this method, as nothing should be modifying the
143
+ # internal state at runtime anyway, but this makes sure an exception will
144
+ # be raised if you try to modify the internal state after calling this.
132
145
  #
133
- # route do
134
- # r.on :foo=>'bar' do
135
- # # matches when param foo has value bar
136
- # end
137
- # end
138
- # end
146
+ # Note that freezing the class prevents you from subclassing it, mostly because
147
+ # it would cause some plugins to break.
148
+ def freeze
149
+ @opts.freeze
150
+ @middleware.freeze
151
+ super
152
+ end
153
+
139
154
  def hash_matcher(key, &block)
140
- request_module{define_method(:"match_#{key}", &block)}
155
+ RodaPlugins.deprecate("Roda.hash_matcher is deprecated and will be removed in Roda 2. It has been moved to the hash_matcher plugin.")
156
+ self::RodaRequest.send(:define_method, :"match_#{key}", &block)
141
157
  end
142
158
 
143
159
  # When inheriting Roda, copy the shared data into the subclass,
144
160
  # and setup the request and response subclasses.
145
161
  def inherited(subclass)
162
+ raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
146
163
  super
147
164
  subclass.instance_variable_set(:@inherit_middleware, @inherit_middleware)
148
165
  subclass.instance_variable_set(:@middleware, @inherit_middleware ? @middleware.dup : [])
@@ -150,6 +167,9 @@ class Roda
150
167
  subclass.opts.to_a.each do |k,v|
151
168
  if (v.is_a?(Array) || v.is_a?(Hash)) && !v.frozen?
152
169
  subclass.opts[k] = v.dup
170
+ if v.is_a?(RodaDeprecateMutation)
171
+ subclass.opts[k].extend(RodaDeprecateMutation)
172
+ end
153
173
  end
154
174
  end
155
175
  subclass.instance_variable_set(:@route_block, @route_block)
@@ -172,73 +192,25 @@ class Roda
172
192
  # Roda.plugin PluginModule
173
193
  # Roda.plugin :csrf
174
194
  def plugin(plugin, *args, &block)
175
- if plugin.is_a?(Symbol)
176
- plugin = RodaPlugins.load_plugin(plugin)
177
- end
178
-
179
- if plugin.respond_to?(:load_dependencies)
180
- plugin.load_dependencies(self, *args, &block)
181
- end
182
-
183
- if defined?(plugin::InstanceMethods)
184
- include(plugin::InstanceMethods)
185
- end
186
- if defined?(plugin::ClassMethods)
187
- extend(plugin::ClassMethods)
188
- end
189
- if defined?(plugin::RequestMethods)
190
- self::RodaRequest.send(:include, plugin::RequestMethods)
191
- end
192
- if defined?(plugin::RequestClassMethods)
193
- self::RodaRequest.extend(plugin::RequestClassMethods)
194
- end
195
- if defined?(plugin::ResponseMethods)
196
- self::RodaResponse.send(:include, plugin::ResponseMethods)
197
- end
198
- if defined?(plugin::ResponseClassMethods)
199
- self::RodaResponse.extend(plugin::ResponseClassMethods)
200
- end
201
-
202
- if plugin.respond_to?(:configure)
203
- plugin.configure(self, *args, &block)
204
- end
195
+ raise RodaError, "Cannot subclass a frozen Roda class" if frozen?
196
+ plugin = RodaPlugins.load_plugin(plugin) if plugin.is_a?(Symbol)
197
+ plugin.load_dependencies(self, *args, &block) if plugin.respond_to?(:load_dependencies)
198
+ include(plugin::InstanceMethods) if defined?(plugin::InstanceMethods)
199
+ extend(plugin::ClassMethods) if defined?(plugin::ClassMethods)
200
+ self::RodaRequest.send(:include, plugin::RequestMethods) if defined?(plugin::RequestMethods)
201
+ self::RodaRequest.extend(plugin::RequestClassMethods) if defined?(plugin::RequestClassMethods)
202
+ self::RodaResponse.send(:include, plugin::ResponseMethods) if defined?(plugin::ResponseMethods)
203
+ self::RodaResponse.extend(plugin::ResponseClassMethods) if defined?(plugin::ResponseClassMethods)
204
+ plugin.configure(self, *args, &block) if plugin.respond_to?(:configure)
205
205
  end
206
206
 
207
- # Include the given module in the request class. If a block
208
- # is provided instead of a module, create a module using the
209
- # the block. Example:
210
- #
211
- # Roda.request_module SomeModule
212
- #
213
- # Roda.request_module do
214
- # def description
215
- # "#{request_method} #{path_info}"
216
- # end
217
- # end
218
- #
219
- # Roda.route do |r|
220
- # r.description
221
- # end
222
207
  def request_module(mod = nil, &block)
208
+ RodaPlugins.deprecate("Roda.request_module is deprecated and will be removed in Roda 2. It has been moved to the module_include plugin.")
223
209
  module_include(:request, mod, &block)
224
210
  end
225
211
 
226
- # Include the given module in the response class. If a block
227
- # is provided instead of a module, create a module using the
228
- # the block. Example:
229
- #
230
- # Roda.response_module SomeModule
231
- #
232
- # Roda.response_module do
233
- # def error!
234
- # self.status = 500
235
- # end
236
- # end
237
- #
238
- # Roda.route do |r|
239
- # response.error!
240
- # end
241
212
  def response_module(mod = nil, &block)
213
+ RodaPlugins.deprecate("Roda.response_module is deprecated and will be removed in Roda 2. It has been moved to the module_include plugin.")
242
214
  module_include(:response, mod, &block)
243
215
  end
244
216
 
@@ -271,7 +243,7 @@ class Roda
271
243
  #
272
244
  # Roda.use Rack::Session::Cookie, :secret=>ENV['secret']
273
245
  def use(*args, &block)
274
- @middleware << [args, block]
246
+ @middleware << [args, block].freeze
275
247
  build_rack_app
276
248
  end
277
249
 
@@ -287,7 +259,7 @@ class Roda
287
259
  end
288
260
  end
289
261
 
290
- # Backbone of the request_module and response_module support.
262
+ # REMOVE20
291
263
  def module_include(type, mod)
292
264
  if type == :response
293
265
  klass = self::RodaResponse
@@ -357,7 +329,7 @@ class Roda
357
329
  end
358
330
 
359
331
  # The session hash for the current request. Raises RodaError
360
- # if no session existsExample:
332
+ # if no session exists. Example:
361
333
  #
362
334
  # session # => {}
363
335
  def session
@@ -599,7 +571,11 @@ class Roda
599
571
  e = @env
600
572
  "#{e[SCRIPT_NAME]}#{e[PATH_INFO]}"
601
573
  end
602
- alias full_path_info path
574
+
575
+ def full_path_info
576
+ RodaPlugins.deprecate("RodaRequest#full_path_info is deprecated and will be removed in Roda 2. Switch to using #path.")
577
+ path
578
+ end
603
579
 
604
580
  # The current path to match requests against. This is the same as PATH_INFO
605
581
  # in the environment, which gets updated as the request is being routed.
@@ -649,7 +625,7 @@ class Roda
649
625
  # response.status = 200
650
626
  # response['Header-Name'] = 'Header value'
651
627
  def response
652
- scope.response
628
+ @scope.response
653
629
  end
654
630
 
655
631
  # Return the Roda class related to this request.
@@ -888,9 +864,8 @@ class Roda
888
864
  args.all?{|arg| match(arg)}
889
865
  end
890
866
 
891
- # Match files with the given extension. Requires that the
892
- # request path end with the extension.
893
867
  def match_extension(ext)
868
+ RodaPlugins.deprecate("The :extension matcher is deprecated and will be removed in Roda 2. It has been moved to the path_matchers plugin.")
894
869
  consume(self.class.cached_matcher([:extension, ext]){/([^\\\/]+)\.#{ext}/})
895
870
  end
896
871
 
@@ -904,17 +879,15 @@ class Roda
904
879
  end
905
880
  end
906
881
 
907
- # Match the given parameter if present, even if the parameter is empty.
908
- # Adds any match to the captures.
909
882
  def match_param(key)
883
+ RodaPlugins.deprecate("The :param matcher is deprecated and will be removed in Roda 2. It has been moved to the param_matchers plugin.")
910
884
  if v = self[key]
911
885
  @captures << v
912
886
  end
913
887
  end
914
888
 
915
- # Match the given parameter if present and not empty.
916
- # Adds any match to the captures.
917
889
  def match_param!(key)
890
+ RodaPlugins.deprecate("The :param! matcher is deprecated and will be removed in Roda 2. It has been moved to the param_matchers plugin.")
918
891
  if (v = self[key]) && !v.empty?
919
892
  @captures << v
920
893
  end
@@ -949,6 +922,9 @@ class Roda
949
922
  DEFAULT_HEADERS = {"Content-Type" => "text/html".freeze}.freeze
950
923
  LOCATION = "Location".freeze
951
924
 
925
+ # The body for the current response.
926
+ attr_reader :body
927
+
952
928
  # The hash of response headers for the current response.
953
929
  attr_reader :headers
954
930
 
@@ -983,14 +959,8 @@ class Roda
983
959
  DEFAULT_HEADERS
984
960
  end
985
961
 
986
- # Modify the headers to include a Set-Cookie value that
987
- # deletes the cookie. A value hash can be provided to
988
- # override the default one used to delete the cookie.
989
- # Example:
990
- #
991
- # response.delete_cookie('foo')
992
- # response.delete_cookie('foo', :domain=>'example.org')
993
962
  def delete_cookie(key, value = {})
963
+ RodaPlugins.deprecate("RodaResponse#delete_cookie is deprecated and will be removed in Roda 2. It has been moved to the cookies plugin.")
994
964
  ::Rack::Utils.delete_cookie_header!(@headers, key, value)
995
965
  end
996
966
 
@@ -1055,11 +1025,8 @@ class Roda
1055
1025
  self.class.roda_class
1056
1026
  end
1057
1027
 
1058
- # Set the cookie with the given key in the headers.
1059
- #
1060
- # response.set_cookie('foo', 'bar')
1061
- # response.set_cookie('foo', :value=>'bar', :domain=>'example.org')
1062
1028
  def set_cookie(key, value)
1029
+ RodaPlugins.deprecate("RodaResponse#set_cookie is deprecated and will be removed in Roda 2. It has been moved to the cookies plugin.")
1063
1030
  ::Rack::Utils.set_cookie_header!(@headers, key, value)
1064
1031
  end
1065
1032
 
@@ -197,7 +197,7 @@ class Roda
197
197
  # and compressing files (default: false)
198
198
  # :css_dir :: Directory name containing your css source, inside :path (default: 'css')
199
199
  # :css_headers :: A hash of additional headers for your rendered css files
200
- # :css_opts :: Template options to pass to the render plugin (via :opts) when rendering css assets
200
+ # :css_opts :: Template options to pass to the render plugin (via :template_opts) when rendering css assets
201
201
  # :css_route :: Route under :prefix for css assets (default: :css_dir)
202
202
  # :dependencies :: A hash of dependencies for your asset files. Keys should be paths to asset files,
203
203
  # values should be arrays of paths your asset files depends on. This is used to
@@ -207,7 +207,7 @@ class Roda
207
207
  # :headers :: A hash of additional headers for both js and css rendered files
208
208
  # :js_dir :: Directory name containing your javascript source, inside :path (default: 'js')
209
209
  # :js_headers :: A hash of additional headers for your rendered javascript files
210
- # :js_opts :: Template options to pass to the render plugin (via :opts) when rendering javascript assets
210
+ # :js_opts :: Template options to pass to the render plugin (via :template_opts) when rendering javascript assets
211
211
  # :js_route :: Route under :prefix for javascript assets (default: :js_dir)
212
212
  # :path :: Path to your asset source directory (default: 'assets')
213
213
  # :prefix :: Prefix for assets path in your URL/routes (default: 'assets')
@@ -242,7 +242,7 @@ class Roda
242
242
 
243
243
  # Load the render and caching plugins plugins, since the assets plugin
244
244
  # depends on them.
245
- def self.load_dependencies(app, _opts = {})
245
+ def self.load_dependencies(app, _opts = nil)
246
246
  app.plugin :render
247
247
  app.plugin :caching
248
248
  end
@@ -520,7 +520,7 @@ class Roda
520
520
  if file.end_with?(".#{type}")
521
521
  ::File.read(file)
522
522
  else
523
- render_asset_file(file, :opts=>self.class.assets_opts[:"#{type}_opts"])
523
+ render_asset_file(file, :template_opts=>self.class.assets_opts[:"#{type}_opts"])
524
524
  end
525
525
  end
526
526
 
@@ -141,7 +141,10 @@ class Roda
141
141
  # :headers :: Set default additional headers to use when calling view
142
142
  def self.configure(app, opts=OPTS)
143
143
  app.opts[:chunk_by_default] = opts[:chunk_by_default]
144
- app.opts[:chunk_headers] = opts[:headers]
144
+ if opts[:headers]
145
+ app.opts[:chunk_headers] = (app.opts[:chunk_headers] || {}).merge(opts[:headers])
146
+ app.opts[:chunk_headers].extend(RodaDeprecateMutation)
147
+ end
145
148
  end
146
149
 
147
150
  # Rack response body instance for chunked responses