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
@@ -1,5 +1,4 @@
1
1
  require 'webmachine/decision/helpers'
2
- require 'webmachine/decision/fsm'
3
2
  require 'webmachine/translation'
4
3
 
5
4
  module Webmachine
@@ -25,7 +24,7 @@ module Webmachine
25
24
  trace_request(request)
26
25
  loop do
27
26
  trace_decision(state)
28
- result = send(state)
27
+ result = handle_exceptions { send(state) }
29
28
  case result
30
29
  when Fixnum # Response code
31
30
  respond(result)
@@ -36,18 +35,26 @@ module Webmachine
36
35
  raise InvalidResource, t('fsm_broke', :state => state, :result => result.inspect)
37
36
  end
38
37
  end
39
- rescue MalformedRequest => malformed
40
- Webmachine.render_error(400, request, response, :message => malformed.message)
41
- respond(400)
42
- rescue Exception => e # Handle all exceptions without crashing the server
43
- code = resource.handle_exception(e)
44
- code = (100...600).include?(code) ? (code) : (500)
45
- respond(code)
38
+ rescue Exception => e
39
+ Webmachine.render_error(500, request, response, :message => e.message)
40
+ ensure
41
+ trace_response(response)
46
42
  end
47
43
 
48
44
  private
49
45
 
46
+ def handle_exceptions
47
+ yield
48
+ rescue MalformedRequest => e
49
+ Webmachine.render_error(400, request, response, :message => e.message)
50
+ 400
51
+ rescue Exception => e
52
+ code = resource.handle_exception(e)
53
+ (100...600).include?(code) ? (code) : (500)
54
+ end
55
+
50
56
  def respond(code, headers={})
57
+ response.code = code
51
58
  response.headers.merge!(headers)
52
59
  case code
53
60
  when 404
@@ -56,10 +63,13 @@ module Webmachine
56
63
  response.headers.delete('Content-Type')
57
64
  add_caching_headers
58
65
  end
59
- response.code = code
60
- resource.finish_request
66
+
67
+ response.code = handle_exceptions do
68
+ resource.finish_request
69
+ response.code
70
+ end
71
+
61
72
  ensure_content_length
62
- trace_response(response)
63
73
  end
64
74
 
65
75
  # When tracing is disabled, this does nothing.
@@ -1,12 +1,15 @@
1
+ require 'stringio'
1
2
  require 'webmachine/streaming'
2
3
  require 'webmachine/media_type'
4
+ require 'webmachine/quoted_string'
5
+ require 'webmachine/etags'
3
6
 
4
7
  module Webmachine
5
8
  module Decision
6
9
  # Methods that assist the Decision {Flow}.
7
10
  module Helpers
8
- # Pattern for quoted headers
9
- QUOTED = /^"(.*)"$/
11
+ include QuotedString
12
+ include Streaming
10
13
 
11
14
  # Determines if the response has a body/entity set.
12
15
  def has_response_body?
@@ -29,6 +32,8 @@ module Webmachine
29
32
  response.body = case body
30
33
  when String # 1.8 treats Strings as Enumerable
31
34
  resource.send(encoder, resource.send(charsetter, body))
35
+ when IO, StringIO
36
+ IOEncoder.new(resource, encoder, charsetter, body)
32
37
  when Fiber
33
38
  FiberEncoder.new(resource, encoder, charsetter, body)
34
39
  when Enumerable
@@ -40,7 +45,7 @@ module Webmachine
40
45
  resource.send(encoder, resource.send(charsetter, body))
41
46
  end
42
47
  end
43
- if String === response.body
48
+ if body_is_fixed_length?
44
49
  set_content_length
45
50
  else
46
51
  response.headers.delete 'Content-Length'
@@ -48,24 +53,6 @@ module Webmachine
48
53
  end
49
54
  end
50
55
 
51
- # Ensures that a header is quoted (like ETag)
52
- def ensure_quoted_header(value)
53
- if value =~ QUOTED
54
- value
55
- else
56
- '"' << value << '"'
57
- end
58
- end
59
-
60
- # Unquotes request headers (like ETag)
61
- def unquote_header(value)
62
- if value =~ QUOTED
63
- $1
64
- else
65
- value
66
- end
67
- end
68
-
69
56
  # Assists in receiving request bodies
