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.
- data/Gemfile +9 -0
- data/README.md +65 -2
- data/lib/webmachine.rb +1 -0
- data/lib/webmachine/adapters.rb +4 -1
- data/lib/webmachine/adapters/hatetepe.rb +104 -0
- data/lib/webmachine/adapters/lazy_request_body.rb +32 -0
- data/lib/webmachine/adapters/rack.rb +2 -1
- data/lib/webmachine/adapters/reel.rb +45 -0
- data/lib/webmachine/adapters/webrick.rb +1 -30
- data/lib/webmachine/decision/falsey.rb +10 -0
- data/lib/webmachine/decision/flow.rb +28 -26
- data/lib/webmachine/decision/fsm.rb +22 -12
- data/lib/webmachine/decision/helpers.rb +17 -23
- data/lib/webmachine/etags.rb +69 -0
- data/lib/webmachine/headers.rb +42 -0
- data/lib/webmachine/quoted_string.rb +39 -0
- data/lib/webmachine/resource.rb +17 -0
- data/lib/webmachine/resource/callbacks.rb +1 -1
- data/lib/webmachine/resource/entity_tags.rb +17 -0
- data/lib/webmachine/streaming.rb +9 -61
- data/lib/webmachine/streaming/callable_encoder.rb +21 -0
- data/lib/webmachine/streaming/encoder.rb +24 -0
- data/lib/webmachine/streaming/enumerable_encoder.rb +20 -0
- data/lib/webmachine/streaming/fiber_encoder.rb +25 -0
- data/lib/webmachine/streaming/io_encoder.rb +65 -0
- data/lib/webmachine/trace/fsm.rb +9 -4
- data/lib/webmachine/trace/resource_proxy.rb +2 -4
- data/lib/webmachine/trace/static/tracelist.erb +2 -2
- data/lib/webmachine/trace/trace_resource.rb +3 -2
- data/lib/webmachine/version.rb +1 -1
- data/spec/webmachine/adapters/hatetepe_spec.rb +64 -0
- data/spec/webmachine/adapters/rack_spec.rb +18 -8
- data/spec/webmachine/adapters/reel_spec.rb +23 -0
- data/spec/webmachine/decision/falsey_spec.rb +8 -0
- data/spec/webmachine/decision/flow_spec.rb +12 -0
- data/spec/webmachine/decision/fsm_spec.rb +101 -0
- data/spec/webmachine/decision/helpers_spec.rb +68 -8
- data/spec/webmachine/dispatcher/route_spec.rb +1 -1
- data/spec/webmachine/dispatcher_spec.rb +1 -1
- data/spec/webmachine/errors_spec.rb +1 -1
- data/spec/webmachine/etags_spec.rb +75 -0
- data/spec/webmachine/headers_spec.rb +72 -0
- data/spec/webmachine/trace/fsm_spec.rb +5 -0
- data/spec/webmachine/trace/resource_proxy_spec.rb +1 -3
- data/webmachine.gemspec +1 -2
- 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
|
40
|
-
Webmachine.render_error(
|
41
|
-
|
42
|
-
|
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
|
-
|
60
|
-
|
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
|
-
|
9
|
-
|
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
|
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'] =
|
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
|
data/lib/webmachine/headers.rb
CHANGED
@@ -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
|
data/lib/webmachine/resource.rb
CHANGED
@@ -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
|
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
|
data/lib/webmachine/streaming.rb
CHANGED
@@ -1,63 +1,11 @@
|
|
1
|
-
begin
|
2
|
-
require 'fiber'
|
3
|
-
rescue LoadError
|
4
|
-
require 'webmachine/fiber18'
|
5
|
-
end
|
6
|
-
|
7
1
|
module Webmachine
|
8
|
-
#
|
9
|
-
|
10
|
-
#
|
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'
|