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.
@@ -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