roda 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +70 -0
  3. data/README.rdoc +261 -302
  4. data/Rakefile +1 -1
  5. data/doc/release_notes/1.2.0.txt +406 -0
  6. data/lib/roda.rb +206 -124
  7. data/lib/roda/plugins/all_verbs.rb +11 -10
  8. data/lib/roda/plugins/assets.rb +5 -5
  9. data/lib/roda/plugins/backtracking_array.rb +12 -5
  10. data/lib/roda/plugins/caching.rb +10 -8
  11. data/lib/roda/plugins/class_level_routing.rb +94 -0
  12. data/lib/roda/plugins/content_for.rb +6 -0
  13. data/lib/roda/plugins/default_headers.rb +4 -11
  14. data/lib/roda/plugins/delay_build.rb +42 -0
  15. data/lib/roda/plugins/delegate.rb +64 -0
  16. data/lib/roda/plugins/drop_body.rb +33 -0
  17. data/lib/roda/plugins/empty_root.rb +48 -0
  18. data/lib/roda/plugins/environments.rb +68 -0
  19. data/lib/roda/plugins/error_email.rb +1 -2
  20. data/lib/roda/plugins/error_handler.rb +1 -1
  21. data/lib/roda/plugins/halt.rb +7 -5
  22. data/lib/roda/plugins/head.rb +4 -2
  23. data/lib/roda/plugins/header_matchers.rb +17 -9
  24. data/lib/roda/plugins/hooks.rb +16 -32
  25. data/lib/roda/plugins/json.rb +4 -10
  26. data/lib/roda/plugins/mailer.rb +233 -0
  27. data/lib/roda/plugins/match_affix.rb +48 -0
  28. data/lib/roda/plugins/multi_route.rb +9 -11
  29. data/lib/roda/plugins/multi_run.rb +81 -0
  30. data/lib/roda/plugins/named_templates.rb +93 -0
  31. data/lib/roda/plugins/not_allowed.rb +43 -48
  32. data/lib/roda/plugins/path.rb +63 -2
  33. data/lib/roda/plugins/render.rb +79 -48
  34. data/lib/roda/plugins/render_each.rb +6 -0
  35. data/lib/roda/plugins/sinatra_helpers.rb +523 -0
  36. data/lib/roda/plugins/slash_path_empty.rb +25 -0
  37. data/lib/roda/plugins/static_path_info.rb +64 -0
  38. data/lib/roda/plugins/streaming.rb +1 -1
  39. data/lib/roda/plugins/view_subdirs.rb +12 -8
  40. data/lib/roda/version.rb +1 -1
  41. data/spec/integration_spec.rb +33 -0
  42. data/spec/plugin/backtracking_array_spec.rb +24 -18
  43. data/spec/plugin/class_level_routing_spec.rb +138 -0
  44. data/spec/plugin/delay_build_spec.rb +23 -0
  45. data/spec/plugin/delegate_spec.rb +20 -0
  46. data/spec/plugin/drop_body_spec.rb +20 -0
  47. data/spec/plugin/empty_root_spec.rb +14 -0
  48. data/spec/plugin/environments_spec.rb +31 -0
  49. data/spec/plugin/h_spec.rb +1 -3
  50. data/spec/plugin/header_matchers_spec.rb +14 -0
  51. data/spec/plugin/hooks_spec.rb +3 -5
  52. data/spec/plugin/mailer_spec.rb +191 -0
  53. data/spec/plugin/match_affix_spec.rb +22 -0
  54. data/spec/plugin/multi_run_spec.rb +31 -0
  55. data/spec/plugin/named_templates_spec.rb +65 -0
  56. data/spec/plugin/path_spec.rb +66 -2
  57. data/spec/plugin/render_spec.rb +46 -1
  58. data/spec/plugin/sinatra_helpers_spec.rb +534 -0
  59. data/spec/plugin/slash_path_empty_spec.rb +22 -0
  60. data/spec/plugin/static_path_info_spec.rb +50 -0
  61. data/spec/request_spec.rb +23 -0
  62. data/spec/response_spec.rb +12 -1
  63. metadata +48 -6
