webmachine 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +12 -10
- data/Guardfile +1 -1
- data/README.md +1 -1
- data/examples/application.rb +35 -0
- data/lib/webmachine.rb +4 -3
- data/lib/webmachine/adapter.rb +36 -0
- data/lib/webmachine/adapters/mongrel.rb +18 -12
- data/lib/webmachine/adapters/rack.rb +26 -7
- data/lib/webmachine/adapters/webrick.rb +20 -16
- data/lib/webmachine/application.rb +108 -0
- data/lib/webmachine/chunked_body.rb +2 -2
- data/lib/webmachine/configuration.rb +24 -14
- data/lib/webmachine/decision/conneg.rb +9 -10
- data/lib/webmachine/decision/flow.rb +25 -28
- data/lib/webmachine/decision/fsm.rb +21 -22
- data/lib/webmachine/decision/helpers.rb +3 -3
- data/lib/webmachine/dispatcher.rb +18 -10
- data/lib/webmachine/dispatcher/route.rb +54 -17
- data/lib/webmachine/errors.rb +1 -1
- data/lib/webmachine/headers.rb +2 -2
- data/lib/webmachine/media_type.rb +2 -2
- data/lib/webmachine/request.rb +78 -3
- data/lib/webmachine/resource.rb +3 -2
- data/lib/webmachine/resource/authentication.rb +4 -3
- data/lib/webmachine/resource/callbacks.rb +4 -3
- data/lib/webmachine/resource/encodings.rb +4 -3
- data/lib/webmachine/response.rb +3 -2
- data/lib/webmachine/streaming.rb +4 -4
- data/lib/webmachine/version.rb +1 -1
- data/spec/webmachine/adapter_spec.rb +40 -0
- data/spec/webmachine/adapters/mongrel_spec.rb +22 -0
- data/spec/webmachine/adapters/rack_spec.rb +34 -8
- data/spec/webmachine/adapters/webrick_spec.rb +18 -0
- data/spec/webmachine/application_spec.rb +73 -0
- data/spec/webmachine/dispatcher/route_spec.rb +59 -2
- data/spec/webmachine/dispatcher_spec.rb +17 -5
- data/spec/webmachine/request_spec.rb +158 -1
- data/webmachine.gemspec +6 -27
- metadata +304 -112
- data/spec/tests.org +0 -80
data/lib/webmachine/request.rb
CHANGED
@@ -9,6 +9,8 @@ module Webmachine
|
|
9
9
|
attr_reader :method, :uri, :headers, :body
|
10
10
|
attr_accessor :disp_path, :path_info, :path_tokens
|
11
11
|
|
12
|
+
STANDARD_HTTP_METHODS = %w[GET HEAD POST PUT DELETE TRACE CONNECT OPTIONS]
|
13
|
+
|
12
14
|
# @param [String] method the HTTP request method
|
13
15
|
# @param [URI] uri the requested URI, including host, scheme and
|
14
16
|
# port
|
@@ -55,12 +57,85 @@ module Webmachine
|
|
55
57
|
def query
|
56
58
|
unless @query
|
57
59
|
@query = {}
|
58
|
-
uri.query.split(/&/).each do |kv|
|
60
|
+
(uri.query || '').split(/&/).each do |kv|
|
59
61
|
k, v = CGI.unescape(kv).split(/=/)
|
60
62
|
@query[k] = v if k && v
|
61
63
|
end
|
62
64
|
end
|
63
65
|
@query
|
64
66
|
end
|
65
|
-
|
66
|
-
|
67
|
+
|
68
|
+
# Is this an HTTPS request?
|
69
|
+
#
|
70
|
+
# @return [Boolean]
|
71
|
+
# true if this request was made via HTTPS
|
72
|
+
def https?
|
73
|
+
uri.scheme == "https"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Is this a GET request?
|
77
|
+
#
|
78
|
+
# @return [Boolean]
|
79
|
+
# true if this request was made with the GET method
|
80
|
+
def get?
|
81
|
+
method == "GET"
|
82
|
+
end
|
83
|
+
|
84
|
+
# Is this a HEAD request?
|
85
|
+
#
|
86
|
+
# @return [Boolean]
|
87
|
+
# true if this request was made with the HEAD method
|
88
|
+
def head?
|
89
|
+
method == "HEAD"
|
90
|
+
end
|
91
|
+
|
92
|
+
# Is this a POST request?
|
93
|
+
#
|
94
|
+
# @return [Boolean]
|
95
|
+
# true if this request was made with the GET method
|
96
|
+
def post?
|
97
|
+
method == "POST"
|
98
|
+
end
|
99
|
+
|
100
|
+
# Is this a PUT request?
|
101
|
+
#
|
102
|
+
# @return [Boolean]
|
103
|
+
# true if this request was made with the PUT method
|
104
|
+
def put?
|
105
|
+
method == "PUT"
|
106
|
+
end
|
107
|
+
|
108
|
+
# Is this a DELETE request?
|
109
|
+
#
|
110
|
+
# @return [Boolean]
|
111
|
+
# true if this request was made with the DELETE method
|
112
|
+
def delete?
|
113
|
+
method == "DELETE"
|
114
|
+
end
|
115
|
+
|
116
|
+
# Is this a TRACE request?
|
117
|
+
#
|
118
|
+
# @return [Boolean]
|
119
|
+
# true if this request was made with the TRACE method
|
120
|
+
def trace?
|
121
|
+
method == "TRACE"
|
122
|
+
end
|
123
|
+
|
124
|
+
# Is this a CONNECT request?
|
125
|
+
#
|
126
|
+
# @return [Boolean]
|
127
|
+
# true if this request was made with the CONNECT method
|
128
|
+
def connect?
|
129
|
+
method == "CONNECT"
|
130
|
+
end
|
131
|
+
|
132
|
+
# Is this an OPTIONS request?
|
133
|
+
#
|
134
|
+
# @return [Boolean]
|
135
|
+
# true if this request was made with the OPTIONS method
|
136
|
+
def options?
|
137
|
+
method == "OPTIONS"
|
138
|
+
end
|
139
|
+
|
140
|
+
end # class Request
|
141
|
+
end # module Webmachine
|
data/lib/webmachine/resource.rb
CHANGED
data/lib/webmachine/response.rb
CHANGED
data/lib/webmachine/streaming.rb
CHANGED
@@ -26,7 +26,7 @@ module Webmachine
|
|
26
26
|
yield resource.send(encoder, resource.send(charsetter, block.to_s))
|
27
27
|
end
|
28
28
|
end
|
29
|
-
end
|
29
|
+
end # class EnumerableEncoder
|
30
30
|
|
31
31
|
# Implements a streaming encoder for callable bodies, such as
|
32
32
|
# Proc. (essentially futures)
|
@@ -44,7 +44,7 @@ module Webmachine
|
|
44
44
|
def to_proc
|
45
45
|
method(:call).to_proc
|
46
46
|
end
|
47
|
-
end
|
47
|
+
end # class CallableEncoder
|
48
48
|
|
49
49
|
# Implements a streaming encoder for Fibers with the same API as the
|
50
50
|
# EnumerableEncoder. This will resume the Fiber until it terminates
|
@@ -59,5 +59,5 @@ module Webmachine
|
|
59
59
|
yield resource.send(encoder, resource.send(charsetter, chunk.to_s))
|
60
60
|
end
|
61
61
|
end
|
62
|
-
end
|
63
|
-
end
|
62
|
+
end # class FiberEncoder
|
63
|
+
end # module Webmachine
|
data/lib/webmachine/version.rb
CHANGED
@@ -0,0 +1,40 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Webmachine::Adapter do
|
4
|
+
let(:configuration) { Webmachine::Configuration.default }
|
5
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
6
|
+
let(:adapter) do
|
7
|
+
described_class.new(configuration, dispatcher)
|
8
|
+
end
|
9
|
+
|
10
|
+
describe "#initialize" do
|
11
|
+
it "stores the provided configuration" do
|
12
|
+
adapter.configuration.should eql configuration
|
13
|
+
end
|
14
|
+
|
15
|
+
it "stores the provided dispatcher" do
|
16
|
+
adapter.dispatcher.should eql dispatcher
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe ".run" do
|
21
|
+
it "creates a new adapter and runs it" do
|
22
|
+
adapter = mock(described_class)
|
23
|
+
|
24
|
+
described_class.should_receive(:new).
|
25
|
+
with(configuration, dispatcher).
|
26
|
+
and_return(adapter)
|
27
|
+
|
28
|
+
adapter.should_receive(:run)
|
29
|
+
|
30
|
+
described_class.run(configuration, dispatcher)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "#run" do
|
35
|
+
it "raises a NotImplementedError" do
|
36
|
+
lambda { adapter.run }.should raise_exception(NotImplementedError)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
begin
|
4
|
+
describe Webmachine::Adapters::Mongrel do
|
5
|
+
let(:configuration) { Webmachine::Configuration.default }
|
6
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
7
|
+
|
8
|
+
let(:adapter) do
|
9
|
+
described_class.new(configuration, dispatcher)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "inherits from Webmachine::Adapter" do
|
13
|
+
adapter.should be_a_kind_of(Webmachine::Adapter)
|
14
|
+
end
|
15
|
+
|
16
|
+
it "implements #run" do
|
17
|
+
described_class.instance_methods(false).map {|m| m.to_sym }.should include :run
|
18
|
+
end
|
19
|
+
end
|
20
|
+
rescue LoadError
|
21
|
+
warn "Platform is #{RUBY_PLATFORM}: skipping mongrel adapter spec."
|
22
|
+
end
|
@@ -11,8 +11,6 @@ module Test
|
|
11
11
|
end
|
12
12
|
|
13
13
|
describe Webmachine::Adapters::Rack do
|
14
|
-
let(:adapter) { described_class }
|
15
|
-
|
16
14
|
let(:env) do
|
17
15
|
{ "REQUEST_METHOD" => "GET",
|
18
16
|
"SCRIPT_NAME" => "",
|
@@ -29,33 +27,61 @@ describe Webmachine::Adapters::Rack do
|
|
29
27
|
"rack.run_once" => false }
|
30
28
|
end
|
31
29
|
|
30
|
+
let(:configuration) { Webmachine::Configuration.new('0.0.0.0', 8080, :Rack, {}) }
|
31
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
32
|
+
let(:adapter) do
|
33
|
+
described_class.new(configuration, dispatcher)
|
34
|
+
end
|
35
|
+
|
36
|
+
subject { adapter }
|
37
|
+
|
32
38
|
before do
|
33
|
-
|
34
|
-
|
39
|
+
dispatcher.add_route ['test'], Test::Resource
|
40
|
+
end
|
41
|
+
|
42
|
+
it "inherits from Webmachine::Adapter" do
|
43
|
+
adapter.should be_a_kind_of(Webmachine::Adapter)
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#run" do
|
47
|
+
before do
|
48
|
+
configuration.adapter_options[:debug] = true
|
49
|
+
end
|
50
|
+
|
51
|
+
it "starts a rack server with the correct options" do
|
52
|
+
Rack::Server.should_receive(:start).with(
|
53
|
+
:app => adapter,
|
54
|
+
:Port => configuration.port,
|
55
|
+
:Host => configuration.ip,
|
56
|
+
:debug => true
|
57
|
+
)
|
58
|
+
|
59
|
+
adapter.run
|
60
|
+
end
|
35
61
|
end
|
36
62
|
|
37
63
|
it "should proxy request to webmachine" do
|
38
|
-
code, headers, body =
|
64
|
+
code, headers, body = subject.call(env)
|
39
65
|
code.should == 200
|
40
66
|
headers["Content-Type"].should == "text/html"
|
41
67
|
body.should include "<html><body>testing</body></html>"
|
42
68
|
end
|
43
69
|
|
44
70
|
it "should set Server header" do
|
45
|
-
code, headers, body =
|
71
|
+
code, headers, body = subject.call(env)
|
46
72
|
headers.should have_key "Server"
|
47
73
|
end
|
48
74
|
|
49
75
|
it "should handle non-success correctly" do
|
50
76
|
env["PATH_INFO"] = "/missing"
|
51
|
-
code, headers, body =
|
77
|
+
code, headers, body = subject.call(env)
|
52
78
|
code.should == 404
|
53
79
|
headers["Content-Type"].should == "text/html"
|
54
80
|
end
|
55
81
|
|
56
82
|
it "should handle empty bodies correctly" do
|
57
83
|
env["HTTP_ACCEPT"] = "application/json"
|
58
|
-
code, headers, body =
|
84
|
+
code, headers, body = subject.call(env)
|
59
85
|
code.should == 406
|
60
86
|
headers.should_not have_key "Content-Type"
|
61
87
|
headers.should_not have_key "Content-Length"
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Webmachine::Adapters::WEBrick do
|
4
|
+
let(:configuration) { Webmachine::Configuration.default }
|
5
|
+
let(:dispatcher) { Webmachine::Dispatcher.new }
|
6
|
+
|
7
|
+
let(:adapter) do
|
8
|
+
described_class.new(configuration, dispatcher)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "inherits from Webmachine::Adapter" do
|
12
|
+
adapter.should be_a_kind_of(Webmachine::Adapter)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "implements #run" do
|
16
|
+
described_class.instance_methods(false).map {|m| m.to_sym }.should include :run
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Webmachine::Application do
|
4
|
+
let(:application) { described_class.new }
|
5
|
+
let(:test_resource) { Class.new(Webmachine::Resource) }
|
6
|
+
|
7
|
+
it "accepts a Configuration when initialized" do
|
8
|
+
config = Webmachine::Configuration.new('1.1.1.1', 9999, :Mongrel, {})
|
9
|
+
described_class.new(config).configuration.should be(config)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "is yielded into a block provided during initialization" do
|
13
|
+
yielded_app = nil
|
14
|
+
described_class.new do |app|
|
15
|
+
app.should be_kind_of(Webmachine::Application)
|
16
|
+
yielded_app = app
|
17
|
+
end.should be(yielded_app)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "is initialized with the default Configration if none is given" do
|
21
|
+
application.configuration.should eq(Webmachine::Configuration.default)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns the receiver from the configure call so you can chain it" do
|
25
|
+
application.configure { |c| }.should equal(application)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "is configurable" do
|
29
|
+
application.configure do |config|
|
30
|
+
config.should be_kind_of(Webmachine::Configuration)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "is initialized with an empty Dispatcher" do
|
35
|
+
application.dispatcher.routes.should be_empty
|
36
|
+
end
|
37
|
+
|
38
|
+
it "can have routes added" do
|
39
|
+
route = nil
|
40
|
+
resource = test_resource # overcome instance_eval :/
|
41
|
+
|
42
|
+
application.routes.should be_empty
|
43
|
+
|
44
|
+
application.routes do
|
45
|
+
route = add ['*'], resource
|
46
|
+
end
|
47
|
+
|
48
|
+
route.should be_kind_of(Webmachine::Dispatcher::Route)
|
49
|
+
application.routes.should eq([route])
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#adapter" do
|
53
|
+
let(:adapter_class) { application.adapter_class }
|
54
|
+
|
55
|
+
it "returns an instance of it's adapter class" do
|
56
|
+
application.adapter.should be_an_instance_of(adapter_class)
|
57
|
+
end
|
58
|
+
|
59
|
+
it "is memoized" do
|
60
|
+
application.adapter.should eql application.adapter
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it "can be run" do
|
65
|
+
application.adapter.should_receive(:run)
|
66
|
+
application.run
|
67
|
+
end
|
68
|
+
|
69
|
+
it "can be queried about its configured adapter" do
|
70
|
+
expected = Webmachine::Adapters.const_get(application.configuration.adapter)
|
71
|
+
application.adapter_class.should equal(expected)
|
72
|
+
end
|
73
|
+
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Webmachine::Dispatcher::Route do
|
4
|
-
let(:
|
4
|
+
let(:method) { "GET" }
|
5
|
+
let(:uri) { URI.parse("http://localhost:8080/") }
|
6
|
+
let(:request){ Webmachine::Request.new(method, uri, Webmachine::Headers.new, "") }
|
5
7
|
let(:resource){ Class.new(Webmachine::Resource) }
|
6
8
|
|
7
9
|
matcher :match_route do |*expected|
|
@@ -37,6 +39,61 @@ describe Webmachine::Dispatcher::Route do
|
|
37
39
|
it { should_not match_route [] }
|
38
40
|
it { should_not match_route %w{bar *} }
|
39
41
|
end
|
42
|
+
|
43
|
+
context "with a guard on the request method" do
|
44
|
+
let(:uri){ URI.parse("http://localhost:8080/notes") }
|
45
|
+
let(:route) do
|
46
|
+
described_class.new(
|
47
|
+
["notes"],
|
48
|
+
lambda { |request| request.method == "POST" },
|
49
|
+
resource
|
50
|
+
)
|
51
|
+
end
|
52
|
+
subject { route }
|
53
|
+
|
54
|
+
context "when guard passes" do
|
55
|
+
let(:method){ "POST" }
|
56
|
+
it { should be_match(request) }
|
57
|
+
|
58
|
+
context "but the path match fails" do
|
59
|
+
let(:uri){ URI.parse("http://localhost:8080/other") }
|
60
|
+
it { should_not be_match(request) }
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "when guard fails" do
|
65
|
+
let(:method) { "GET" }
|
66
|
+
it { should_not be_match(request) }
|
67
|
+
end
|
68
|
+
|
69
|
+
context "when the guard responds to #call" do
|
70
|
+
let(:guard_class) do
|
71
|
+
Class.new do
|
72
|
+
def initialize(method)
|
73
|
+
@method = method
|
74
|
+
end
|
75
|
+
|
76
|
+
def call(request)
|
77
|
+
request.method == @method
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
let(:route) do
|
83
|
+
described_class.new(["notes"], guard_class.new("POST"), resource)
|
84
|
+
end
|
85
|
+
|
86
|
+
context "when the guard passes" do
|
87
|
+
let(:method){ "POST" }
|
88
|
+
it { should be_match(request) }
|
89
|
+
end
|
90
|
+
|
91
|
+
context "when the guard fails" do
|
92
|
+
# let(:method){ "GET" }
|
93
|
+
it { should_not be_match(request) }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
40
97
|
end
|
41
98
|
|
42
99
|
context "applying bindings" do
|
@@ -66,7 +123,7 @@ describe Webmachine::Dispatcher::Route do
|
|
66
123
|
|
67
124
|
context "with a splat" do
|
68
125
|
subject { described_class.new(['*'], resource) }
|
69
|
-
|
126
|
+
|
70
127
|
it "should assign empty path tokens" do
|
71
128
|
request.path_tokens.should == []
|
72
129
|
end
|