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,33 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Plugin
|
3
|
+
# Report latency information about the EventMachine reactor to the log file.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# plugin Goliath::Plugin::Latency
|
7
|
+
#
|
8
|
+
class Latency
|
9
|
+
# Called by the framework to initialize the plugin
|
10
|
+
#
|
11
|
+
# @param port [Integer] Unused
|
12
|
+
# @param config [Hash] The server configuration data
|
13
|
+
# @param status [Hash] A status hash
|
14
|
+
# @param logger [Log4R::Logger] The logger
|
15
|
+
# @return [Goliath::Plugin::Latency] An instance of the Goliath::Plugin::Latency plugin
|
16
|
+
def initialize(port, config, status, logger)
|
17
|
+
@status = status
|
18
|
+
@config = config
|
19
|
+
@logger = logger
|
20
|
+
|
21
|
+
@last = Time.now.to_f
|
22
|
+
end
|
23
|
+
|
24
|
+
# Called automatically to start the plugin
|
25
|
+
def run
|
26
|
+
EM.add_periodic_timer(1) do
|
27
|
+
@logger.info "LATENCY: #{Time.now.to_f - @last}"
|
28
|
+
@last = Time.now.to_f
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rack/mime'
|
2
|
+
require 'rack/respond_to'
|
3
|
+
|
4
|
+
module Goliath
|
5
|
+
module Rack
|
6
|
+
# Does some basic cleanup / handling of the HTTP_ACCEPT header.
|
7
|
+
# This will remove gzip, deflate, compressed and identity. If
|
8
|
+
# there are no values left the header will be set to \*/\*.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# use Goliath::Rack::DefaultMimeType
|
12
|
+
#
|
13
|
+
class DefaultMimeType
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
def call(env)
|
19
|
+
accept = env['HTTP_ACCEPT'] || ''
|
20
|
+
accept = accept.split(/\s*,\s*/)
|
21
|
+
accept.delete_if { |a| a =~ /gzip|deflate|compressed|identity/ }
|
22
|
+
accept = accept.join(", ")
|
23
|
+
|
24
|
+
env['HTTP_ACCEPT'] = accept
|
25
|
+
env['HTTP_ACCEPT'] = '*/*' if env['HTTP_ACCEPT'] == ''
|
26
|
+
@app.call(env)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
class DefaultResponseFormat
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
async_cb = env['async.callback']
|
10
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
11
|
+
async_cb.call(post_process(status, headers, body))
|
12
|
+
end
|
13
|
+
|
14
|
+
status, headers, body = @app.call(env)
|
15
|
+
post_process(status, headers, body)
|
16
|
+
end
|
17
|
+
|
18
|
+
def post_process(status, headers, body)
|
19
|
+
return [status, headers, body] if body.respond_to?(:to_ary)
|
20
|
+
|
21
|
+
new_body = []
|
22
|
+
if body.respond_to?(:each)
|
23
|
+
body.each { |chunk| new_body << chunk }
|
24
|
+
else
|
25
|
+
new_body << body
|
26
|
+
end
|
27
|
+
new_body.collect! { |item| item.to_s }
|
28
|
+
|
29
|
+
[status, headers, new_body.flatten]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
module Formatters
|
6
|
+
# A simple HTML formatter. This doesn't try to be fancy and just turns
|
7
|
+
# Hashes into tables, Arrays into ordered lists and strings are output
|
8
|
+
# as HTML escaped strings.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# use Goliath::Rack::Formatters::HTML
|
12
|
+
class HTML
|
13
|
+
# Called by the framework to create the formatter.
|
14
|
+
#
|
15
|
+
# @param app The application
|
16
|
+
# @return [Goliath::Rack::Formatters::HTML] The HTML formatter
|
17
|
+
def initialize(app)
|
18
|
+
@app = app
|
19
|
+
end
|
20
|
+
|
21
|
+
def call(env)
|
22
|
+
async_cb = env['async.callback']
|
23
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
24
|
+
async_cb.call(post_process(status, headers, body))
|
25
|
+
end
|
26
|
+
|
27
|
+
status, headers, body = @app.call(env)
|
28
|
+
post_process(status, headers, body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post_process(status, headers, body)
|
32
|
+
body = [to_html(body, false)] if html_response?(headers)
|
33
|
+
[status, headers, body]
|
34
|
+
end
|
35
|
+
|
36
|
+
def html_response?(headers)
|
37
|
+
headers['Content-Type'] =~ %r{^text/html}
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_html(content, fragment=true)
|
41
|
+
html_string = ''
|
42
|
+
html_string += html_header unless fragment
|
43
|
+
|
44
|
+
html_string += case(content.class.to_s)
|
45
|
+
when "Hash" then hash_to_html(content)
|
46
|
+
when "Array" then array_to_html(content)
|
47
|
+
when "String" then string_to_html(content)
|
48
|
+
else string_to_html(content)
|
49
|
+
end
|
50
|
+
|
51
|
+
html_string += html_footer unless fragment
|
52
|
+
html_string
|
53
|
+
end
|
54
|
+
|
55
|
+
def string_to_html(content)
|
56
|
+
::Rack::Utils.escape_html(content.to_s)
|
57
|
+
end
|
58
|
+
|
59
|
+
def hash_to_html(content)
|
60
|
+
html_string = "<table>\n"
|
61
|
+
if content.key?('meta')
|
62
|
+
html_string += "<tr><td>meta</td><td>\n"
|
63
|
+
html_string += to_html(content['meta'])
|
64
|
+
html_string += "</td></tr>\n"
|
65
|
+
content.delete('meta')
|
66
|
+
end
|
67
|
+
|
68
|
+
content.each_pair { |key, value| html_string += "<tr><td>#{to_html(key)}</td><td>#{to_html(value)}</td></tr>\n" }
|
69
|
+
html_string += "</table>\n"
|
70
|
+
html_string
|
71
|
+
end
|
72
|
+
|
73
|
+
def array_to_html(content)
|
74
|
+
html_string = '<ol>\n'
|
75
|
+
content.each { |value| html_string += "<li>#{to_html(value)}</li>\n" }
|
76
|
+
html_string +="</ol>\n"
|
77
|
+
html_string
|
78
|
+
end
|
79
|
+
|
80
|
+
def html_header
|
81
|
+
"<html><body>"
|
82
|
+
end
|
83
|
+
|
84
|
+
def html_footer
|
85
|
+
"</body></html>"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'multi_json'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
module Formatters
|
6
|
+
# A JSON formatter. Uses MultiJson so you can use the JSON
|
7
|
+
# encoder that is right for your project.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# use Goliath::Rack::Formatters::JSON
|
11
|
+
class JSON
|
12
|
+
# Called by the framework to create the formatter.
|
13
|
+
#
|
14
|
+
# @return [Goliath::Rack::Formatters::JSON] The JSON formatter.
|
15
|
+
def initialize(app)
|
16
|
+
@app = app
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
async_cb = env['async.callback']
|
21
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
22
|
+
async_cb.call(post_process(status, headers, body))
|
23
|
+
end
|
24
|
+
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
post_process(status, headers, body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post_process(status, headers, body)
|
30
|
+
if json_response?(headers)
|
31
|
+
body = [MultiJson.encode(body)]
|
32
|
+
end
|
33
|
+
[status, headers, body]
|
34
|
+
end
|
35
|
+
|
36
|
+
def json_response?(headers)
|
37
|
+
headers['Content-Type'] =~ %r{^application/(json|javascript)}
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
module Formatters
|
6
|
+
# A XML formatter. Attempts to convert your data into
|
7
|
+
# an XML document.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# use Goliath::Rack::Formatters::XML
|
11
|
+
class XML
|
12
|
+
# Called by the framework to create the formatter.
|
13
|
+
#
|
14
|
+
# @return [Goliath::Rack::Formatters::XML] The XML formatter.
|
15
|
+
def initialize(app)
|
16
|
+
@app = app
|
17
|
+
end
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
async_cb = env['async.callback']
|
21
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
22
|
+
async_cb.call(post_process(status, headers, body))
|
23
|
+
end
|
24
|
+
|
25
|
+
status, headers, body = @app.call(env)
|
26
|
+
post_process(status, headers, body)
|
27
|
+
end
|
28
|
+
|
29
|
+
def post_process(status, headers, body)
|
30
|
+
body = [to_xml(body, false)] if xml_response?(headers)
|
31
|
+
[status, headers, body]
|
32
|
+
end
|
33
|
+
|
34
|
+
def xml_response?(headers)
|
35
|
+
headers['Content-Type'] =~ %r{^application/xml}
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_xml(content, fragment=true, root='results', item='item')
|
39
|
+
xml_string = ''
|
40
|
+
xml_string += xml_header(root) unless fragment
|
41
|
+
|
42
|
+
xml_string += case(content.class.to_s)
|
43
|
+
when "Hash" then hash_to_xml(content, root, item)
|
44
|
+
when "Array" then array_to_xml(content, root, item)
|
45
|
+
when "String" then string_to_xml(content)
|
46
|
+
else string_to_xml(content)
|
47
|
+
end
|
48
|
+
|
49
|
+
xml_string += xml_footer(root) unless fragment
|
50
|
+
xml_string
|
51
|
+
end
|
52
|
+
|
53
|
+
def string_to_xml(content)
|
54
|
+
::Rack::Utils.escape_html(content.to_s)
|
55
|
+
end
|
56
|
+
|
57
|
+
def hash_to_xml(content, root='results', item='item')
|
58
|
+
xml_string = ''
|
59
|
+
if content.key?('meta')
|
60
|
+
xml_string += xml_item('meta', content['meta'], root)
|
61
|
+
content.delete('meta')
|
62
|
+
end
|
63
|
+
|
64
|
+
content.each_pair { |key, value| xml_string += xml_item(key, value, root) }
|
65
|
+
xml_string
|
66
|
+
end
|
67
|
+
|
68
|
+
def array_to_xml(content, root='results', item='item')
|
69
|
+
xml_string = ''
|
70
|
+
content.each { |value| xml_string += xml_item(item, value, root) }
|
71
|
+
xml_string
|
72
|
+
end
|
73
|
+
|
74
|
+
def xml_header(root)
|
75
|
+
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
|
76
|
+
"<#{root} xmlns:opensearch='http://a9.com/-/spec/opensearch/1.1/'\n" +
|
77
|
+
" xmlns:postrank='http://www.postrank.com/xsd/2007-11-30/postrank'>\n"
|
78
|
+
end
|
79
|
+
|
80
|
+
def xml_footer(root)
|
81
|
+
"</#{root}>"
|
82
|
+
end
|
83
|
+
|
84
|
+
def xml_item(key, value, root)
|
85
|
+
"<#{key}>#{to_xml(value, true, root)}</#{key}>\n"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
# A heartbeat mechanism for the server. This will add a _/status_ endpoint
|
4
|
+
# that returns status 200 and content OK when executed.
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# use Goliath::Rack::Heartbeat
|
8
|
+
#
|
9
|
+
class Heartbeat
|
10
|
+
def initialize(app)
|
11
|
+
@app = app
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
if env['PATH_INFO'] == '/status'
|
16
|
+
[200, {}, 'OK']
|
17
|
+
else
|
18
|
+
@app.call(env)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Goliath
|
2
|
+
module Rack
|
3
|
+
# A middleware to wrap the response into a JSONP callback.
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# use Goliath::Rack::JSONP
|
7
|
+
#
|
8
|
+
class JSONP
|
9
|
+
def initialize(app)
|
10
|
+
@app = app
|
11
|
+
end
|
12
|
+
|
13
|
+
def call(env)
|
14
|
+
async_cb = env['async.callback']
|
15
|
+
|
16
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
17
|
+
async_cb.call(post_process(env, status, headers, body))
|
18
|
+
end
|
19
|
+
status, headers, body = @app.call(env)
|
20
|
+
post_process(env, status, headers, body)
|
21
|
+
end
|
22
|
+
|
23
|
+
def post_process(env, status, headers, body)
|
24
|
+
return [status, headers, body] unless env.params['callback']
|
25
|
+
|
26
|
+
response = ""
|
27
|
+
if body.respond_to?(:each)
|
28
|
+
body.each { |s| response << s }
|
29
|
+
else
|
30
|
+
response = body
|
31
|
+
end
|
32
|
+
|
33
|
+
[status, headers, ["#{env.params['callback']}(#{response})"]]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rack/utils'
|
2
|
+
|
3
|
+
module Goliath
|
4
|
+
module Rack
|
5
|
+
# A middle ware to parse params. This will parse both the
|
6
|
+
# query string parameters and the body and place them into
|
7
|
+
# the _params_ hash of the Goliath::Env for the request.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# use Goliath::Rack::Params
|
11
|
+
#
|
12
|
+
class Params
|
13
|
+
def initialize(app)
|
14
|
+
@app = app
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
env['params'] = retrieve_params(env)
|
19
|
+
@app.call(env)
|
20
|
+
end
|
21
|
+
|
22
|
+
def retrieve_params(env)
|
23
|
+
params = {}
|
24
|
+
params.merge!(::Rack::Utils.parse_query(env['QUERY_STRING']))
|
25
|
+
params.merge!(::Rack::Utils.parse_query(env['rack.input'].read)) unless env['rack.input'].nil?
|
26
|
+
params
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'rack/mime'
|
2
|
+
require 'rack/respond_to'
|
3
|
+
|
4
|
+
module Goliath
|
5
|
+
module Rack
|
6
|
+
# The render middleware will set the Content-Type of the response
|
7
|
+
# based on the provided HTTP_ACCEPT headers.
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# use Goliath::Rack::Render
|
11
|
+
#
|
12
|
+
class Render
|
13
|
+
include ::Rack::RespondTo
|
14
|
+
|
15
|
+
def initialize(app, types = nil)
|
16
|
+
@app = app
|
17
|
+
::Rack::RespondTo.media_types = [types].flatten if types
|
18
|
+
end
|
19
|
+
|
20
|
+
def call(env)
|
21
|
+
async_cb = env['async.callback']
|
22
|
+
|
23
|
+
env['async.callback'] = Proc.new do |status, headers, body|
|
24
|
+
async_cb.call(post_process(env, status, headers, body))
|
25
|
+
end
|
26
|
+
|
27
|
+
status, headers, body = @app.call(env)
|
28
|
+
post_process(env, status, headers, body)
|
29
|
+
end
|
30
|
+
|
31
|
+
def post_process(env, status, headers, body)
|
32
|
+
::Rack::RespondTo.env = env
|
33
|
+
|
34
|
+
# the respond_to block is what actually triggers the
|
35
|
+
# setting of selected_media_type, so it's required
|
36
|
+
|
37
|
+
respond_to do |format|
|
38
|
+
format.json { body }
|
39
|
+
format.html { body }
|
40
|
+
format.xml { body }
|
41
|
+
format.rss { body }
|
42
|
+
format.js { body }
|
43
|
+
end
|
44
|
+
|
45
|
+
extra = { 'Content-Type' => get_content_type(env),
|
46
|
+
'Server' => 'PostRank Goliath API Server',
|
47
|
+
'Vary' => [headers.delete('Vary'), 'Accept'].compact.join(',') }
|
48
|
+
|
49
|
+
[status, extra.merge(headers), body]
|
50
|
+
end
|
51
|
+
|
52
|
+
def get_content_type(env)
|
53
|
+
fmt = env.params['format']
|
54
|
+
fmt = fmt.last if fmt.is_a?(Array)
|
55
|
+
|
56
|
+
type = if fmt.nil? || fmt =~ /^\s*$/
|
57
|
+
::Rack::RespondTo.selected_media_type
|
58
|
+
else
|
59
|
+
::Rack::RespondTo::MediaType(fmt)
|
60
|
+
end
|
61
|
+
|
62
|
+
"#{type}; charset=utf-8"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|