webmachine 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/Gemfile +9 -0
  2. data/README.md +65 -2
  3. data/lib/webmachine.rb +1 -0
  4. data/lib/webmachine/adapters.rb +4 -1
  5. data/lib/webmachine/adapters/hatetepe.rb +104 -0
  6. data/lib/webmachine/adapters/lazy_request_body.rb +32 -0
  7. data/lib/webmachine/adapters/rack.rb +2 -1
  8. data/lib/webmachine/adapters/reel.rb +45 -0
  9. data/lib/webmachine/adapters/webrick.rb +1 -30
  10. data/lib/webmachine/decision/falsey.rb +10 -0
  11. data/lib/webmachine/decision/flow.rb +28 -26
  12. data/lib/webmachine/decision/fsm.rb +22 -12
  13. data/lib/webmachine/decision/helpers.rb +17 -23
  14. data/lib/webmachine/etags.rb +69 -0
  15. data/lib/webmachine/headers.rb +42 -0
  16. data/lib/webmachine/quoted_string.rb +39 -0
  17. data/lib/webmachine/resource.rb +17 -0
  18. data/lib/webmachine/resource/callbacks.rb +1 -1
  19. data/lib/webmachine/resource/entity_tags.rb +17 -0
  20. data/lib/webmachine/streaming.rb +9 -61
  21. data/lib/webmachine/streaming/callable_encoder.rb +21 -0
  22. data/lib/webmachine/streaming/encoder.rb +24 -0
  23. data/lib/webmachine/streaming/enumerable_encoder.rb +20 -0
  24. data/lib/webmachine/streaming/fiber_encoder.rb +25 -0
  25. data/lib/webmachine/streaming/io_encoder.rb +65 -0
  26. data/lib/webmachine/trace/fsm.rb +9 -4
  27. data/lib/webmachine/trace/resource_proxy.rb +2 -4
  28. data/lib/webmachine/trace/static/tracelist.erb +2 -2
  29. data/lib/webmachine/trace/trace_resource.rb +3 -2
  30. data/lib/webmachine/version.rb +1 -1
  31. data/spec/webmachine/adapters/hatetepe_spec.rb +64 -0
  32. data/spec/webmachine/adapters/rack_spec.rb +18 -8
  33. data/spec/webmachine/adapters/reel_spec.rb +23 -0
  34. data/spec/webmachine/decision/falsey_spec.rb +8 -0
  35. data/spec/webmachine/decision/flow_spec.rb +12 -0
  36. data/spec/webmachine/decision/fsm_spec.rb +101 -0
  37. data/spec/webmachine/decision/helpers_spec.rb +68 -8
  38. data/spec/webmachine/dispatcher/route_spec.rb +1 -1
  39. data/spec/webmachine/dispatcher_spec.rb +1 -1
  40. data/spec/webmachine/errors_spec.rb +1 -1
  41. data/spec/webmachine/etags_spec.rb +75 -0
  42. data/spec/webmachine/headers_spec.rb +72 -0
  43. data/spec/webmachine/trace/fsm_spec.rb +5 -0
  44. data/spec/webmachine/trace/resource_proxy_spec.rb +1 -3
  45. data/webmachine.gemspec +1 -2
  46. metadata +49 -20
@@ -414,6 +414,18 @@ describe Webmachine::Decision::Flow do
414
414
  subject.run
415
415
  response.code.should_not == 404
416
416
  end
417
+
418
+ it "should not reply with 404 for truthy non-booleans" do
419
+ resource.exist = []
420
+ subject.run
421
+ response.code.should_not == 404
422
+ end
423
+
424
+ it "should reply with 404 for nil" do
425
+ resource.exist = nil
426
+ subject.run
427
+ response.code.should == 404
428
+ end
417
429
  end
418
430
 
419
431
  # Conditional requests/preconditions
