webmachine 1.0.0 → 1.1.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 (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
data/Gemfile CHANGED
@@ -6,6 +6,15 @@ gemspec
6
6
 
7
7
  gem 'bundler'
8
8
 
9
+ group :webservers do
10
+ gem 'mongrel', '~> 1.2.beta', :platform => [:mri, :rbx]
11
+ if RUBY_VERSION >= '1.9'
12
+ gem 'reel', '>= 0.1.0', :platform => [:ruby_19, :jruby]
13
+ gem 'nio4r'
14
+ end
15
+ gem 'hatetepe', '~> 0.5'
16
+ end
17
+
9
18
  group :guard do
10
19
  gem 'guard-rspec'
11
20
  case RbConfig::CONFIG['host_os']
data/README.md CHANGED
@@ -100,6 +100,19 @@ end
100
100
  Webmachine.application.run
101
101
  ```
102
102
 
103
+ Webmachine includes adapters for [Webrick][webrick], [Mongrel][mongrel],
104
+ [Reel][reel], and [Hatetepe]. Additionally, the [Rack][rack] adapter lets it
105
+ run on any webserver that provides a Rack interface. It also lets it run on
106
+ [Shotgun][shotgun] ([example][shotgun_example]).
107
+
108
+ [webrick]: http://rubydoc.info/stdlib/webrick
109
+ [mongrel]: https://github.com/evan/mongrel
110
+ [reel]: https://github.com/celluloid/reel
111
+ [hatetepe]: https://github.com/lgierth/hatetepe
112
+ [rack]: https://github.com/rack/rack
113
+ [shotgun]: https://github.com/rtomayko/shotgun
114
+ [shotgun_example]: https://gist.github.com/4389220
115
+
103
116
  ### Visual debugger
104
117
 
105
118
  It can be hard to understand all of the decisions that Webmachine
@@ -156,9 +169,24 @@ for an example of how to enable the debugger.
156
169
  * Includes the visual debugger so you can look through the decision
157
170
  graph to determine how your resources are behaving.
158
171
 
159
- ## Problems/TODOs
172
+ ## Caveats
173
+
174
+ * The [Reel](https://github.com/celluloid/reel) adapter might fail with a
175
+ `SystemStackError` on MRI (< 2.0) due to its limited fiber stack size.
176
+ The only known solution is to switch to JRuby, Rubinius or MRI 2.0.
177
+
178
+
179
+ ## Documentation & Finding Help
160
180
 
161
- * Command-line tools, and general polish.
181
+ * [API documentation](http://rubydoc.info/gems/webmachine/frames/file/README.md)
182
+ * IRC channel #webmachine on freenode
183
+
184
+ ## Related libraries
185
+
186
+ * [irwebmachine](https://github.com/robgleeson/irwebmachine) - IRB/Pry debugging of Webmachine applications
187
+ * [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
162
190
 
163
191
  ## LICENSE
164
192
 
@@ -168,6 +196,41 @@ LICENSE for details.
168
196
 
169
197
  ## Changelog
170
198
 
199
+ ### 1.1.0 January 12, 2013
200
+
201
+ 1.1.0 is a major feature release that adds the Reel and Hatetepe
202
+ adapters, support for "weak" entity tags, streaming IO response
203
+ bodies, better error handling, a shortcut for spinning up specific
204
+ resources, and a bunch of bugfixes. Added Tony Arcieri, Sebastian
205
+ Edwards, Russell Garner, Justin McPherson, Paweł Pacana, and Nicholas
206
+ Young as contributors. Thank you for your contributions!
207
+
208
+ * Added Reel adapter.
209
+ * The trace resource now opens static files in binary mode to ensure
210
+ compatibility on Windows.
211
+ * The trace resource uses absolute URIs for its traces.
212
+ * Added Hatetepe adapter.
213
+ * Added direct weak entity tag support.
214
+ * Related libraries are linked from the README.
215
+ * Removed some circular requires.
216
+ * Fixed documentation for the `valid_content_headers?` callback.
217
+ * Fixed `Headers` initialization by downcasing incoming header names.
218
+ * Added a `Headers#fetch` method.
219
+ * Conventionally "truthy" and "falsey" values (non-nil, non-false) can
220
+ now be returned from callbacks that expect a boolean return value.
221
+ * Updated to the latest RSpec.
222
+ * Added support for IO response bodies (minimal).
223
+ * Moved streaming encoders to their own module for clarity.
224
+ * Added `Resource#run` that starts up a web server with default
225
+ configuration options and the catch-all route to the resource.
226
+ * The exception handling flow was improved, clarifying the
227
+ `handle_exception` and `finish_request` callbacks.
228
+ * Fix incompatibilities with Rack.
229
+ * The request URI will not be initialized with parts that are not
230
+ present in the HTTP request.
231
+ * The tracing will now commit to storage after the response has been
232
+ traced.
233
+
171
234
  ### 1.0.0 July 7, 2012
172
235
 
173
236
  1.0.0 is a major feature release that finally includes the visual
data/lib/webmachine.rb CHANGED
@@ -3,6 +3,7 @@ require 'webmachine/cookie'
3
3
  require 'webmachine/headers'
4
4
  require 'webmachine/request'
5
5
  require 'webmachine/response'
6
+ require 'webmachine/etags'
6
7
  require 'webmachine/errors'
7
8
  require 'webmachine/decision'
8
9
  require 'webmachine/streaming'
@@ -1,9 +1,12 @@
1
+ require 'webmachine/adapters/lazy_request_body'
1
2
  require 'webmachine/adapters/webrick'
2
3
 
3
4
  module Webmachine
4
5
  # Contains classes and modules that connect Webmachine to Ruby
5
6
  # application servers.
6
7
  module Adapters
7
- autoload :Mongrel, 'webmachine/adapters/mongrel'
8
+ autoload :Mongrel, 'webmachine/adapters/mongrel'
9
+ autoload :Reel, 'webmachine/adapters/reel'
10
+ autoload :Hatetepe, 'webmachine/adapters/hatetepe'
8
11
  end
9
12
  end
@@ -0,0 +1,104 @@
1
+ require 'hatetepe/server'
2
+
3
+ unless Hatetepe::VERSION >= '0.5.0'
4
+ raise LoadError, 'webmachine only supports hatetepe >= 0.5.0'
5
+ end
6
+
7
+ require 'webmachine/version'
8
+ require 'webmachine/headers'
9
+ require 'webmachine/request'
10
+ require 'webmachine/response'
11
+ require 'webmachine/dispatcher'
12
+ require 'webmachine/chunked_body'
13
+
14
+ module Webmachine
15
+ module Adapters
16
+ class Hatetepe < Adapter
17
+ def options
18
+ {
19
+ :host => configuration.ip,
20
+ :port => configuration.port,
21
+ :app => [
22
+ ::Hatetepe::Server::Pipeline,
23
+ ::Hatetepe::Server::KeepAlive,
24
+ method(:call)
25
+ ]
26
+ }
27
+ end
28
+
29
+ def run
30
+ EM.epoll
31
+ EM.synchrony do
32
+ ::Hatetepe::Server.start(options)
33
+ trap("INT") { EM.stop }
34
+ end
35
+ end
36
+
37
+ def call(request, &respond)
38
+ response = Webmachine::Response.new
39
+ dispatcher.dispatch(convert_request(request), response)
40
+
41
+ respond.call(convert_response(response))
42
+ end
43
+
44
+ private
45
+
46
+ def convert_request(request)
47
+ args = [
48
+ request.verb,
49
+ build_request_uri(request),
50
+ Webmachine::Headers[request.headers.dup],
51
+ Body.new(request.body)
52
+ ]
53
+ Webmachine::Request.new(*args)
54
+ end
55
+
56
+ def convert_response(response)
57
+ response.headers["Server"] = [
58
+ Webmachine::SERVER_STRING,
59
+ "hatetepe/#{::Hatetepe::VERSION}"
60
+ ].join(" ")
61
+
62
+ args = [
63
+ response.code.to_i,
64
+ response.headers,
65
+ convert_body(response.body)
66
+ ]
67
+ ::Hatetepe::Response.new(*args)
68
+ end
69
+
70
+ def convert_body(body)
71
+ if body.respond_to?(:call)
72
+ [ body.call ]
73
+ elsif body.respond_to?(:to_s)
74
+ [ body.to_s ]
75
+ else
76
+ body
77
+ end
78
+ end
79
+
80
+ def build_request_uri(request)
81
+ uri = URI.parse(request.uri)
82
+ uri.scheme = "http"
83
+
84
+ host = request.headers.fetch("Host", "").split(":")
85
+ uri.host = host[0] || configuration.ip
86
+ uri.port = host[1].to_i || configuration.port
87
+
88
+ URI.parse(uri.to_s)
89
+ end
90
+
91
+ class Body < Struct.new(:body)
92
+ def each(&block)
93
+ body.rewind
94
+ body.each(&block)
95
+ end
96
+
97
+ def to_s
98
+ body.rewind
99
+ body.read.to_s
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,32 @@
1
+
2
+ module Webmachine
3
+ module Adapters
4
+ # Wraps a request body so that it can be passed to
5
+ # {Request} while still lazily evaluating the body.
6
+ class LazyRequestBody
7
+ def initialize(request)
8
+ @request = request
9
+ end
10
+
11
+ # Converts the body to a String so you can work with the entire
12
+ # thing.
13
+ def to_s
14
+ @value ? @value.join : @request.body
15
+ end
16
+
17
+ # Iterates over the body in chunks. If the body has previously
18
+ # been read, this method can be called again and get the same
19
+ # sequence of chunks.
20
+ # @yield [chunk]
21
+ # @yieldparam [String] chunk a chunk of the request body
22
+ 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
29
+ end
30
+ end # class RequestBody
31
+ end # module Adapters
32
+ end # module Webmachine
@@ -74,7 +74,8 @@ module Webmachine
74
74
  end
75
75
  end
76
76
 
77
- [rack_status, rack_headers, rack_body]
77
+ rack_res = ::Rack::Response.new(rack_body, rack_status, rack_headers)
78
+ rack_res.finish
78
79
  end
79
80
 
80
81
  # Wraps the Rack input so it can be treated like a String or
@@ -0,0 +1,45 @@
1
+ require 'reel'
2
+ require 'webmachine/version'
3
+ require 'webmachine/headers'
4
+ require 'webmachine/request'
5
+ require 'webmachine/response'
6
+ require 'webmachine/dispatcher'
7
+ require 'webmachine/chunked_body'
8
+
9
+ module Webmachine
10
+ module Adapters
11
+ class Reel < Adapter
12
+ def run
13
+ options = {
14
+ :port => configuration.port,
15
+ :host => configuration.ip
16
+ }.merge(configuration.adapter_options)
17
+ server = ::Reel::Server.supervise(options[:host], options[:port], &method(:process))
18
+ trap("INT"){ server.terminate; exit 0 }
19
+ sleep
20
+ end
21
+
22
+ 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)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -35,7 +35,7 @@ module Webmachine
35
35
  request = Webmachine::Request.new(wreq.request_method,
36
36
  wreq.request_uri,
37
37
  header,
38
- RequestBody.new(wreq))
38
+ LazyRequestBody.new(wreq))
39
39
  response = Webmachine::Response.new
