roda-cj 0.9.2 → 0.9.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +26 -0
- data/README.rdoc +46 -12
- data/lib/roda.rb +183 -151
- data/lib/roda/plugins/all_verbs.rb +1 -1
- data/lib/roda/plugins/backtracking_array.rb +91 -0
- data/lib/roda/plugins/csrf.rb +60 -0
- data/lib/roda/plugins/halt.rb +2 -2
- data/lib/roda/plugins/json.rb +84 -0
- data/lib/roda/plugins/pass.rb +1 -1
- data/lib/roda/plugins/per_thread_caching.rb +70 -0
- data/lib/roda/plugins/render.rb +8 -35
- data/lib/roda/plugins/symbol_matchers.rb +75 -0
- data/lib/roda/plugins/symbol_views.rb +40 -0
- data/lib/roda/plugins/view_subdirs.rb +53 -0
- data/lib/roda/version.rb +1 -1
- data/spec/matchers_spec.rb +42 -0
- data/spec/plugin/backtracking_array_spec.rb +38 -0
- data/spec/plugin/csrf_spec.rb +49 -0
- data/spec/plugin/json_spec.rb +50 -0
- data/spec/plugin/pass_spec.rb +1 -1
- data/spec/plugin/per_thread_caching_spec.rb +28 -0
- data/spec/plugin/render_spec.rb +2 -1
- data/spec/plugin/symbol_matchers_spec.rb +62 -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/request_spec.rb +9 -0
- metadata +30 -2
@@ -0,0 +1,91 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The backtracking_array plugin changes the handling of array
|
4
|
+
# matchers such that if one of the array entries matches, but
|
5
|
+
# a later match argument fails, it will backtrack and try the
|
6
|
+
# next entry in the array. For example, the following match
|
7
|
+
# block does not match +/a/b+ by default:
|
8
|
+
#
|
9
|
+
# r.is ['a', 'a/b'] do |path|
|
10
|
+
# ...
|
11
|
+
# end
|
12
|
+
#
|
13
|
+
# This is because the <tt>'a'</tt> entry in the array matches, which
|
14
|
+
# makes the array match. However, the next matcher is the
|
15
|
+
# terminal matcher (since +r.is+ was used), and since the
|
16
|
+
# path is not terminal as it still contains +/b+ after
|
17
|
+
# matching <tt>'a'</tt>.
|
18
|
+
#
|
19
|
+
# With the backtracking_array plugin, when the terminal matcher
|
20
|
+
# fails, matching will go on to the next entry in the array,
|
21
|
+
# <tt>'a/b'</tt>, which will also match. Since <tt>'a/b'</tt>
|
22
|
+
# matches the path fully, the terminal matcher also matches,
|
23
|
+
# and the match block yields.
|
24
|
+
module BacktrackingArray
|
25
|
+
module RequestMethods
|
26
|
+
PATH_INFO = "PATH_INFO".freeze
|
27
|
+
SCRIPT_NAME = "SCRIPT_NAME".freeze
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# When matching for a single array, after a successful
|
32
|
+
# array element match, attempt to match all remaining
|
33
|
+
# elements. If the remaining elements could not be
|
34
|
+
# matched, reset the state and continue to the next
|
35
|
+
# entry in the array.
|
36
|
+
def _match_array(arg, rest=nil)
|
37
|
+
return super unless rest
|
38
|
+
|
39
|
+
script = env[SCRIPT_NAME]
|
40
|
+
path = env[PATH_INFO]
|
41
|
+
caps = captures.dup
|
42
|
+
arg.each do |v|
|
43
|
+
if match(v, rest)
|
44
|
+
if v.is_a?(String)
|
45
|
+
captures.push(v)
|
46
|
+
end
|
47
|
+
|
48
|
+
if match_all(rest)
|
49
|
+
return true
|
50
|
+
end
|
51
|
+
|
52
|
+
# Matching all remaining elements failed, reset state
|
53
|
+
captures.replace(caps)
|
54
|
+
env[SCRIPT_NAME] = script
|
55
|
+
env[PATH_INFO] = path
|
56
|
+
end
|
57
|
+
end
|
58
|
+
false
|
59
|
+
end
|
60
|
+
|
61
|
+
# If any of the args are an array, handle backtracking such
|
62
|
+
# that if a later matcher fails, we roll back to the current
|
63
|
+
# matcher and proceed to the next entry in the array.
|
64
|
+
def match_all(args)
|
65
|
+
args = args.dup
|
66
|
+
until args.empty?
|
67
|
+
arg = args.shift
|
68
|
+
if match(arg, args)
|
69
|
+
return true if arg.is_a?(Array)
|
70
|
+
else
|
71
|
+
return
|
72
|
+
end
|
73
|
+
end
|
74
|
+
true
|
75
|
+
end
|
76
|
+
|
77
|
+
# When matching an array, include the remaining arguments,
|
78
|
+
# otherwise, just match the single argument.
|
79
|
+
def match(v, rest = nil)
|
80
|
+
if v.is_a?(Array)
|
81
|
+
_match_array(v, rest)
|
82
|
+
else
|
83
|
+
super(v)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
register_plugin(:backtracking_array, BacktrackingArray)
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'rack/csrf'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The csrf plugin adds CSRF protection using rack_csrf, along with
|
6
|
+
# some csrf helper methods to use in your views. To use it, load
|
7
|
+
# the plugin, with the options hash passed to Rack::Csrf:
|
8
|
+
#
|
9
|
+
# plugin :csrf, :raise=>true
|
10
|
+
#
|
11
|
+
# This adds the following instance methods:
|
12
|
+
#
|
13
|
+
# csrf_field :: The field name to use for the hidden/meta csrf tag.
|
14
|
+
# csrf_header :: The http header name to use for submitting csrf token via
|
15
|
+
# headers (useful for javascript).
|
16
|
+
# csrf_metatag :: An html meta tag string containing the token, suitable
|
17
|
+
# for placing in the page header
|
18
|
+
# csrf_tag :: An html hidden input tag string containing the token, suitable
|
19
|
+
# for placing in an html form.
|
20
|
+
# csrf_token :: The value of the csrf token, in case it needs to be accessed
|
21
|
+
# directly.
|
22
|
+
module Csrf
|
23
|
+
CSRF = ::Rack::Csrf
|
24
|
+
|
25
|
+
# Load the Rack::Csrf middleware into the app with the given options.
|
26
|
+
def self.configure(app, opts={})
|
27
|
+
app.use CSRF, opts
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
# The name of the hidden/meta csrf tag.
|
32
|
+
def csrf_field
|
33
|
+
CSRF.field
|
34
|
+
end
|
35
|
+
|
36
|
+
# The http header name to use for submitting csrf token via headers.
|
37
|
+
def csrf_header
|
38
|
+
CSRF.header
|
39
|
+
end
|
40
|
+
|
41
|
+
# An html meta tag string containing the token.
|
42
|
+
def csrf_metatag(opts={})
|
43
|
+
CSRF.metatag(env, opts)
|
44
|
+
end
|
45
|
+
|
46
|
+
# An html hidden input tag string containing the token.
|
47
|
+
def csrf_tag
|
48
|
+
CSRF.tag(env)
|
49
|
+
end
|
50
|
+
|
51
|
+
# The value of the csrf token.
|
52
|
+
def csrf_token
|
53
|
+
CSRF.token(env)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
register_plugin(:csrf, Csrf)
|
59
|
+
end
|
60
|
+
end
|
data/lib/roda/plugins/halt.rb
CHANGED
@@ -54,7 +54,7 @@ class Roda
|
|
54
54
|
when String
|
55
55
|
response.write v
|
56
56
|
when Array
|
57
|
-
|
57
|
+
throw :halt, v
|
58
58
|
else
|
59
59
|
raise Roda::RodaError, "singular argument to #halt must be Integer, String, or Array"
|
60
60
|
end
|
@@ -69,7 +69,7 @@ class Roda
|
|
69
69
|
raise Roda::RodaError, "too many arguments given to #halt (accepts 0-3, received #{res.length})"
|
70
70
|
end
|
71
71
|
|
72
|
-
|
72
|
+
super()
|
73
73
|
end
|
74
74
|
end
|
75
75
|
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
class Roda
|
4
|
+
module RodaPlugins
|
5
|
+
# The json plugin allows matching 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
|
data/lib/roda/plugins/pass.rb
CHANGED
@@ -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
|
data/lib/roda/plugins/render.rb
CHANGED
@@ -27,9 +27,8 @@ class Roda
|
|
27
27
|
#
|
28
28
|
# The following options are supported:
|
29
29
|
#
|
30
|
-
# :cache ::
|
31
|
-
#
|
32
|
-
# automatically use the default template cache.
|
30
|
+
# :cache :: nil/false to not cache templates (useful for development), defaults
|
31
|
+
# to true to automatically use the default template cache.
|
33
32
|
# :engine :: The tilt engine to use for rendering, defaults to 'erb'.
|
34
33
|
# :ext :: The file extension to assume for view files, defaults to the :engine
|
35
34
|
# option.
|
@@ -65,34 +64,6 @@ class Roda
|
|
65
64
|
# If you pass a hash as the first argument to +view+ or +render+, it should
|
66
65
|
# have either +:inline+ or +:path+ as one of the keys.
|
67
66
|
module Render
|
68
|
-
# Default template cache. Thread-safe so that multiple threads can
|
69
|
-
# simultaneously use the cache.
|
70
|
-
class Cache
|
71
|
-
# Mutex used to synchronize access to the cache. Uses a
|
72
|
-
# singleton mutex to reduce memory.
|
73
|
-
MUTEX = ::Mutex.new
|
74
|
-
|
75
|
-
# Initialize the cache.
|
76
|
-
def initialize
|
77
|
-
MUTEX.synchronize{@cache = {}}
|
78
|
-
end
|
79
|
-
|
80
|
-
# Clear the cache.
|
81
|
-
alias clear initialize
|
82
|
-
|
83
|
-
# If the template is found in the cache under the given key,
|
84
|
-
# return it, otherwise yield to get the template, and
|
85
|
-
# store the template under the given key
|
86
|
-
def fetch(key)
|
87
|
-
unless template = MUTEX.synchronize{@cache[key]}
|
88
|
-
template = yield
|
89
|
-
MUTEX.synchronize{@cache[key] = template}
|
90
|
-
end
|
91
|
-
|
92
|
-
template
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
67
|
# Setup default rendering options. See Render for details.
|
97
68
|
def self.configure(app, opts={})
|
98
69
|
if app.opts[:render]
|
@@ -112,8 +83,7 @@ class Roda
|
|
112
83
|
if RUBY_VERSION >= "1.9"
|
113
84
|
opts[:opts][:default_encoding] ||= Encoding.default_external
|
114
85
|
end
|
115
|
-
cache = opts.fetch(:cache, true)
|
116
|
-
opts[:cache] = Cache.new if cache == true
|
86
|
+
opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, true)
|
117
87
|
end
|
118
88
|
|
119
89
|
module ClassMethods
|
@@ -125,7 +95,7 @@ class Roda
|
|
125
95
|
opts = subclass.opts[:render] = render_opts.dup
|
126
96
|
opts[:layout_opts] = opts[:layout_opts].dup
|
127
97
|
opts[:opts] = opts[:opts].dup
|
128
|
-
opts[:cache] =
|
98
|
+
opts[:cache] = thread_safe_cache if opts[:cache]
|
129
99
|
end
|
130
100
|
|
131
101
|
# Return the render options for this class.
|
@@ -198,7 +168,10 @@ class Roda
|
|
198
168
|
# to get the template.
|
199
169
|
def cached_template(path, &block)
|
200
170
|
if cache = render_opts[:cache]
|
201
|
-
cache
|
171
|
+
unless template = cache[path]
|
172
|
+
template = cache[path] = yield
|
173
|
+
end
|
174
|
+
template
|
202
175
|
else
|
203
176
|
yield
|
204
177
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The symbol_matchers plugin allows you do define custom regexps to use
|
4
|
+
# for specific symbols. For example, if you have a route such as:
|
5
|
+
#
|
6
|
+
# r.on :username do
|
7
|
+
# # ...
|
8
|
+
# end
|
9
|
+
#
|
10
|
+
# By default this will match all segments. However, if your usernames
|
11
|
+
# must be 6-20 characters, and can only contain +a-z+ and +0-9+, you can do:
|
12
|
+
#
|
13
|
+
# plugin :symbol_matchers
|
14
|
+
# symbol_matcher :username, /([a-z0-9]{6,20})/
|
15
|
+
#
|
16
|
+
# Then the route will only if the path is +/foobar123+, but not if it is
|
17
|
+
# +/foo+, +/FooBar123+, or +/foobar_123+.
|
18
|
+
#
|
19
|
+
# Note that this feature does not apply to just symbols, but also to
|
20
|
+
# embedded colons in strings, so the following:
|
21
|
+
#
|
22
|
+
# r.on "users/:username" do
|
23
|
+
# # ...
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Would match +/users/foobar123+, but not +/users/foo+, +/users/FooBar123+,
|
27
|
+
# or +/users/foobar_123+.
|
28
|
+
#
|
29
|
+
# By default, this plugin sets up the following symbol matchers:
|
30
|
+
#
|
31
|
+
# :d :: <tt>/\d+/</tt>, a decimal segment
|
32
|
+
# :format :: <tt>/(?:\.(\w+))?/</tt>, an optional format
|
33
|
+
# :opt :: <tt>/(?:\/([^\/]+))?</tt>, an optional segment
|
34
|
+
# :optd :: <tt>/(?:\/(\d+))?</tt>, an optional decimal segment
|
35
|
+
# :w :: <tt>/\w+/</tt>, a alphanumeric segment
|
36
|
+
#
|
37
|
+
# Note that because of how segment matching works, :format, :opt, and :optd
|
38
|
+
# are only going to work inside of a string, like this:
|
39
|
+
#
|
40
|
+
# r.is "album:opt" do |id|
|
41
|
+
# # matches /album (yielding nil) and /album/foo (yielding "foo")
|
42
|
+
# # does not match /album/ or /album/foo/bar
|
43
|
+
module SymbolMatchers
|
44
|
+
def self.configure(app)
|
45
|
+
app.symbol_matcher(:d, /(\d+)/)
|
46
|
+
app.symbol_matcher(:format, /(?:\.(\w+))?/)
|
47
|
+
app.symbol_matcher(:opt, /(?:\/([^\/]+))?/)
|
48
|
+
app.symbol_matcher(:optd, /(?:\/(\d+))?/)
|
49
|
+
app.symbol_matcher(:w, /(\w+)/)
|
50
|
+
end
|
51
|
+
|
52
|
+
module ClassMethods
|
53
|
+
# Set the regexp to use for the given symbol, instead of the default.
|
54
|
+
def symbol_matcher(s, re)
|
55
|
+
request_module{define_method(:"match_symbol_#{s}"){re}}
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
module RequestMethods
|
60
|
+
# Allow for symbol specific regexps, by using match_symbol_#{s} if
|
61
|
+
# defined. If not defined, calls super for the default behavior.
|
62
|
+
def _match_symbol_regexp(s)
|
63
|
+
meth = :"match_symbol_#{s}"
|
64
|
+
if respond_to?(meth)
|
65
|
+
send(meth)
|
66
|
+
else
|
67
|
+
super
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
register_plugin(:symbol_matchers, SymbolMatchers)
|
74
|
+
end
|
75
|
+
end
|