roda-cj 0.9.2 → 0.9.3
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 +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
|