40
40
  @dispatcher.dispatch(request, response)
41
41
  wres.status = response.code.to_i
@@ -61,35 +61,6 @@ module Webmachine
61
61
  end
62
62
  end
63
63
  end # class Server
64
-
65
- # Wraps the WEBrick request body so that it can be passed to
66
- # {Request} while still lazily evaluating the body.
67
- class RequestBody
68
- def initialize(request)
69
- @request = request
70
- end
71
-
72
- # Converts the body to a String so you can work with the entire
73
- # thing.
74
- def to_s
75
- @value ? @value.join : @request.body
76
- end
77
-
78
- # Iterates over the body in chunks. If the body has previously
79
- # been read, this method can be called again and get the same
80
- # sequence of chunks.
81
- # @yield [chunk]
82
- # @yieldparam [String] chunk a chunk of the request body
83
- def each
84
- if @value
85
- @value.each {|chunk| yield chunk }
86
- else
87
- @value = []
88
- @request.body {|chunk| @value << chunk; yield chunk }
89
- end
90
- end
91
- end # class RequestBody
92
-
93
64
  end # module WEBrick
94
65
  end # module Adapters
95
66
  end # module Webmachine
@@ -0,0 +1,10 @@
1
+ module Webmachine
2
+ module Decision
3
+ Falsey = Object.new
4
+
5
+ def Falsey.===(other)
6
+ !other
7
+ end
8
+ end
9
+ end
10
+
@@ -1,7 +1,9 @@
1
1
  require 'time'
