goliath 0.9.1 → 0.9.2

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 (94) hide show
  1. data/.gitignore +1 -0
  2. data/HISTORY +50 -0
  3. data/README.md +2 -0
  4. data/examples/activerecord/srv.rb +1 -3
  5. data/examples/async_aroundware_demo.rb +81 -0
  6. data/examples/async_upload.rb +1 -2
  7. data/examples/auth_and_rate_limit.rb +143 -0
  8. data/examples/chunked_streaming.rb +37 -0
  9. data/examples/conf_test.rb +1 -3
  10. data/examples/config/auth_and_rate_limit.rb +30 -0
  11. data/examples/config/template.rb +8 -0
  12. data/examples/content_stream.rb +1 -3
  13. data/examples/echo.rb +8 -6
  14. data/examples/env_use_statements.rb +17 -0
  15. data/examples/gziped.rb +1 -3
  16. data/examples/public/stylesheets/style.css +296 -0
  17. data/examples/rack_routes.rb +65 -3
  18. data/examples/rasterize/rasterize.js +15 -0
  19. data/examples/rasterize/rasterize.rb +36 -0
  20. data/examples/rasterize/rasterize_and_shorten.rb +37 -0
  21. data/examples/stream.rb +2 -2
  22. data/examples/template.rb +48 -0
  23. data/examples/test_rig.rb +125 -0
  24. data/examples/valid.rb +4 -2
  25. data/examples/views/debug.haml +4 -0
  26. data/examples/views/joke.markdown +13 -0
  27. data/examples/views/layout.erb +12 -0
  28. data/examples/views/layout.haml +39 -0
  29. data/examples/views/root.haml +28 -0
  30. data/goliath.gemspec +10 -3
  31. data/lib/goliath.rb +0 -36
  32. data/lib/goliath/api.rb +137 -26
  33. data/lib/goliath/application.rb +71 -21
  34. data/lib/goliath/connection.rb +4 -2
  35. data/lib/goliath/constants.rb +1 -0
  36. data/lib/goliath/env.rb +40 -1
  37. data/lib/goliath/goliath.rb +30 -15
  38. data/lib/goliath/headers.rb +2 -2
  39. data/lib/goliath/plugins/latency.rb +8 -2
  40. data/lib/goliath/rack.rb +18 -0
  41. data/lib/goliath/rack/async_aroundware.rb +56 -0
  42. data/lib/goliath/rack/async_middleware.rb +93 -0
  43. data/lib/goliath/rack/builder.rb +42 -0
  44. data/lib/goliath/rack/default_response_format.rb +3 -15
  45. data/lib/goliath/rack/formatters.rb +11 -0
  46. data/lib/goliath/rack/formatters/html.rb +2 -18
  47. data/lib/goliath/rack/formatters/json.rb +2 -17
  48. data/lib/goliath/rack/formatters/plist.rb +32 -0
  49. data/lib/goliath/rack/formatters/xml.rb +23 -31
  50. data/lib/goliath/rack/formatters/yaml.rb +27 -0
  51. data/lib/goliath/rack/jsonp.rb +1 -13
  52. data/lib/goliath/rack/params.rb +55 -27
  53. data/lib/goliath/rack/render.rb +13 -22
  54. data/lib/goliath/rack/templates.rb +357 -0
  55. data/lib/goliath/rack/tracer.rb +11 -12
  56. data/lib/goliath/rack/validation.rb +12 -0
  57. data/lib/goliath/rack/validation/default_params.rb +0 -2
  58. data/lib/goliath/rack/validation/numeric_range.rb +11 -2
  59. data/lib/goliath/rack/validation/request_method.rb +3 -2
  60. data/lib/goliath/rack/validation/required_param.rb +13 -11
  61. data/lib/goliath/rack/validation/required_value.rb +11 -15
  62. data/lib/goliath/rack/validator.rb +51 -0
  63. data/lib/goliath/request.rb +34 -20
  64. data/lib/goliath/response.rb +3 -2
  65. data/lib/goliath/runner.rb +5 -11
  66. data/lib/goliath/server.rb +2 -1
  67. data/lib/goliath/synchrony/mongo_receiver.rb +64 -0
  68. data/lib/goliath/synchrony/response_receiver.rb +64 -0
  69. data/lib/goliath/test_helper.rb +39 -21
  70. data/lib/goliath/validation.rb +2 -0
  71. data/lib/goliath/{rack/validation_error.rb → validation/error.rb} +0 -16
  72. data/lib/goliath/validation/standard_http_errors.rb +31 -0
  73. data/lib/goliath/version.rb +1 -1
  74. data/spec/integration/http_log_spec.rb +16 -16
  75. data/spec/integration/rack_routes_spec.rb +144 -0
  76. data/spec/integration/reloader_spec.rb +4 -4
  77. data/spec/integration/template_spec.rb +54 -0
  78. data/spec/integration/trace_spec.rb +23 -0
  79. data/spec/integration/valid_spec.rb +21 -0
  80. data/spec/spec_helper.rb +3 -1
  81. data/spec/unit/api_spec.rb +30 -0
  82. data/spec/unit/rack/builder_spec.rb +40 -0
  83. data/spec/unit/rack/formatters/plist_spec.rb +51 -0
  84. data/spec/unit/rack/formatters/yaml_spec.rb +53 -0
  85. data/spec/unit/rack/params_spec.rb +22 -0
  86. data/spec/unit/rack/render_spec.rb +10 -5
  87. data/spec/unit/rack/validation/numeric_range_spec.rb +8 -1
  88. data/spec/unit/rack/validation/request_method_spec.rb +8 -8
  89. data/spec/unit/rack/validation/required_param_spec.rb +19 -15
  90. data/spec/unit/rack/validation/required_value_spec.rb +10 -13
  91. data/spec/unit/server_spec.rb +4 -4
  92. data/spec/unit/validation/standard_http_errors_spec.rb +21 -0
  93. metadata +177 -35
  94. data/spec/unit/rack/validation_error_spec.rb +0 -40
