webmachine 1.1.0 → 1.2.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 (36) hide show
  1. data/Gemfile +5 -4
  2. data/README.md +25 -2
  3. data/examples/debugger.rb +7 -0
  4. data/examples/logging.rb +41 -0
  5. data/lib/webmachine/adapters.rb +1 -1
  6. data/lib/webmachine/adapters/hatetepe.rb +5 -1
  7. data/lib/webmachine/adapters/lazy_request_body.rb +7 -7
  8. data/lib/webmachine/adapters/mongrel.rb +27 -12
  9. data/lib/webmachine/adapters/rack.rb +42 -6
  10. data/lib/webmachine/adapters/reel.rb +91 -23
  11. data/lib/webmachine/adapters/webrick.rb +12 -6
  12. data/lib/webmachine/application.rb +1 -0
  13. data/lib/webmachine/cookie.rb +1 -1
  14. data/lib/webmachine/dispatcher.rb +7 -1
  15. data/lib/webmachine/events.rb +179 -0
  16. data/lib/webmachine/events/instrumented_event.rb +19 -0
  17. data/lib/webmachine/response.rb +1 -1
  18. data/lib/webmachine/streaming/io_encoder.rb +5 -1
  19. data/lib/webmachine/trace.rb +18 -0
  20. data/lib/webmachine/trace/fsm.rb +4 -1
  21. data/lib/webmachine/trace/listener.rb +12 -0
  22. data/lib/webmachine/version.rb +1 -1
  23. data/spec/spec_helper.rb +11 -0
  24. data/spec/support/adapter_lint.rb +125 -0
  25. data/spec/support/test_resource.rb +73 -0
  26. data/spec/webmachine/adapters/hatetepe_spec.rb +2 -2
  27. data/spec/webmachine/adapters/mongrel_spec.rb +6 -51
  28. data/spec/webmachine/adapters/rack_spec.rb +22 -155
  29. data/spec/webmachine/adapters/reel_spec.rb +59 -7
  30. data/spec/webmachine/adapters/webrick_spec.rb +7 -13
  31. data/spec/webmachine/cookie_spec.rb +1 -1
  32. data/spec/webmachine/decision/helpers_spec.rb +10 -0
  33. data/spec/webmachine/events_spec.rb +58 -0
  34. data/spec/webmachine/request_spec.rb +41 -0
  35. data/webmachine.gemspec +1 -1
  36. metadata +63 -24
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
1
  require 'rbconfig'
2
2
 
3
- source :rubygems
3
+ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
@@ -8,10 +8,11 @@ gem 'bundler'
8
8
 
9
9
  group :webservers do
10
10
  gem 'mongrel', '~> 1.2.beta', :platform => [:mri, :rbx]
11
+
11
12
  if RUBY_VERSION >= '1.9'
12
- gem 'reel', '>= 0.1.0', :platform => [:ruby_19, :jruby]
13
- gem 'nio4r'
13
+ gem 'reel', '~> 0.3.0', :platform => [:ruby_19, :ruby_20, :jruby]
14
14
  end
15
+
15
16
  gem 'hatetepe', '~> 0.5'
16
17
  end
17
18
 
@@ -29,7 +30,7 @@ group :guard do
29
30
  end
30
31
 
31
32
  group :docs do
32
- platform :mri do
33
+ platform :mri_19, :mri_20 do
33
34
  gem 'redcarpet'
34
35
  end
35
36
  end
data/README.md CHANGED
@@ -179,14 +179,16 @@ for an example of how to enable the debugger.
179
179
  ## Documentation & Finding Help
180
180
 
