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
@@ -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'