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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +62 -0
  3. data/README.rdoc +362 -167
  4. data/Rakefile +2 -2
  5. data/doc/release_notes/1.0.0.txt +329 -0
  6. data/lib/roda.rb +553 -180
  7. data/lib/roda/plugins/_erubis_escaping.rb +28 -0
  8. data/lib/roda/plugins/all_verbs.rb +7 -9
  9. data/lib/roda/plugins/backtracking_array.rb +92 -0
  10. data/lib/roda/plugins/content_for.rb +46 -0
  11. data/lib/roda/plugins/csrf.rb +60 -0
  12. data/lib/roda/plugins/flash.rb +53 -7
  13. data/lib/roda/plugins/halt.rb +8 -14
  14. data/lib/roda/plugins/head.rb +56 -0
  15. data/lib/roda/plugins/header_matchers.rb +2 -2
  16. data/lib/roda/plugins/json.rb +84 -0
  17. data/lib/roda/plugins/multi_route.rb +50 -10
  18. data/lib/roda/plugins/not_allowed.rb +140 -0
  19. data/lib/roda/plugins/pass.rb +13 -6
  20. data/lib/roda/plugins/per_thread_caching.rb +70 -0
  21. data/lib/roda/plugins/render.rb +20 -33
  22. data/lib/roda/plugins/render_each.rb +61 -0
  23. data/lib/roda/plugins/symbol_matchers.rb +79 -0
  24. data/lib/roda/plugins/symbol_views.rb +40 -0
  25. data/lib/roda/plugins/view_subdirs.rb +53 -0
  26. data/lib/roda/version.rb +3 -0
  27. data/spec/matchers_spec.rb +61 -5
  28. data/spec/plugin/_erubis_escaping_spec.rb +29 -0
  29. data/spec/plugin/backtracking_array_spec.rb +38 -0
  30. data/spec/plugin/content_for_spec.rb +34 -0
  31. data/spec/plugin/csrf_spec.rb +49 -0
  32. data/spec/plugin/flash_spec.rb +69 -5
  33. data/spec/plugin/head_spec.rb +35 -0
  34. data/spec/plugin/json_spec.rb +50 -0
  35. data/spec/plugin/multi_route_spec.rb +22 -6
  36. data/spec/plugin/not_allowed_spec.rb +55 -0
  37. data/spec/plugin/pass_spec.rb +8 -2
  38. data/spec/plugin/per_thread_caching_spec.rb +28 -0
  39. data/spec/plugin/render_each_spec.rb +30 -0
  40. data/spec/plugin/render_spec.rb +7 -1
  41. data/spec/plugin/symbol_matchers_spec.rb +68 -0
  42. data/spec/plugin/symbol_views_spec.rb +32 -0
  43. data/spec/plugin/view_subdirs_spec.rb +45 -0
  44. data/spec/plugin_spec.rb +11 -1
  45. data/spec/redirect_spec.rb +21 -4
  46. data/spec/request_spec.rb +9 -0
  47. metadata +49 -5
@@ -27,10 +27,11 @@ class Roda
27
27
  #
28
28
  # The following options are supported:
29
29
  #
30
- # :cache :: A specific cache to store templates in, or nil/false to not
31
- # cache templates (useful for development), defaults to true to
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
- # 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
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
- cache = opts.fetch(:cache, true)
116
- opts[:cache] = Cache.new if cache == true
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] = Cache.new if 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.fetch(path, &block)
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
@@ -0,0 +1,3 @@
1
+ class Roda
2
+ RodaVersion = '1.0.0'.freeze
3
+ end
@@ -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.on "" do
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 "styles" do
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("/styles/reset.css").should == 'reset'
628
- status("/styles/reset.bar").should == 404
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 == '&lt;&gt; <>'
27
+ end
28
+ end
29
+ end