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