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
@@ -58,9 +58,15 @@ class Roda
58
58
  # Define routing methods that will store class level routes.
59
59
  [:root, :on, :is, :get, :post, :delete, :head, :options, :link, :patch, :put, :trace, :unlink].each do |meth|
60
60
  define_method(meth) do |*args, &block|
61
- opts[:class_level_routes] << [meth, args, block]
61
+ opts[:class_level_routes] << [meth, args, block].freeze
62
62
  end
63
63
  end
64
+
65
+ # Freeze the class level routes so that there can be no thread safety issues at runtime.
66
+ def freeze
67
+ opts[:class_level_routes].freeze
68
+ super
69
+ end
64
70
  end
65
71
 
66
72
  module InstanceMethods
@@ -0,0 +1,34 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The cookies plugin adds response methods for handling cookies.
4
+ # Currently, you can set cookies with +set_cookie+ and delete cookies
5
+ # with +delete_cookie+:
6
+ #
7
+ # response.set_cookie('foo', 'bar')
8
+ # response.delete_cookie('foo')
9
+ module Cookies
10
+ module ResponseMethods
11
+ # Modify the headers to include a Set-Cookie value that
12
+ # deletes the cookie. A value hash can be provided to
13
+ # override the default one used to delete the cookie.
14
+ # Example:
15
+ #
16
+ # response.delete_cookie('foo')
17
+ # response.delete_cookie('foo', :domain=>'example.org')
18
+ def delete_cookie(key, value = {})
19
+ ::Rack::Utils.delete_cookie_header!(@headers, key, value)
20
+ end
21
+
22
+ # Set the cookie with the given key in the headers.
23
+ #
24
+ # response.set_cookie('foo', 'bar')
25
+ # response.set_cookie('foo', :value=>'bar', :domain=>'example.org')
26
+ def set_cookie(key, value)
27
+ ::Rack::Utils.set_cookie_header!(@headers, key, value)
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(:cookies, Cookies)
33
+ end
34
+ end
@@ -12,15 +12,16 @@ class Roda
12
12
  #
13
13
  # plugin :default_headers, 'Content-Type'=>'text/csv'
14
14
  #
15
- # You can also modify the default headers later:
15
+ # You can modify the default headers later by loading the
16
+ # plugin again:
16
17
  #
17
- # plugin :default_headers
18
- # default_headers['Foo'] = 'bar'
19
- # default_headers.merge!('Bar'=>'baz')
18
+ # plugin :default_headers, 'Foo'=>'bar'
19
+ # plugin :default_headers, 'Bar'=>'baz'
20
20
  module DefaultHeaders
21
21
  # Merge the given headers into the existing default headers, if any.
22
22
  def self.configure(app, headers={})
23
- (app.opts[:default_headers] ||= {}).merge!(headers)
23
+ app.opts[:default_headers] = (app.opts[:default_headers] || {}).merge(headers)
24
+ app.opts[:default_headers].extend(RodaDeprecateMutation)
24
25
  end
25
26
 
26
27
  module ClassMethods
@@ -33,7 +34,7 @@ class Roda
33
34
  module ResponseMethods
34
35
  # Get the default headers from the related roda class.
35
36
  def default_headers
36
- roda_class.default_headers.dup
37
+ roda_class.default_headers
37
38
  end
38
39
  end
39
40
  end
@@ -2,7 +2,7 @@ class Roda
2
2
  module RodaPlugins
3
3
  # The delegate plugin allows you to easily setup instance methods in
4
4
  # the scope of the route block that call methods on the related
5
- # request or response, which may offer a simpler API in some cases.
5
+ # request, response, or class which may offer a simpler API in some cases.
6
6
  # Roda doesn't automatically setup such delegate methods because
7
7
  # it pollutes the application's method namespace, but this plugin
8
8
  # allows the user to do so.
@@ -43,6 +43,13 @@ class Roda
43
43
  # end
44
44
  module Delegate
45
45
  module ClassMethods