@@ -0,0 +1,25 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The slash_path_empty plugin considers "/" as an empty path,
4
+ # in addition to the default of "" being considered an empty
5
+ # path. This makes it so +r.is+ without an argument will match
6
+ # a path of "/", and +r.is+ and verb methods such as +r.get+ and
7
+ # +r.post+ will match if the path is "/" after the arguments
8
+ # are processed. This can make it easier to handle applications
9
+ # where a trailing "/" in the path should be ignored.
10
+ module SlashPathEmpty
11
+ SLASH = "/".freeze
12
+
13
+ module RequestMethods
14
+ private
15
+
16
+ # Consider the path empty if it is "/".
17
+ def empty_path?
18
+ super || remaining_path == SLASH
19
+ end
20
+ end
21
+ end
22
+
23
+ register_plugin(:slash_path_empty, SlashPathEmpty)
24
+ end
25
+ end
@@ -0,0 +1,64 @@
1
+ class Roda
2
+ module RodaPlugins
3
+ # The static_path_info plugin changes Roda's behavior so that the
4
+ # SCRIPT_NAME/PATH_INFO environment settings are not modified
5
+ # while the request is beind routed, improving performance. If
6
+ # you have any helpers that operate on PATH_INFO or SCRIPT_NAME,
7
+ # their behavior will not change depending on where they are
8
+ # called in the routing tree.
9
+ #
10
+ # This still updates SCRIPT_NAME/PATH_INFO before dispatching to
11
+ # another rack app via +r.run+.
12
+ module StaticPathInfo
13
+ module RequestMethods
14
+ PATH_INFO = "PATH_INFO".freeze
15
+ SCRIPT_NAME = "SCRIPT_NAME".freeze
16
+
17
+ # The current path to match requests against. This is initialized
18
+ # to PATH_INFO when the request is created.
19
+ attr_reader :remaining_path
20
+
21
+ # Set remaining_path when initializing.
22
+ def initialize(*)
23
+ super
24
+ @remaining_path = @env[PATH_INFO]
25
+ end
26
+
27
+ # The already matched part of the path, including the original SCRIPT_NAME.
28
+ def matched_path
29
+ e = @env
30
+ e[SCRIPT_NAME] + e[PATH_INFO].chomp(@remaining_path)
31
+ end
32
+
33
+ # Update SCRIPT_NAME/PATH_INFO based on the current remaining_path
34
+ # before dispatching to another rack app, so the app still works as
35
+ # a URL mapper.
36
+ def run(_)
37
+ e = @env
38
+ path = @remaining_path
39
+ e[SCRIPT_NAME] += e[PATH_INFO].chomp(path)
40
+ e[PATH_INFO] = path
41
+ super
42
+ end
43
+
44
+ private
45
+
46
+ # Update remaining_path with the remaining characters
47
+ def update_remaining_path(remaining)
48
+ @remaining_path = remaining
49
+ end
50
+
51
+ # Yield to the block, restoring the remaining_path before
52
+ # the method returns.
53
+ def keep_remaining_path
54
+ path = @remaining_path
55
+ yield
56
+ ensure
57
+ @remaining_path = path
58
+ end
59
+ end
60
+ end
61
+
62
+ register_plugin(:static_path_info, StaticPathInfo)
63
+ end
64
+ end
@@ -154,7 +154,7 @@ class Roda
154
154
  end
155
155
  end
156
156
 
157
- throw :halt, response.finish_with_body(Stream.new(opts, &block))
157
+ throw :halt, @_response.finish_with_body(Stream.new(opts, &block))
158
158
  end
159
159
  end
160
160
  end
@@ -23,15 +23,18 @@ class Roda
23
23
  # end
24
24
  # end
25
25
  #
26
- # This plugin should be loaded after the render plugin, since
27
- # it works by overriding parts of the render plugin.
28
- #
29
26
  # Note that when a view subdirectory is set, the layout will
30
27
  # also be looked up in the subdirectory unless it contains
31
28
  # a slash. So if you want to use a view subdirectory for
32
29
  # templates but have a shared layout, you should make sure your
33
30
  # layout contains a slash, similar to the example above.
34
31
  module ViewSubdirs
32
+ # Load the render plugin before this plugin, since this plugin
33
+ # works by overriding a method in the render plugin.
34
+ def self.load_dependencies(app)
35
+ app.plugin :render
36
+ end
37
+
35
38
  module InstanceMethods
36
39
  # Set the view subdirectory to use. This can be set to nil
37
40
  # to not use a view subdirectory.
@@ -44,12 +47,13 @@ class Roda
44
47
  # Override the template name to use the view subdirectory if the
45
48
  # there is a view subdirectory and the template name does not
46
49
  # contain a slash.
47
- def template_path(template, opts)
48
- t = template.to_s
49
- if (v = @_view_subdir) && t !~ /\//
50
- template = "#{v}/#{t}"
50
+ def template_name(opts)
51
+ name = super
52
+ if (v = @_view_subdir) && name !~ /\//
53
+ "#{v}/#{name}"
54
+ else
55
+ name
51
56
  end
52
- super
53
57
  end
54
58
  end
55
59
  end
@@ -1,3 +1,3 @@
1
1
  class Roda
2
- RodaVersion = '1.1.0'.freeze
2
+ RodaVersion = '1.2.0'.freeze
3
3
  end
@@ -35,6 +35,25 @@ describe "integration" do
35
35
  body('/hello').should == 'D First Second Block'
36
36
  end
37
37
 