70
57
  def accept_helper
71
58
  content_type = MediaType.parse(request.content_type || 'application/octet-stream')
@@ -90,7 +77,7 @@ module Webmachine
90
77
  # Adds caching-related headers to the response.
91
78
  def add_caching_headers
92
79
  if etag = resource.generate_etag
93
- response.headers['ETag'] = ensure_quoted_header(etag)
80
+ response.headers['ETag'] = ETag.new(etag).to_s
94
81
  end
95
82
  if expires = resource.expires
96
83
  response.headers['Expires'] = expires.httpdate
@@ -106,7 +93,7 @@ module Webmachine
106
93
  case
107
94
  when response.headers['Transfer-Encoding']
108
95
  return
109
- when [204, 304].include?(response.code)
96
+ when [204, 205, 304].include?(response.code)
110
97
  response.headers.delete 'Content-Length'
111
98
  when has_response_body?
112
99
  set_content_length
@@ -123,6 +110,13 @@ module Webmachine
123
110
  response.headers['Content-Length'] = response.body.length.to_s
124
111
  end
125
112
  end
113
+
114
+ # Determines whether the response is of a fixed lenghth, i.e. it
115
+ # is a String or IO with known size.
116
+ def body_is_fixed_length?
117
+ response.body.respond_to?(:bytesize) &&
118
+ Fixnum === response.body.bytesize
119
+ end
126
120
  end # module Helpers
127
121
  end # module Decision
128
122
  end # module Webmachine
@@ -0,0 +1,69 @@
1
+ require 'webmachine/quoted_string'
2
+
3
+ module Webmachine
4
+ # A wrapper around entity tags that encapsulates their semantics.
5
+ # This class by itself represents a "strong" entity tag.
6
+ class ETag
7
+ include QuotedString
8
+ # The pattern for a weak entity tag
9
+ WEAK_ETAG = /^W\/#{QUOTED_STRING}$/.freeze
10
+
11
+ def self.new(etag)
12
+ return etag if ETag === etag
13
+ klass = etag =~ WEAK_ETAG ? WeakETag : self
14
+ klass.send(:allocate).tap do |obj|
15
+ obj.send(:initialize, etag)
16
+ end
17
+ end
18
+
19
+ attr_reader :etag
20
+
21
+ def initialize(etag)
22
+ @etag = quote(etag)
23
+ end
24
+
25
+ # An entity tag is equivalent to another entity tag if their
26
+ # quoted values are equivalent. It is also equivalent to a String
27
+ # which represents the equivalent ETag.
28
+ def ==(other)
29
+ case other
30
+ when ETag
31
+ other.etag == @etag
32
+ when String
33
+ quote(other) == @etag
34
+ end
35
+ end
36
+
37
+ # Converts the entity tag into a string appropriate for use in a
38
+ # header.
39
+ def to_s
40
+ quote @etag
41
+ end
42
+ end
43
+
44
+ # A Weak Entity Tag, which can be used to compare entities which are
45
+ # semantically equivalent, but do not have the same byte-content. A
46
+ # WeakETag is equivalent to another entity tag if the non-weak
47
+ # portions are equivalent. It is also equivalent to a String which
48
+ # represents the equivalent strong or weak ETag.
49
+ class WeakETag < ETag
50
+ # Converts the WeakETag to a String for use in a header.
51
+ def to_s
52
+ "W/#{super}"
53
+ end
54
+
55
+ private
56
+ def unquote(str)
57
+ if str =~ WEAK_ETAG
58
+ unescape_quotes $1
59
+ else
60
+ super
61
+ end
62
+ end
63
+
64
+ def quote(str)
65
+ str = unescape_quotes($1) if str =~ WEAK_ETAG
66
+ super
67
+ end
68
+ end
69
+ end
@@ -3,6 +3,7 @@ module Webmachine
3
3
  class Headers < ::Hash
4
4
  # Convert CGI-style Hash into Request headers
5
5
  # @param [Hash] env a hash of CGI-style env/headers
6
+ # @return [Webmachine::Headers]
6
7
  def self.from_cgi(env)
7
8
  env.inject(new) do |h,(k,v)|
