webmachine 1.0.0 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|