2
2
  require 'digest/md5'
3
3
  require 'webmachine/decision/conneg'
4
+ require 'webmachine/decision/falsey'
4
5
  require 'webmachine/translation'
6
+ require 'webmachine/etags'
5
7
 
6
8
  module Webmachine
7
9
  module Decision
@@ -27,30 +29,30 @@ module Webmachine
27
29
  include Translation
28
30
 
29
31
  # Handles standard decisions where halting is allowed
30
- def decision_test(test, value, iftrue, iffalse)
32
+ def decision_test(test, iftrue, iffalse)
31
33
  case test
32
- when value
33
- iftrue
34
34
  when Fixnum # Allows callbacks to "halt" with a given response code
35
35
  test
36
- else
36
+ when Falsey
37
37
  iffalse
38
+ else
39
+ iftrue
38
40
  end
39
41
  end
40
42
 
41
43
  # Service available?
42
44
  def b13
43
- decision_test(resource.service_available?, true, :b12, 503)
45
+ decision_test(resource.service_available?, :b12, 503)
44
46
  end
45
47
 
46
48
  # Known method?
47
49
  def b12
48
- decision_test(resource.known_methods.include?(request.method), true, :b11, 501)
50
+ decision_test(resource.known_methods.include?(request.method), :b11, 501)
49
51
  end
