webmachine 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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