@@ -1,4 +1,4 @@
1
- require 'goliath/rack/validation_error'
1
+ require 'goliath/rack/validator'
2
2
 
3
3
  module Goliath
4
4
  module Rack
@@ -9,6 +9,7 @@ module Goliath
9
9
  # use Goliath::Rack::Validation::RequestMethod, %w(GET POST)
10
10
  #
11
11
  class RequestMethod
12
+ include Goliath::Rack::Validator
12
13
  attr_reader :methods
13
14
 
14
15
  ERROR = 'Invalid request method'
@@ -24,7 +25,7 @@ module Goliath
24
25
  end
25
26
 
26
27
  def call(env)
27
- raise Goliath::Validation::Error.new(400, ERROR) unless methods.include?(env['REQUEST_METHOD'])
28
+ return validation_error(405, ERROR, "Allow" => methods.map{|m| m.to_s.upcase}.join(', ')) unless methods.include?(env['REQUEST_METHOD'])
28
29
  @app.call(env)
29
30
  end
30
31
  end
@@ -1,4 +1,4 @@
1
- require 'goliath/rack/validation_error'
1
+ require 'goliath/rack/validator'
2
2
 
3
3
  module Goliath
4
4
  module Rack
@@ -9,7 +9,8 @@ module Goliath
9
9
  # use Goliath::Rack::Validation::RequiredParam, {:key => 'mode', :type => 'Mode'}
10
10
  #
11
11
  class RequiredParam
12
- attr_reader :type, :key
12
+ include Goliath::Rack::Validator
13
+ attr_reader :type, :key, :message
13
14
 
14
15
  # Creates the Goliath::Rack::Validation::RequiredParam validator
15
16
  #
@@ -17,38 +18,39 @@ module Goliath
17
18
  # @param opts [Hash] The validator options
18
19
  # @option opts [String] :key The key to look for in params (default: id)
19
20
  # @option opts [String] :type The type string to put in the error message. (default: :key)
21
+ # @option opts [String] :message The message string to display after the type string. (default: 'identifier missing')
20
22
  # @return [Goliath::Rack::Validation::RequiredParam] The validator
21
23
  def initialize(app, opts = {})
22
24
  @app = app
23
25
  @key = opts[:key] || 'id'
24
26
  @type = opts[:type] || @key.capitalize
27
+ @message = opts[:message] || 'identifier missing'
25
28
  end