@@ -0,0 +1,101 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::Decision::FSM do
4
+ include_context 'default resource'
5
+
6
+ subject { described_class.new(resource, request, response) }
7
+
8
+ describe 'handling of exceptions from decision methods' do
9
+ let(:exception) { Exception.new }
10
+
11
+ before do
12
+ subject.stub(Webmachine::Decision::Flow::START) { raise exception }
13
+ end
14
+
15
+ it 'calls resource.handle_exception' do
16
+ resource.should_receive(:handle_exception).with(exception)
17
+ subject.run
18
+ end
19
+
20
+ it 'calls resource.finish_request' do
21
+ resource.should_receive(:finish_request)
22
+ subject.run
23
+ end
24
+ end
25
+
26
+ describe 'handling of exceptions from resource.handle_exception' do
27
+ let(:exception) { Exception.new('an error message') }
28
+
29
+ before do
30
+ subject.stub(Webmachine::Decision::Flow::START) { raise }
31
+ resource.stub(:handle_exception) { raise exception }
32
+ end
33
+
34
+ it 'does not call resource.handle_exception again' do
35
+ resource.should_receive(:handle_exception).once { raise }
36
+ subject.run
37
+ end
38
+
39
+ it 'does not call resource.finish_request' do
40
+ resource.should_not_receive(:finish_request)
41
+ subject.run
42
+ end
43
+
44
+ it 'renders an error' do
45
+ Webmachine.
46
+ should_receive(:render_error).
47
+ with(500, request, response, { :message => exception.message })
48
+ subject.run
49
+ end
50
+ end
51
+
52
+ describe 'handling of exceptions from resource.finish_request' do
53
+ let(:exception) { Exception.new }
54
+
55
+ before do
56
+ resource.stub(:finish_request) { raise exception }
57
+ end
58
+
59
+ it 'calls resource.handle_exception' do
60
+ resource.should_receive(:handle_exception).with(exception)
61
+ subject.run
62
+ end
63
+
64
+ it 'does not call resource.finish_request again' do
65
+ resource.should_receive(:finish_request).once { raise }
66
+ subject.run
67
+ end
68
+ end
69
+
70
+ it "sets the response code before calling finish_request" do
71
+ resource_class.class_eval do
72
+ class << self
73
+ attr_accessor :current_response_code
74
+ end
75
+
76
+ def to_html
77
+ 201
78
+ end
79
+
80
+ def finish_request
81
+ self.class.current_response_code = response.code
82
+ end
83
+ end
84
+
85
+ subject.run
86
+
87
+ resource_class.current_response_code.should be(201)
88
+ end
89
+
90
+ it 'respects a response code set by resource.finish_request' do
91
+ resource_class.class_eval do
92
+ def finish_request
93
+ response.code = 451
94
+ end
95
+ end
96
+
97
+ subject.run
98
+
99
+ response.code.should be(451)
100
+ end
101
+ end
@@ -47,7 +47,7 @@ describe Webmachine::Decision::Helpers do
47
47
  end
48
48
 
49
49
  context "setting the Content-Length header when responding" do
50
- [204, 304].each do |code|
50
+ [204, 205, 304].each do |code|
51
51
  it "removes the header for entity-less response code #{code}" do
52
52
  response.headers['Content-Length'] = '0'
53
53
  response.body = nil
@@ -57,9 +57,9 @@ describe Webmachine::Decision::Helpers do
57
57
  end
58
58
 
59
59
  (200..599).each do |code|
60
- # 204 and 304 have no bodies, 404 is set to a default non-zero
61
- # response by Webmachine
62
- next if code == 204 || code == 304 || code == 404
60
+ # 204, 205 and 304 have no bodies, 404 is set to a default
61
+ # non-zero response by Webmachine
62
+ next if [204, 205, 304, 404].include? code
63
63
 
64
64
  it "adds the header for response code #{code} that should include an entity but has an empty body" do
65
65
  response.code = code
@@ -70,7 +70,7 @@ describe Webmachine::Decision::Helpers do
70
70
  end
71
71
 
72
72
  (200..599).each do |code|
73
- next if code == 204 || code == 304
73
+ next if [204, 205, 304].include? code
74
74
 
