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.
- data/Gemfile +5 -4
- data/README.md +25 -2
- data/examples/debugger.rb +7 -0
- data/examples/logging.rb +41 -0
- data/lib/webmachine/adapters.rb +1 -1
- data/lib/webmachine/adapters/hatetepe.rb +5 -1
- data/lib/webmachine/adapters/lazy_request_body.rb +7 -7
- data/lib/webmachine/adapters/mongrel.rb +27 -12
- data/lib/webmachine/adapters/rack.rb +42 -6
- data/lib/webmachine/adapters/reel.rb +91 -23
- data/lib/webmachine/adapters/webrick.rb +12 -6
- data/lib/webmachine/application.rb +1 -0
- data/lib/webmachine/cookie.rb +1 -1
- data/lib/webmachine/dispatcher.rb +7 -1
- data/lib/webmachine/events.rb +179 -0
- data/lib/webmachine/events/instrumented_event.rb +19 -0
- data/lib/webmachine/response.rb +1 -1
- data/lib/webmachine/streaming/io_encoder.rb +5 -1
- data/lib/webmachine/trace.rb +18 -0
- data/lib/webmachine/trace/fsm.rb +4 -1
- data/lib/webmachine/trace/listener.rb +12 -0
- data/lib/webmachine/version.rb +1 -1
- data/spec/spec_helper.rb +11 -0
- data/spec/support/adapter_lint.rb +125 -0
- data/spec/support/test_resource.rb +73 -0
- data/spec/webmachine/adapters/hatetepe_spec.rb +2 -2
- data/spec/webmachine/adapters/mongrel_spec.rb +6 -51
- data/spec/webmachine/adapters/rack_spec.rb +22 -155
- data/spec/webmachine/adapters/reel_spec.rb +59 -7
- data/spec/webmachine/adapters/webrick_spec.rb +7 -13
- data/spec/webmachine/cookie_spec.rb +1 -1
- data/spec/webmachine/decision/helpers_spec.rb +10 -0
- data/spec/webmachine/events_spec.rb +58 -0
- data/spec/webmachine/request_spec.rb +41 -0
- data/webmachine.gemspec +1 -1
- metadata +63 -24
data/Gemfile
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'rbconfig'
|
2
2
|
|
3
|
-
source
|
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',
|
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 :
|
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
|
-
|
189
|
-
|
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
|
data/examples/logging.rb
ADDED
@@ -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
|
data/lib/webmachine/adapters.rb
CHANGED
@@ -6,7 +6,7 @@ module Webmachine
|
|
6
6
|
# application servers.
|
7
7
|
module Adapters
|
8
8
|
autoload :Mongrel, 'webmachine/adapters/mongrel'
|
9
|
-
|
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") {
|
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
|
-
@
|
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
|
-
|
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
|
55
|
-
vs.
|
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
|
-
|
68
|
-
|
69
|
-
|
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
|
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.
|
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
|
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
|
-
|
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 =
|
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/
|
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
|
-
|
18
|
-
|
19
|
-
|
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
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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
|