26
29
 
27
30
  def call(env)
28
- key_valid!(env['params'])
31
+ return validation_error(400, "#{@type} #{@message}") unless key_valid?(env['params'])
29
32
  @app.call(env)
30
33
  end
31
34
 
32
- def key_valid!(params)
33
- error = false
35
+ def key_valid?(params)
34
36
  if !params.has_key?(key) || params[key].nil? ||
35
37
  (params[key].is_a?(String) && params[key] =~ /^\s*$/)
36
- error = true
38
+ return false
37
39
  end
38
40
 
39
41
  if params[key].is_a?(Array)
40
42
  unless params[key].compact.empty?
41
43
  params[key].each do |k|
42
- return unless k.is_a?(String)
43
- return unless k =~ /^\s*$/
44
+ return true unless k.is_a?(String)
45
+ return true unless k =~ /^\s*$/
44
46
  end
45
47
  end
46
- error = true
48
+ return false
47
49
  end
48
50
 
49
- raise Goliath::Validation::Error.new(400, "#{@type} identifier missing") if error
51
+ true
50
52
  end
51
53
  end
52
54
  end
53
55
  end
54
- end
56
+ end
@@ -1,4 +1,4 @@
1
- require 'goliath/rack/validation_error'
1
+ require 'goliath/rack/validator'
2
2
 
3
3
  module Goliath
4
4
  module Rack
@@ -10,6 +10,8 @@ module Goliath
10
10
  # use Goliath::Rack::Validation::RequiredValue, {:key => 'baz', :values => 'awesome'}
11
11
  #
12
12
  class RequiredValue
13
+ include Goliath::Rack::Validator
14
+
13
15
  attr_reader :key, :values
14
16
 
15
17
  # Creates the Goliath::Rack::Validation::RequiredValue validator.
@@ -26,31 +28,25 @@ module Goliath
26
28
  end
27
29
 
28
30
  def call(env)
29
- value_valid!(env['params'])
31
+ return validation_error(400, "Provided #{@key} is invalid") unless value_valid?(env['params'])
30
32
  @app.call(env)
31
33
  end
32
34
 
33
- def value_valid!(params)
34
- error = false
35
+ def value_valid?(params)
35
36
  if !params.has_key?(key) || params[key].nil? ||
36
37
  (params[key].is_a?(String) && params[key] =~ /^\s*$/)
37
- error = true
38
+ return false
38
39
  end
39
40
 
40
41
  if params[key].is_a?(Array)
41
- error = true if params[key].empty?
42
-
43
- params[key].each do |k|
44
- unless values.include?(k)
45
- error = true
46
- break
47
- end
48
- end
42
+ return false if params[key].empty?
43
+ params[key].each { |k| return false unless values.include?(k) }
44
+
49
45
  elsif !values.include?(params[key])
50
- error = true
46
+ return false
51
47
  end
52
48
 
53
- raise Goliath::Validation::Error.new(400, "Provided #{@key} is invalid") if error
49
+ true
54
50
  end
55
51
  end
56
52
  end
@@ -0,0 +1,51 @@
1
+ module Goliath
2
+ module Rack
3
+ module Validator
4
+ module_function
5
+
6
+ # @param status_code [Integer] HTTP status code for this error.
7
+ # @param msg [String] message to inject into the response body.
8
+ # @param headers [Hash] Response headers to preserve in an error response;
9
+ # (the Content-Length header, if any, is removed)
10
+ def validation_error(status_code, msg, headers={})
11
+ headers.delete('Content-Length')
12
+ [status_code, headers, {:error => msg}]
13
+ end
14
+
15
+ # Execute a block of code safely.
16
+ #
17
+ # If the block raises any exception that derives from
18
+ # Goliath::Validation::Error (see specifically those in
19
+ # goliath/validation/standard_http_errors.rb), it will be turned into the
20
+ # corresponding 4xx response with a corresponding message.
21
+ #
22
+ # If the block raises any other kind of error, we log it and return a
23
+ # less-communicative 500 response.
24
+ #
25
+ # @example
26
+ # # will convert the ForbiddenError exception into a 403 response
27
+ # # and an uncaught error in do_something_risky! into a 500 response
28
+ # safely(env, headers) do
29
+ # raise ForbiddenError unless account_info['valid'] == true
30
+ # do_something_risky!
31
+ # [status, headers, body]
32
+ # end
33
+ #
34
+ #
35
+ # @param env [Goliath::Env] The current request env
36
+ # @param headers [Hash] Response headers to preserve in an error response
37
+ #
38
+ def safely(env, headers={})
39
+ begin
40
+ yield
41
+ rescue Goliath::Validation::Error => e
42
+ validation_error(e.status_code, e.message, headers)
43
+ rescue Exception => e
44
+ env.logger.error(e.message)
45
+ env.logger.error(e.backtrace.join("\n"))
46
+ validation_error(500, e.message, headers)
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,7 @@
1
1
  require 'eventmachine'