75
75
  it "does not add the header when Transfer-Encoding is set on code #{code}" do
76
76
  response.headers['Transfer-Encoding'] = 'chunked'
@@ -115,7 +115,7 @@ describe Webmachine::Decision::Helpers do
115
115
 
116
116
  it "wraps the response body in an EnumerableEncoder" do
117
117
  subject.encode_body
118
- Webmachine::EnumerableEncoder.should === response.body
118
+ Webmachine::Streaming::EnumerableEncoder.should === response.body
119
119
  end
120
120
 
121
121
  it_should_behave_like "a non-String body"
@@ -126,7 +126,7 @@ describe Webmachine::Decision::Helpers do
126
126
 
127
127
  it "wraps the response body in a CallableEncoder" do
128
128
  subject.encode_body
129
- Webmachine::CallableEncoder.should === response.body
129
+ Webmachine::Streaming::CallableEncoder.should === response.body
130
130
  end
131
131
 
132
132
  it_should_behave_like "a non-String body"
@@ -137,10 +137,70 @@ describe Webmachine::Decision::Helpers do
137
137
 
138
138
  it "wraps the response body in a FiberEncoder" do
139
139
  subject.encode_body
140
- Webmachine::FiberEncoder.should === response.body
140
+ Webmachine::Streaming::FiberEncoder.should === response.body
141
141
  end
142
142
 
143
143
  it_should_behave_like "a non-String body"
144
144
  end
145
+
146
+ context "with a File body" do
147
+ before { response.body = File.open("spec/spec_helper.rb", "r") }
148
+
149
+ it "wraps the response body in an IOEncoder" do
150
+ subject.encode_body
151
+ Webmachine::Streaming::IOEncoder.should === response.body
152
+ end
153
+
154
+ it "sets the Content-Length header to the size of the file" do
155
+ subject.encode_body
156
+ response.headers['Content-Length'].should == File.stat('spec/spec_helper.rb').size.to_s
157
+ end
158
+
159
+ context "when the resource provides a non-identity encoding that the client accepts" do
160
+ let(:resource) do
161
+ resource_with do
162
+ def encodings_provided
163
+ { "deflate" => :encode_deflate, "identity" => :encode_identity }
164
+ end
165
+ end
166
+ end
167
+
168
+ let(:headers) do
169
+ Webmachine::Headers.new({"Accept-Encoding" => "deflate, identity"})
170
+ end
171
+
172
+ it_should_behave_like "a non-String body"
173
+ end
174
+ end
175
+
176
+ context "with a StringIO body" do
177
+ before { response.body = StringIO.new("A VERY LONG STRING, NOT") }
178
+
179
+ it "wraps the response body in an IOEncoder" do
180
+ subject.encode_body
181
+ Webmachine::Streaming::IOEncoder.should === response.body
182
+ end
183
+
184
+ it "sets the Content-Length header to the size of the string" do
185
+ subject.encode_body
186
+ response.headers['Content-Length'].should == response.body.size.to_s
187
+ end
188
+
189
+ context "when the resource provides a non-identity encoding that the client accepts" do
190
+ let(:resource) do
191
+ resource_with do
192
+ def encodings_provided
193
+ { "deflate" => :encode_deflate, "identity" => :encode_identity }
194
+ end
195
+ end
196
+ end
197
+
198
+ let(:headers) do
199
+ Webmachine::Headers.new({"Accept-Encoding" => "deflate, identity"})
200
+ end
201
+
202
+ it_should_behave_like "a non-String body"
203
+ end
204
+ end
145
205
  end
146
206
  end
@@ -7,7 +7,7 @@ describe Webmachine::Dispatcher::Route do
7
7
  let(:resource){ Class.new(Webmachine::Resource) }
8
8
 
9
9
  matcher :match_route do |*expected|
10
- route = described_class.new(expected[0], resource, expected[1] || {})
10
+ route = Webmachine::Dispatcher::Route.new(expected[0], Class.new(Webmachine::Resource), expected[1] || {})
11
11
  match do |actual|
