roda 1.1.0 → 1.2.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 (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