2
2
  require 'goliath/constants'
3
+ require 'goliath/response'
4
+ require 'goliath/validation'
3
5
  require 'async_rack'
4
6
  require 'stringio'
5
7
 
@@ -26,8 +28,11 @@ module Goliath
26
28
 
27
29
  @env[STREAM_SEND] = proc { |data| callback { @conn.send_data(data) } }
28
30
  @env[STREAM_CLOSE] = proc { callback { @conn.terminate_request(false) } }
29
- @env[STREAM_START] = proc do
31
+ @env[STREAM_START] = proc do |status, headers|
30
32
  callback do
33
+ @response.status = status
34
+ @response.headers = headers
35
+
31
36
  @conn.send_data(@response.head)
32
37
  @conn.send_data(@response.headers_output)
33
38
  end
@@ -125,14 +130,15 @@ module Goliath
125
130
  #
126
131
  # @return [Nil]
127
132
  def process
128
- begin
129
- @state = :finished
130
- @env['rack.input'].rewind if @env['rack.input']
131
- post_process(@app.call(@env))
132
-
133
- rescue Exception => e
134
- server_exception(e)
135
- end
133
+ Fiber.new {
134
+ begin
135
+ @state = :finished
136
+ @env['rack.input'].rewind if @env['rack.input']
137
+ post_process(@app.call(@env))
138
+ rescue Exception => e
139
+ server_exception(e)
140
+ end
141
+ }.resume
136
142
  end
137
143
 
138
144
  # Invoked by the app / middleware once the request
@@ -164,10 +170,10 @@ module Goliath
164
170
  @response.status, @response.headers, @response.body = status, headers, body
165
171
  @response.each { |chunk| @conn.send_data(chunk) }
166
172
  @env[RACK_LOGGER].info("Status: #{@response.status}, " +
167
- "Content-Length: #{@response.headers['Content-Length']}, " +
168
- "Response Time: #{"%.2f" % ((Time.now.to_f - @env[:start_time]) * 1000)}ms")
173
+ "Content-Length: #{@response.headers['Content-Length']}, " +
174
+ "Response Time: #{"%.2f" % ((Time.now.to_f - @env[:start_time]) * 1000)}ms")
169
175
 
170
- @conn.terminate_request(keep_alive)
176
+ @conn.terminate_request(keep_alive)
171
177
  rescue Exception => e
172
178
  server_exception(e)
173
179
  end
@@ -185,24 +191,32 @@ module Goliath
185
191
  # @param e [Exception] The exception to log
186
192
  # @return [Nil]
187
193
  def server_exception(e)
188
- @env[RACK_LOGGER].error("#{e.message}\n#{e.backtrace.join("\n")}")
189
- post_process([500, {}, 'An error happened'])
194
+ if e.is_a?(Goliath::Validation::Error)
195
+ status, headers, body = [e.status_code, {}, ('{"error":"%s"}'%e.message)] #
196
+ else
197
+ @env[RACK_LOGGER].error("#{e.message}\n#{e.backtrace.join("\n")}")
198
+ status, headers, body = [500, {}, 'An error happened']
199
+ end
200
+ headers['Content-Length'] = body.bytesize.to_s
201
+ @env[:terminate_connection] = true
202
+ post_process([status, headers, body])
190
203
  end
191
204
 
192
205
  # Used to determine if the connection should be kept open
193
206
  #
194
207
  # @return [Boolean] True to keep the connection open, false otherwise
195
208
  def keep_alive
