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.
- 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
|