webmachine 0.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/.gitignore +28 -0
- data/Gemfile +16 -0
- data/Guardfile +11 -0
- data/README.md +89 -0
- data/Rakefile +31 -0
- data/examples/webrick.rb +19 -0
- data/lib/webmachine/adapters/webrick.rb +74 -0
- data/lib/webmachine/adapters.rb +15 -0
- data/lib/webmachine/decision/conneg.rb +304 -0
- data/lib/webmachine/decision/flow.rb +502 -0
- data/lib/webmachine/decision/fsm.rb +79 -0
- data/lib/webmachine/decision/helpers.rb +80 -0
- data/lib/webmachine/decision.rb +12 -0
- data/lib/webmachine/dispatcher/route.rb +85 -0
- data/lib/webmachine/dispatcher.rb +40 -0
- data/lib/webmachine/errors.rb +37 -0
- data/lib/webmachine/headers.rb +16 -0
- data/lib/webmachine/locale/en.yml +28 -0
- data/lib/webmachine/request.rb +56 -0
- data/lib/webmachine/resource/callbacks.rb +362 -0
- data/lib/webmachine/resource/encodings.rb +36 -0
- data/lib/webmachine/resource.rb +48 -0
- data/lib/webmachine/response.rb +49 -0
- data/lib/webmachine/streaming.rb +27 -0
- data/lib/webmachine/translation.rb +11 -0
- data/lib/webmachine/version.rb +4 -0
- data/lib/webmachine.rb +19 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/tests.org +57 -0
- data/spec/webmachine/decision/conneg_spec.rb +152 -0
- data/spec/webmachine/decision/flow_spec.rb +1030 -0
- data/spec/webmachine/dispatcher/route_spec.rb +109 -0
- data/spec/webmachine/dispatcher_spec.rb +34 -0
- data/spec/webmachine/headers_spec.rb +19 -0
- data/spec/webmachine/request_spec.rb +24 -0
- data/webmachine.gemspec +44 -0
- metadata +137 -0
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'webmachine/resource/callbacks'
|
2
|
+
require 'webmachine/resource/encodings'
|
3
|
+
|
4
|
+
module Webmachine
|
5
|
+
# Resource is the primary building block of Webmachine applications,
|
6
|
+
# and describes families of HTTP resources. It includes all of the
|
7
|
+
# methods you might want to override to customize the behavior of
|
8
|
+
# the resource. The simplest resource family you can implement
|
9
|
+
# looks like this:
|
10
|
+
#
|
11
|
+
# class HelloWorldResource < Webmachine::Resource
|
12
|
+
# def to_html
|
13
|
+
# "<html><body>Hello, world!</body></html>"
|
14
|
+
# end
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# For more information about how response decisions are made in
|
18
|
+
# Webmachine based on your resource class, refer to the diagram at
|
19
|
+
# {http://webmachine.basho.com/images/http-headers-status-v3.png}.
|
20
|
+
class Resource
|
21
|
+
include Callbacks
|
22
|
+
include Encodings
|
23
|
+
|
24
|
+
attr_reader :request, :response
|
25
|
+
|
26
|
+
# Creates a new {Resource}, initializing it with the request and
|
27
|
+
# response. Note that you may still override {#initialize} to
|
28
|
+
# initialize your resource. It will be called after the request
|
29
|
+
# and response ivars are set.
|
30
|
+
# @param [Request] request the request object
|
31
|
+
# @param [Response] response the response object
|
32
|
+
# @return [Resource] the new resource
|
33
|
+
def self.new(request, response)
|
34
|
+
instance = allocate
|
35
|
+
instance.instance_variable_set(:@request, request)
|
36
|
+
instance.instance_variable_set(:@response, response)
|
37
|
+
instance.send :initialize
|
38
|
+
instance
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
# When no specific charsets are provided, this acts as an identity
|
43
|
+
# on the response body. Probably deserves some refactoring.
|
44
|
+
def charset_nop(x)
|
45
|
+
x
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Webmachine
|
2
|
+
# Represents an HTTP response from Webmachine.
|
3
|
+
class Response
|
4
|
+
# @return [Hash] Response headers that will be sent to the client
|
5
|
+
attr_reader :headers
|
6
|
+
|
7
|
+
# @return [Fixnum] The HTTP status code of the response
|
8
|
+
attr_accessor :code
|
9
|
+
|
10
|
+
# @return [String, #each] The response body
|
11
|
+
attr_accessor :body
|
12
|
+
|
13
|
+
# @return [true,false] Whether the response is a redirect
|
14
|
+
attr_accessor :redirect
|
15
|
+
|
16
|
+
# @return [Array] the list of states that were traversed
|
17
|
+
attr_reader :trace
|
18
|
+
|
19
|
+
# @return [Symbol] When an error has occurred, the last state the
|
20
|
+
# FSM was in
|
21
|
+
attr_accessor :end_state
|
22
|
+
|
23
|
+
# @return [String] The error message when responding with an error
|
24
|
+
# code
|
25
|
+
attr_accessor :error
|
26
|
+
|
27
|
+
# Creates a new Response object with the appropriate defaults.
|
28
|
+
def initialize
|
29
|
+
@headers = {}
|
30
|
+
@trace = []
|
31
|
+
self.code = 200
|
32
|
+
self.redirect = false
|
33
|
+
end
|
34
|
+
|
35
|
+
# Indicate that the response should be a redirect. This is only
|
36
|
+
# used when processing a POST request in {Callbacks#process_post}
|
37
|
+
# to indicate that the client should request another resource
|
38
|
+
# using GET. Either pass the URI of the target resource, or
|
39
|
+
# manually set the Location header using {#headers}.
|
40
|
+
# @param [String, URI] location the target of the redirection
|
41
|
+
def do_redirect(location=nil)
|
42
|
+
headers['Location'] = location.to_s if location
|
43
|
+
self.redirect = true
|
44
|
+
end
|
45
|
+
|
46
|
+
alias :is_redirect? :redirect
|
47
|
+
alias :redirect_to :do_redirect
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Webmachine
|
2
|
+
class StreamingEncoder
|
3
|
+
def initialize(resource, encoder, charsetter, body)
|
4
|
+
@resource, @encoder, @charsetter, @body = resource, encoder, charsetter, body
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
class EnumerableEncoder < StreamingEncoder
|
9
|
+
include Enumerable
|
10
|
+
|
11
|
+
def each
|
12
|
+
body.each do |block|
|
13
|
+
yield @resource.send(@encoder, resource.send(@charsetter, block))
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class CallableEncoder < StreamingEncoder
|
19
|
+
def call
|
20
|
+
@resource.send(@encoder, @resource.send(@charsetter, body.call))
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_proc
|
24
|
+
method(:call).to_proc
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/webmachine.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'webmachine/headers'
|
2
|
+
require 'webmachine/request'
|
3
|
+
require 'webmachine/response'
|
4
|
+
require 'webmachine/errors'
|
5
|
+
require 'webmachine/decision'
|
6
|
+
require 'webmachine/streaming'
|
7
|
+
require 'webmachine/adapters'
|
8
|
+
require 'webmachine/dispatcher'
|
9
|
+
require 'webmachine/resource'
|
10
|
+
require 'webmachine/version'
|
11
|
+
|
12
|
+
# Webmachine is a toolkit for making well-behaved HTTP applications.
|
13
|
+
# It is based on the Erlang library of the same name.
|
14
|
+
module Webmachine
|
15
|
+
# Starts Webmachine serving requests
|
16
|
+
def self.run
|
17
|
+
Adapters.const_get(adapter).run
|
18
|
+
end
|
19
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path("..", __FILE__)
|
2
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'webmachine'
|
6
|
+
require 'rspec'
|
7
|
+
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.mock_with :rspec
|
10
|
+
config.filter_run :focus => true
|
11
|
+
config.run_all_when_everything_filtered = true
|
12
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
13
|
+
end
|
data/spec/tests.org
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
* 2,2 Basic Rules
|
2
|
+
- HTTP/1.1 header field values can be folded onto multiple lines if
|
3
|
+
the continuation line begins with a space or horizontal tab. All
|
4
|
+
linear white space, including folding, has the same semantics as
|
5
|
+
SP. A recipient MAY replace any linear white space with a single
|
6
|
+
SP before interpreting the field value or forwarding the message
|
7
|
+
downstream.
|
8
|
+
- Many HTTP/1.1 header field values consist of words separated by
|
9
|
+
LWS or special characters. These special characters MUST be in a
|
10
|
+
quoted string to be used within a parameter value (as defined in
|
11
|
+
section 3.6).
|
12
|
+
* 3.1 HTTP Version
|
13
|
+
- Note that the major and minor numbers MUST be treated as separate
|
14
|
+
integers and that each MAY be incremented higher than a single
|
15
|
+
digit.
|
16
|
+
- Leading zeros MUST be ignored by recipients and MUST NOT be sent.
|
17
|
+
- An application that sends a request or response message that
|
18
|
+
includes HTTP-Version of "HTTP/1.1" MUST be at least conditionally
|
19
|
+
compliant with this specification.
|
20
|
+
- Applications that are at least conditionally compliant with this
|
21
|
+
specification SHOULD use an HTTP-Version of "HTTP/1.1" in their
|
22
|
+
messages, and MUST do so for any message that is not compatible
|
23
|
+
with HTTP/1.0.
|
24
|
+
- Since the protocol version indicates the protocol capability of
|
25
|
+
the sender, a proxy/gateway MUST NOT send a message with a version
|
26
|
+
indicator which is greater than its actual version. If a higher
|
27
|
+
version request is received, the proxy/gateway MUST either
|
28
|
+
downgrade the request version, or respond with an error, or switch
|
29
|
+
to tunnel behavior.
|
30
|
+
- Due to interoperability problems with HTTP/1.0 proxies discovered
|
31
|
+
since the publication of RFC 2068 [33], caching proxies MUST,
|
32
|
+
gateways MAY, and tunnels MUST NOT upgrade the request to the
|
33
|
+
highest version they support. The proxy/gateway's response to that
|
34
|
+
request MUST be in the same major version as the request.
|
35
|
+
* 3.2 URIs
|
36
|
+
** General Syntax
|
37
|
+
- Servers MUST be able to handle the URI of any resource they serve.
|
38
|
+
- Servers SHOULD be able to handle URIs of unbounded length if they
|
39
|
+
provide GET-based forms that could generate such URIs.
|
40
|
+
- A server SHOULD return 414 (Request-URI Too Long) status if a URI
|
41
|
+
is longer than the server can handle
|
42
|
+
** http URL
|
43
|
+
- If the abs_path is not present in the URL, it MUST be given as
|
44
|
+
"/" when used as a Request-URI for a resource.
|
45
|
+
- If a proxy receives a fully qualified domain name, the proxy MUST
|
46
|
+
NOT change the host name.
|
47
|
+
** URI Comparison
|
48
|
+
- When comparing two URIs to decide if they match or not, a client
|
49
|
+
SHOULD use a case-sensitive octet-by-octet comparison of the
|
50
|
+
entire URIs, with these exceptions:
|
51
|
+
- A port that is empty or not given is equivalent to the default
|
52
|
+
port for that URI-reference
|
53
|
+
- Comparisons of host names MUST be case-insensitive
|
54
|
+
- Comparisons of scheme names MUST be case-insensitive
|
55
|
+
- An empty abs_path is equivalent to an abs_path of "/"
|
56
|
+
* 3.3 Date/Time Formats
|
57
|
+
|
@@ -0,0 +1,152 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Decision::Conneg do
|
4
|
+
let(:request) { Webmachine::Request.new("GET", URI.parse("http://localhost:8080/"), Webmachine::Headers["accept" => "*/*"], "") }
|
5
|
+
let(:response) { Webmachine::Response.new }
|
6
|
+
let(:resource) do
|
7
|
+
Class.new(Webmachine::Resource) do
|
8
|
+
def to_html; "hello world!"; end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
subject do
|
12
|
+
Webmachine::Decision::FSM.new(resource, request, response)
|
13
|
+
end
|
14
|
+
|
15
|
+
context "choosing a media type" do
|
16
|
+
it "should not choose a type when none are provided" do
|
17
|
+
subject.choose_media_type([], "*/*").should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "should not choose a type when none are acceptable" do
|
21
|
+
subject.choose_media_type(["text/html"], "application/json").should be_nil
|
22
|
+
end
|
23
|
+
|
24
|
+
it "should choose the first acceptable type" do
|
25
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
26
|
+
"application/xml, text/html, */*").should == "application/xml"
|
27
|
+
end
|
28
|
+
|
29
|
+
it "should choose the type that matches closest when matching subparams" do
|
30
|
+
subject.choose_media_type(["text/html",
|
31
|
+
["text/html", {"charset" => "iso8859-1"}]],
|
32
|
+
"text/html;charset=iso8859-1, application/xml").
|
33
|
+
should == "text/html;charset=iso8859-1"
|
34
|
+
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should choose the preferred type over less-preferred types" do
|
38
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
39
|
+
"application/xml;q=0.7, text/html, */*").should == "text/html"
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should raise an exception when a media-type is improperly formatted" do
|
44
|
+
expect {
|
45
|
+
subject.choose_media_type(["text/html", "application/xml"],
|
46
|
+
"bah;")
|
47
|
+
}.to raise_error(Webmachine::MalformedRequest)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context "choosing an encoding" do
|
52
|
+
it "should not set the encoding when none are provided" do
|
53
|
+
subject.choose_encoding({}, "identity, gzip")
|
54
|
+
subject.metadata['Content-Encoding'].should be_nil
|
55
|
+
subject.response.headers['Content-Encoding'].should be_nil
|
56
|
+
end
|
57
|
+
|
58
|
+
it "should not set the Content-Encoding header when it is identity" do
|
59
|
+
subject.choose_encoding({"gzip"=> :encode_gzip, "identity" => :encode_identity}, "identity")
|
60
|
+
subject.metadata['Content-Encoding'].should == 'identity'
|
61
|
+
response.headers['Content-Encoding'].should be_nil
|
62
|
+
end
|
63
|
+
|
64
|
+
it "should choose the first acceptable encoding" do
|
65
|
+
subject.choose_encoding({"gzip" => :encode_gzip}, "identity, gzip")
|
66
|
+
subject.metadata['Content-Encoding'].should == 'gzip'
|
67
|
+
response.headers['Content-Encoding'].should == 'gzip'
|
68
|
+
end
|
69
|
+
|
70
|
+
it "should choose the preferred encoding over less-preferred encodings" do
|
71
|
+
subject.choose_encoding({"gzip" => :encode_gzip, "identity" => :encode_identity}, "gzip, identity;q=0.7")
|
72
|
+
subject.metadata['Content-Encoding'].should == 'gzip'
|
73
|
+
response.headers['Content-Encoding'].should == 'gzip'
|
74
|
+
end
|
75
|
+
|
76
|
+
it "should not set the encoding if none are acceptable" do
|
77
|
+
subject.choose_encoding({"gzip" => :encode_gzip}, "identity")
|
78
|
+
subject.metadata['Content-Encoding'].should be_nil
|
79
|
+
response.headers['Content-Encoding'].should be_nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "choosing a charset" do
|
84
|
+
it "should not set the charset when none are provided" do
|
85
|
+
subject.choose_charset([], "ISO-8859-1")
|
86
|
+
subject.metadata['Charset'].should be_nil
|
87
|
+
end
|
88
|
+
|
89
|
+
it "should choose the first acceptable charset" do
|
90
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII, UTF-8")
|
91
|
+
subject.metadata['Charset'].should == "US-ASCII"
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should choose the preferred charset over less-preferred charsets" do
|
95
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "US-ASCII;q=0.7, UTF-8")
|
96
|
+
subject.metadata['Charset'].should == "UTF-8"
|
97
|
+
end
|
98
|
+
|
99
|
+
it "should not set the charset if none are acceptable" do
|
100
|
+
subject.choose_charset([["UTF-8", :to_utf8],["US-ASCII", :to_ascii]], "ISO-8859-1")
|
101
|
+
subject.metadata['Charset'].should be_nil
|
102
|
+
end
|
103
|
+
|
104
|
+
it "should choose a charset case-insensitively" do
|
105
|
+
subject.choose_charset([["UtF-8", :to_utf8],["US-ASCII", :to_ascii]], "iso-8859-1, utf-8")
|
106
|
+
subject.metadata['Charset'].should == "utf-8"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
context "choosing a language" do
|
111
|
+
it "should not set the language when none are provided" do
|
112
|
+
subject.choose_language([], "en")
|
113
|
+
subject.metadata['Language'].should be_nil
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should choose the first acceptable language" do
|
117
|
+
subject.choose_language(['en', 'en-US', 'es'], "en-US, es")
|
118
|
+
subject.metadata['Language'].should == "en-US"
|
119
|
+
response.headers['Content-Language'].should == "en-US"
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should choose the preferred language over less-preferred languages" do
|
123
|
+
subject.choose_language(['en', 'en-US', 'es'], "en-US;q=0.6, es")
|
124
|
+
subject.metadata['Language'].should == "es"
|
125
|
+
response.headers['Content-Language'].should == "es"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should select the first language if all are acceptable" do
|
129
|
+
subject.choose_language(['en', 'fr', 'es'], "*")
|
130
|
+
subject.metadata['Language'].should == "en"
|
131
|
+
response.headers['Content-Language'].should == "en"
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should select the closest acceptable language when an exact match is not available" do
|
135
|
+
subject.choose_language(['en-US', 'es'], "en, fr")
|
136
|
+
subject.metadata['Language'].should == 'en-US'
|
137
|
+
response.headers['Content-Language'].should == 'en-US'
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should not set the language if none are acceptable" do
|
141
|
+
subject.choose_language(['en'], 'es')
|
142
|
+
subject.metadata['Language'].should be_nil
|
143
|
+
response.headers.should_not include('Content-Language')
|
144
|
+
end
|
145
|
+
|
146
|
+
it "should choose a language case-insensitively" do
|
147
|
+
subject.choose_language(['en-US', 'ZH'], 'zh-ch, EN')
|
148
|
+
subject.metadata['Language'].should == 'en-US'
|
149
|
+
response.headers['Content-Language'].should == 'en-US'
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|