209
+ return false if @env[:terminate_connection]
196
210
  case @env[HTTP_VERSION]
197
211
  # HTTP 1.1: all requests are persistent requests, client
198
212
  # must send a Connection:close header to indicate otherwise
199
- when '1.1' then
200
- (@env[HTTP_PREFIX + CONNECTION].downcase != 'close') rescue true
213
+ when '1.1' then
214
+ (@env[HTTP_PREFIX + CONNECTION].downcase != 'close') rescue true
201
215
 
202
- # HTTP 1.0: all requests are non keep-alive, client must
203
- # send a Connection: Keep-Alive to indicate otherwise
204
- when '1.0' then
205
- (@env[HTTP_PREFIX + CONNECTION].downcase == 'keep-alive') rescue false
216
+ # HTTP 1.0: all requests are non keep-alive, client must
217
+ # send a Connection: Keep-Alive to indicate otherwise
218
+ when '1.0' then
219
+ (@env[HTTP_PREFIX + CONNECTION].downcase == 'keep-alive') rescue false
206
220
  end
207
221
  end
208
222
  end
@@ -18,11 +18,12 @@ module Goliath
18
18
  # The body to send
19
19
  attr_accessor :body
20
20
 
21
- SERVER = 'Server'.freeze
22
- DATE = 'Date'.freeze
21
+ SERVER = 'Server'
22
+ DATE = 'Date'
23
23
 
24
24
  # Used to signal that a response is a streaming response
25
25
  STREAMING = :goliath_stream_response
26
+ CHUNKED_STREAM_HEADERS = { 'Transfer-Encoding' => 'chunked' }
26
27
 
27
28
  def initialize
28
29
  @headers = Goliath::Headers.new
@@ -4,7 +4,7 @@ require 'optparse'
4
4
  require 'log4r'
5
5
 
6
6
  module Goliath
7
- # The Goliath::Runner is responsible for parsing any provided options, settting up the
7
+ # The Goliath::Runner is responsible for parsing any provided options, setting up the
8
8
  # rack application, creating a logger, and then executing the Goliath::Server with the loaded information.
9
9
  class Runner
10
10
  # The address of the server @example 127.0.0.1
@@ -63,6 +63,7 @@ module Goliath
63
63
  def initialize(argv, api)
64
64
  api.options_parser(options_parser, options) if api
65
65
  options_parser.parse!(argv)
66
+ Goliath.env = options.delete(:env)
66
67
 
67
68
  @api = api
68
69
  @address = options.delete(:address)
@@ -88,7 +89,8 @@ module Goliath
88
89
 
89
90
  :daemonize => false,
90
91
  :verbose => false,
91
- :log_stdout => false
92
+ :log_stdout => false,
93
+ :env => :development,
92
94
  }
93
95
 
94
96
  @options_parser ||= OptionParser.new do |opts|
@@ -97,7 +99,7 @@ module Goliath
97
99
  opts.separator ""
98
100
  opts.separator "Server options:"
99
101
 
100
- opts.on('-e', '--environment NAME', "Set the execution environment (prod, dev or test) (default: #{Goliath.env})") { |val| Goliath.env = val }
102
+ opts.on('-e', '--environment NAME', "Set the execution environment (prod, dev or test) (default: #{@options[:env]})") { |val| @options[:env] = val }
101
103
 
102
104
  opts.on('-a', '--address HOST', "Bind to HOST address (default: #{@options[:address]})") { |addr| @options[:address] = addr }
103
105
  opts.on('-p', '--port PORT', "Use PORT (default: #{@options[:port]})") { |port| @options[:port] = port.to_i }
@@ -115,14 +117,6 @@ module Goliath
115
117
  end
116
118
  end
117
119
 
118
- # Given a block, this will use Rack::Builder to create the application
119
- #
120
- # @param blk [Block] The application block to load
121
- # @return [Object] The Rack application
122
- def load_app(&blk)
123
- @app = ::Rack::Builder.app(&blk)
124
- end
125
-
126
120
  # Stores the list of plugins to be used by the server
127
121
  #
128
122
  # @param plugins [Array] The list of plugins to use
@@ -68,7 +68,7 @@ module Goliath
68
68
  # start listening for requests
69
69
  #