12
12
  request.uri.path = actual if String === actual
13
13
  route.match?(request)
@@ -29,7 +29,7 @@ describe Webmachine::Dispatcher do
29
29
  it "should add routes" do
30
30
  expect {
31
31
  dispatcher.add_route ['*'], resource
32
- }.should_not raise_error
32
+ }.to_not raise_error
33
33
  end
34
34
 
35
35
  it "should have add_route return the newly created route" do
@@ -7,7 +7,7 @@ describe "Webmachine errors" do
7
7
  res = Webmachine::Response.new
8
8
 
9
9
  Webmachine.render_error(404, req, res)
10
- res.code.should be(404)
10
+ res.code.should == 404
11
11
  end
12
12
  end
13
13
  end
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+
3
+ describe Webmachine::ETag do
4
+ let(:etag_str){ '"deadbeef12345678"' }
5
+ let(:etag) { described_class.new etag_str }
6
+
7
+ subject { etag }
8
+
9
+ it { should == etag_str }
10
+ it { should be_kind_of(described_class) }
11
+ its(:to_s) { should == '"deadbeef12345678"' }
12
+ its(:etag) { should == '"deadbeef12345678"' }
13
+ it { should == described_class.new(etag_str.dup) }
14
+
15
+ context "when the original etag is unquoted" do
16
+ let(:etag_str) { 'deadbeef12345678' }
17
+
18
+ it { should == etag_str }
19
+ its(:to_s) { should == '"deadbeef12345678"' }
20
+ its(:etag) { should == '"deadbeef12345678"' }
21
+ it { should == described_class.new(etag_str.dup) }
22
+ end
23
+
24
+ context "when the original etag contains unbalanced quotes" do
25
+ let(:etag_str) { 'deadbeef"12345678' }
26
+
27
+ it { should == etag_str }
28
+ its(:to_s) { should == '"deadbeef\\"12345678"' }
29
+ its(:etag) { should == '"deadbeef\\"12345678"' }
30
+ it { should == described_class.new(etag_str.dup) }
31
+ end
32
+ end
33
+
34
+ describe Webmachine::WeakETag do
35
+ let(:strong_etag){ '"deadbeef12345678"' }
36
+ let(:weak_etag) { described_class.new strong_etag }
37
+
38
+ subject { weak_etag }
39
+
40
+ it { should == strong_etag }
41
+ it { should be_kind_of(described_class) }
42
+ its(:to_s) { should == 'W/"deadbeef12345678"' }
43
+ its(:etag) { should == '"deadbeef12345678"' }
44
+ it { should == described_class.new(strong_etag.dup) }
45
+
46
+ context "when the original etag is unquoted" do
47
+ let(:strong_etag) { 'deadbeef12345678' }
48
+
49
+ it { should == strong_etag }
50
+ it { should be_kind_of(described_class) }
51
+ its(:to_s) { should == 'W/"deadbeef12345678"' }
52
+ its(:etag) { should == '"deadbeef12345678"' }
53
+ it { should == described_class.new(strong_etag.dup) }
54
+ end
55
+
56
+ context "when the original etag contains unbalanced quotes" do
57
+ let(:strong_etag) { 'deadbeef"12345678' }
58
+
59
+ it { should == strong_etag }
60
+ it { should be_kind_of(described_class) }
61
+ its(:to_s) { should == 'W/"deadbeef\\"12345678"' }
62
+ its(:etag) { should == '"deadbeef\\"12345678"' }
63
+ it { should == described_class.new(strong_etag.dup) }
64
+ end
65
+
66
+ context "when the original etag is already a weak tag" do
67
+ let(:strong_etag) { 'W/"deadbeef12345678"' }
68
+
69
+ it { should == strong_etag }
70
+ it { should be_kind_of(described_class) }
71
+ its(:to_s) { should == 'W/"deadbeef12345678"' }
72
+ its(:etag) { should == '"deadbeef12345678"' }
73
+ it { should == described_class.new(strong_etag.dup) }
74
+ end
75
+ end
@@ -14,6 +14,78 @@ describe Webmachine::Headers do
14
14
  end
