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
@@ -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
@@ -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 commits
39
- # the trace to separate storage which can be discovered by the
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
@@ -6,8 +6,8 @@
6
6
  <body>
7
7
  <h1>Traces</h1>
8
8
  <ul>
9
- <% traces.each do |trace| %>
10
- <li><a href="<%= trace %>"><%= trace %></a></li>
9
+ <% traces.each do |(trace, uri)| %>
10
+ <li><a href="<%= uri %>"><%= trace %></a></li>
11
11
  <% end %>
12
12
  </ul>
13
13
  </body>
@@ -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
- File.read(@file)
76
+ open(@file, "rb") {|io| io.read }
77
77
  end
78
78
 
79
79
  def produce_list
80
- traces = Trace.traces
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
 
@@ -1,6 +1,6 @@
1
1
  module Webmachine
2
2
  # Library version
3
- VERSION = "1.0.0"
3
+ VERSION = "1.1.0"
4
4
 
5
5
  # String for use in "Server" HTTP response header, which includes
6
6
  # the {VERSION}.
@@ -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.headers["Content-Type"].should == "text/html"
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.headers.should have_key("Server")
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.headers["Set-Cookie"].should == "cookie=monster\nrodeo=clown"
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.headers.should_not have_key("Content-Type")
133
- last_response.headers.should_not have_key("Content-Length")
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.headers["Transfer-Encoding"].should == "chunked"
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.headers["Transfer-Encoding"].should == "chunked"
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