70
70
  # @return Does not return until the server has halted.
71
- def start
71
+ def start(&blk)
72
72
  EM.synchrony do
73
73
  trap("INT") { EM.stop }
74
74
  trap("TERM") { EM.stop }
@@ -90,6 +90,7 @@ module Goliath
90
90
  conn.options = options
91
91
  end
92
92
 
93
+ blk.call(self) if blk
93
94
  end
94
95
  end
95
96
 
@@ -0,0 +1,64 @@
1
+ require 'goliath/synchrony/response_receiver'
2
+
3
+ module Goliath
4
+ module Synchrony
5
+ #
6
+ # Currently, you must provide in the env a method 'mongo' that returns a mongo
7
+ # collection or collection proxy (probably by setting it up in the config).
8
+ #
9
+ # This will almost certainly change to something less crappy.
10
+ #
11
+ class MongoReceiver
12
+ include Goliath::Synchrony::ResponseReceiver
13
+ include EM::Deferrable
14
+ include Goliath::Rack::Validator
15
+
16
+ def initialize(env, db_name)
17
+ @env = env
18
+ @pending_queries = 0
19
+ @db = env.config[db_name]
20
+ end
21
+
22
+ def db
23
+ @db
24
+ end
25
+
26
+ def finished?
27
+ response_received? && (@pending_queries == 0)
28
+ end
29
+
30
+ def enqueue(handle, req_id)
31
+ # ... requests aren't deferrables so they're tracked in @pending_queries
32
+ end
33
+
34
+ def find(collection, selector={}, opts={}, &block)
35
+ @pending_queries += 1
36
+ db.collection(collection).find(selector, opts) do |result|
37
+ yield result
38
+ @pending_queries -= 1
39
+ self.succeed if finished?
40
+ end
41
+ end
42
+
43
+ def first(collection, selector={}, opts={}, &block)
44
+ opts[:limit] = 1
45
+ find(collection, selector, opts) do |result|
46
+ yield result.first
47
+ end
48
+ end
49
+
50
+ def insert(collection, *args)
51
+ db.collection(collection).insert(*args)
52
+ end
53
+ def update(collection, *args)
54
+ db.collection(collection).update(*args)
55
+ end
56
+ def save(collection, *args)
57
+ db.collection(collection).save(*args)
58
+ end
59
+ def remove(collection, *args)
60
+ db.collection(collection).remove(*args)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,64 @@
1
+ module Goliath
2
+ module Synchrony
3
+
4
+ #
5
+ # FIXME: generalize this to work with any deferrable
6
+ module ResponseReceiver
7
+ attr_accessor :env, :status, :headers, :body
8
+
9
+ # Override this method in your middleware to perform any preprocessing
10
+ # (launching a deferred request, perhaps)
11
+ def pre_process
12
+ end
13
+
14
+ # Override this method in your middleware to perform any postprocessing. This
15
+ # will only be invoked when the deferrable and the response have been
16
+ # received.
17
+ #
18
+ # @return [Array] array contains [status, headers, body]
19
+ def post_process
20
+ [status, headers, body]
21
+ end
22
+
23
+ # Invoked by the async_callback chain. Stores the [status, headers, body]
24
+ # for post_process'ing
25
+ def call shb
26
+ return shb if shb.first == Goliath::Connection::AsyncResponse.first
27
+ @status, @headers, @body = shb
28
+ succeed if finished?
29
+ end
30
+
31
+ # Have we received a response?
32
+ def response_received?
33
+ !! @status
34
+ end
35
+ end
36
+
37
+ class MultiReceiver < EM::Synchrony::Multi
38
+ include ResponseReceiver
39
+
40
+ # Create a new MultiReceiver
41
+ # @param env [Goliath::Env] the current environment
42
+ def initialize env
43
+ @env = env
44
+ super()
45
+ end
46
+
47
+ alias_method :enqueue, :add
48
+
49
+ def successes
50
+ responses[:callback]
51
+ end
52
+
53
+ def failures
54
+ responses[:errback]
55
+ end
56
+
57
+ # Finished if we received a response and the multi request is finished
58
+ def finished?
59
+ super && response_received?
60
+ end
61
+ end
62
+
63
+ end
64
+ end