goliath 0.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of goliath might be problematic. Click here for more details.
- data/.gitignore +15 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/Gemfile +3 -0
- data/LICENSE +66 -0
- data/README.md +86 -0
- data/Rakefile +18 -0
- data/examples/activerecord/config/srv.rb +7 -0
- data/examples/activerecord/srv.rb +37 -0
- data/examples/async_upload.rb +34 -0
- data/examples/conf_test.rb +27 -0
- data/examples/config/conf_test.rb +12 -0
- data/examples/config/echo.rb +1 -0
- data/examples/config/http_log.rb +7 -0
- data/examples/config/shared.rb +5 -0
- data/examples/custom_server.rb +57 -0
- data/examples/echo.rb +37 -0
- data/examples/gziped.rb +40 -0
- data/examples/hello_world.rb +10 -0
- data/examples/http_log.rb +85 -0
- data/examples/rack_routes.rb +44 -0
- data/examples/stream.rb +37 -0
- data/examples/valid.rb +19 -0
- data/goliath.gemspec +41 -0
- data/lib/goliath.rb +38 -0
- data/lib/goliath/api.rb +165 -0
- data/lib/goliath/application.rb +90 -0
- data/lib/goliath/connection.rb +94 -0
- data/lib/goliath/constants.rb +51 -0
- data/lib/goliath/env.rb +92 -0
- data/lib/goliath/goliath.rb +49 -0
- data/lib/goliath/headers.rb +37 -0
- data/lib/goliath/http_status_codes.rb +44 -0
- data/lib/goliath/plugins/latency.rb +33 -0
- data/lib/goliath/rack/default_mime_type.rb +30 -0
- data/lib/goliath/rack/default_response_format.rb +33 -0
- data/lib/goliath/rack/formatters/html.rb +90 -0
- data/lib/goliath/rack/formatters/json.rb +42 -0
- data/lib/goliath/rack/formatters/xml.rb +90 -0
- data/lib/goliath/rack/heartbeat.rb +23 -0
- data/lib/goliath/rack/jsonp.rb +38 -0
- data/lib/goliath/rack/params.rb +30 -0
- data/lib/goliath/rack/render.rb +66 -0
- data/lib/goliath/rack/tracer.rb +31 -0
- data/lib/goliath/rack/validation/boolean_value.rb +59 -0
- data/lib/goliath/rack/validation/default_params.rb +46 -0
- data/lib/goliath/rack/validation/numeric_range.rb +59 -0
- data/lib/goliath/rack/validation/request_method.rb +33 -0
- data/lib/goliath/rack/validation/required_param.rb +54 -0
- data/lib/goliath/rack/validation/required_value.rb +58 -0
- data/lib/goliath/rack/validation_error.rb +38 -0
- data/lib/goliath/request.rb +199 -0
- data/lib/goliath/response.rb +93 -0
- data/lib/goliath/runner.rb +236 -0
- data/lib/goliath/server.rb +149 -0
- data/lib/goliath/test_helper.rb +118 -0
- data/lib/goliath/version.rb +4 -0
- data/spec/integration/async_request_processing.rb +23 -0
- data/spec/integration/echo_spec.rb +27 -0
- data/spec/integration/keepalive_spec.rb +28 -0
- data/spec/integration/pipelining_spec.rb +43 -0
- data/spec/integration/valid_spec.rb +24 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/unit/connection_spec.rb +59 -0
- data/spec/unit/env_spec.rb +55 -0
- data/spec/unit/headers_spec.rb +53 -0
- data/spec/unit/rack/default_mime_type_spec.rb +34 -0
- data/spec/unit/rack/formatters/json_spec.rb +54 -0
- data/spec/unit/rack/formatters/xml_spec.rb +66 -0
- data/spec/unit/rack/heartbeat_spec.rb +47 -0
- data/spec/unit/rack/params_spec.rb +94 -0
- data/spec/unit/rack/render_spec.rb +87 -0
- data/spec/unit/rack/validation/boolean_value_spec.rb +54 -0
- data/spec/unit/rack/validation/default_params_spec.rb +71 -0
- data/spec/unit/rack/validation/numeric_range_spec.rb +96 -0
- data/spec/unit/rack/validation/request_method_spec.rb +47 -0
- data/spec/unit/rack/validation/required_param_spec.rb +92 -0
- data/spec/unit/rack/validation/required_value_spec.rb +99 -0
- data/spec/unit/rack/validation_error_spec.rb +40 -0
- data/spec/unit/request_spec.rb +59 -0
- data/spec/unit/response_spec.rb +35 -0
- data/spec/unit/runner_spec.rb +129 -0
- data/spec/unit/server_spec.rb +137 -0
- metadata +409 -0
@@ -0,0 +1,118 @@
|
|
1
|
+
require 'em-synchrony'
|
2
|
+
require 'em-synchrony/em-http'
|
3
|
+
|
4
|
+
require 'goliath/server'
|
5
|
+
require 'rack'
|
6
|
+
|
7
|
+
module Goliath
|
8
|
+
# Methods to help with testing Goliath APIs
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# describe Echo do
|
12
|
+
# include Goliath::TestHelper
|
13
|
+
#
|
14
|
+
# let(:err) { Proc.new { fail "API request failed" } }
|
15
|
+
# it 'returns the echo param' do
|
16
|
+
# with_api(Echo) do
|
17
|
+
# get_request({:query => {:echo => 'test'}}, err) do |c|
|
18
|
+
# b = Yajl::Parser.parse(c.response)
|
19
|
+
# b['response'].should == 'test'
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
module TestHelper
|
26
|
+
def self.included(mod)
|
27
|
+
Goliath.env = 'test'
|
28
|
+
end
|
29
|
+
|
30
|
+
# Builds the rack middleware chain for the given API
|
31
|
+
#
|
32
|
+
# @param klass [Class] The API class to build the middlewares for
|
33
|
+
# @return [Object] The Rack middleware chain
|
34
|
+
def build_app(klass)
|
35
|
+
::Rack::Builder.new do
|
36
|
+
klass.middlewares.each do |mw|
|
37
|
+
use(*(mw[0..1].compact), &mw[2])
|
38
|
+
end
|
39
|
+
run klass.new
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Launches an instance of a given API server. The server
|
44
|
+
# will launch on the default settings of localhost port 9000.
|
45
|
+
#
|
46
|
+
# @param api [Class] The API class to launch
|
47
|
+
# @param port [Integer] The port to run the server on
|
48
|
+
# @return [Nil]
|
49
|
+
def server(api, port = 9000)
|
50
|
+
s = Goliath::Server.new
|
51
|
+
s.logger = mock('log').as_null_object
|
52
|
+
s.api = api.new
|
53
|
+
s.app = build_app(api)
|
54
|
+
s.port = port
|
55
|
+
s.start
|
56
|
+
s
|
57
|
+
end
|
58
|
+
|
59
|
+
# Stops the launched API
|
60
|
+
#
|
61
|
+
# @return [Nil]
|
62
|
+
def stop
|
63
|
+
EM.stop
|
64
|
+
end
|
65
|
+
|
66
|
+
# Wrapper for launching API and executing given code block. This
|
67
|
+
# will start the EventMachine reactor running.
|
68
|
+
#
|
69
|
+
# @param api [Class] The API class to launch
|
70
|
+
# @param blk [Proc] The code to execute after the server is launched.
|
71
|
+
# @note This will not return until stop is called.
|
72
|
+
def with_api(api, &blk)
|
73
|
+
EM.synchrony do
|
74
|
+
@api_server = server(api)
|
75
|
+
blk.call
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
# Helper method to setup common callbacks for various request methods.
|
80
|
+
# The given err and callback handlers will be attached and a callback
|
81
|
+
# to stop the reactor will be added.
|
82
|
+
#
|
83
|
+
# @param req [EM::HttpRequest] The HTTP request to augment
|
84
|
+
# @param errback [Proc] An error handler to attach
|
85
|
+
# @param blk [Proc] The callback handler to attach
|
86
|
+
# @return [Nil]
|
87
|
+
# @api private
|
88
|
+
def hookup_request_callbacks(req, errback, &blk)
|
89
|
+
req.callback &blk
|
90
|
+
req.callback { stop }
|
91
|
+
|
92
|
+
req.errback &errback if errback
|
93
|
+
req.errback { stop }
|
94
|
+
end
|
95
|
+
|
96
|
+
# Make a GET request the currently launched API.
|
97
|
+
#
|
98
|
+
# @param request_data [Hash] Any data to pass to the GET request.
|
99
|
+
# @param errback [Proc] An error handler to attach
|
100
|
+
# @param blk [Proc] The callback block to execute
|
101
|
+
def get_request(request_data = {}, errback = nil, &blk)
|
102
|
+
path = request_data.delete(:path) || ''
|
103
|
+
req = EM::HttpRequest.new("http://localhost:9000#{path}").get(request_data)
|
104
|
+
hookup_request_callbacks(req, errback, &blk)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Make a POST request the currently launched API.
|
108
|
+
#
|
109
|
+
# @param request_data [Hash] Any data to pass to the POST request.
|
110
|
+
# @param errback [Proc] An error handler to attach
|
111
|
+
# @param blk [Proc] The callback block to execute
|
112
|
+
def post_request(request_data = {}, errback = nil, &blk)
|
113
|
+
path = request_data.delete(:path) || ''
|
114
|
+
req = EM::HttpRequest.new("http://localhost:9000#{path}").post(request_data)
|
115
|
+
hookup_request_callbacks(req, errback, &blk)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require File.join(File.dirname(__FILE__), '../../', 'examples/async_upload')
|
3
|
+
|
4
|
+
describe 'Async Request processing' do
|
5
|
+
include Goliath::TestHelper
|
6
|
+
|
7
|
+
it 'asynchronously processes the incoming request' do
|
8
|
+
with_api(AsyncUpload) do
|
9
|
+
request_data = {
|
10
|
+
:body => {:some => :data},
|
11
|
+
:head => {'X-Upload' => 'custom'}
|
12
|
+
}
|
13
|
+
|
14
|
+
err = Proc.new { fail "API request failed" }
|
15
|
+
|
16
|
+
post_request(request_data, err) do |c|
|
17
|
+
resp = Yajl::Parser.parse(c.response)
|
18
|
+
resp['body'].should match('some=data')
|
19
|
+
resp['head'].should include('X-Upload')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require File.join(File.dirname(__FILE__), '../../', 'examples/echo')
|
3
|
+
|
4
|
+
describe Echo do
|
5
|
+
include Goliath::TestHelper
|
6
|
+
|
7
|
+
let(:err) { Proc.new { fail "API request failed" } }
|
8
|
+
|
9
|
+
it 'returns the echo param' do
|
10
|
+
with_api(Echo) do
|
11
|
+
get_request({:query => {:echo => 'test'}}, err) do |c|
|
12
|
+
b = Yajl::Parser.parse(c.response)
|
13
|
+
b['response'].should == 'test'
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'returns error without echo' do
|
19
|
+
with_api(Echo) do
|
20
|
+
get_request({}, err) do |c|
|
21
|
+
b = Yajl::Parser.parse(c.response)
|
22
|
+
b['error'].should_not be_nil
|
23
|
+
b['error'].should == 'Echo identifier missing'
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require File.join(File.dirname(__FILE__), '../../', 'examples/echo')
|
3
|
+
|
4
|
+
describe 'HTTP Keep-Alive support' do
|
5
|
+
include Goliath::TestHelper
|
6
|
+
|
7
|
+
it 'serves multiple requests via single connection' do
|
8
|
+
with_api(Echo) do
|
9
|
+
conn = EM::HttpRequest.new('http://localhost:9000')
|
10
|
+
r1 = conn.get(:query => {:echo => 'test'}, :keepalive => true)
|
11
|
+
|
12
|
+
r1.errback { fail }
|
13
|
+
r1.callback do |c|
|
14
|
+
b = Yajl::Parser.parse(c.response)
|
15
|
+
b['response'].should == 'test'
|
16
|
+
|
17
|
+
r2 = conn.get(:query => {:echo => 'test2'})
|
18
|
+
r2.errback { fail }
|
19
|
+
r2.callback do |c|
|
20
|
+
b = Yajl::Parser.parse(c.response)
|
21
|
+
b['response'].should == 'test2'
|
22
|
+
|
23
|
+
stop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
class Interleaving < Goliath::API
|
4
|
+
use Goliath::Rack::Params
|
5
|
+
|
6
|
+
def response(env)
|
7
|
+
delay = env.params['delay']
|
8
|
+
EM::Synchrony.sleep(delay.to_f)
|
9
|
+
|
10
|
+
[200, {}, delay]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe 'HTTP Pipelining support' do
|
15
|
+
include Goliath::TestHelper
|
16
|
+
|
17
|
+
it 'serves multiple requests via single connection' do
|
18
|
+
with_api(Interleaving) do
|
19
|
+
start = Time.now.to_f
|
20
|
+
res = []
|
21
|
+
|
22
|
+
conn = EM::HttpRequest.new('http://localhost:9000')
|
23
|
+
r1 = conn.aget :query => {:delay => 0.3}, :keepalive => true
|
24
|
+
r2 = conn.aget :query => {:delay => 0.2}
|
25
|
+
|
26
|
+
r1.errback { fail }
|
27
|
+
r1.callback do |c|
|
28
|
+
res << c.response
|
29
|
+
c.response.should match('0.3')
|
30
|
+
end
|
31
|
+
|
32
|
+
r2.errback { fail }
|
33
|
+
r2.callback do |c|
|
34
|
+
res << c.response
|
35
|
+
|
36
|
+
res.should == ['0.3', '0.2']
|
37
|
+
(Time.now.to_f - start).should be_within(0.1).of(0.3)
|
38
|
+
|
39
|
+
stop
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require File.join(File.dirname(__FILE__), '../../', 'examples/valid')
|
3
|
+
|
4
|
+
describe Valid do
|
5
|
+
include Goliath::TestHelper
|
6
|
+
|
7
|
+
let(:err) { Proc.new { fail "API request failed" } }
|
8
|
+
|
9
|
+
it 'returns OK with param' do
|
10
|
+
with_api(Valid) do
|
11
|
+
get_request({:query => {:test => 'test'}}, err) do |c|
|
12
|
+
c.response.should == 'OK'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'returns error without param' do
|
18
|
+
with_api(Valid) do
|
19
|
+
get_request({}, err) do |c|
|
20
|
+
c.response.should == '[:error, "Test identifier missing"]'
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Goliath::Connection do
|
4
|
+
before(:each) do
|
5
|
+
@c = Goliath::Connection.new('blah')
|
6
|
+
end
|
7
|
+
|
8
|
+
describe 'configuration' do
|
9
|
+
it 'accepts an app' do
|
10
|
+
app = mock('app')
|
11
|
+
@c.app = app
|
12
|
+
@c.app.should == app
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'accepts a logger' do
|
16
|
+
logger = mock('logger')
|
17
|
+
@c.logger = logger
|
18
|
+
@c.logger.should == logger
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'accepts a status object' do
|
22
|
+
status = mock('status')
|
23
|
+
@c.status = status
|
24
|
+
@c.status.should == status
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'accepts config' do
|
28
|
+
config = mock('config')
|
29
|
+
@c.config = config
|
30
|
+
@c.config.should == config
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'post_init' do
|
35
|
+
it 'sets up the parser' do
|
36
|
+
@c.post_init
|
37
|
+
@c.instance_variable_get("@parser").should_not be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'receive_data' do
|
42
|
+
it 'passes data to the http parser' do
|
43
|
+
request_mock = mock("parser").as_null_object
|
44
|
+
request_mock.should_receive(:<<)
|
45
|
+
|
46
|
+
@c.instance_variable_set("@parser", request_mock)
|
47
|
+
@c.receive_data('more_data')
|
48
|
+
end
|
49
|
+
|
50
|
+
it "closes the connection when a parse error is received" do
|
51
|
+
current_mock = mock("current").as_null_object
|
52
|
+
current_mock.should_receive(:close)
|
53
|
+
|
54
|
+
@c.instance_variable_set("@current", current_mock)
|
55
|
+
lambda { @c.receive_data("bad data") }.should_not raise_error
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'goliath/env'
|
3
|
+
|
4
|
+
describe Goliath::Env do
|
5
|
+
before(:each) do
|
6
|
+
@env = Goliath::Env.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'responds to []=' do
|
10
|
+
lambda { @env['test'] = 'blah' }.should_not raise_error(Exception)
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'responds to []' do
|
14
|
+
@env['test'] = 'blah'
|
15
|
+
lambda { @env['test'].should == 'blah' }.should_not raise_error(Exception)
|
16
|
+
end
|
17
|
+
|
18
|
+
context '#method_missing' do
|
19
|
+
it 'allows access to items as methods' do
|
20
|
+
@env['db'] = 'test'
|
21
|
+
@env.db.should == 'test'
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'allows access to config items as methods' do
|
25
|
+
@env['config'] = {}
|
26
|
+
@env['config']['db'] = 'test'
|
27
|
+
@env.db.should == 'test'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
context '#respond_to?' do
|
32
|
+
it 'returns true for items in the hash' do
|
33
|
+
@env['test'] = 'true'
|
34
|
+
@env.respond_to?(:test).should be_true
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'returns false for items not in hash' do
|
38
|
+
@env.respond_to?(:test).should be_false
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'returns true for items in the config hash' do
|
42
|
+
@env['config'] = {'test' => true}
|
43
|
+
@env.respond_to?(:test).should be_true
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'returns false for items not in the config hash' do
|
47
|
+
@env['config'] = {}
|
48
|
+
@env.respond_to?(:test).should be_false
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'delegates if not found' do
|
52
|
+
@env.respond_to?(:[]).should be_true
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'goliath/headers'
|
3
|
+
|
4
|
+
describe Goliath::Headers do
|
5
|
+
before(:each) do
|
6
|
+
@h = Goliath::Headers.new
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'outputs in the correct format' do
|
10
|
+
@h['my_header'] = 'my_value'
|
11
|
+
@h.to_s.should == "my_header: my_value\r\n"
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'suppresses duplicate keys' do
|
15
|
+
@h['my_header'] = 'my_value1'
|
16
|
+
@h['my_header'] = 'my_value2'
|
17
|
+
@h.to_s.should == "my_header: my_value1\r\n"
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'returns true if a key has been set' do
|
21
|
+
@h['my_header'] = 'my_value'
|
22
|
+
@h.has_key?('my_header').should be_true
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'returns false if the key has not been set' do
|
26
|
+
@h.has_key?('my_header').should be_false
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'ignores nil values' do
|
30
|
+
@h['my_header'] = nil
|
31
|
+
@h.to_s.should == ''
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'allows a value after setting nil' do
|
35
|
+
@h['my_header'] = nil
|
36
|
+
@h['my_header'] = 'my_value'
|
37
|
+
@h.to_s.should == "my_header: my_value\r\n"
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'formats time as an http time' do
|
41
|
+
time = Time.now
|
42
|
+
@h['my_time'] = time
|
43
|
+
@h.to_s.should == "my_time: #{time.httpdate}\r\n"
|
44
|
+
end
|
45
|
+
|
46
|
+
%w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).each do |key|
|
47
|
+
it "allows #{key} as to be duplicate" do
|
48
|
+
@h[key] = 'value1'
|
49
|
+
@h[key] = 'value2'
|
50
|
+
@h.to_s.should == "#{key}: value1\r\n#{key}: value2\r\n"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|