46
+ # Delegate the given methods to the class
47
+ def class_delegate(*meths)
48
+ meths.each do |meth|
49
+ define_method(meth){|*a, &block| self.class.send(meth, *a, &block)}
50
+ end
51
+ end
52
+
46
53
  # Delegate the given methods to the request
47
54
  def request_delegate(*meths)
48
55
  meths.each do |meth|
@@ -0,0 +1,33 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The delete_empty_headers plugin deletes any headers whose
4
+ # value is set to the empty string. Because of how default headers are
5
+ # set in Roda, if you have a default header but don't want
6
+ # to set it for a specific request, you need to use this plugin
7
+ # and set the header value to the empty string, and Roda will automatically
8
+ # delete the header.
9
+ module DeleteEmptyHeaders
10
+ module ResponseMethods
11
+ # Delete any empty headers when calling finish
12
+ def finish
13
+ delelete_empty_headers(super)
14
+ end
15
+
16
+ # Delete any empty headers when calling finish_with_body
17
+ def finish_with_body(_)
18
+ delelete_empty_headers(super)
19
+ end
20
+
21
+ private
22
+
23
+ # Delete any empty headers from response
24
+ def delelete_empty_headers(res)
25
+ res[1].delete_if{|_, v| v.is_a?(String) && v.empty?}
26
+ res
27
+ end
28
+ end
29
+ end
30
+
31
+ register_plugin(:delete_empty_headers, DeleteEmptyHeaders)
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The delete_nil_headers plugin deletes any headers whose
4
+ # value is set to nil. Because of how default headers are
5
+ # set in Roda, if you have a default header but don't want
6
+ # to set it for a specific request, you need to use this plugin
7
+ # and set the value to nil so that
8
+ #
9
+ # The following example will return "&lt;foo&gt;" as the body.
10
+ #
11
+ # plugin :h
12
+ #
13
+ # route do |r|
14
+ # h('<foo>')
15
+ # end
16
+ module DeleteHeaders
17
+ module ResponseMethods
18
+ def finish
19
+ res = super
20
+ res[1].delete_if{|_, v| v.nil?}
21
+ res
22
+ end
23
+
24
+ def finish_with_body(_)
25
+ res = super
26
+ res[1].delete_if{|_, v| v.nil?}
27
+ res
28
+ end
29
+ end
30
+ end
31
+
32
+ register_plugin(:delete_headers, DeleteHeaders)
33
+ end
34
+ end
@@ -45,10 +45,6 @@ class Roda
45
45
  end
46
46
 
47
47
  module ClassMethods
48
- # The current environment for the application, which should be stored
49
- # as a symbol.
50
- attr_accessor :environment
51
-
52
48
  # If no environments are given or one of the given environments
53
49
  # matches the current environment, yield the receiver to the block.
54
50
  def configure(*envs)
@@ -57,6 +53,18 @@ class Roda
57
53
  end
58
54
  end
59
55
 
56
+ # The current environment for the application, which should be stored
57
+ # as a symbol.
58
+ def environment
59
+ opts[:environment]
60
+ end
61
+
62
+ # Override the environment for the application, instead of using
63
+ # RACK_ENV.
64
+ def environment=(v)
65
+ opts[:environment] = v
66
+ end
67
+
60
68
  [:development, :test, :production].each do |env|
61
69
  define_method("#{env}?"){environment == env}
62
70
  end
@@ -77,10 +77,13 @@ END
77
77
  def self.configure(app, opts={})
78
78
  email_opts = app.opts[:error_email] ||= DEFAULTS
79
79
  email_opts = email_opts.merge(opts)
80
+ email_opts[:headers] = email_opts[:headers].dup
80
81
  unless email_opts[:to] && email_opts[:from]
81
82
  raise RodaError, "must provide :to and :from options to error_email plugin"
82
83
  end
83
84
  app.opts[:error_email] = email_opts
85
+ app.opts[:error_email].extend(RodaDeprecateMutation)
86
+ app.opts[:error_email][:headers].extend(RodaDeprecateMutation)
84
87
  end
85
88
 
86
89
  module ClassMethods
@@ -88,7 +91,9 @@ END
88
91
  # the superclass.
89
92
  def inherited(subclass)
