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