webmachine 1.0.0 → 1.1.0

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