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.
- checksums.yaml +4 -4
- data/CHANGELOG +42 -0
- data/README.rdoc +73 -144
- data/doc/conventions.rdoc +10 -8
- data/doc/release_notes/1.3.0.txt +109 -0
- data/lib/roda.rb +67 -100
- data/lib/roda/plugins/assets.rb +4 -4
- data/lib/roda/plugins/chunked.rb +4 -1
- data/lib/roda/plugins/class_level_routing.rb +7 -1
- data/lib/roda/plugins/cookies.rb +34 -0
- data/lib/roda/plugins/default_headers.rb +7 -6
- data/lib/roda/plugins/delegate.rb +8 -1
- data/lib/roda/plugins/delete_empty_headers.rb +33 -0
- data/lib/roda/plugins/delete_nil_headers.rb +34 -0
- data/lib/roda/plugins/environments.rb +12 -4
- data/lib/roda/plugins/error_email.rb +6 -1
- data/lib/roda/plugins/error_handler.rb +7 -4
- data/lib/roda/plugins/hash_matcher.rb +32 -0
- data/lib/roda/plugins/header_matchers.rb +12 -2
- data/lib/roda/plugins/json.rb +9 -6
- data/lib/roda/plugins/module_include.rb +92 -0
- data/lib/roda/plugins/multi_route.rb +7 -0
- data/lib/roda/plugins/multi_run.rb +11 -5
- data/lib/roda/plugins/named_templates.rb +7 -1
- data/lib/roda/plugins/not_found.rb +6 -0
- data/lib/roda/plugins/param_matchers.rb +43 -0
- data/lib/roda/plugins/path_matchers.rb +51 -0
- data/lib/roda/plugins/render.rb +32 -14
- data/lib/roda/plugins/static_path_info.rb +10 -3
- data/lib/roda/plugins/symbol_matchers.rb +1 -1
- data/lib/roda/version.rb +13 -1
- data/spec/freeze_spec.rb +28 -0
- data/spec/plugin/class_level_routing_spec.rb +26 -0
- data/spec/plugin/content_for_spec.rb +1 -2
- data/spec/plugin/cookies_spec.rb +25 -0
- data/spec/plugin/default_headers_spec.rb +4 -7
- data/spec/plugin/delegate_spec.rb +4 -1
- data/spec/plugin/delete_empty_headers_spec.rb +15 -0
- data/spec/plugin/error_handler_spec.rb +31 -0
- data/spec/plugin/hash_matcher_spec.rb +27 -0
- data/spec/plugin/header_matchers_spec.rb +15 -0
- data/spec/plugin/json_spec.rb +1 -2
- data/spec/plugin/mailer_spec.rb +2 -2
- data/spec/plugin/module_include_spec.rb +31 -0
- data/spec/plugin/multi_route_spec.rb +14 -0
- data/spec/plugin/multi_run_spec.rb +41 -0
- data/spec/plugin/named_templates_spec.rb +25 -0
- data/spec/plugin/not_found_spec.rb +29 -0
- data/spec/plugin/param_matchers_spec.rb +37 -0
- data/spec/plugin/path_matchers_spec.rb +42 -0
- data/spec/plugin/render_spec.rb +33 -8
- data/spec/plugin/static_path_info_spec.rb +6 -0
- data/spec/plugin/view_subdirs_spec.rb +1 -2
- data/spec/response_spec.rb +12 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/version_spec.rb +8 -2
- 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
|
15
|
+
# You can modify the default headers later by loading the
|
16
|
+
# plugin again:
|
16
17
|
#
|
17
|
-
# plugin :default_headers
|
18
|
-
# default_headers
|
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]
|
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
|
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
|
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 "<foo>" 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
|
-
|
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,
|
25
|
-
# before executing the error handler.
|
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
|
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
|
data/lib/roda/plugins/json.rb
CHANGED
@@ -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
|
32
|
-
#
|
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
|
-
|
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
|