15
15
  end
16
16
 
17
+ describe ".[]" do
18
+ context "Webmachine::Headers['Content-Type', 'application/json']" do
19
+ it "creates a hash with lowercase keys" do
20
+ headers = described_class[
21
+ 'Content-Type', 'application/json',
22
+ 'Accept', 'application/json'
23
+ ]
24
+
25
+ headers.to_hash.should == {
26
+ 'content-type' => 'application/json',
27
+ 'accept' => 'application/json'
28
+ }
29
+ end
30
+ end
31
+
32
+ context "Webmachine::Headers[[['Content-Type', 'application/json']]]" do
33
+ it "creates a hash with lowercase keys" do
34
+ headers = described_class[
35
+ [
36
+ ['Content-Type', 'application/json'],
37
+ ['Accept', 'application/json']
38
+ ]
39
+ ]
40
+
41
+ headers.to_hash.should == {
42
+ 'content-type' => 'application/json',
43
+ 'accept' => 'application/json'
44
+ }
45
+ end
46
+ end
47
+
48
+ context "Webmachine::Headers['Content-Type' => 'application/json']" do
49
+ it "creates a hash with lowercase keys" do
50
+ headers = described_class[
51
+ 'Content-Type' => 'application/json',
52
+ 'Accept' => 'application/json'
53
+ ]
54
+
55
+ headers.to_hash.should == {
56
+ 'content-type' => 'application/json',
57
+ 'accept' => 'application/json'
58
+ }
59
+ end
60
+ end
61
+ end
62
+
63
+ describe "#fetch" do
64
+ subject { described_class['Content-Type' => 'application/json'] }
65
+
66
+ it "returns the value for the given key" do
67
+ subject.fetch('conTent-tYpe').should == 'application/json'
68
+ end
69
+
70
+ context "acessing a missing key" do
71
+ it "raises an IndexError" do
72
+ expect { subject.fetch('accept') }.to raise_error(IndexError)
73
+ end
74
+
75
+ context "and a default value given" do
76
+ it "returns the default value if the key does not exist" do
77
+ subject.fetch('accept', 'text/html').should == 'text/html'
78
+ end
79
+ end
80
+
81
+ context "and a block given" do
82
+ it "passes the value to the block and returns the block's result" do
83
+ subject.fetch('access') {|k| "#{k} not found"}.should == 'access not found'
84
+ end
85
+ end
86
+ end
87
+ end
88
+
17
89
  context "filtering with #grep" do
18
90
  subject { described_class["content-type" => "text/plain", "etag" => '"abcdef1234567890"'] }
19
91
  it "should filter keys by the given pattern" do
@@ -18,6 +18,11 @@ describe Webmachine::Trace::FSM do
18
18
  response.trace.should_not be_empty
19
19
  Webmachine::Trace.traces.should have(1).item
20
20
  end
21
+
22
+ it "commits the trace to separate storage when the request has finished processing" do
23
+ Webmachine::Trace.should_receive(:record).with(subject.resource.object_id.to_s, response.trace).and_return(true)
24
+ subject.run
25
+ end
21
26
  end
22
27
 
23
28
  context "when tracing is disabled" do
@@ -27,9 +27,7 @@ describe Webmachine::Trace::ResourceProxy do
27
27
  response.trace[-1].should == {:type => :result, :value => "<html><body>Hello, world!</body></html>"}
28
28
  end
29
29
 
30
- it "commits the trace to separate storage when the request has finished processing" do
31
- Webmachine::Trace.should_receive(:record).with(subject.object_id.to_s, [{:type=>:attempt, :name=>"(default)#finish_request"},
32
- {:type=>:result, :value=>nil}]).and_return(true)
30
+ it "sets the trace id header when the request has finished processing" do
33
31
  subject.finish_request
34
32
  response.headers["X-Webmachine-Trace-Id"].should == subject.object_id.to_s
35
33
  end