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.

Files changed (84) hide show
  1. data/.gitignore +15 -0
  2. data/.rspec +2 -0
  3. data/.yardopts +2 -0
  4. data/Gemfile +3 -0
  5. data/LICENSE +66 -0
  6. data/README.md +86 -0
  7. data/Rakefile +18 -0
  8. data/examples/activerecord/config/srv.rb +7 -0
  9. data/examples/activerecord/srv.rb +37 -0
  10. data/examples/async_upload.rb +34 -0
  11. data/examples/conf_test.rb +27 -0
  12. data/examples/config/conf_test.rb +12 -0
  13. data/examples/config/echo.rb +1 -0
  14. data/examples/config/http_log.rb +7 -0
  15. data/examples/config/shared.rb +5 -0
  16. data/examples/custom_server.rb +57 -0
  17. data/examples/echo.rb +37 -0
  18. data/examples/gziped.rb +40 -0
  19. data/examples/hello_world.rb +10 -0
  20. data/examples/http_log.rb +85 -0
  21. data/examples/rack_routes.rb +44 -0
  22. data/examples/stream.rb +37 -0
  23. data/examples/valid.rb +19 -0
  24. data/goliath.gemspec +41 -0
  25. data/lib/goliath.rb +38 -0
  26. data/lib/goliath/api.rb +165 -0
  27. data/lib/goliath/application.rb +90 -0
  28. data/lib/goliath/connection.rb +94 -0
  29. data/lib/goliath/constants.rb +51 -0
  30. data/lib/goliath/env.rb +92 -0
  31. data/lib/goliath/goliath.rb +49 -0
  32. data/lib/goliath/headers.rb +37 -0
  33. data/lib/goliath/http_status_codes.rb +44 -0
  34. data/lib/goliath/plugins/latency.rb +33 -0
  35. data/lib/goliath/rack/default_mime_type.rb +30 -0
  36. data/lib/goliath/rack/default_response_format.rb +33 -0
  37. data/lib/goliath/rack/formatters/html.rb +90 -0
  38. data/lib/goliath/rack/formatters/json.rb +42 -0
  39. data/lib/goliath/rack/formatters/xml.rb +90 -0
  40. data/lib/goliath/rack/heartbeat.rb +23 -0
  41. data/lib/goliath/rack/jsonp.rb +38 -0
  42. data/lib/goliath/rack/params.rb +30 -0
  43. data/lib/goliath/rack/render.rb +66 -0
  44. data/lib/goliath/rack/tracer.rb +31 -0
  45. data/lib/goliath/rack/validation/boolean_value.rb +59 -0
  46. data/lib/goliath/rack/validation/default_params.rb +46 -0
  47. data/lib/goliath/rack/validation/numeric_range.rb +59 -0
  48. data/lib/goliath/rack/validation/request_method.rb +33 -0
  49. data/lib/goliath/rack/validation/required_param.rb +54 -0
  50. data/lib/goliath/rack/validation/required_value.rb +58 -0
  51. data/lib/goliath/rack/validation_error.rb +38 -0
  52. data/lib/goliath/request.rb +199 -0
  53. data/lib/goliath/response.rb +93 -0
  54. data/lib/goliath/runner.rb +236 -0
  55. data/lib/goliath/server.rb +149 -0
  56. data/lib/goliath/test_helper.rb +118 -0
  57. data/lib/goliath/version.rb +4 -0
  58. data/spec/integration/async_request_processing.rb +23 -0
  59. data/spec/integration/echo_spec.rb +27 -0
  60. data/spec/integration/keepalive_spec.rb +28 -0
  61. data/spec/integration/pipelining_spec.rb +43 -0
  62. data/spec/integration/valid_spec.rb +24 -0
  63. data/spec/spec_helper.rb +6 -0
  64. data/spec/unit/connection_spec.rb +59 -0
  65. data/spec/unit/env_spec.rb +55 -0
  66. data/spec/unit/headers_spec.rb +53 -0
  67. data/spec/unit/rack/default_mime_type_spec.rb +34 -0
  68. data/spec/unit/rack/formatters/json_spec.rb +54 -0
  69. data/spec/unit/rack/formatters/xml_spec.rb +66 -0
  70. data/spec/unit/rack/heartbeat_spec.rb +47 -0
  71. data/spec/unit/rack/params_spec.rb +94 -0
  72. data/spec/unit/rack/render_spec.rb +87 -0
  73. data/spec/unit/rack/validation/boolean_value_spec.rb +54 -0
  74. data/spec/unit/rack/validation/default_params_spec.rb +71 -0
  75. data/spec/unit/rack/validation/numeric_range_spec.rb +96 -0
  76. data/spec/unit/rack/validation/request_method_spec.rb +47 -0
  77. data/spec/unit/rack/validation/required_param_spec.rb +92 -0
  78. data/spec/unit/rack/validation/required_value_spec.rb +99 -0
  79. data/spec/unit/rack/validation_error_spec.rb +40 -0
  80. data/spec/unit/request_spec.rb +59 -0
  81. data/spec/unit/response_spec.rb +35 -0
  82. data/spec/unit/runner_spec.rb +129 -0
  83. data/spec/unit/server_spec.rb +137 -0
  84. metadata +409 -0
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env ruby
2
+ $:<< '../lib' << 'lib'
3
+
4
+ require 'goliath'
5
+
6
+ class HelloWorld < Goliath::API
7
+ def response(env)
8
+ [200, {}, "hello world"]
9
+ end
10
+ end
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env ruby
2
+ #
3
+ # Simple example that takes all requests and forwards them to
4
+ # another API using EM-HTTP-Request. Information about the
5
+ # request and response is then stored into a Mongo database.
6
+ #
7
+
8
+ $: << "../lib" << "./lib"
9
+
10
+ require 'rubygems'
11
+ require 'goliath'
12
+ require 'em-mongo'
13
+ require 'em-http'
14
+ require 'em-synchrony/em-http'
15
+ require 'pp'
16
+
17
+ class HttpLog < Goliath::API
18
+ use ::Rack::Reloader, 0 if Goliath.dev?
19
+ use Goliath::Rack::Params
20
+
21
+ def on_headers(env, headers)
22
+ env.logger.info 'proxying new request: ' + headers.inspect
23
+ env['client-headers'] = headers
24
+ end
25
+
26
+ def response(env)
27
+ start_time = Time.now.to_f
28
+
29
+ params = {:head => env['client-headers'], :query => env.params}
30
+
31
+ req = EM::HttpRequest.new("#{forwarder}#{env[Goliath::Request::REQUEST_PATH]}")
32
+ resp = case(env[Goliath::Request::REQUEST_METHOD])
33
+ when 'GET' then req.get(params)
34
+ when 'POST' then req.post(params.merge(:body => env[Goliath::Request::RACK_INPUT].read))
35
+ when 'HEAD' then req.head(params)
36
+ else p "UNKNOWN METHOD #{env[Goliath::Request::REQUEST_METHOD]}"
37
+ end
38
+
39
+ process_time = Time.now.to_f - start_time
40
+
41
+ response_headers = {}
42
+ resp.response_header.each_pair do |k, v|
43
+ response_headers[to_http_header(k)] = v
44
+ end
45
+
46
+ record(process_time, resp, env['client-headers'], response_headers)
47
+
48
+ [resp.response_header.status, response_headers, resp.response]
49
+ end
50
+
51
+ # Need to convert from the CONTENT_TYPE we'll get back from the server
52
+ # to the normal Content-Type header
53
+ def to_http_header(k)
54
+ k.downcase.split('_').collect { |e| e.capitalize }.join('-')
55
+ end
56
+
57
+ # Write the request information into mongo
58
+ def record(process_time, resp, client_headers, response_headers)
59
+ e = env
60
+ EM.next_tick do
61
+ doc = {
62
+ request: {
63
+ http_method: e[Goliath::Request::REQUEST_METHOD],
64
+ path: e[Goliath::Request::REQUEST_PATH],
65
+ headers: client_headers,
66
+ params: e.params
67
+ },
68
+ response: {
69
+ status: resp.response_header.status,
70
+ length: resp.response.length,
71
+ headers: response_headers,
72
+ body: resp.response
73
+ },
74
+ process_time: process_time,
75
+ date: Time.now.to_i
76
+ }
77
+
78
+ if e[Goliath::Request::RACK_INPUT]
79
+ doc[:request][:body] = e[Goliath::Request::RACK_INPUT].read
80
+ end
81
+
82
+ e.mongo.insert(doc)
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+ $:<< '../lib' << 'lib'
3
+
4
+ require 'goliath'
5
+
6
+ # Example demonstrating how to have an API acting as a router.
7
+ # RackRoutes defines multiple uris and how to map them accordingly.
8
+ # Some of these routes are redirected to other Goliath API.
9
+ #
10
+ # The reason why only the last API is being used by the Goliath Server
11
+ # is because its name matches the filename.
12
+ # All the APIs are available but by default the server will use the one
13
+ # matching the file name.
14
+
15
+ # Our custom Goliath API
16
+ class HelloWorld < Goliath::API
17
+ def response(env)
18
+ [200, {}, "hello world!"]
19
+ end
20
+ end
21
+
22
+ class Bonjour < Goliath::API
23
+ def response(env)
24
+ [200, {}, "bonjour!"]
25
+ end
26
+ end
27
+
28
+ class RackRoutes < Goliath::API
29
+ map '/version' do
30
+ run Proc.new { |env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
31
+ end
32
+
33
+ map "/hello_world" do
34
+ run HelloWorld.new
35
+ end
36
+
37
+ map "/bonjour" do
38
+ run Bonjour.new
39
+ end
40
+
41
+ map '/' do
42
+ run Proc.new { |env| [404, {"Content-Type" => "text/html"}, ["Try /version /hello_world or /bonjour"]] }
43
+ end
44
+ end
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env ruby
2
+ $:<< '../lib' << 'lib'
3
+
4
+ #
5
+ # A simple HTTP streaming API which returns a 200 response for any GET request
6
+ # and then emits numbers 1 through 10 in 1 second intervals, and then closes the
7
+ # connection.
8
+ #
9
+ # A good use case for this pattern would be to provide a stream of updates or a
10
+ # 'firehose' like API to stream data back to the clients. Simply hook up to your
11
+ # datasource and then stream the data to your clients via HTTP.
12
+ #
13
+
14
+ require 'goliath'
15
+
16
+ class Stream < Goliath::API
17
+ def on_close(env)
18
+ env.logger.info "Connection closed."
19
+ end
20
+
21
+ def response(env)
22
+ i = 0
23
+ pt = EM.add_periodic_timer(1) do
24
+ env.stream_send("#{i} ")
25
+ i += 1
26
+ end
27
+
28
+ EM.add_timer(10) do
29
+ pt.cancel
30
+
31
+ env.stream_send("!! BOOM !!\n")
32
+ env.stream_close
33
+ end
34
+
35
+ [200, {}, Goliath::Response::STREAMING]
36
+ end
37
+ end
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env ruby
2
+ $:<< '../lib' << 'lib'
3
+
4
+ require 'goliath'
5
+
6
+ class Valid < Goliath::API
7
+
8
+ # reload code on every request in dev environment
9
+ use ::Rack::Reloader, 0 if Goliath.dev?
10
+
11
+ use Goliath::Rack::Params
12
+ use Goliath::Rack::ValidationError
13
+
14
+ use Goliath::Rack::Validation::RequiredParam, {:key => 'test'}
15
+
16
+ def response(env)
17
+ [200, {}, 'OK']
18
+ end
19
+ end
@@ -0,0 +1,41 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'goliath/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'goliath'
7
+ s.version = Goliath::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ['dan sinclair', 'Ilya Grigorik']
10
+ s.email = ['dj2@everburning.com', 'ilya@igvita.com']
11
+ s.homepage = 'http://labs.postrank.com/'
12
+ s.summary = 'Framework for writing API servers'
13
+ s.description = s.summary
14
+
15
+ s.required_ruby_version = '>=1.9.2'
16
+
17
+ s.add_dependency 'eventmachine', '>= 1.0.0.beta.1'
18
+ s.add_dependency 'em-synchrony', '>= 0.3.0.beta.1'
19
+ s.add_dependency 'http_parser.rb'
20
+ s.add_dependency 'log4r'
21
+
22
+ s.add_dependency 'rack'
23
+ s.add_dependency 'rack-contrib'
24
+ s.add_dependency 'rack-respond_to'
25
+ s.add_dependency 'async-rack'
26
+ s.add_dependency 'multi_json'
27
+
28
+ s.add_development_dependency 'rspec', '>2.0'
29
+ s.add_development_dependency 'nokogiri'
30
+ s.add_development_dependency 'em-http-request', '>= 1.0.0.beta.1'
31
+ s.add_development_dependency 'em-mongo'
32
+ s.add_development_dependency 'yajl-ruby'
33
+ s.add_development_dependency 'rack-rewrite'
34
+
35
+ s.add_development_dependency 'yard'
36
+ s.add_development_dependency 'bluecloth'
37
+
38
+ s.files = `git ls-files`.split("\n")
39
+ s.test_files = `git ls-files -- spec/*`.split("\n")
40
+ s.require_paths = ['lib']
41
+ end
@@ -0,0 +1,38 @@
1
+ require 'eventmachine'
2
+ require 'http/parser'
3
+ require 'async_rack'
4
+ require 'stringio'
5
+
6
+ require 'goliath/version'
7
+ require 'goliath/goliath'
8
+ require 'goliath/runner'
9
+ require 'goliath/server'
10
+ require 'goliath/constants'
11
+ require 'goliath/connection'
12
+ require 'goliath/request'
13
+ require 'goliath/response'
14
+ require 'goliath/headers'
15
+ require 'goliath/http_status_codes'
16
+
17
+ require 'goliath/rack/default_response_format'
18
+ require 'goliath/rack/heartbeat'
19
+ require 'goliath/rack/params'
20
+ require 'goliath/rack/render'
21
+ require 'goliath/rack/default_mime_type'
22
+ require 'goliath/rack/tracer'
23
+ require 'goliath/rack/validation_error'
24
+ require 'goliath/rack/formatters/json'
25
+ require 'goliath/rack/formatters/html'
26
+ require 'goliath/rack/formatters/xml'
27
+ require 'goliath/rack/jsonp'
28
+
29
+ require 'goliath/rack/validation/request_method'
30
+ require 'goliath/rack/validation/required_param'
31
+ require 'goliath/rack/validation/required_value'
32
+ require 'goliath/rack/validation/numeric_range'
33
+ require 'goliath/rack/validation/default_params'
34
+ require 'goliath/rack/validation/boolean_value'
35
+
36
+ require 'goliath/api'
37
+
38
+ require 'goliath/application'
@@ -0,0 +1,165 @@
1
+ require 'goliath/response'
2
+ require 'goliath/request'
3
+
4
+ module Goliath
5
+ # All Goliath APIs subclass Goliath::API. All subclasses _must_ override the
6
+ # {#response} method.
7
+ #
8
+ # @example
9
+ # require 'goliath'
10
+ #
11
+ # class HelloWorld < Goliath::API
12
+ # def response(env)
13
+ # [200, {}, "hello world"]
14
+ # end
15
+ # end
16
+ #
17
+ class API
18
+ class << self
19
+ # Retrieves the middlewares defined by this API server
20
+ #
21
+ # @return [Array] array contains [middleware class, args, block]
22
+ def middlewares
23
+ @middlewares ||= [[::Rack::ContentLength, nil, nil],
24
+ [Goliath::Rack::DefaultResponseFormat, nil, nil]]
25
+ end
26
+
27
+ # Specify a middleware to be used by the API
28
+ #
29
+ # @example
30
+ # use Goliath::Rack::Validation::RequiredParam, {:key => 'echo'}
31
+ #
32
+ # use ::Rack::Rewrite do
33
+ # rewrite %r{^(.*?)\??gziped=(.*)$}, lambda { |match, env| "#{match[1]}?echo=#{match[2]}" }
34
+ # end
35
+ #
36
+ # @param name [Class] The middleware class to use
37
+ # @param args Any arguments to pass to the middeware
38
+ # @param block A block to pass to the middleware
39
+ def use(name, args = nil, &block)
40
+ middlewares.push([name, args, block])
41
+ end
42
+
43
+ # Returns the plugins configured for this API
44
+ #
45
+ # @return [Array] array contains [plugin name, args]
46
+ def plugins
47
+ @plugins ||= []
48
+ end
49
+
50
+ # Specify a plugin to be used by the API
51
+ #
52
+ # @example
53
+ # plugin Goliath::Plugin::Latency
54
+ #
55
+ # @param name [Class] The plugin class to use
56
+ # @param args The arguments to the plugin
57
+ def plugin(name, *args)
58
+ plugins.push([name, args])
59
+ end
60
+
61
+ # Returns the router maps configured for the API
62
+ #
63
+ # @return [Array] array contains [path, block]
64
+ def maps
65
+ @maps ||= []
66
+ end
67
+
68
+ # Specify a router map to be used by the API
69
+ #
70
+ # @example
71
+ # map '/version' do
72
+ # run Proc.new {|env| [200, {"Content-Type" => "text/html"}, ["Version 0.1"]] }
73
+ # end
74
+ #
75
+ # @param name [String] The URL path to map
76
+ # @param block The code to execute
77
+ def map(name, &block)
78
+ maps.push([name, block])
79
+ end
80
+ end
81
+
82
+ # Default stub method to add options into the option parser.
83
+ #
84
+ # @example
85
+ # def options_parser(opts, options)
86
+ # options[:test] = 0
87
+ # opts.on('-t', '--test NUM', "The test number") { |val| options[:test] = val.to_i }
88
+ # end
89
+ #
90
+ # @param opts [OptionParser] The options parser
91
+ # @param options [Hash] The hash to insert the parsed options into
92
+ def options_parser(opts, options)
93
+ end
94
+
95
+ # Accessor for the current env object
96
+ #
97
+ # @note This will not work in a streaming server. You must pass around the env object.
98
+ #
99
+ # @return [Goliath::Env] The current environment data for the request
100
+ def env
101
+ Thread.current[Goliath::Constants::GOLIATH_ENV]
102
+ end
103
+
104
+ # The API will proxy missing calls to the env object if possible.
105
+ #
106
+ # The two entries in this example are equivalent as long as you are not
107
+ # in a streaming server.
108
+ #
109
+ # @example
110
+ # logger.info "Hello"
111
+ # env.logger.info "Hello"
112
+
113
+ def method_missing(name, *args, &blk)
114
+ name = name.to_s
115
+ if env.respond_to?(name)
116
+ env.send(name, *args, &blk)
117
+ else
118
+ super(name.to_sym, *args, &blk)
119
+ end
120
+ end
121
+
122
+ # {#call} is executed automatically by the middleware chain and will setup
123
+ # the environment for the {#response} method to execute. This includes setting
124
+ # up a new Fiber, handing any execptions thrown from the API and executing
125
+ # the appropriate callback method for the API.
126
+ #
127
+ # @param env [Goliath::Env] The request environment
128
+ # @return [Goliath::Connection::AsyncResponse] An async response.
129
+ def call(env)
130
+ Fiber.new {
131
+ begin
132
+ Thread.current[Goliath::Constants::GOLIATH_ENV] = env
133
+ status, headers, body = response(env)
134
+
135
+ if body == Goliath::Response::STREAMING
136
+ env[Goliath::Constants::STREAM_START].call(status, headers)
137
+ else
138
+ env[Goliath::Constants::ASYNC_CALLBACK].call([status, headers, body])
139
+ end
140
+
141
+ rescue Exception => e
142
+ env.logger.error(e.message)
143
+ env.logger.error(e.backtrace.join("\n"))
144
+
145
+ env[Goliath::Constants::ASYNC_CALLBACK].call([400, {}, {:error => e.message}])
146
+ end
147
+ }.resume
148
+
149
+ Goliath::Connection::AsyncResponse
150
+ end
151
+
152
+ # Response is the main implementation method for Goliath APIs. All APIs
153
+ # should override this method in order to do any actual work.
154
+ #
155
+ # The response method will be executed in a new Fiber and wrapped in a
156
+ # begin rescue block to handle an thrown API errors.
157
+ #
158
+ # @param env [Goliath::Env] The request environment
159
+ # @return [Array] Array contains [Status code, Headers Hash, Body]
160
+ def response(env)
161
+ env.logger.error('You need to implement response')
162
+ [400, {}, {:error => 'No response implemented'}]
163
+ end
164
+ end
165
+ end