38
+ it "should clear middleware when clear_middleware! is called" do
39
+ c = @c
40
+ app(:bare) do
41
+ use c, "First", "Second" do
42
+ "Block"
43
+ end
44
+
45
+ route do |r|
46
+ r.get "hello" do
47
+ "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
48
+ end
49
+ end
50
+
51
+ clear_middleware!
52
+ end
53
+
54
+ body('/hello').should == 'D '
55
+ end
56
+
38
57
  it "should support adding middleware using use after route block setup" do
39
58
  c = @c
40
59
  app(:bare) do
@@ -64,6 +83,20 @@ describe "integration" do
64
83
  body('/hello').should == 'D 1 2 3'
65
84
  end
66
85
 
86
+ it "should not inherit middleware in subclass if inhert_middleware = false" do
87
+ c = @c
88
+ app(:bare){use(c, '1', '2'){"3"}}
89
+ @app.inherit_middleware = false
90
+ @app = Class.new(@app)
91
+ @app.route do |r|
92
+ r.get "hello" do
93
+ "D #{r.env['m.first']} #{r.env['m.second']} #{r.env['m.block']}"
94
+ end
95
+ end
96
+
97
+ body('/hello').should == 'D '
98
+ end
99
+
67
100
  it "should inherit route in subclass" do
68
101
  c = @c
69
102
  app(:bare) do
@@ -16,23 +16,29 @@ describe "backtracking_array plugin" do
16
16
  end
17
17
  end
18
18
 
19
- status.should == 404
20
-
21
- body("/a").should == 'a'
22
- body("/a/b").should == 'a/b'
23
- status("/a/b/").should == 404
24
-
25
- body("/c/d").should == 'c-d'
26
- body("/c/e").should == 'c-e'
27
- body("/c/d/d").should == 'c/d-d'
28
- body("/c/d/e").should == 'c/d-e'
29
- status("/c/d/").should == 404
30
-
31
- body("/f").should == 'f'
32
- body("/f/g").should == 'f/g'
33
- body("/g").should == 'g'
34
- body("/g/h").should == 'g/h'
35
- status("/f/g/").should == 404
36
- status("/g/h/").should == 404
19
+ tests = lambda do
20
+ status.should == 404
21
+
22
+ body("/a").should == 'a'
23
+ body("/a/b").should == 'a/b'
24
+ status("/a/b/").should == 404
25
+
26
+ body("/c/d").should == 'c-d'
27
+ body("/c/e").should == 'c-e'
28
+ body("/c/d/d").should == 'c/d-d'
29
+ body("/c/d/e").should == 'c/d-e'
30
+ status("/c/d/").should == 404
31
+
32
+ body("/f").should == 'f'
33
+ body("/f/g").should == 'f/g'
34
+ body("/g").should == 'g'
35
+ body("/g/h").should == 'g/h'
36
+ status("/f/g/").should == 404
37
+ status("/g/h/").should == 404
38
+ end
39
+
40
+ tests.call
41
+ app.plugin(:static_path_info)
42
+ tests.call
37
43
  end
38
44
  end
