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