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,90 @@
1
+ module Goliath
2
+ # The main execution class for Goliath. This will execute in the at_exit
3
+ # handler to run the server.
4
+ #
5
+ # @private
6
+ class Application
7
+ # Most of this stuff is straight out of sinatra.
8
+
9
+ # Set of caller regex's to be skippe when looking for our API file
10
+ CALLERS_TO_IGNORE = [ # :nodoc:
11
+ /\/goliath(\/(application))?\.rb$/, # all goliath code
12
+ /rubygems\/custom_require\.rb$/, # rubygems require hacks
13
+ /bundler(\/runtime)?\.rb/, # bundler require hacks
14
+ /<internal:/ # internal in ruby >= 1.9.2
15
+ ]
16
+
17
+ # @todo add rubinius (and hopefully other VM impls) ignore patterns ...
18
+ CALLERS_TO_IGNORE.concat(RUBY_IGNORE_CALLERS) if defined?(RUBY_IGNORE_CALLERS)
19
+
20
+ # Like Kernel#caller but excluding certain magic entries and without
21
+ # line / method information; the resulting array contains filenames only.
22
+ def self.caller_files
23
+ caller_locations.map { |file, line| file }
24
+ end
25
+
26
+ # Like caller_files, but containing Arrays rather than strings with the
27
+ # first element being the file, and the second being the line.
28
+ def self.caller_locations
29
+ caller(1).
30
+ map { |line| line.split(/:(?=\d|in )/)[0,2] }.
31
+ reject { |file, line| CALLERS_TO_IGNORE.any? { |pattern| file =~ pattern } }
32
+ end
33
+
34
+ # Find the app_file that was used to execute the application
35
+ #
36
+ # @return [String] The app file
37
+ def self.app_file
38
+ c = caller_files.first
39
+ c = $0 if !c || c.empty?
40
+ c
41
+ end
42
+
43
+ # Execute the application
44
+ #
45
+ # @return [Nil]
46
+ def self.run!
47
+ file = File.basename(app_file, '.rb')
48
+ klass = Kernel.const_get(camel_case(file))
49
+ api = klass.new
50
+
51
+ runner = Goliath::Runner.new(ARGV, api)
52
+ runner.load_app do
53
+ klass.middlewares.each do |mw|
54
+ use(*(mw[0..1].compact), &mw[2])
55
+ end
56
+
57
+ # If you use map you can't use run as
58
+ # the rack builder will blowup.
59
+ if klass.maps.empty?
60
+ run api
61
+ else
62
+ klass.maps.each do |mp|
63
+ map(mp.first, &mp.last)
64
+ end
65
+ end
66
+ end
67
+
68
+ runner.load_plugins(klass.plugins)
69
+ runner.run
70
+ end
71
+
72
+ private
73
+
74
+ # Convert a string to camel case
75
+ #
76
+ # @param str [String] The string to convert
77
+ # @return [String] The camel cased string
78
+ def self.camel_case(str)
79
+ return str if str !~ /_/ && str =~ /[A-Z]+.*/
80
+
81
+ str.split('_').map { |e| e.capitalize }.join
82
+ end
83
+ end
84
+
85
+ at_exit do
86
+ if $!.nil? && $0 == Goliath::Application.app_file
87
+ Application.run!
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,94 @@
1
+ require 'http/parser'
2
+ require 'goliath/env'
3
+
4
+ module Goliath
5
+ # The Goliath::Connection class handles sending and receiving data
6
+ # from the client.
7
+ #
8
+ # @private
9
+ class Connection < EM::Connection
10
+ include Constants
11
+
12
+ attr_accessor :app, :api, :port, :logger, :status, :config, :options
13
+ attr_reader :parser
14
+
15
+ AsyncResponse = [-1, {}, []].freeze
16
+
17
+ def post_init
18
+ @current = nil
19
+ @requests = []
20
+ @pending = []
21
+
22
+ @parser = Http::Parser.new
23
+ @parser.on_headers_complete = proc do |h|
24
+
25
+ env = Goliath::Env.new
26
+ env[OPTIONS] = options
27
+ env[SERVER_PORT] = port.to_s
28
+ env[LOGGER] = logger
29
+ env[OPTIONS] = options
30
+ env[STATUS] = status
31
+ env[CONFIG] = config
32
+ env[REMOTE_ADDR] = remote_address
33
+
34
+ env[ASYNC_HEADERS] = @api.method(:on_headers) if @api.respond_to? :on_headers
35
+ env[ASYNC_BODY] = @api.method(:on_body) if @api.respond_to? :on_body
36
+ env[ASYNC_CLOSE] = @api.method(:on_close) if @api.respond_to? :on_close
37
+
38
+ r = Goliath::Request.new(@app, self, env)
39
+ r.parse_header(h, @parser)
40
+
41
+ @requests.push(r)
42
+ end
43
+
44
+ @parser.on_body = proc do |data|
45
+ @requests.first.parse(data)
46
+ end
47
+
48
+ @parser.on_message_complete = proc do
49
+ req = @requests.shift
50
+
51
+ if @current.nil?
52
+ @current = req
53
+ @current.succeed
54
+ else
55
+ @pending.push(req)
56
+ end
57
+
58
+ req.process
59
+ end
60
+ end
61
+
62
+ def receive_data(data)
63
+ begin
64
+ @parser << data
65
+ rescue HTTP::Parser::Error => e
66
+ terminate_request(false)
67
+ end
68
+ end
69
+
70
+ def unbind
71
+ @requests.map {|r| r.close }
72
+ @pending.map {|r| r.close }
73
+ @current.close if @current
74
+ end
75
+
76
+ def terminate_request(keep_alive)
77
+ if req = @pending.shift
78
+ @current = req
79
+ @current.succeed
80
+ else
81
+ @current.close
82
+ @current = nil
83
+ end
84
+
85
+ close_connection_after_writing rescue nil if !keep_alive
86
+ end
87
+
88
+ def remote_address
89
+ Socket.unpack_sockaddr_in(get_peername)[1]
90
+ rescue Exception
91
+ nil
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,51 @@
1
+ module Goliath
2
+ # Constants used by the system to access data.
3
+ module Constants
4
+ INITIAL_BODY = ''
5
+ # Force external_encoding of request's body to ASCII_8BIT
6
+ INITIAL_BODY.encode!(Encoding::ASCII_8BIT) if INITIAL_BODY.respond_to?(:encode)
7
+
8
+ SERVER_SOFTWARE = 'SERVER_SOFTWARE'
9
+ SERVER = 'Goliath'
10
+
11
+ HTTP_PREFIX = 'HTTP_'
12
+ LOCALHOST = 'localhost'
13
+ LOGGER = 'logger'
14
+ STATUS = 'status'
15
+ CONFIG = 'config'
16
+ OPTIONS = 'options'
17
+
18
+ RACK_INPUT = 'rack.input'
19
+ RACK_VERSION = 'rack.version'
20
+ RACK_ERRORS = 'rack.errors'
21
+ RACK_MULTITHREAD = 'rack.multithread'
22
+ RACK_MULTIPROCESS = 'rack.multiprocess'
23
+ RACK_RUN_ONCE = 'rack.run_once'
24
+ RACK_VERSION_NUM = [1, 0]
25
+
26
+ ASYNC_CALLBACK = 'async.callback'
27
+ ASYNC_HEADERS = 'async.headers'
28
+ ASYNC_BODY = 'async.body'
29
+ ASYNC_CLOSE = 'async.close'
30
+
31
+ STREAM_START = 'stream.start'
32
+ STREAM_SEND = 'stream.send'
33
+ STREAM_CLOSE = 'stream.close'
34
+
35
+ SERVER_NAME = 'SERVER_NAME'
36
+ SERVER_PORT = 'SERVER_PORT'
37
+ SCRIPT_NAME = 'SCRIPT_NAME'
38
+ REMOTE_ADDR = 'REMOTE_ADDR'
39
+ CONTENT_LENGTH = 'CONTENT_LENGTH'
40
+ REQUEST_METHOD = 'REQUEST_METHOD'
41
+ REQUEST_URI = 'REQUEST_URI'
42
+ QUERY_STRING = 'QUERY_STRING'
43
+ HTTP_VERSION = 'HTTP_VERSION'
44
+ REQUEST_PATH = 'REQUEST_PATH'
45
+ PATH_INFO = 'PATH_INFO'
46
+ FRAGMENT = 'FRAGMENT'
47
+ CONNECTION = 'CONNECTION'
48
+
49
+ GOLIATH_ENV = 'goliath.env'
50
+ end
51
+ end
@@ -0,0 +1,92 @@
1
+ require 'goliath/constants'
2
+
3
+ module Goliath
4
+ # Holds information for the current request.
5
+ #
6
+ # Goliath::Env also provides access to the logger, configuration information
7
+ # and anything else set into the config data during initialization.
8
+ class Env < Hash
9
+ include Constants
10
+
11
+ # Create a new Goliath::Env object
12
+ #
13
+ # @return [Goliath::Env] The Goliath::Env object
14
+ def initialize
15
+ self[SERVER_SOFTWARE] = SERVER
16
+ self[SERVER_NAME] = LOCALHOST
17
+ self[RACK_VERSION] = RACK_VERSION_NUM
18
+ self[RACK_ERRORS] = STDERR
19
+ self[RACK_MULTITHREAD] = false
20
+ self[RACK_MULTIPROCESS] = false
21
+ self[RACK_RUN_ONCE] = false
22
+
23
+ self[:start_time] = Time.now.to_f
24
+ self[:time] = Time.now.to_f
25
+ self[:trace] = []
26
+ end
27
+
28
+ # Add a trace timer with the given name into the environment. The tracer will
29
+ # provide information on the amount of time since the previous call to {#trace}
30
+ # or since the Goliath::Env object was initialized.
31
+ #
32
+ # @example
33
+ # trace("initialize hash")
34
+ # ....
35
+ # trace("Do something else")
36
+ #
37
+ # @param name [String] The name of the trace to add
38
+ def trace(name)
39
+ self[:trace].push([name, "%.2f" % ((Time.now.to_f - self[:time]) * 1000)])
40
+ self[:time] = Time.now.to_f
41
+ end
42
+
43
+ # Retrieve the tracer stats for this request environment. This can then be
44
+ # returned in the headers hash to in development to provide some simple
45
+ # timing information for the various API components.
46
+ #
47
+ # @example
48
+ # [200, {}, {:meta => {:trace => env.trace_stats}}, {}]
49
+ #
50
+ # @return [Array] Array of [name, time] pairs with a Total entry added.d
51
+ def trace_stats
52
+ self[:trace] + [['total', self[:trace].collect { |s| s[1].to_f }.inject(:+).to_s]]
53
+ end
54
+
55
+ # If the API is a streaming API this will send the provided data to the client.
56
+ # There will be no processing done on the data when this is called so it's the
57
+ # APIs responsibility to have the data formatted as needed.
58
+ #
59
+ # @param data [String] The data to send to the client.
60
+ def stream_send(data)
61
+ self[STREAM_SEND].call(data)
62
+ end
63
+
64
+ # If the API is a streaming API this will be executed by the API to signal that
65
+ # the stream is complete. This will close the connection with the client.
66
+ def stream_close
67
+ self[STREAM_CLOSE].call
68
+ end
69
+
70
+ # @param name [Symbol] The method to check if we respond to it.
71
+ # @return [Boolean] True if the Env responds to the method, false otherwise
72
+ def respond_to?(name)
73
+ return true if has_key?(name.to_s)
74
+ return true if self['config'] && self['config'].has_key?(name.to_s)
75
+ super
76
+ end
77
+
78
+ # The Goliath::Env will provide any of it's keys as a method. It will also provide
79
+ # any of the keys in the config object as methods. The methods will return
80
+ # the value of the key. If the key doesn't exist in either hash this will
81
+ # fall back to the standard method_missing implementation.
82
+ #
83
+ # @param name [Symbol] The method to look for
84
+ # @param args The arguments
85
+ # @param blk A block
86
+ def method_missing(name, *args, &blk)
87
+ return self[name.to_s] if has_key?(name.to_s)
88
+ return self['config'][name.to_s] if self['config'] && self['config'].has_key?(name.to_s)
89
+ super(name, *args, &blk)
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,49 @@
1
+ require 'eventmachine'
2
+ require 'http/parser'
3
+ require 'async_rack'
4
+
5
+ # The Goliath Framework
6
+ module Goliath
7
+ module_function
8
+
9
+ @env = 'development'
10
+
11
+ # Retrieves the current goliath environment
12
+ #
13
+ # @return [String] the current environment
14
+ def env
15
+ @env
16
+ end
17
+
18
+ # Sets the current goliath environment
19
+ #
20
+ # @param [String] env the environment string of [dev|prod|test]
21
+ def env=(env)
22
+ case(env)
23
+ when 'dev' then @env = 'development'
24
+ when 'prod' then @env = 'production'
25
+ when 'test' then @env = 'test'
26
+ end
27
+ end
28
+
29
+ # Determines if we are in the production environment
30
+ #
31
+ # @return [Boolean] true if current environemnt is production, false otherwise
32
+ def prod?
33
+ @env == 'production'
34
+ end
35
+
36
+ # Determines if we are in the development environment
37
+ #
38
+ # @return [Boolean] true if current environemnt is development, false otherwise
39
+ def dev?
40
+ @env == 'development'
41
+ end
42
+
43
+ # Determines if we are in the test environment
44
+ #
45
+ # @return [Boolean] true if current environemnt is test, false otherwise
46
+ def test?
47
+ @env == 'test'
48
+ end
49
+ end
@@ -0,0 +1,37 @@
1
+ module Goliath
2
+ # @private
3
+ class Headers
4
+ HEADER_FORMAT = "%s: %s\r\n".freeze
5
+ ALLOWED_DUPLICATES = %w(Set-Cookie Set-Cookie2 Warning WWW-Authenticate).freeze
6
+
7
+ def initialize
8
+ @sent = {}
9
+ @out = []
10
+ end
11
+
12
+ def []=(key, value)
13
+ return if @sent.has_key?(key) && !(ALLOWED_DUPLICATES.include?(key))
14
+
15
+ value = case value
16
+ when Time then value.httpdate
17
+ when NilClass then return
18
+ else value.to_s
19
+ end
20
+
21
+ @sent[key] = value
22
+ @out << HEADER_FORMAT % [key, value]
23
+ end
24
+
25
+ def [](key)
26
+ @sent[key]
27
+ end
28
+
29
+ def has_key?(key)
30
+ @sent[key] ? true : false
31
+ end
32
+
33
+ def to_s
34
+ @out.join
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,44 @@
1
+ module Goliath
2
+ # Every standard HTTP code mapped to the appropriate message.
3
+ #
4
+ # @author Mongrel.
5
+ HTTP_STATUS_CODES = {
6
+ 100 => 'Continue',
7
+ 101 => 'Switching Protocols',
8
+ 200 => 'OK',
9
+ 201 => 'Created',
10
+ 202 => 'Accepted',
11
+ 203 => 'Non-Authoritative Information',
12
+ 204 => 'No Content',
13
+ 205 => 'Reset Content',
14
+ 206 => 'Partial Content',
15
+ 300 => 'Multiple Choices',
16
+ 301 => 'Moved Permanently',
17
+ 302 => 'Moved Temporarily',
18
+ 303 => 'See Other',
19
+ 304 => 'Not Modified',
20
+ 305 => 'Use Proxy',
21
+ 400 => 'Bad Request',
22
+ 401 => 'Unauthorized',
23
+ 402 => 'Payment Required',
24
+ 403 => 'Forbidden',
25
+ 404 => 'Not Found',
26
+ 405 => 'Method Not Allowed',
27
+ 406 => 'Not Acceptable',
28
+ 407 => 'Proxy Authentication Required',
29
+ 408 => 'Request Time-out',
30
+ 409 => 'Conflict',
31
+ 410 => 'Gone',
32
+ 411 => 'Length Required',
33
+ 412 => 'Precondition Failed',
34
+ 413 => 'Request Entity Too Large',
35
+ 414 => 'Request-URI Too Large',
36
+ 415 => 'Unsupported Media Type',
37
+ 500 => 'Internal Server Error',
38
+ 501 => 'Not Implemented',
39
+ 502 => 'Bad Gateway',
40
+ 503 => 'Service Unavailable',
41
+ 504 => 'Gateway Time-out',
42
+ 505 => 'HTTP Version not supported'
43
+ }
44
+ end