8
9
  if k =~ /^HTTP_(\w+)$/ || k =~ /^(CONTENT_(?:TYPE|LENGTH))$/
@@ -12,6 +13,24 @@ module Webmachine
12
13
  end
13
14
  end
14
15
 
16
+ # Creates a new headers object populated with the given objects.
17
+ # It supports the same forms as {Hash.[]}.
18
+ #
19
+ # @overload [](key, value, ...)
20
+ # Pairs of keys and values
21
+ # @param [Object] key
22
+ # @param [Object] value
23
+ # @overload [](array)
24
+ # Array of key-value pairs
25
+ # @param [Array<Object, Object>, ...]
26
+ # @overload [](object)
27
+ # Object convertible to a hash
28
+ # @param [Object]
29
+ # @return [Webmachine::Headers]
30
+ def self.[](*args)
31
+ super(super(*args).map {|k, v| [k.to_s.downcase, v]})
32
+ end
33
+
15
34
  # Fetch a header
16
35
  def [](key)
17
36
  super transform_key(key)
@@ -22,6 +41,29 @@ module Webmachine
22
41
  super transform_key(key), value
23
42
  end
24
43
 
44
+ # Returns the value for the given key. If the key can't be found,
45
+ # there are several options:
46
+ # With no other arguments, it will raise a KeyError exception;
47
+ # if default is given, then that will be returned;
48
+ # if the optional code block is specified, then that will be run and its
49
+ # result returned.
50
+ #
51
+ # @overload fetch(key)
52
+ # A key
53
+ # @param [Object] key
54
+ # @overload fetch(key, default)
55
+ # A key and a default value
56
+ # @param [Object] key
57
+ # @param [Object] default
58
+ # @overload fetch(key) {|key| block }
59
+ # A key and a code block
60
+ # @param [Object]
61
+ # @yield [key] Passes the key to the block
62
+ # @return [Object] the value for the key or the default
63
+ def fetch(*args, &block)
64
+ super(transform_key(args.shift), *args, &block)
65
+ end
66
+
25
67
  # Delete a header
26
68
  def delete(key)
27
69
  super transform_key(key)
