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
data/lib/roda/plugins/render.rb
CHANGED
@@ -27,10 +27,11 @@ 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'.
|
33
|
+
# :escape :: Use Roda's Erubis escaping support, which handles postfix
|
34
|
+
# conditions inside <%= %> tags.
|
34
35
|
# :ext :: The file extension to assume for view files, defaults to the :engine
|
35
36
|
# option.
|
36
37
|
# :layout :: The base name of the layout file, defaults to 'layout'.
|
@@ -50,6 +51,9 @@ class Roda
|
|
50
51
|
# There are a couple of additional options to +view+ and +render+ that are
|
51
52
|
# available at runtime:
|
52
53
|
#
|
54
|
+
# :content :: Only respected by +view+, provides the content to render
|
55
|
+
# inside the layout, instead of rendering a template to get
|
56
|
+
# the content.
|
53
57
|
# :inline :: Use the value given as the template code, instead of looking
|
54
58
|
# for template code in a file.
|
55
59
|
# :locals :: Hash of local variables to make available inside the template.
|
@@ -65,31 +69,9 @@ class Roda
|
|
65
69
|
# If you pass a hash as the first argument to +view+ or +render+, it should
|
66
70
|
# have either +:inline+ or +:path+ as one of the keys.
|
67
71
|
module Render
|
68
|
-
|
69
|
-
|
70
|
-
|
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
|
72
|
+
def self.load_dependencies(app, opts={})
|
73
|
+
if opts[:escape]
|
74
|
+
app.plugin :_erubis_escaping
|
93
75
|
end
|
94
76
|
end
|
95
77
|
|
@@ -112,8 +94,10 @@ class Roda
|
|
112
94
|
if RUBY_VERSION >= "1.9"
|
113
95
|
opts[:opts][:default_encoding] ||= Encoding.default_external
|
114
96
|
end
|
115
|
-
|
116
|
-
|
97
|
+
if opts[:escape]
|
98
|
+
opts[:opts][:engine_class] = ErubisEscaping::Eruby
|
99
|
+
end
|
100
|
+
opts[:cache] = app.thread_safe_cache if opts.fetch(:cache, true)
|
117
101
|
end
|
118
102
|
|
119
103
|
module ClassMethods
|
@@ -125,7 +109,7 @@ class Roda
|
|
125
109
|
opts = subclass.opts[:render] = render_opts.dup
|
126
110
|
opts[:layout_opts] = opts[:layout_opts].dup
|
127
111
|
opts[:opts] = opts[:opts].dup
|
128
|
-
opts[:cache] =
|
112
|
+
opts[:cache] = thread_safe_cache if opts[:cache]
|
129
113
|
end
|
130
114
|
|
131
115
|
# Return the render options for this class.
|
@@ -179,7 +163,7 @@ class Roda
|
|
179
163
|
end
|
180
164
|
end
|
181
165
|
|
182
|
-
content = render(template, opts)
|
166
|
+
content = opts[:content] || render(template, opts)
|
183
167
|
|
184
168
|
if layout = opts.fetch(:layout, render_opts[:layout])
|
185
169
|
if layout_opts = opts[:layout_opts]
|
@@ -198,7 +182,10 @@ class Roda
|
|
198
182
|
# to get the template.
|
199
183
|
def cached_template(path, &block)
|
200
184
|
if cache = render_opts[:cache]
|
201
|
-
cache
|
185
|
+
unless template = cache[path]
|
186
|
+
template = cache[path] = yield
|
187
|
+
end
|
188
|
+
template
|
202
189
|
else
|
203
190
|
yield
|
204
191
|
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The render_each plugin allows you to render a template for each
|
4
|
+
# value in an enumerable, returning the concatention of all of the
|
5
|
+
# template renderings. For example:
|
6
|
+
#
|
7
|
+
# render_each([1,2,3], :foo)
|
8
|
+
#
|
9
|
+
# will render the +foo+ template 3 times. Each time the template
|
10
|
+
# is rendered, the local variable +foo+ will contain the given
|
11
|
+
# value (e.g. on the first rendering +foo+ is 1).
|
12
|
+
#
|
13
|
+
# You can pass additional render options via an options hash:
|
14
|
+
#
|
15
|
+
# render_each([1,2,3], :foo, :views=>'partials')
|
16
|
+
#
|
17
|
+
# One additional option supported by is +:local+, which sets the
|
18
|
+
# local variable containing the current value to use. So:
|
19
|
+
#
|
20
|
+
# render_each([1,2,3], :foo, :local=>:bar)
|
21
|
+
#
|
22
|
+
# Will render the +foo+ template, but the local variable used inside
|
23
|
+
# the template will be +bar+. You can use <tt>:local=>nil</tt> to
|
24
|
+
# not set a local variable inside the template.
|
25
|
+
module RenderEach
|
26
|
+
module InstanceMethods
|
27
|
+
EMPTY_STRING = ''.freeze
|
28
|
+
|
29
|
+
# For each value in enum, render the given template using the
|
30
|
+
# given opts. The template and options hash are passed to +render+.
|
31
|
+
# Additional options supported:
|
32
|
+
# :local :: The local variable to use for the current enum value
|
33
|
+
# inside the template. An explicit +nil+ value does not
|
34
|
+
# set a local variable. If not set, uses the template name.
|
35
|
+
def render_each(enum, template, opts={})
|
36
|
+
if as = opts.has_key?(:local)
|
37
|
+
as = opts[:local]
|
38
|
+
else
|
39
|
+
as = template.to_s.to_sym
|
40
|
+
end
|
41
|
+
|
42
|
+
if as
|
43
|
+
opts = opts.dup
|
44
|
+
if locals = opts[:locals]
|
45
|
+
locals = opts[:locals] = locals.dup
|
46
|
+
else
|
47
|
+
locals = opts[:locals] = {}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
enum.map do |v|
|
52
|
+
locals[as] = v if as
|
53
|
+
render(template, opts)
|
54
|
+
end.join
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
register_plugin(:render_each, RenderEach)
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,79 @@
|
|
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 nonempty 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/extension
|
33
|
+
# :opt :: <tt>/(?:\/([^\/]+))?</tt>, an optional segment
|
34
|
+
# :optd :: <tt>/(?:\/(\d+))?</tt>, an optional decimal segment
|
35
|
+
# :rest :: <tt>/(.*)/</tt>, all remaining characters, if any
|
36
|
+
# :w :: <tt>/(\w+)/</tt>, a alphanumeric segment
|
37
|
+
#
|
38
|
+
# Note that because of how segment matching works, :format, :opt, and :optd
|
39
|
+
# are only going to work inside of a string, like this:
|
40
|
+
#
|
41
|
+
# r.is "album:opt" do |id| end
|
42
|
+
# # matches /album (yielding nil) and /album/foo (yielding "foo")
|
43
|
+
# # does not match /album/ or /album/foo/bar
|
44
|
+
module SymbolMatchers
|
45
|
+
def self.configure(app)
|
46
|
+
app.symbol_matcher(:d, /(\d+)/)
|
47
|
+
app.symbol_matcher(:format, /(?:\.(\w+))?/)
|
48
|
+
app.symbol_matcher(:opt, /(?:\/([^\/]+))?/)
|
49
|
+
app.symbol_matcher(:optd, /(?:\/(\d+))?/)
|
50
|
+
app.symbol_matcher(:rest, /(.*)/)
|
51
|
+
app.symbol_matcher(:w, /(\w+)/)
|
52
|
+
end
|
53
|
+
|
54
|
+
module ClassMethods
|
55
|
+
# Set the regexp to use for the given symbol, instead of the default.
|
56
|
+
def symbol_matcher(s, re)
|
57
|
+
request_module{define_method(:"match_symbol_#{s}"){re}}
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
module RequestMethods
|
62
|
+
private
|
63
|
+
|
64
|
+
# Allow for symbol specific regexps, by using match_symbol_#{s} if
|
65
|
+
# defined. If not defined, calls super for the default behavior.
|
66
|
+
def _match_symbol_regexp(s)
|
67
|
+
meth = :"match_symbol_#{s}"
|
68
|
+
if respond_to?(meth)
|
69
|
+
send(meth)
|
70
|
+
else
|
71
|
+
super
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
register_plugin(:symbol_matchers, SymbolMatchers)
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The symbol_views plugin allows match blocks to return
|
4
|
+
# symbols, and consider those symbols as views to use for the
|
5
|
+
# response body. So you can take code like:
|
6
|
+
#
|
7
|
+
# r.root do
|
8
|
+
# view :index
|
9
|
+
# end
|
10
|
+
# r.is "foo" do
|
11
|
+
# view :foo
|
12
|
+
# end
|
13
|
+
#
|
14
|
+
# and DRY it up:
|
15
|
+
#
|
16
|
+
# r.root do
|
17
|
+
# :index
|
18
|
+
# end
|
19
|
+
# r.is "foo" do
|
20
|
+
# :foo
|
21
|
+
# end
|
22
|
+
module SymbolViews
|
23
|
+
module RequestMethods
|
24
|
+
private
|
25
|
+
|
26
|
+
# If the block result is a symbol, consider the symbol a
|
27
|
+
# template name and use the template view as the body.
|
28
|
+
def block_result_body(result)
|
29
|
+
if result.is_a?(Symbol)
|
30
|
+
scope.view(result)
|
31
|
+
else
|
32
|
+
super
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
register_plugin(:symbol_views, SymbolViews)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
class Roda
|
2
|
+
module RodaPlugins
|
3
|
+
# The view_subdirs plugin is designed for sites that have
|
4
|
+
# outgrown a flat view directory and use subdirectories
|
5
|
+
# for views. It allows you to set the view directory to
|
6
|
+
# use, and template names that do not contain a slash will
|
7
|
+
# automatically use that view subdirectory. Example:
|
8
|
+
#
|
9
|
+
# plugin :render
|
10
|
+
# plugin :view_subdirs
|
11
|
+
#
|
12
|
+
# route do |r|
|
13
|
+
# r.on "users" do
|
14
|
+
# set_view_subdir 'users'
|
15
|
+
#
|
16
|
+
# r.get :id do
|
17
|
+
# view 'profile' # uses ./views/users/profile.erb
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# r.get 'list' do
|
21
|
+
# view 'lists/users' # uses ./views/lists/users.erb
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# This plugin should be loaded after the render plugin, since
|
27
|
+
# it works by overriding parts of the render plugin.
|
28
|
+
module ViewSubdirs
|
29
|
+
module InstanceMethods
|
30
|
+
# Set the view subdirectory to use. This can be set to nil
|
31
|
+
# to not use a view subdirectory.
|
32
|
+
def set_view_subdir(v)
|
33
|
+
@_view_subdir = v
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
# Override the template name to use the view subdirectory if the
|
39
|
+
# there is a view subdirectory and the template name does not
|
40
|
+
# contain a slash.
|
41
|
+
def template_path(template, opts)
|
42
|
+
t = template.to_s
|
43
|
+
if (v = @_view_subdir) && t !~ /\//
|
44
|
+
template = "#{v}/#{t}"
|
45
|
+
end
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
register_plugin(:view_subdirs, ViewSubdirs)
|
52
|
+
end
|
53
|
+
end
|
data/lib/roda/version.rb
ADDED
data/spec/matchers_spec.rb
CHANGED
@@ -467,14 +467,28 @@ describe "path matchers" do
|
|
467
467
|
status('/useradf').should == 404
|
468
468
|
end
|
469
469
|
|
470
|
-
it "matching the root" do
|
470
|
+
it "matching the root with a string" do
|
471
471
|
app do |r|
|
472
|
-
r.
|
472
|
+
r.is "" do
|
473
473
|
"Home"
|
474
474
|
end
|
475
475
|
end
|
476
476
|
|
477
477
|
body.should == 'Home'
|
478
|
+
status("//").should == 404
|
479
|
+
status("/foo").should == 404
|
480
|
+
end
|
481
|
+
|
482
|
+
it "matching the root with the root method" do
|
483
|
+
app do |r|
|
484
|
+
r.root do
|
485
|
+
"Home"
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
body.should == 'Home'
|
490
|
+
status('REQUEST_METHOD'=>'POST').should == 404
|
491
|
+
status("//").should == 404
|
478
492
|
status("/foo").should == 404
|
479
493
|
end
|
480
494
|
end
|
@@ -614,18 +628,34 @@ describe "request verb methods" do
|
|
614
628
|
end
|
615
629
|
end
|
616
630
|
|
631
|
+
describe "all matcher" do
|
632
|
+
it "should match only all all arguments match" do
|
633
|
+
app do |r|
|
634
|
+
r.is :all=>['foo', :y] do |file|
|
635
|
+
file
|
636
|
+
end
|
637
|
+
end
|
638
|
+
|
639
|
+
body("/foo/bar").should == 'bar'
|
640
|
+
status.should == 404
|
641
|
+
status("/foo").should == 404
|
642
|
+
status("/foo/").should == 404
|
643
|
+
status("/foo/bar/baz").should == 404
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
617
647
|
describe "extension matcher" do
|
618
648
|
it "should match given file extensions" do
|
619
649
|
app do |r|
|
620
|
-
r.on "
|
650
|
+
r.on "css" do
|
621
651
|
r.on :extension=>"css" do |file|
|
622
652
|
file
|
623
653
|
end
|
624
654
|
end
|
625
655
|
end
|
626
656
|
|
627
|
-
body("/
|
628
|
-
status("/
|
657
|
+
body("/css/reset.css").should == 'reset'
|
658
|
+
status("/css/reset.bar").should == 404
|
629
659
|
end
|
630
660
|
end
|
631
661
|
|
@@ -656,3 +686,29 @@ describe "route block that returns string" do
|
|
656
686
|
body.should == '+1'
|
657
687
|
end
|
658
688
|
end
|
689
|
+
|
690
|
+
describe "hash_matcher" do
|
691
|
+
it "should enable the handling of arbitrary hash keys" do
|
692
|
+
app(:bare) do
|
693
|
+
hash_matcher(:foos){|v| consume(self.class.cached_matcher(:"foos-#{v}"){/((?:foo){#{v}})/})}
|
694
|
+
route do |r|
|
695
|
+
r.is :foos=>1 do |f|
|
696
|
+
"1#{f}"
|
697
|
+
end
|
698
|
+
r.is :foos=>2 do |f|
|
699
|
+
"2#{f}"
|
700
|
+
end
|
701
|
+
r.is :foos=>3 do |f|
|
702
|
+
"3#{f}"
|
703
|
+
end
|
704
|
+
end
|
705
|
+
end
|
706
|
+
|
707
|
+
body("/foo").should == '1foo'
|
708
|
+
body("/foofoo").should == '2foofoo'
|
709
|
+
body("/foofoofoo").should == '3foofoofoo'
|
710
|
+
status("/foofoofoofoo").should == 404
|
711
|
+
status.should == 404
|
712
|
+
end
|
713
|
+
end
|
714
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'erubis'
|
5
|
+
require 'tilt/erb'
|
6
|
+
begin
|
7
|
+
require 'tilt/erubis'
|
8
|
+
rescue LoadError
|
9
|
+
# Tilt 1 support
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
warn "tilt or erubis not installed, skipping _erubis_escaping plugin test"
|
13
|
+
else
|
14
|
+
describe "_erubis_escaping plugin" do
|
15
|
+
before do
|
16
|
+
app(:bare) do
|
17
|
+
plugin :render, :escape=>true
|
18
|
+
|
19
|
+
route do |r|
|
20
|
+
render(:inline=>'<%= "<>" %> <%== "<>" %><%= "<>" if false %>')
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should escape inside <%= %> and not inside <%== %>, and handle postfix conditionals" do
|
26
|
+
body.should == '<> <>'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|