roda 0.9.0 → 1.0.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 +62 -0
- data/README.rdoc +362 -167
- data/Rakefile +2 -2
- data/doc/release_notes/1.0.0.txt +329 -0
- data/lib/roda.rb +553 -180
- data/lib/roda/plugins/_erubis_escaping.rb +28 -0
- data/lib/roda/plugins/all_verbs.rb +7 -9
- data/lib/roda/plugins/backtracking_array.rb +92 -0
- data/lib/roda/plugins/content_for.rb +46 -0
- data/lib/roda/plugins/csrf.rb +60 -0
- data/lib/roda/plugins/flash.rb +53 -7
- data/lib/roda/plugins/halt.rb +8 -14
- data/lib/roda/plugins/head.rb +56 -0
- data/lib/roda/plugins/header_matchers.rb +2 -2
- data/lib/roda/plugins/json.rb +84 -0
- data/lib/roda/plugins/multi_route.rb +50 -10
- data/lib/roda/plugins/not_allowed.rb +140 -0
- data/lib/roda/plugins/pass.rb +13 -6
- data/lib/roda/plugins/per_thread_caching.rb +70 -0
- data/lib/roda/plugins/render.rb +20 -33
- data/lib/roda/plugins/render_each.rb +61 -0
- data/lib/roda/plugins/symbol_matchers.rb +79 -0
- data/lib/roda/plugins/symbol_views.rb +40 -0
- data/lib/roda/plugins/view_subdirs.rb +53 -0
- data/lib/roda/version.rb +3 -0
- data/spec/matchers_spec.rb +61 -5
- data/spec/plugin/_erubis_escaping_spec.rb +29 -0
- data/spec/plugin/backtracking_array_spec.rb +38 -0
- data/spec/plugin/content_for_spec.rb +34 -0
- data/spec/plugin/csrf_spec.rb +49 -0
- data/spec/plugin/flash_spec.rb +69 -5
- data/spec/plugin/head_spec.rb +35 -0
- data/spec/plugin/json_spec.rb +50 -0
- data/spec/plugin/multi_route_spec.rb +22 -6
- data/spec/plugin/not_allowed_spec.rb +55 -0
- data/spec/plugin/pass_spec.rb +8 -2
- data/spec/plugin/per_thread_caching_spec.rb +28 -0
- data/spec/plugin/render_each_spec.rb +30 -0
- data/spec/plugin/render_spec.rb +7 -1
- data/spec/plugin/symbol_matchers_spec.rb +68 -0
- data/spec/plugin/symbol_views_spec.rb +32 -0
- data/spec/plugin/view_subdirs_spec.rb +45 -0
- data/spec/plugin_spec.rb +11 -1
- data/spec/redirect_spec.rb +21 -4
- data/spec/request_spec.rb +9 -0
- metadata +49 -5
@@ -35,14 +35,14 @@ class Roda
|
|
35
35
|
|
36
36
|
# Match if the given mimetype is one of the accepted mimetypes.
|
37
37
|
def match_accept(mimetype)
|
38
|
-
if env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
|
38
|
+
if @env["HTTP_ACCEPT"].to_s.split(',').any?{|s| s.strip == mimetype}
|
39
39
|
response["Content-Type"] = mimetype
|
40
40
|
end
|
41
41
|
end
|
42
42
|
|
43
43
|
# Match if the given uppercase key is present inside the environment.
|
44
44
|
def match_header(key)
|
45
|
-
env[key.upcase.tr("-","_")]
|
45
|
+
@env[key.upcase.tr("-","_")]
|
46
46
|
end
|
47
47
|
|
48
48
|
# Match if the host of the request is the same as the hostname.
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The json plugin allows match blocks to return
|
6
|
+
# arrays or hashes, and have those arrays or hashes be
|
7
|
+
# converted to json which is used as the response body.
|
8
|
+
# It also sets the response content type to application/json.
|
9
|
+
# So you can take code like:
|
10
|
+
#
|
11
|
+
# r.root do
|
12
|
+
# response['Content-Type'] = 'application/json'
|
13
|
+
# [1, 2, 3].to_json
|
14
|
+
# end
|
15
|
+
# r.is "foo" do
|
16
|
+
# response['Content-Type'] = 'application/json'
|
17
|
+
# {'a'=>'b'}.to_json
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# and DRY it up:
|
21
|
+
#
|
22
|
+
# plugin :json
|
23
|
+
# r.root do
|
24
|
+
# [1, 2, 3]
|
25
|
+
# end
|
26
|
+
# r.is "foo" do
|
27
|
+
# {'a'=>'b'}
|
28
|
+
# end
|
29
|
+
#
|
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:
|
33
|
+
#
|
34
|
+
# plugin :json
|
35
|
+
# json_result_classes << Sequel::Model
|
36
|
+
module Json
|
37
|
+
# Set the classes to automatically convert to JSON
|
38
|
+
def self.configure(app)
|
39
|
+
app.instance_eval do
|
40
|
+
@json_result_classes ||= [Array, Hash]
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module ClassMethods
|
45
|
+
# The classes that should be automatically converted to json
|
46
|
+
attr_reader :json_result_classes
|
47
|
+
|
48
|
+
# Copy the json_result_classes into the subclass
|
49
|
+
def inherited(subclass)
|
50
|
+
super
|
51
|
+
subclass.instance_variable_set(:@json_result_classes, json_result_classes.dup)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
module RequestMethods
|
56
|
+
CONTENT_TYPE = 'Content-Type'.freeze
|
57
|
+
APPLICATION_JSON = 'application/json'.freeze
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# If the result is an instance of one of the json_result_classes,
|
62
|
+
# convert the result to json and return it as the body, using the
|
63
|
+
# application/json content-type.
|
64
|
+
def block_result_body(result)
|
65
|
+
case result
|
66
|
+
when *self.class.roda_class.json_result_classes
|
67
|
+
response[CONTENT_TYPE] = APPLICATION_JSON
|
68
|
+
convert_to_json(result)
|
69
|
+
else
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Convert the given object to JSON. Uses to_json by default,
|
75
|
+
# but can be overridden to use a different implementation.
|
76
|
+
def convert_to_json(obj)
|
77
|
+
obj.to_json
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
register_plugin(:json, Json)
|
83
|
+
end
|
84
|
+
end
|
@@ -1,39 +1,56 @@
|
|
1
1
|
class Roda
|
2
2
|
module RodaPlugins
|
3
3
|
# The multi_route plugin allows for multiple named routes, which the
|
4
|
-
# main route block can dispatch to by name at any point
|
5
|
-
# route doesn't handle the request, execution will continue,
|
6
|
-
# named route does handle the request, the response by
|
7
|
-
# will be returned.
|
4
|
+
# main route block can dispatch to by name at any point by calling +route+.
|
5
|
+
# If the named route doesn't handle the request, execution will continue,
|
6
|
+
# and if the named route does handle the request, the response returned by
|
7
|
+
# the named route will be returned.
|
8
|
+
#
|
9
|
+
# In addition, this also adds the +r.multi_route+ method, which will assume
|
10
|
+
# check if the first segment in the path matches a named route, and dispatch
|
11
|
+
# to that named route.
|
8
12
|
#
|
9
13
|
# Example:
|
10
14
|
#
|
11
15
|
# plugin :multi_route
|
12
16
|
#
|
13
|
-
# route(
|
17
|
+
# route('foo') do |r|
|
14
18
|
# r.is 'bar' do
|
15
19
|
# '/foo/bar'
|
16
20
|
# end
|
17
21
|
# end
|
18
22
|
#
|
19
|
-
# route(
|
23
|
+
# route('bar') do |r|
|
20
24
|
# r.is 'foo' do
|
21
25
|
# '/bar/foo'
|
22
26
|
# end
|
23
27
|
# end
|
24
28
|
#
|
25
29
|
# route do |r|
|
30
|
+
# r.multi_route
|
31
|
+
#
|
32
|
+
# # or
|
33
|
+
#
|
26
34
|
# r.on "foo" do
|
27
|
-
# route
|
35
|
+
# r.route 'foo'
|
28
36
|
# end
|
29
37
|
#
|
30
38
|
# r.on "bar" do
|
31
|
-
# route
|
39
|
+
# r.route 'bar'
|
32
40
|
# end
|
33
41
|
# end
|
34
42
|
#
|
35
43
|
# Note that in multi-threaded code, you should not attempt to add a
|
36
44
|
# named route after accepting requests.
|
45
|
+
#
|
46
|
+
# If you want to use the +r.multi_route+ method, use string names for the
|
47
|
+
# named routes. Also, you can provide a block to +r.multi_route+ that is
|
48
|
+
# called if the route matches but the named route did not handle the
|
49
|
+
# request:
|
50
|
+
#
|
51
|
+
# r.multi_route do
|
52
|
+
# "default body"
|
53
|
+
# end
|
37
54
|
module MultiRoute
|
38
55
|
# Initialize storage for the named routes.
|
39
56
|
def self.configure(app)
|
@@ -47,6 +64,11 @@ class Roda
|
|
47
64
|
subclass.instance_variable_set(:@named_routes, @named_routes.dup)
|
48
65
|
end
|
49
66
|
|
67
|
+
# An names for the currently stored named routes
|
68
|
+
def named_routes
|
69
|
+
@named_routes.keys
|
70
|
+
end
|
71
|
+
|
50
72
|
# Return the named route with the given name.
|
51
73
|
def named_route(name)
|
52
74
|
@named_routes[name]
|
@@ -64,10 +86,28 @@ class Roda
|
|
64
86
|
end
|
65
87
|
end
|
66
88
|
|
67
|
-
module
|
89
|
+
module RequestClassMethods
|
90
|
+
# A regexp matching any of the current named routes.
|
91
|
+
def named_route_regexp
|
92
|
+
@named_route_regexp ||= /(#{Regexp.union(roda_class.named_routes)})/
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
module RequestMethods
|
97
|
+
# Check if the first segment in the path matches any of the current
|
98
|
+
# named routes. If so, call that named route. If not, do nothing.
|
99
|
+
# If the named route does not handle the request, and a block
|
100
|
+
# is given, yield to the block.
|
101
|
+
def multi_route
|
102
|
+
on self.class.named_route_regexp do |section|
|
103
|
+
route(section)
|
104
|
+
yield if block_given?
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
68
108
|
# Dispatch to the named route with the given name.
|
69
109
|
def route(name)
|
70
|
-
instance_exec(
|
110
|
+
scope.instance_exec(self, &self.class.roda_class.named_route(name))
|
71
111
|
end
|
72
112
|
end
|
73
113
|
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The not_allowed plugin makes Roda attempt to automatically
|
4
|
+
# support the 405 Method Not Allowed response status. The plugin
|
5
|
+
# changes the +r.get+ and +r.post+ verb methods to automatically
|
6
|
+
# return a 405 status if they are called with any arguments, and
|
7
|
+
# the arguments match but the request method does not match. So
|
8
|
+
# this code:
|
9
|
+
#
|
10
|
+
# r.get '' do
|
11
|
+
# "a"
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# will return a 200 response for <tt>GET /</tt> and a 405
|
15
|
+
# response for <tt>POST /</tt>.
|
16
|
+
#
|
17
|
+
# This plugin also changes the +r.is+ method so that if you use
|
18
|
+
# a verb method inside +r.is+, it returns a 405 status if none
|
19
|
+
# of the verb methods match. So this code:
|
20
|
+
#
|
21
|
+
# r.is '' do
|
22
|
+
# r.get do
|
23
|
+
# "a"
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# r.post do
|
27
|
+
# "b"
|
28
|
+
# end
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# will return a 200 response for <tt>GET /</tt> and <tt>POST /</tt>,
|
32
|
+
# but a 405 response for <tt>PUT /</tt>.
|
33
|
+
#
|
34
|
+
# Note that this plugin will probably not do what you want for
|
35
|
+
# code such as:
|
36
|
+
#
|
37
|
+
# r.get '' do
|
38
|
+
# "a"
|
39
|
+
# end
|
40
|
+
#
|
41
|
+
# r.post '' do
|
42
|
+
# "b"
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# Since for a <tt>POST /</tt> request, when +r.get+ method matches
|
46
|
+
# the path but not the request method, it will return an immediate
|
47
|
+
# 405 response. You must DRY up this code for it work correctly,
|
48
|
+
# like this:
|
49
|
+
#
|
50
|
+
# r.is '' do
|
51
|
+
# r.get do
|
52
|
+
# "a"
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# r.post do
|
56
|
+
# "b"
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# In all cases where it uses a 405 response, it also sets the +Allow+
|
61
|
+
# header in the response to contain the request methods supported.
|
62
|
+
#
|
63
|
+
# To make this affect the verb methods added by the all_verbs plugin,
|
64
|
+
# load this plugin first.
|
65
|
+
module NotAllowed
|
66
|
+
# Redefine the +r.get+ and +r.post+ methods when loading the plugin.
|
67
|
+
def self.configure(app)
|
68
|
+
app.request_module do
|
69
|
+
app::RodaRequest.def_verb_method(self, :get)
|
70
|
+
app::RodaRequest.def_verb_method(self, :post)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
module RequestClassMethods
|
75
|
+
# Define a method named +verb+ in the given module which will
|
76
|
+
# return a 405 response if the method is called with any
|
77
|
+
# arguments and the arguments terminally match but the
|
78
|
+
# request method does not.
|
79
|
+
#
|
80
|
+
# If called without any arguments, check to see if the call
|
81
|
+
# is inside a terminal match, and in that case record the
|
82
|
+
# request method used.
|
83
|
+
def def_verb_method(mod, verb)
|
84
|
+
mod.class_eval(<<-END, __FILE__, __LINE__+1)
|
85
|
+
def #{verb}(*args, &block)
|
86
|
+
if args.empty?
|
87
|
+
@_is_verbs << "#{verb.to_s.upcase}" if @_is_verbs
|
88
|
+
always(&block) if #{verb == :get ? :is_get : verb}?
|
89
|
+
else
|
90
|
+
args << ::Roda::RodaPlugins::Base::RequestMethods::TERM
|
91
|
+
if_match(args) do |*args|
|
92
|
+
if #{verb == :get ? :is_get : verb}?
|
93
|
+
block_result(yield(*args))
|
94
|
+
throw :halt, response.finish
|
95
|
+
end
|
96
|
+
response.status = 405
|
97
|
+
response['Allow'] = '#{verb.to_s.upcase}'
|
98
|
+
nil
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
END
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
module RequestMethods
|
107
|
+
# Keep track of verb calls inside the block. If there are any
|
108
|
+
# verb calls inside the block, but the block returned, assume
|
109
|
+
# that the verb calls inside the block did not match, and
|
110
|
+
# since there was already a successful terminal match, the
|
111
|
+
# request method must not be allowed, so return a 405
|
112
|
+
# response in that case.
|
113
|
+
def is(*verbs)
|
114
|
+
super(*verbs) do
|
115
|
+
begin
|
116
|
+
@_is_verbs = []
|
117
|
+
|
118
|
+
ret = if verbs.empty?
|
119
|
+
yield
|
120
|
+
else
|
121
|
+
yield(*captures)
|
122
|
+
end
|
123
|
+
|
124
|
+
unless @_is_verbs.empty?
|
125
|
+
response.status = 405
|
126
|
+
response['Allow'] = @_is_verbs.join(', ')
|
127
|
+
end
|
128
|
+
|
129
|
+
ret
|
130
|
+
ensure
|
131
|
+
@_is_verbs = nil
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
register_plugin(:not_allowed, NotAllowed)
|
139
|
+
end
|
140
|
+
end
|
data/lib/roda/plugins/pass.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
class Roda
|
2
2
|
module RodaPlugins
|
3
|
-
# The pass plugin adds a request +pass+ method to skip the current
|
3
|
+
# The pass plugin adds a request +pass+ method to skip the current match
|
4
4
|
# block as if it did not match.
|
5
5
|
#
|
6
6
|
# plugin :pass
|
@@ -17,14 +17,21 @@ class Roda
|
|
17
17
|
# end
|
18
18
|
module Pass
|
19
19
|
module RequestMethods
|
20
|
-
#
|
21
|
-
def
|
20
|
+
# Skip the current match block as if it did not match.
|
21
|
+
def pass
|
22
|
+
throw :pass
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# Handle passing inside the match block.
|
28
|
+
def always
|
22
29
|
catch(:pass){super}
|
23
30
|
end
|
24
31
|
|
25
|
-
#
|
26
|
-
def
|
27
|
-
|
32
|
+
# Handle passing inside the match block.
|
33
|
+
def if_match(_)
|
34
|
+
catch(:pass){super}
|
28
35
|
end
|
29
36
|
end
|
30
37
|
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The per_thread_caching plugin changes the default cache
|
4
|
+
# from being a shared thread safe cache to a separate cache per
|
5
|
+
# thread. This means getting or setting values no longer
|
6
|
+
# needs a mutex on non-MRI ruby implementations, which may be
|
7
|
+
# faster when using a thread pool. However, since the caches
|
8
|
+
# are no longer shared, this will take up more memory.
|
9
|
+
#
|
10
|
+
# Note that it does not make sense to use this plugin on MRI,
|
11
|
+
# since the default cache on MRI doesn't use a mutex as it
|
12
|
+
# is already thread safe due to the GVL.
|
13
|
+
#
|
14
|
+
# Using this plugin changes the matcher regexp cache to use
|
15
|
+
# per-thread caches, and changes the default for future
|
16
|
+
# thread-safe caches to use per-thread caches.
|
17
|
+
#
|
18
|
+
# If you want the render plugin's template cache to use
|
19
|
+
# per-thread caches, you should load this plugin before the
|
20
|
+
# render plugin.
|
21
|
+
module PerThreadCaching
|
22
|
+
def self.configure(app)
|
23
|
+
app::RodaRequest.match_pattern_cache = app.thread_safe_cache
|
24
|
+
end
|
25
|
+
|
26
|
+
class Cache
|
27
|
+
# Mutex used to ensure multiple per-thread caches
|
28
|
+
# don't use the same key
|
29
|
+
MUTEX = ::Mutex.new
|
30
|
+
|
31
|
+
n = 0
|
32
|
+
# Auto incrementing number proc used to make sure
|
33
|
+
# multiple thread-thread caches don't use the same key.
|
34
|
+
N = lambda{MUTEX.synchronize{n += 1}}
|
35
|
+
|
36
|
+
# Store unique symbol used to look up in the per
|
37
|
+
# thread caches.
|
38
|
+
def initialize
|
39
|
+
@o = :"roda_per_thread_cache_#{N.call}"
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the current thread's cached value.
|
43
|
+
def [](key)
|
44
|
+
_hash[key]
|
45
|
+
end
|
46
|
+
|
47
|
+
# Set the current thread's cached value.
|
48
|
+
def []=(key, value)
|
49
|
+
_hash[key] = value
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# The current thread's cache.
|
55
|
+
def _hash
|
56
|
+
::Thread.current[@o] ||= {}
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
module ClassMethods
|
61
|
+
# Use the per-thread cache instead of the default cache.
|
62
|
+
def thread_safe_cache
|
63
|
+
Cache.new
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
register_plugin(:per_thread_caching, PerThreadCaching)
|
69
|
+
end
|
70
|
+
end
|