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