webmachine 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +9 -0
- data/README.md +65 -2
- data/lib/webmachine.rb +1 -0
- data/lib/webmachine/adapters.rb +4 -1
- data/lib/webmachine/adapters/hatetepe.rb +104 -0
- data/lib/webmachine/adapters/lazy_request_body.rb +32 -0
- data/lib/webmachine/adapters/rack.rb +2 -1
- data/lib/webmachine/adapters/reel.rb +45 -0
- data/lib/webmachine/adapters/webrick.rb +1 -30
- data/lib/webmachine/decision/falsey.rb +10 -0
- data/lib/webmachine/decision/flow.rb +28 -26
- data/lib/webmachine/decision/fsm.rb +22 -12
- data/lib/webmachine/decision/helpers.rb +17 -23
- data/lib/webmachine/etags.rb +69 -0
- data/lib/webmachine/headers.rb +42 -0
- data/lib/webmachine/quoted_string.rb +39 -0
- data/lib/webmachine/resource.rb +17 -0
- data/lib/webmachine/resource/callbacks.rb +1 -1
- data/lib/webmachine/resource/entity_tags.rb +17 -0
- data/lib/webmachine/streaming.rb +9 -61
- data/lib/webmachine/streaming/callable_encoder.rb +21 -0
- data/lib/webmachine/streaming/encoder.rb +24 -0
- data/lib/webmachine/streaming/enumerable_encoder.rb +20 -0
- data/lib/webmachine/streaming/fiber_encoder.rb +25 -0
- data/lib/webmachine/streaming/io_encoder.rb +65 -0
- data/lib/webmachine/trace/fsm.rb +9 -4
- data/lib/webmachine/trace/resource_proxy.rb +2 -4
- data/lib/webmachine/trace/static/tracelist.erb +2 -2
- data/lib/webmachine/trace/trace_resource.rb +3 -2
- data/lib/webmachine/version.rb +1 -1
- data/spec/webmachine/adapters/hatetepe_spec.rb +64 -0
- data/spec/webmachine/adapters/rack_spec.rb +18 -8
- data/spec/webmachine/adapters/reel_spec.rb +23 -0
- data/spec/webmachine/decision/falsey_spec.rb +8 -0
- data/spec/webmachine/decision/flow_spec.rb +12 -0
- data/spec/webmachine/decision/fsm_spec.rb +101 -0
- data/spec/webmachine/decision/helpers_spec.rb +68 -8
- data/spec/webmachine/dispatcher/route_spec.rb +1 -1
- data/spec/webmachine/dispatcher_spec.rb +1 -1
- data/spec/webmachine/errors_spec.rb +1 -1
- data/spec/webmachine/etags_spec.rb +75 -0
- data/spec/webmachine/headers_spec.rb +72 -0
- data/spec/webmachine/trace/fsm_spec.rb +5 -0
- data/spec/webmachine/trace/resource_proxy_spec.rb +1 -3
- data/webmachine.gemspec +1 -2
- 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
|
61
|
-
# response by Webmachine
|
62
|
-
next if
|
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
|
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 =
|
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)
|
@@ -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 "
|
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
|