90
93
  super
91
- subclass.opts[:error_email][:headers] = subclass.opts[:error_email][:headers].dup
94
+ opts = subclass.opts[:error_email].dup
95
+ opts[:headers] = opts[:headers].dup.extend(RodaDeprecateMutation)
96
+ subclass.opts[:error_email] = opts.extend(RodaDeprecateMutation)
92
97
  end
93
98
  end
94
99
 
@@ -21,9 +21,11 @@ class Roda
21
21
  # In both cases, the exception instance is passed into the block,
22
22
  # and the block can return the request body via a string.
23
23
  #
24
- # If an exception is raised, the response status will be set to 500
25
- # before executing the error handler. The error handler can change
26
- # the response status if necessary.
24
+ # If an exception is raised, a new response will be used, with the
25
+ # default status set to 500, before executing the error handler.
26
+ # The error handler can change the response status if necessary,
27
+ # as well set headers and/or write to the body, just like a regular
28
+ # request.
27
29
  module ErrorHandler
28
30
  # If a block is given, automatically call the +error+ method on
29
31
  # the Roda class with it.
@@ -51,7 +53,8 @@ class Roda
51
53
  def _route
52
54
  super
53
55
  rescue => e
54
- @_response.status = 500
56
+ res = @_response = self.class::RodaResponse.new
57
+ res.status = 500
55
58
  super{handle_error(e)}
56
59
  end
57
60
 
@@ -0,0 +1,32 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The hash_matcher plugin adds the hash_matcher class method, which
4
+ # allows for easily defining hash matchers:
5
+ #
6
+ # class App < Roda
7
+ # hash_matcher(:foo) do |v|
8
+ # self['foo'] == v
9
+ # end
10
+ #
11
+ # route do
12
+ # r.on :foo=>'bar' do
13
+ # # matches when param foo has value bar
14
+ # end
15
+ # end
16
+ # end
17
+ module HashMatcher
18
+ module ClassMethods
19
+ # Create a match_#{key} method in the request class using the given
20
+ # block, so that using a hash key in a request match method will
21
+ # call the block. The block should return nil or false to not
22
+ # match, and anything else to match. See the HashMatcher module
23
+ # documentation for an example.
24
+ def hash_matcher(key, &block)
25
+ self::RodaRequest.send(:define_method, :"match_#{key}", &block)
26
+ end
27
+ end
28
+ end
29
+
30
+ register_plugin(:hash_matcher, HashMatcher)
31
+ end
32
+ end
@@ -15,6 +15,8 @@ class Roda
15
15
  #
16
16
  # r.on :host=>'foo.example.com' do
17
17
  # end
18
+ # r.on :host=>/\A\w+.example.com/ do
19
+ # end
18
20
  #
19
21
  # It adds a +:user_agent+ matcher for matching on a user agent patterns, which
20
22
  # yields the regexp captures to the block:
@@ -42,10 +44,18 @@ class Roda
42
44
 
43
45
  # Match if the given uppercase key is present inside the environment.
44
46
  def match_header(key)
45
- @env[key.upcase.tr("-","_")]
47
+ if v = @env[key.upcase.tr("-","_")]
48
+ if roda_class.opts[:match_header_yield]
49
+ @captures << v
50
+ else
51
+ RodaPlugins.deprecate("The :header hash matcher will yield the header value in Roda 2. To turn on the Roda 2 behavior, set opts[:match_header_yield] to true for your Roda class.")
52
+ end
53
+ end
54
+ v
46
55
  end
47
56
 
48
- # Match if the host of the request is the same as the hostname.
57
+ # Match if the host of the request is the same as the hostname. +hostname+
58
+ # can be a regexp or a string.
49
59
  def match_host(hostname)
50
60
  hostname === host
51
61
  end
@@ -28,15 +28,18 @@ class Roda
28
28
  # end
29
29
  #
30
30
  # By default, only arrays and hashes are handled, but you
31
- # can automatically convert other types to json by adding
32
- # them to json_result_classes:
31
+ # can specifically set the allowed classes to json by adding
32
+ # using the :classes option when loading the plugin:
33
33
  #