50
52
 
51
53
  # URI too long?
52
54
  def b11
53
- decision_test(resource.uri_too_long?(request.uri), true, 414, :b10)
55
+ decision_test(resource.uri_too_long?(request.uri), 414, :b10)
54
56
  end
55
57
 
56
58
  # Method allowed?
@@ -90,7 +92,7 @@ module Webmachine
90
92
 
91
93
  # Malformed?
92
94
  def b9b
93
- decision_test(resource.malformed_request?, true, 400, :b8)
95
+ decision_test(resource.malformed_request?, 400, :b8)
94
96
  end
95
97
 
96
98
  # Authorized?
@@ -111,22 +113,22 @@ module Webmachine
111
113
 
112
114
  # Forbidden?
113
115
  def b7
114
- decision_test(resource.forbidden?, true, 403, :b6)
116
+ decision_test(resource.forbidden?, 403, :b6)
115
117
  end
116
118
 
117
119
  # Okay Content-* Headers?
118
120
  def b6
119
- decision_test(resource.valid_content_headers?(request.headers.grep(/content-/)), true, :b5, 501)
121
+ decision_test(resource.valid_content_headers?(request.headers.grep(/content-/)), :b5, 501)
120
122
  end
121
123
 
122
124
  # Known Content-Type?
123
125
  def b5
124
- decision_test(resource.known_content_type?(request.content_type), true, :b4, 415)
126
+ decision_test(resource.known_content_type?(request.content_type), :b4, 415)
125
127
  end
126
128
 
127
129
  # Req Entity Too Large?
128
130
  def b4
129
- decision_test(resource.valid_entity_length?(request.content_length), true, :b3, 413)
131
+ decision_test(resource.valid_entity_length?(request.content_length), :b3, 413)
130
132
  end
131
133
 
132
134
  # OPTIONS?
@@ -223,7 +225,7 @@ module Webmachine
223
225
  def g7