@@ -0,0 +1,138 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "class_level_routing plugin" do
4
+ before do
5
+ app(:bare) do
6
+ plugin :class_level_routing
7
+ plugin :all_verbs
8
+
9
+ root do
10
+ 'root'
11
+ end
12
+
13
+ on "foo" do
14
+ request.get "bar" do
15
+ "foobar"
16
+ end
17
+
18
+ "foo"
19
+ end
20
+
21
+ is "d:d" do |x|
22
+ request.get do
23
+ "bazget#{x}"
24
+ end
25
+
26
+ request.post do
27
+ "bazpost#{x}"
28
+ end
29
+ end
30
+
31
+ meths = %w'get post delete head options patch put trace'
32
+ meths.concat(%w'link unlink') if ::Rack::Request.method_defined?("link?")
33
+ meths.each do |meth|
34
+ send(meth, :d) do |m|
35
+ "x-#{meth}-#{m}"
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ it "adds class methods for setting up routes" do
42
+ body.should == 'root'
43
+ body('/foo').should == 'foo'
44
+ body('/foo/bar').should == 'foobar'
45
+ body('/dgo').should == 'bazgetgo'
46
+ body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
47
+ body('/bar').should == "x-get-bar"
48
+ body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
49
+ body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
50
+ body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
51
+ body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
52
+ body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
53
+ body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
54
+ body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
55
+ if ::Rack::Request.method_defined?("link?")
56
+ body('/bar', 'REQUEST_METHOD'=>'LINK').should == "x-link-bar"
57
+ body('/bar', 'REQUEST_METHOD'=>'UNLINK').should == "x-unlink-bar"
58
+ end
59
+
60
+ status.should == 200
61
+ status("/asdfa/asdf").should == 404
62
+
63
+ @app = Class.new(app)
64
+ body.should == 'root'
65
+ body('/foo').should == 'foo'
66
+ body('/foo/bar').should == 'foobar'
67
+ body('/dgo').should == 'bazgetgo'
68
+ body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
69
+ body('/bar').should == "x-get-bar"
70
+ body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
71
+ body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
72
+ body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
73
+ body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
74
+ body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
75
+ body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
76
+ body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
77
+ end
78
+
79
+ it "only calls class level routes if routing tree doesn't handle request" do
80
+ app.route do |r|
81
+ r.root do
82
+ 'iroot'
83
+ end
84
+
85
+ r.get 'foo' do
86
+ 'ifoo'
87
+ end
88
+
89
+ r.on 'bar' do
90
+ r.get true do
91
+ response.status = 404
92
+ ''
93
+ end
94
+ r.post true do
95
+ 'ibar'
96
+ end
97
+ end
98
+ end
99
+
100
+ body.should == 'iroot'
101
+ body('/foo').should == 'ifoo'
102
+ body('/foo/bar').should == 'foobar'
103
+ body('/dgo').should == 'bazgetgo'
104
+ body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
105
+ body('/bar').should == ""
106
+ body('/bar', 'REQUEST_METHOD'=>'POST').should == "ibar"
107
+ body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
108
+ body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
109
+ body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
110
+ body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
111
+ body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
112
+ body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
113
+ end
114
+
115
+ it "works with the not_found plugin if loaded before" do
116
+ app.plugin :not_found do
117
+ "nf"
118
+ end
119
+
120
+ body.should == 'root'
121
+ body('/foo').should == 'foo'
122
+ body('/foo/bar').should == 'foobar'
123
+ body('/dgo').should == 'bazgetgo'
124
+ body('/dgo', 'REQUEST_METHOD'=>'POST').should == 'bazpostgo'
125
+ body('/bar').should == "x-get-bar"
126
+ body('/bar', 'REQUEST_METHOD'=>'POST').should == "x-post-bar"
127
+ body('/bar', 'REQUEST_METHOD'=>'DELETE').should == "x-delete-bar"
128
+ body('/bar', 'REQUEST_METHOD'=>'HEAD').should == "x-head-bar"
129
+ body('/bar', 'REQUEST_METHOD'=>'OPTIONS').should == "x-options-bar"
130
+ body('/bar', 'REQUEST_METHOD'=>'PATCH').should == "x-patch-bar"
131
+ body('/bar', 'REQUEST_METHOD'=>'PUT').should == "x-put-bar"
132
+ body('/bar', 'REQUEST_METHOD'=>'TRACE').should == "x-trace-bar"
133
+
134
+ status.should == 200
135
+ status("/asdfa/asdf").should == 404
136
+ body("/asdfa/asdf").should == "nf"
137
+ end
138
+ end
@@ -0,0 +1,23 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "delay_build plugin" do
4
+ it "does not build rack app until app is called" do
5
+ app(:delay_build){"a"}
6
+ app.instance_variable_get(:@app).should == nil
7
+ body.should == "a"
8
+ app.instance_variable_get(:@app).should_not == nil
9
+ end
10
+
11
+ it "only rebuilds the app if build! is called" do
12
+ app(:delay_build){"a"}
13
+ body.should == "a"
14
+ c = Class.new do
15
+ def initialize(_) end
16
+ def call(_) [200, {}, ["b"]] end
17
+ end
18
+ app.use c
19
+ body.should == "a"
20
+ app.build!
21
+ body.should == "b"
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "delegate plugin" do
4
+ it "adds request_delegate and response_delegate class methods for delegating" do
5
+ app(:bare) do
6
+ plugin :delegate
7
+ request_delegate :root
8
+ response_delegate :headers
9
+
10
+ route do
11
+ root do
12
+ headers['Content-Type'] = 'foo'
13
+ end
14
+ end
15
+ end
16
+
17
+ header('Content-Type').should == 'foo'
18
+ status('/foo').should == 404
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ require File.expand_path("spec_helper", File.dirname(File.dirname(__FILE__)))
2
+
3
+ describe "drop_body plugin" do
4
+ it "automatically drops body and Content-Type/Content-Length headers for responses without a body" do
5
+ app(:drop_body) do |r|
6
+ response.status = r.path.to_i
7
+ response.write('a')
8
+ end
9
+
10
+ [101, 102, 204, 205, 304].each do |i|
11
+ body(i.to_s).should == ''
12
+ header('Content-Type', i.to_s).should == nil
13
+ header('Content-Length', i.to_s).should == nil
14
+ end
15
+
16
+ body('200').should == 'a'
17
+ header('Content-Type', '200').should == 'text/html'
18
+ header('Content-Length', '200').should == '1'
19
+ end
20
+ end