webmachine 1.0.0 → 1.1.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 (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