roda 1.2.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|