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
@@ -0,0 +1,21 @@
|
|
1
|
+
module Webmachine
|
2
|
+
module Streaming
|
3
|
+
# Implements a streaming encoder for callable bodies, such as
|
4
|
+
# Proc. (essentially futures)
|
5
|
+
# @api private
|
6
|
+
class CallableEncoder < Encoder
|
7
|
+
# Encodes the output of the body Proc.
|
8
|
+
# @return [String]
|
9
|
+
def call
|
10
|
+
resource.send(encoder, resource.send(charsetter, body.call.to_s))
|
11
|
+
end
|
12
|
+
|
13
|
+
# Converts this encoder into a Proc.
|
14
|
+
# @return [Proc] a closure that wraps the {#call} method
|
15
|
+
# @see #call
|
16
|
+
def to_proc
|
17
|
+
method(:call).to_proc
|
18
|
+
end
|
19
|
+
end # class CallableEncoder
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Webmachine
|
2
|
+
module Streaming
|
3
|
+
# Subclasses of this class implement means for streamed/chunked
|
4
|
+
# response bodies to be coerced to the negotiated character set and
|
5
|
+
# encoded automatically as they are output to the client.
|
6
|
+
# @api private
|
7
|
+
class Encoder
|
8
|
+
attr_accessor :resource, :encoder, :charsetter, :body
|
9
|
+
|
10
|
+
def initialize(resource, encoder, charsetter, body)
|
11
|
+
@resource, @encoder, @charsetter, @body = resource, encoder, charsetter, body
|
12
|
+
end
|
13
|
+
|
14
|
+
protected
|
15
|
+
# @return [true, false] whether the stream will be modified by
|
16
|
+
# the encoder and/or charsetter. Only returns true if using the
|
17
|
+
# built-in "encode_identity" and "charset_nop" methods.
|
18
|
+
def is_unencoded?
|
19
|
+
encoder.to_s == "encode_identity" &&
|
20
|
+
charsetter.to_s == "charset_nop"
|
21
|
+
end
|
22
|
+
end # class Encoder
|
23
|
+
end # module Streaming
|
24
|
+
end # module Webmachine
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Webmachine
|
2
|
+
module Streaming
|
3
|
+
# Implements a streaming encoder for Enumerable response bodies, such as
|
4
|
+
# Arrays.
|
5
|
+
# @api private
|
6
|
+
class EnumerableEncoder < Encoder
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
# Iterates over the body, encoding and yielding individual chunks
|
10
|
+
# of the response entity.
|
11
|
+
# @yield [chunk]
|
12
|
+
# @yieldparam [String] chunk a chunk of the response, encoded
|
13
|
+
def each
|
14
|
+
body.each do |block|
|
15
|
+
yield resource.send(encoder, resource.send(charsetter, block.to_s))
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end # class EnumerableEncoder
|
19
|
+
end # module Streaming
|
20
|
+
end # module Webmachine
|
@@ -0,0 +1,25 @@
|
|
1
|
+
begin
|
2
|
+
require 'fiber'
|
3
|
+
rescue LoadError
|
4
|
+
require 'webmachine/fiber18'
|
5
|
+
end
|
6
|
+
|
7
|
+
module Webmachine
|
8
|
+
module Streaming
|
9
|
+
# Implements a streaming encoder for Fibers with the same API as the
|
10
|
+
# EnumerableEncoder. This will resume the Fiber until it terminates
|
11
|
+
# or returns a falsey value.
|
12
|
+
# @api private
|
13
|
+
class FiberEncoder < Encoder
|
14
|
+
include Enumerable
|
15
|
+
|
16
|
+
# Iterates over the body by yielding to the fiber.
|
17
|
+
# @api private
|
18
|
+
def each
|
19
|
+
while body.alive? && chunk = body.resume
|
20
|
+
yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end # class FiberEncoder
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'stringio'
|
2
|
+
|
3
|
+
module Webmachine
|
4
|
+
module Streaming
|
5
|
+
# Implements a streaming encoder for IO response bodies, such as
|
6
|
+
# File objects.
|
7
|
+
# @api private
|
8
|
+
class IOEncoder < Encoder
|
9
|
+
include Enumerable
|
10
|
+
CHUNK_SIZE = 8192
|
11
|
+
# Iterates over the IO, encoding and yielding individual chunks
|
12
|
+
# of the response entity.
|
13
|
+
# @yield [chunk]
|
14
|
+
# @yieldparam [String] chunk a chunk of the response, encoded
|
15
|
+
def each
|
16
|
+
while chunk = body.read(CHUNK_SIZE) && chunk != ""
|
17
|
+
yield resource.send(encoder, resource.send(charsetter, chunk))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# If IO#copy_stream is supported, and the stream is unencoded,
|
22
|
+
# optimize the output by copying directly. Otherwise, defers to
|
23
|
+
# using #each.
|
24
|
+
# @param [IO] outstream the output stream to copy the body into
|
25
|
+
def copy_stream(outstream)
|
26
|
+
if can_copy_stream?
|
27
|
+
IO.copy_stream(body, outstream)
|
28
|
+
else
|
29
|
+
each {|chunk| outstream << chunk }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns the length of the IO stream, if known. Returns nil if
|
34
|
+
# the stream uses an encoder or charsetter that might modify the
|
35
|
+
# length of the stream, or the stream size is unknown.
|
36
|
+
# @return [Fixnum] the size, in bytes, of the underlying IO, or
|
37
|
+
# nil if unsupported
|
38
|
+
def size
|
39
|
+
if is_unencoded?
|
40
|
+
if is_string_io?
|
41
|
+
body.size
|
42
|
+
else
|
43
|
+
begin
|
44
|
+
body.stat.size
|
45
|
+
rescue SystemCallError
|
46
|
+
# IO objects might raise an Errno if stat is unsupported.
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
alias bytesize size
|
54
|
+
|
55
|
+
private
|
56
|
+
def can_copy_stream?
|
57
|
+
IO.respond_to?(:copy_stream) && is_unencoded? && !is_string_io?
|
58
|
+
end
|
59
|
+
|
60
|
+
def is_string_io?
|
61
|
+
StringIO === body
|
62
|
+
end
|
63
|
+
end # class IOEncoder
|
64
|
+
end # module Streaming
|
65
|
+
end # module Webmachine
|
data/lib/webmachine/trace/fsm.rb
CHANGED
@@ -16,7 +16,8 @@ module Webmachine
|
|
16
16
|
}
|
17
17
|
end
|
18
18
|
|
19
|
-
# Adds the response to the trace
|
19
|
+
# Adds the response to the trace and then commits the trace to
|
20
|
+
# separate storage which can be discovered by the debugger.
|
20
21
|
# @param [Webmachine::Response] response the response to be traced
|
21
22
|
def trace_response(response)
|
22
23
|
response.trace << {
|
@@ -25,6 +26,8 @@ module Webmachine
|
|
25
26
|
:headers => response.headers,
|
26
27
|
:body => trace_response_body(response.body)
|
27
28
|
}
|
29
|
+
ensure
|
30
|
+
Trace.record(resource.object_id.to_s, response.trace)
|
28
31
|
end
|
29
32
|
|
30
33
|
# Adds a decision to the trace.
|
@@ -43,14 +46,16 @@ module Webmachine
|
|
43
46
|
# Works around streaming encoders where possible
|
44
47
|
def trace_response_body(body)
|
45
48
|
case body
|
46
|
-
when FiberEncoder
|
49
|
+
when Streaming::FiberEncoder
|
47
50
|
# TODO: figure out how to properly rewind or replay the
|
48
51
|
# fiber
|
49
52
|
body.inspect
|
50
|
-
when EnumerableEncoder
|
53
|
+
when Streaming::EnumerableEncoder
|
51
54
|
body.body.join
|
52
|
-
when CallableEncoder
|
55
|
+
when Streaming::CallableEncoder
|
53
56
|
body.body.call.to_s
|
57
|
+
when Streaming::IOEncoder
|
58
|
+
body.body.inspect
|
54
59
|
else
|
55
60
|
body.to_s
|
56
61
|
end
|
@@ -35,14 +35,12 @@ module Webmachine
|
|
35
35
|
proxy_callback :charset_nop, *args
|
36
36
|
end
|
37
37
|
|
38
|
-
# Calls the resource's finish_request method and then
|
39
|
-
#
|
40
|
-
# debugger.
|
38
|
+
# Calls the resource's finish_request method and then sets the trace id
|
39
|
+
# header in the response.
|
41
40
|
def finish_request(*args)
|
42
41
|
proxy_callback :finish_request, *args
|
43
42
|
ensure
|
44
43
|
resource.response.headers['X-Webmachine-Trace-Id'] = object_id.to_s
|
45
|
-
Trace.record(object_id.to_s, resource.response.trace)
|
46
44
|
end
|
47
45
|
|
48
46
|
private
|
@@ -73,11 +73,12 @@ module Webmachine
|
|
73
73
|
# TODO: Add support for IO objects as response bodies,
|
74
74
|
# allowing server optimizations like sendfile or chunked
|
75
75
|
# downloads
|
76
|
-
|
76
|
+
open(@file, "rb") {|io| io.read }
|
77
77
|
end
|
78
78
|
|
79
79
|
def produce_list
|
80
|
-
|
80
|
+
base = request.uri.path.chomp("/")
|
81
|
+
traces = Trace.traces.map {|t| [ t, "#{base}/#{t}" ] }
|
81
82
|
self.class.tracelist.result(binding)
|
82
83
|
end
|
83
84
|
|
data/lib/webmachine/version.rb
CHANGED
@@ -0,0 +1,64 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
examples = proc do
|
4
|
+
let(:configuration) { Webmachine::Configuration.default }
|
5
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
6
|
+
let(:adapter) { described_class.new(configuration, dispatcher) }
|
7
|
+
|
8
|
+
it "inherits from Webmachine::Adapter" do
|
9
|
+
adapter.should be_a(Webmachine::Adapter)
|
10
|
+
end
|
11
|
+
|
12
|
+
describe "#run" do
|
13
|
+
it "starts a server" do
|
14
|
+
Hatetepe::Server.should_receive(:start).with(adapter.options) { EM.stop }
|
15
|
+
adapter.run
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
describe "#call" do
|
20
|
+
let :request do
|
21
|
+
Hatetepe::Request.new(:get, "/", {}, StringIO.new("hello, world!"))
|
22
|
+
end
|
23
|
+
|
24
|
+
it "builds a string-like and enumerable request body" do
|
25
|
+
dispatcher.should_receive(:dispatch) do |req, res|
|
26
|
+
req.body.to_s.should eq("hello, world!")
|
27
|
+
enum_to_s(req.body).should eq("hello, world!")
|
28
|
+
end
|
29
|
+
adapter.call(request) {}
|
30
|
+
end
|
31
|
+
|
32
|
+
shared_examples "enumerable response body" do
|
33
|
+
before do
|
34
|
+
dispatcher.stub(:dispatch) {|_, response| response.body = body }
|
35
|
+
end
|
36
|
+
|
37
|
+
it "builds an enumerable response body" do
|
38
|
+
adapter.call(request) do |response|
|
39
|
+
enum_to_s(response.body).should eq("bye, world!")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "with normal response" do
|
45
|
+
let(:body) { "bye, world!" }
|
46
|
+
|
47
|
+
it_behaves_like "enumerable response body"
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "with streaming response" do
|
51
|
+
let(:body) { proc { "bye, world!" } }
|
52
|
+
|
53
|
+
it_behaves_like "enumerable response body"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def enum_to_s(enum)
|
58
|
+
Enumerator.new(enum).to_a.join
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
if RUBY_VERSION >= "1.9"
|
63
|
+
describe Webmachine::Adapters::Hatetepe, &examples
|
64
|
+
end
|
@@ -2,6 +2,7 @@ require 'spec_helper'
|
|
2
2
|
require 'webmachine/adapters/rack'
|
3
3
|
require 'rack'
|
4
4
|
require 'rack/test'
|
5
|
+
require 'rack/lint'
|
5
6
|
|
6
7
|
module Test
|
7
8
|
class Resource < Webmachine::Resource
|
@@ -51,7 +52,7 @@ describe Webmachine::Adapters::Rack do
|
|
51
52
|
let(:adapter) do
|
52
53
|
described_class.new(configuration, dispatcher)
|
53
54
|
end
|
54
|
-
let(:app) { adapter }
|
55
|
+
let(:app) { Rack::Lint.new(adapter) }
|
55
56
|
|
56
57
|
before do
|
57
58
|
dispatcher.add_route ['test'], Test::Resource
|
@@ -81,13 +82,14 @@ describe Webmachine::Adapters::Rack do
|
|
81
82
|
it "should proxy request to webmachine" do
|
82
83
|
get "/test"
|
83
84
|
last_response.status.should == 200
|
84
|
-
last_response.
|
85
|
+
last_response.original_headers["Content-Type"].should == "text/html"
|
85
86
|
last_response.body.should == "<html><body>testing</body></html>"
|
86
87
|
end
|
87
88
|
|
88
89
|
it "should build a string-like request body" do
|
89
90
|
dispatcher.should_receive(:dispatch) do |request, response|
|
90
91
|
request.body.to_s.should eq("Hello, World!")
|
92
|
+
response.headers["Content-Type"] = "text/plain"
|
91
93
|
end
|
92
94
|
request "/test", :method => "GET", :input => "Hello, World!"
|
93
95
|
end
|
@@ -96,6 +98,7 @@ describe Webmachine::Adapters::Rack do
|
|
96
98
|
chunks = []
|
97
99
|
dispatcher.should_receive(:dispatch) do |request, response|
|
98
100
|
request.body.each { |chunk| chunks << chunk }
|
101
|
+
response.headers["Content-Type"] = "text/plain"
|
99
102
|
end
|
100
103
|
request "/test", :method => "GET", :input => "Hello, World!"
|
101
104
|
chunks.join.should eq("Hello, World!")
|
@@ -109,14 +112,14 @@ describe Webmachine::Adapters::Rack do
|
|
109
112
|
|
110
113
|
it "should set Server header" do
|
111
114
|
get "/test"
|
112
|
-
last_response.
|
115
|
+
last_response.original_headers.should have_key("Server")
|
113
116
|
end
|
114
117
|
|
115
118
|
it "should set Set-Cookie header" do
|
116
119
|
get "/test"
|
117
120
|
# Yes, Rack expects multiple values for a given cookie to be
|
118
121
|
# \n separated.
|
119
|
-
last_response.
|
122
|
+
last_response.original_headers["Set-Cookie"].should == "cookie=monster\nrodeo=clown"
|
120
123
|
end
|
121
124
|
|
122
125
|
it "should handle non-success correctly" do
|
@@ -129,8 +132,8 @@ describe Webmachine::Adapters::Rack do
|
|
129
132
|
header "CONTENT_TYPE", "application/json"
|
130
133
|
post "/test"
|
131
134
|
last_response.status.should == 204
|
132
|
-
last_response.
|
133
|
-
last_response.
|
135
|
+
last_response.original_headers.should_not have_key("Content-Type")
|
136
|
+
last_response.original_headers.should_not have_key("Content-Length")
|
134
137
|
last_response.body.should == ""
|
135
138
|
end
|
136
139
|
|
@@ -145,7 +148,7 @@ describe Webmachine::Adapters::Rack do
|
|
145
148
|
header "ACCEPT", "application/vnd.webmachine.streaming+enum"
|
146
149
|
get "/test"
|
147
150
|
last_response.status.should == 200
|
148
|
-
last_response.
|
151
|
+
last_response.original_headers["Transfer-Encoding"].should == "chunked"
|
149
152
|
last_response.body.split("\r\n").should == %W{6 Hello, 6 World! 0}
|
150
153
|
end
|
151
154
|
|
@@ -153,7 +156,14 @@ describe Webmachine::Adapters::Rack do
|
|
153
156
|
header "ACCEPT", "application/vnd.webmachine.streaming+proc"
|
154
157
|
get "/test"
|
155
158
|
last_response.status.should == 200
|
156
|
-
last_response.
|
159
|
+
last_response.original_headers["Transfer-Encoding"].should == "chunked"
|
157
160
|
last_response.body.split("\r\n").should == %W{6 Stream 0}
|
158
161
|
end
|
162
|
+
|
163
|
+
it "should receive Content-Type on Not acceptable response" do
|
164
|
+
header "ACCEPT", "text/plain"
|
165
|
+
get "/test"
|
166
|
+
last_response.status.should == 406
|
167
|
+
last_response.original_headers.should have_key('Content-Type')
|
168
|
+
end
|
159
169
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
if RUBY_VERSION >= "1.9"
|
4
|
+
describe Webmachine::Adapters::Reel do
|
5
|
+
let(:configuration) { Webmachine::Configuration.default }
|
6
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
7
|
+
let(:adapter) do
|
8
|
+
described_class.new(configuration, dispatcher)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'inherits from Webmachine::Adapter' do
|
12
|
+
adapter.should be_a_kind_of(Webmachine::Adapter)
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'implements #run' do
|
16
|
+
adapter.should respond_to(:run)
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'implements #process' do
|
20
|
+
adapter.should respond_to(:process)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Decision::Falsey do
|
4
|
+
specify { (described_class.=== false).should be_true }
|
5
|
+
specify { (described_class.=== nil).should be_true }
|
6
|
+
specify { (described_class.=== true).should be_false }
|
7
|
+
specify { (described_class.=== []).should be_false }
|
8
|
+
end
|