webmachine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,11 @@
1
+ require 'i18n'
2
+
3
+ I18n.config.load_path << File.expand_path("../locale/en.yml", __FILE__)
4
+
5
+ module Webmachine
6
+ module Translation
7
+ def t(key, options={})
8
+ ::I18n.t(key, options.merge(:scope => :webmachine))
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,4 @@
1
+ module Webmachine
2
+ VERSION = "0.1.0"
3
+ SERVER_STRING = "Webmachine-Ruby/#{VERSION}"
4
+ 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
@@ -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