@@ -0,0 +1,39 @@
1
+ module Webmachine
2
+ # Helper methods for dealing with the 'quoted-string' type often
3
+ # present in header values.
4
+ module QuotedString
5
+ # The pattern for a 'quoted-string' type
6
+ QUOTED_STRING = /"((?:\\"|[^"])*)"/.freeze
7
+
8
+ # The pattern for a 'quoted-string' type, without any other content.
9
+ QS_ANCHORED = /^#{QUOTED_STRING}$/.freeze
10
+
11
+ # Removes surrounding quotes from a quoted-string
12
+ def unquote(str)
13
+ if str =~ QS_ANCHORED
14
+ unescape_quotes $1
15
+ else
16
+ str
17
+ end
18
+ end
19
+
20
+ # Ensures that quotes exist around a quoted-string
21
+ def quote(str)
22
+ if str =~ QS_ANCHORED
23
+ str
24
+ else
25
+ %Q{"#{escape_quotes str}"}
26
+ end
27
+ end
28
+
29
+ # Escapes quotes within a quoted string.
30
+ def escape_quotes(str)
31
+ str.gsub(/"/, '\\"')
32
+ end
33
+
34
+ # Unescapes quotes within a quoted string
35
+ def unescape_quotes(str)
36
+ str.gsub(%r{\\}, '')
37
+ end
38
+ end
39
+ end
@@ -1,5 +1,6 @@
1
1
  require 'webmachine/resource/callbacks'
2
2
  require 'webmachine/resource/encodings'
3
+ require 'webmachine/resource/entity_tags'
3
4
  require 'webmachine/resource/authentication'
4
5
  require 'webmachine/resource/tracing'
5
6
 
@@ -22,6 +23,7 @@ module Webmachine
22
23
  class Resource
23
24
  include Callbacks
24
25
  include Encodings
26
+ include EntityTags
25
27
  include Tracing
26
28
 
27
29
  attr_reader :request, :response
@@ -41,6 +43,21 @@ module Webmachine
41
43
  instance
42
44
  end
43
45
 
46
+ #
47
+ # Starts a web server that serves requests for a subclass of
48
+ # Webmachine::Resource.
49
+ #
50
+ # @return [void]
51
+ #
52
+ def self.run
53
+ resource = self
54
+ Application.new do |app|
55
+ app.routes do |router|
56
+ router.add ["*"], resource
57
+ end
58
+ end.run
59
+ end
60
+
44
61
  private
45
62
  # When no specific charsets are provided, this acts as an identity
46
63
  # on the response body. Probably deserves some refactoring.
@@ -88,7 +88,7 @@ module Webmachine
88
88
 
89
89
  # If the request includes any invalid Content-* headers, this
90
90
  # should return false, which will result in a '501 Not
91
- # Implemented' response. Defaults to false.
91
+ # Implemented' response. Defaults to true.
92
92
  # @param [Hash] content_headers Request headers that begin with
93
93
  # 'Content-'
94
94
  # @return [true,false] Whether the Content-* headers are invalid
@@ -0,0 +1,17 @@
1
+ require 'webmachine/etags'
2
+
3
+ module Webmachine
4
+ class Resource
5
+ module EntityTags
6
+ # Marks a generated entity tag (etag) as "weak", meaning that
7
+ # other representations of the resource may be semantically equivalent.
8
+ # @return [WeakETag] a weak version of the given ETag string
9
+ # @param [String] str the ETag to mark as weak
10
+ # @see http://tools.ietf.org/html/rfc2616#section-13.3.3
11
+ # @see http://tools.ietf.org/html/rfc2616#section-14.19
12
+ def weak_etag(str)
13
+ WeakETag.new(str)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,63 +1,11 @@
1
- begin
2
- require 'fiber'
3
- rescue LoadError
4
- require 'webmachine/fiber18'
5
- end
6
-
7
1
  module Webmachine
8
- # Subclasses of this class implement means for streamed/chunked
9
- # response bodies to be coerced to the negotiated character set and
10
- # encoded automatically as they are output to the client.
11
- # @api private
12
- StreamingEncoder = Struct.new(:resource, :encoder, :charsetter, :body)
13
-
14
- # Implements a streaming encoder for Enumerable response bodies, such as
15
- # Arrays.
16
- # @api private
17
- class EnumerableEncoder < StreamingEncoder
18
- include Enumerable
19
-
20
- # Iterates over the body, encoding and yielding individual chunks
21
- # of the response entity.
22
- # @yield [chunk]
23
- # @yieldparam [String] chunk a chunk of the response, encoded
24
- def each
25
- body.each do |block|
26
- yield resource.send(encoder, resource.send(charsetter, block.to_s))
27
- end
28
- end
29
- end # class EnumerableEncoder
30
-
31
- # Implements a streaming encoder for callable bodies, such as
32
- # Proc. (essentially futures)
33
- # @api private
34
- class CallableEncoder < StreamingEncoder
35
- # Encodes the output of the body Proc.
36
- # @return [String]
37
- def call
38
- resource.send(encoder, resource.send(charsetter, body.call.to_s))
39
- end
40
-
41
- # Converts this encoder into a Proc.
42
- # @return [Proc] a closure that wraps the {#call} method
43
- # @see #call
44
- def to_proc
45
- method(:call).to_proc
46
- end
47
- end # class CallableEncoder
48
-
49
- # Implements a streaming encoder for Fibers with the same API as the
50
- # EnumerableEncoder. This will resume the Fiber until it terminates
51
- # or returns a falsey value.
52
- # @api private
53
- class FiberEncoder < EnumerableEncoder
54
-
55
- # Iterates over the body by yielding to the fiber.
56
- # @api private
57
- def each
58
- while body.alive? && chunk = body.resume
59
- yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
60
- end
61
- end
62
- end # class FiberEncoder
2
+ # Namespace for classes that support streaming response bodies.
3
+ module Streaming
4
+ end # module Streaming
63
5
  end # module Webmachine
6
+
7
+ require 'webmachine/streaming/encoder'
8
+ require 'webmachine/streaming/enumerable_encoder'
9
+ require 'webmachine/streaming/io_encoder'
10
+ require 'webmachine/streaming/callable_encoder'
11
+ require 'webmachine/streaming/fiber_encoder'