181
181
  * [API documentation](http://rubydoc.info/gems/webmachine/frames/file/README.md)
182
+ * [Mailing list](mailto:webmachine.rb@librelist.com)
182
183
  * IRC channel #webmachine on freenode
183
184
 
184
185
  ## Related libraries
185
186
 
186
187
  * [irwebmachine](https://github.com/robgleeson/irwebmachine) - IRB/Pry debugging of Webmachine applications
187
188
  * [webmachine-test](https://github.com/bernd/webmachine-test) - Helpers for testing Webmachine applications
188
- - [webmachine-linking](https://github.com/petejohanson/webmachine-linking) - Helpers for linking between Resources, and Web Linking
189
- - [webmachine-sprockets](https://github.com/lgierth/webmachine-sprockets) - Integration with Sprockets assets packaging system
189
+ * [webmachine-linking](https://github.com/petejohanson/webmachine-linking) - Helpers for linking between Resources, and Web Linking
190
+ * [webmachine-sprockets](https://github.com/lgierth/webmachine-sprockets) - Integration with Sprockets assets packaging system
191
+ * [webmachine-actionview](https://github.com/rgarner/webmachine-actionview) - Integration of some Rails-style view conventions into Webmachine
190
192
 
191
193
  ## LICENSE
192
194
 
@@ -196,6 +198,27 @@ LICENSE for details.
196
198
 
197
199
  ## Changelog
198
200
 
201
+ ### 1.2.0
202
+
203
+ 1.2.0 is a major feature release that adds the Events instrumentation
204
+ framework, support for Websockets in Reel adapter and a bunch of bugfixes.
205
+ Added Justin McPherson and Hendrik Beskow as contributors. Thank you
206
+ for your contributions!
207
+
208
+ * Websockets support in Reel adapter.
209
+ * Added `Events` framework implementing ActiveSupport::Notifications
210
+ instrumentation API.
211
+ * Linked mailing list and related library in README.
212
+ * Fixed operator precedence in `IOEncoder#each`.
213
+ * Fixed typo in Max-Age cookie attribute.
214
+ * Allowed attributes to be set in a `Cookie`.
215
+ * Fixed streaming in Rack adapter from Fiber that is expected
216
+ to block
217
+ * Added a more comprehensive adapter test suite and fixed various bugs
218
+ in the existing adapters.
219
+ * Webmachine::LazyRequestBody no longer double-buffers the request
220
+ body and cannot be rewound.
221
+
199
222
  ### 1.1.0 January 12, 2013
200
223
 
201
224
  1.1.0 is a major feature release that adds the Reel and Hatetepe
data/examples/debugger.rb CHANGED
@@ -20,7 +20,14 @@ class MyTracedResource < Webmachine::Resource
20
20
  end
21
21
  end
22
22
 
23
+ class MyTracer
24
+ def call(name, payload)
25
+ puts "MyTracer #{name} #{payload.inspect}"
26
+ end
27
+ end
28
+
23
29
  # Webmachine::Trace.trace_store = :pstore, "./trace"
30
+ # Webmachine::Trace.trace_listener = [Webmachine::Trace::Listener.new, MyTracer.new]
24
31
 
25
32
  TraceExample = Webmachine::Application.new do |app|
26
33
  app.routes do
@@ -0,0 +1,41 @@
1
+ require 'webmachine'
2
+ require 'time'
3
+ require 'logger'
4
+
5
+ class HelloResource < Webmachine::Resource
6
+ def to_html
7
+ "<html><head><title>Hello from Webmachine</title></head><body>Hello, world!</body></html>\n"
8
+ end
9
+ end
10
+
11
+ class LogListener
12
+ def call(*args)
13
+ handle_event(Webmachine::Events::InstrumentedEvent.new(*args))
14
+ end
15
+
16
+ def handle_event(event)
17
+ request = event.payload[:request]
18
+ resource = event.payload[:resource]
19
+ code = event.payload[:code]
20
+
21
+ puts "[%s] method=%s uri=%s code=%d resource=%s time=%.4f" % [
22
+ Time.now.iso8601, request.method, request.uri.to_s, code, resource,
23
+ event.duration
24
+ ]
25
+ end
26
+ end
27
+
28
+ Webmachine::Events.subscribe('wm.dispatch', LogListener.new)
29
+
30
+ App = Webmachine::Application.new do |app|
31
+ app.routes do
32
+ add_route [], HelloResource
33
+ end
34
+
35
+ app.configure do |config|
36
+ config.adapter = :WEBrick
37
+ config.adapter_options = {:AccessLog => [], :Logger => Logger.new('/dev/null')}
38
+ end
39
+ end
40
+
41
+ App.run
@@ -6,7 +6,7 @@ module Webmachine
6
6
  # application servers.
7
7
  module Adapters
8
8
  autoload :Mongrel, 'webmachine/adapters/mongrel'
9
- autoload :Reel, 'webmachine/adapters/reel'
9
+ autoload :Reel, 'webmachine/adapters/reel'
10
10
  autoload :Hatetepe, 'webmachine/adapters/hatetepe'
11
11
  end
12
12
  end
@@ -30,10 +30,14 @@ module Webmachine
30
30
  EM.epoll
31
31
  EM.synchrony do
32
32
  ::Hatetepe::Server.start(options)
33
- trap("INT") { EM.stop }
33
+ trap("INT") { shutdown }
34
34
  end
35
35
  end
36
36
 
37
+ def shutdown
38
+ EM.stop
39
+ end
40
+
37
41
  def call(request, &respond)
38
42
  response = Webmachine::Response.new
39
43
  dispatcher.dispatch(convert_request(request), response)
@@ -11,7 +11,12 @@ module Webmachine
11
11
  # Converts the body to a String so you can work with the entire
12
12
  # thing.
13
13
  def to_s
14
- @value ? @value.join : @request.body
14
+ @request.body
15
+ end
16
+
17
+ # Converts the body to a String and checks if it is empty.
18
+ def empty?
19
+ to_s.empty?
15
20
  end
16
21
 
17
22
  # Iterates over the body in chunks. If the body has previously
@@ -20,12 +25,7 @@ module Webmachine
20
25
  # @yield [chunk]
21
26
  # @yieldparam [String] chunk a chunk of the request body
22
27
  def each
23
- if @value
24
- @value.each {|chunk| yield chunk }
25
- else
26
- @value = []
27
- @request.body {|chunk| @value << chunk; yield chunk }
28
- end
28
+ @request.body {|chunk| yield chunk }
29
29
  end
30
30
  end # class RequestBody
31
31
  end # module Adapters
@@ -18,14 +18,20 @@ module Webmachine
18
18
  :host => configuration.ip,
19
19
  :dispatcher => dispatcher
20
20
  }.merge(configuration.adapter_options)
21
- config = ::Mongrel::Configurator.new(defaults) do
21
+ @config = ::Mongrel::Configurator.new(defaults) do
22
22
  listener do
23
23
  uri '/', :handler => Webmachine::Adapters::Mongrel::Handler.new(defaults[:dispatcher])
24
24
  end
25
25
  trap("INT") { stop }
26
26
  run
27
27
  end
28
- config.join
28
+ @config.join
29
+ end
30
+
31
+ def shutdown
32
+ # The second argument tells mongrel to block until all listeners are shut down.
33
+ # This causes the mongrel tests to be very slow, but faster methods cause errors.
34
+ @config.stop(false, true) if @config
29
35
  end
30
36
 
31
37
  # A Mongrel handler for Webmachine
@@ -51,11 +57,12 @@ module Webmachine
51
57
  wres.status = response.code.to_i
52
58
  wres.send_status(nil)
53
59
 
54
- response.headers.each { |k, vs|
55
- vs.split("\n").each { |v|
60
+ response.headers.each do |k, vs|
61
+ [*vs].each do |v|
56
62
  wres.header[k] = v
57
- }
58
- }
63
+ end
64
+ end
65
+
59
66
  wres.header['Server'] = [Webmachine::SERVER_STRING, "Mongrel/#{::Mongrel::Const::MONGREL_VERSION}"].join(" ")
60
67
  wres.send_header
61
68
 
@@ -64,16 +71,24 @@ module Webmachine
64
71
  wres.write response.body
65
72
  wres.socket.flush
66
73
  when Enumerable
67
- Webmachine::ChunkedBody.new(response.body).each { |part|
68
- wres.write part
69
- wres.socket.flush
70
- }
74
+ # This might be an IOEncoder with a Content-Length, which shouldn't be chunked.
75
+ if response.headers["Transfer-Encoding"] == "chunked"
76
+ Webmachine::ChunkedBody.new(response.body).each do |part|
77
+ wres.write part
78
+ wres.socket.flush
79
+ end
80
+ else
81
+ response.body.each do |part|
82
+ wres.write part
83
+ wres.socket.flush
84
+ end
85
+ end
71
86
  else
72
87
  if response.body.respond_to?(:call)
73
- Webmachine::ChunkedBody.new(Array(response.body.call)).each { |part|
88
+ Webmachine::ChunkedBody.new(Array(response.body.call)).each do |part|
74
89
  wres.write part
75
90
  wres.socket.flush
76
- }
91
+ end
77
92
  end
78
93
  end
79
94
  ensure
@@ -31,16 +31,23 @@ module Webmachine
31
31
  # MyApplication.run
32
32
  #
33
33
  class Rack < Adapter
34
+ # Used to override default Rack server options (useful in testing)
35
+ DEFAULT_OPTIONS = {}
34
36
 
35
37
  # Start the Rack adapter
36
38
  def run
37
- options = {
39
+ options = DEFAULT_OPTIONS.merge({
38
40
  :app => self,
39
41
  :Port => configuration.port,
40
42
  :Host => configuration.ip
41
- }.merge(configuration.adapter_options)
43
+ }).merge(configuration.adapter_options)
42
44
 
43
- ::Rack::Server.start(options)
45
+ @server = ::Rack::Server.new(options)
46
+ @server.start
47
+ end
48
+
49
+ def shutdown
50
+ @server.server.shutdown if @server
44
51
  end
45
52
 
46
53
  # Handles a Rack-based request.
@@ -59,7 +66,7 @@ module Webmachine
59
66
 
60
67
  response.headers['Server'] = [Webmachine::SERVER_STRING, "Rack/#{::Rack.version}"].join(" ")
61
68
 
62
- rack_status = response.code
69
+ rack_status = response.code
63
70
  rack_headers = response.headers.flattened("\n")
64
71
  rack_body = case response.body
65
72
  when String # Strings are enumerable in ruby 1.8
@@ -68,16 +75,45 @@ module Webmachine
68
75
  if response.body.respond_to?(:call)
69
76
  Webmachine::ChunkedBody.new(Array(response.body.call))
70
77
  elsif response.body.respond_to?(:each)
71
- Webmachine::ChunkedBody.new(response.body)
78
+ # This might be an IOEncoder with a Content-Length, which shouldn't be chunked.
79
+ if response.headers["Transfer-Encoding"] == "chunked"
80
+ Webmachine::ChunkedBody.new(response.body)
81
+ else
82
+ response.body
83
+ end
72
84
  else
73
85
  [response.body.to_s]
74
86
  end
75
87
  end
76
88
 
77
- rack_res = ::Rack::Response.new(rack_body, rack_status, rack_headers)
89
+ rack_res = RackResponse.new(rack_body, rack_status, rack_headers)
78
90
  rack_res.finish
79
91
  end
80
92
 
93
+ class RackResponse
94
+ def initialize(body, status, headers)
95
+ @body = body
96
+ @status = status
97
+ @headers = headers
98
+ end
99
+
100
+ def finish
101
+ @headers['Content-Type'] ||= 'text/html' if rack_release_enforcing_content_type
102
+ @headers.delete('Content-Type') if response_without_body
103
+ [@status, @headers, @body]
104
+ end
105
+
106
+ protected
107
+
108
+ def response_without_body
109
+ ::Rack::Utils::STATUS_WITH_NO_ENTITY_BODY.include? @status
110
+ end
111
+
112
+ def rack_release_enforcing_content_type
113
+ ::Rack.release < '1.5'
114
+ end
115
+ end
116
+
81
117
  # Wraps the Rack input so it can be treated like a String or
82
118
  # Enumerable.
83
119
  # @api private
@@ -4,40 +4,108 @@ require 'webmachine/headers'
4
4
  require 'webmachine/request'
5
5
  require 'webmachine/response'
6
6
  require 'webmachine/dispatcher'
7
- require 'webmachine/chunked_body'
7
+ require 'webmachine/adapters/lazy_request_body'
8
+ require 'set'
8
9
 
9
10
  module Webmachine
10
11
  module Adapters
11
12
  class Reel < Adapter
12
13
  def run
13
- options = {
14
+ @options = {
14
15
  :port => configuration.port,
15
16
  :host => configuration.ip
16
17
  }.merge(configuration.adapter_options)
17
- server = ::Reel::Server.supervise(options[:host], options[:port], &method(:process))
18
- trap("INT"){ server.terminate; exit 0 }
19
- sleep
18
+
19
+ if extra_verbs = configuration.adapter_options[:extra_verbs]
20
+ @extra_verbs = Set.new(extra_verbs.map(&:to_s).map(&:upcase))
21
+ else
22
+ @extra_verbs = Set.new
23
+ end
24
+
25
+ @server = ::Reel::Server.supervise(@options[:host], @options[:port], &method(:process))
26
+
27
+ # FIXME: this will no longer work on Ruby 2.0. We need Celluloid.trap
28
+ trap("INT") { @server.terminate; exit 0 }
29
+ Celluloid::Actor.join(@server)
30
+ end
31
+
32
+ def shutdown
33
+ @server.terminate! if @server
20
34
  end
21
35
 
22
36
  def process(connection)
23
- while wreq = connection.request
24
- header = Webmachine::Headers[wreq.headers.dup]
25
- host_parts = header.fetch('Host').split(':')
26
- path_parts = wreq.url.split('?')
27
- requri = URI::HTTP.build({}.tap do |h|
28
- h[:host] = host_parts.first
29
- h[:port] = host_parts.last.to_i if host_parts.length == 2
30
- h[:path] = path_parts.first
31
- h[:query] = path_parts.last if path_parts.length == 2
32
- end)
33
- request = Webmachine::Request.new(wreq.method.to_s.upcase,
34
- requri,
35
- header,
36
- LazyRequestBody.new(wreq))
37
- response = Webmachine::Response.new
38
- @dispatcher.dispatch(request,response)
39
-
40
- connection.respond ::Reel::Response.new(response.code, response.headers, response.body)
37
+ while request = connection.request
38
+ # Users of the adapter can configure a custom WebSocket handler
39
+ if request.is_a? ::Reel::WebSocket
40
+ if handler = @options[:websocket_handler]
41
+ handler.call(request)
42
+ else
43
+ # Pretend we don't know anything about the WebSocket protocol
44
+ # FIXME: This isn't strictly what RFC 6455 would have us do
45
+ request.respond :bad_request, "WebSockets not supported"
46
+ end
47
+
48
+ next
49
+ end
50
+
51
+ # Optional support for e.g. WebDAV verbs not included in Webmachine's
52
+ # state machine. Do the "Railsy" thing and handle them like POSTs
53
+ # with a magical parameter
54
+ if @extra_verbs.include?(request.method)
55
+ method = "POST"
56
+ param = "_method=#{request.method}"
57
+ uri = request_uri(request.url, request.headers, param)
58
+ else
59
+ method = request.method
60
+ uri = request_uri(request.url, request.headers)
61
+ end
62
+
63
+ wm_headers = Webmachine::Headers[request.headers.dup]
64
+ wm_request = Webmachine::Request.new(method, uri, wm_headers,
65
+ LazyRequestBody.new(request))
66
+ wm_response = Webmachine::Response.new
67
+ @dispatcher.dispatch(wm_request, wm_response)
68
+
69
+ fixup_headers(wm_response)
70
+ fixup_callable_encoder(wm_response)
71
+
72
+ request.respond ::Reel::Response.new(wm_response.code,
73
+ wm_response.headers,
74
+ wm_response.body)
75
+ end
76
+ end
77
+
78
+ def request_uri(path, headers, extra_query_params = nil)
79
+ host_parts = headers.fetch('Host').split(':')
80
+ path_parts = path.split('?')
81
+
82
+ uri_hash = {host: host_parts.first, path: path_parts.first}
83
+
84
+ uri_hash[:port] = host_parts.last.to_i if host_parts.length == 2
85
+ uri_hash[:query] = path_parts.last if path_parts.length == 2
86
+
87
+ if extra_query_params
88
+ if uri_hash[:query]
89
+ uri_hash[:query] << "&#{extra_query_params}"
90
+ else
91
+ uri_hash[:query] = extra_query_params
92
+ end
93
+ end
94
+
95
+ URI::HTTP.build(uri_hash)
96
+ end
97
+
98
+ def fixup_headers(response)
99
+ response.headers.each do |key, value|
100
+ if value.is_a?(Array)
101
+ response.headers[key] = value.join(", ")
102
+ end
103
+ end
104
+ end
105
+
106
+ def fixup_callable_encoder(response)
107
+ if response.body.is_a?(Streaming::CallableEncoder)
108
+ response.body = [response.body.call]
41
109
  end
42
110
  end
43
111
  end