webmachine 1.1.0 → 1.2.0

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