34
- # plugin :json
35
- # json_result_classes << Sequel::Model
34
+ # plugin :json, :classes=>[Array, Hash, Sequel::Model]
36
35
  module Json
37
36
  # Set the classes to automatically convert to JSON
38
- def self.configure(app)
39
- app.opts[:json_result_classes] ||= [Array, Hash]
37
+ def self.configure(app, opts={})
38
+ classes = opts[:classes] || [Array, Hash]
39
+ app.opts[:json_result_classes] ||= []
40
+ app.opts[:json_result_classes] += classes
41
+ app.opts[:json_result_classes].uniq!
42
+ app.opts[:json_result_classes].extend(RodaDeprecateMutation)
40
43
  end
41
44
 
42
45
  module ClassMethods
@@ -0,0 +1,92 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The module_include plugin adds request_module and response_module class methods
4
+ # for adding modules/methods to request/response classes. It's designed to make
5
+ # it easier to add request/response methods for a given roda class. To add a module
6
+ # to the request or response class:
7
+ #
8
+ # Roda.request_module SomeRequestModule
9
+ # Roda.response_module SomeResponseModule
10
+ #
11
+ # Alternatively, you can pass a block to the methods and it will create a module
12
+ # automatically:
13
+ #
14
+ # Roda.request_module do
15
+ # def description
16
+ # "#{request_method} #{path_info}"
17
+ # end
18
+ # end
19
+ module ModuleInclude
20
+ module ClassMethods
21
+ # Include the given module in the request class. If a block
22
+ # is provided instead of a module, create a module using the
23
+ # the block. Example:
24
+ #
25
+ # Roda.request_module SomeModule
26
+ #
27
+ # Roda.request_module do
28
+ # def description
29
+ # "#{request_method} #{path_info}"
30
+ # end
31
+ # end
32
+ #
33
+ # Roda.route do |r|
34
+ # r.description
35
+ # end
36
+ def request_module(mod = nil, &block)
37
+ module_include(:request, mod, &block)
38
+ end
39
+
40
+ # Include the given module in the response class. If a block
41
+ # is provided instead of a module, create a module using the
42
+ # the block. Example:
43
+ #
44
+ # Roda.response_module SomeModule
45
+ #
46
+ # Roda.response_module do
47
+ # def error!
48
+ # self.status = 500
49
+ # end
50
+ # end
51
+ #
52
+ # Roda.route do |r|
53
+ # response.error!
54
+ # end
55
+ def response_module(mod = nil, &block)
56
+ module_include(:response, mod, &block)
57
+ end
58
+
59
+ private
60
+
61
+ # Backbone of the request_module and response_module methods.
62
+ def module_include(type, mod)
63
+ if type == :response
64
+ klass = self::RodaResponse
65
+ iv = :@response_module
66
+ else
67
+ klass = self::RodaRequest
68
+ iv = :@request_module
69
+ end
70
+
71
+ if mod
72
+ raise RodaError, "can't provide both argument and block to response_module" if block_given?
73
+ klass.send(:include, mod)
74
+ else
75
+ if instance_variable_defined?(iv)
76
+ mod = instance_variable_get(iv)
77
+ else
78
+ mod = instance_variable_set(iv, Module.new)
79
+ klass.send(:include, mod)
80
+ end
81
+
82
+ mod.module_eval(&Proc.new) if block_given?
83
+ end
84
+
85
+ mod
86
+ end
87
+ end
88
+ end
89
+
90
+ register_plugin(:module_include, ModuleInclude)
91
+ end
92
+ end
@@ -130,6 +130,13 @@ class Roda
130
130
  end
131
131
 
132
132
  module ClassMethods
133
+ # Freeze the namespaced routes so that there can be no thread safety issues at runtime.
134
+ def freeze
135
+ opts[:namespaced_routes].freeze
136
+ opts[:namespaced_routes].each_value{|v| v.freeze}
137
+ super
138
+ end
139
+
133
140
  # Copy the named routes into the subclass when inheriting.
134
141
  def inherited(subclass)
135
142
  super