224
226
  # This is the first place after all conneg, so set Vary here
225
227
  response.headers['Vary'] = variances.join(", ") if variances.any?
226
- decision_test(resource.resource_exists?, true, :g8, :h7)
228
+ decision_test(resource.resource_exists?, :g8, :h7)
227
229
  end
228
230
 
229
231
  # If-Match exists?
@@ -233,18 +235,18 @@ module Webmachine
233
235
 
234
236
  # If-Match: * exists?
235
237
  def g9
236
- request.if_match == "*" ? :h10 : :g11
238
+ quote(request.if_match) == '"*"' ? :h10 : :g11
237
239
  end
238
240
 
239
241
  # ETag in If-Match
240
242
  def g11
241
- request_etags = request.if_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
242
- request_etags.include?(resource.generate_etag) ? :h10 : 412
243
+ request_etags = request.if_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) }
244
+ request_etags.include?(ETag.new(resource.generate_etag)) ? :h10 : 412
243
245
  end
244
246
 
245
247
  # If-Match exists?
246
248
  def h7
247
- (request.if_match && unquote_header(request.if_match) == '*') ? 412 : :i7
249
+ (request.if_match && unquote(request.if_match) == '*') ? 412 : :i7
248
250
  end
249
251
 
250
252
  # If-Unmodified-Since exists?
@@ -292,7 +294,7 @@ module Webmachine
292
294
 
293
295
  # If-none-match: * exists?
294
296
  def i13
295
- request.if_none_match == "*" ? :j18 : :k13
297
+ quote(request.if_none_match) == '"*"' ? :j18 : :k13
296
298
  end
297
299
 
298
300
  # GET or HEAD?
@@ -315,13 +317,13 @@ module Webmachine
315
317
 
316
318
  # Previously existed?
317
319
  def k7
318
- decision_test(resource.previously_existed?, true, :k5, :l7)
320
+ decision_test(resource.previously_existed?, :k5, :l7)
319
321
  end
320
322
 
321
323
  # Etag in if-none-match?
322
324
  def k13
323
- request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| unquote_header(etag) }
324
- request_etags.include?(resource.generate_etag) ? :j18 : :l13
325
+ request_etags = request.if_none_match.split(/\s*,\s*/).map {|etag| ETag.new(etag) }
326
+ request_etags.include?(ETag.new(resource.generate_etag)) ? :j18 : :l13
325
327
  end
326
328
 
327
329
  # Moved temporarily?
@@ -374,7 +376,7 @@ module Webmachine
374
376
 
375
377
  # Server allows POST to missing resource?
376
378
  def m7
377
- decision_test(resource.allow_missing_post?, true, :n11, 404)
379
+ decision_test(resource.allow_missing_post?, :n11, 404)
378
380
  end
379
381
 
380
382
  # DELETE?
@@ -384,17 +386,17 @@ module Webmachine
384
386
 
385
387
  # DELETE enacted immediately? (Also where DELETE is forced.)
386
388
  def m20
387
- decision_test(resource.delete_resource, true, :m20b, 500)
389
+ decision_test(resource.delete_resource, :m20b, 500)
388
390
  end
389
391
 
390
392
  # Did the DELETE complete?
391
393
  def m20b
392
- decision_test(resource.delete_completed?, true, :o20, 202)
394
+ decision_test(resource.delete_completed?, :o20, 202)
393
395
  end
394
396
 
395
397
  # Server allows POST to missing resource?
396
398
  def n5
397
- decision_test(resource.allow_missing_post?, true, :n11, 410)
399
+ decision_test(resource.allow_missing_post?, :n11, 410)
398
400
  end
399
401
 
400
402
  # Redirect?
@@ -475,7 +477,7 @@ module Webmachine
475
477
 
476
478
  # Multiple choices?
477
479
  def o18b
478
- decision_test(resource.multiple_choices?, true, 300, 200)
480
+ decision_test(resource.multiple_choices?, 300, 200)
479
481
  end
480
482
 
481
483
  # Response includes an entity?