httpkit 0.6.0.pre.3 → 0.6.0.pre.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +15 -0
  2. data/.rspec +0 -1
  3. data/.travis.yml +5 -4
  4. data/Gemfile +0 -2
  5. data/Gemfile.devtools +27 -22
  6. data/README.md +11 -13
  7. data/config/flay.yml +2 -2
  8. data/config/flog.yml +1 -1
  9. data/config/reek.yml +11 -5
  10. data/config/rubocop.yml +47 -5
  11. data/examples/echo_server.rb +2 -2
  12. data/examples/getting_started.rb +16 -10
  13. data/httpkit.gemspec +5 -4
  14. data/lib/httpkit.rb +15 -5
  15. data/lib/httpkit/body.rb +44 -33
  16. data/lib/httpkit/client.rb +52 -23
  17. data/lib/httpkit/client/body_handler.rb +21 -0
  18. data/lib/httpkit/client/{keep_alive.rb → keep_alive_handler.rb} +1 -1
  19. data/lib/httpkit/client/mandatory_handler.rb +29 -0
  20. data/lib/httpkit/client/{timeouts.rb → timeouts_handler.rb} +1 -1
  21. data/lib/httpkit/connection/eventmachine.rb +4 -4
  22. data/lib/httpkit/request.rb +46 -10
  23. data/lib/httpkit/response.rb +37 -5
  24. data/lib/httpkit/serializer.rb +33 -25
  25. data/lib/httpkit/server.rb +14 -19
  26. data/lib/httpkit/server/body_handler.rb +25 -0
  27. data/lib/httpkit/server/{keep_alive.rb → keep_alive_handler.rb} +22 -13
  28. data/lib/httpkit/server/mandatory_handler.rb +23 -0
  29. data/lib/httpkit/server/{timeouts.rb → timeouts_handler.rb} +1 -1
  30. data/lib/httpkit/support/handler_manager.rb +8 -5
  31. data/lib/httpkit/support/message.rb +28 -15
  32. data/lib/httpkit/version.rb +1 -1
  33. data/spec/integration/keep_alive_spec.rb +6 -7
  34. data/spec/integration/smoke_spec.rb +4 -4
  35. data/spec/integration/streaming_spec.rb +2 -3
  36. data/spec/integration/timeouts_spec.rb +6 -6
  37. data/spec/shared/integration/server_client_pair.rb +1 -1
  38. data/spec/spec_helper.rb +3 -2
  39. data/spec/support/handler.rb +1 -1
  40. data/spec/support/helper.rb +6 -4
  41. data/spec/unit/body_spec.rb +6 -0
  42. data/spec/unit/client/keep_alive_handler_spec.rb +6 -0
  43. data/spec/unit/client/mandatory_handler_spec.rb +31 -0
  44. data/spec/unit/client/timeouts_handler_spec.rb +6 -0
  45. data/spec/unit/client_spec.rb +83 -34
  46. data/spec/unit/connection/eventmachine_spec.rb +12 -13
  47. data/spec/unit/httpkit_spec.rb +65 -24
  48. data/spec/unit/promise_spec.rb +1 -1
  49. data/spec/unit/request_spec.rb +2 -10
  50. data/spec/unit/response_spec.rb +7 -15
  51. data/spec/unit/serializer_spec.rb +83 -0
  52. data/spec/unit/server/{keep_alive_spec.rb → keep_alive_handler_spec.rb} +5 -2
  53. data/spec/unit/server/mandatory_handler_spec.rb +30 -0
  54. data/spec/unit/server/timeouts_handler_spec.rb +6 -0
  55. data/spec/unit/server_spec.rb +26 -32
  56. data/spec/unit/support/handler_manager_spec.rb +38 -7
  57. data/spec/unit/support/message_spec.rb +45 -20
  58. metadata +57 -36
  59. data/lib/httpkit/serializer/encoding.rb +0 -43
@@ -1,11 +1,22 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module HTTPkit
4
+ Error = Class.new(StandardError)
5
+
4
6
  class Client
5
- USER_AGENT = 'User-Agent'.freeze
6
- USER_AGENT_VALUE = "httpkit/#{HTTPkit::VERSION}".freeze
7
- HOST = 'Host'.freeze
8
- HOST_VALUE = '%s:%d'.freeze
7
+ DoubleRequestError = Class.new(Error) do
8
+ attr_reader :client, :request
9
+
10
+ def initialize(client, request)
11
+ @client, @request = client, request
12
+ super(sprintf('%s %s already has sequence number %d',
13
+ request.http_method, request.uri, sequence))
14
+ end
15
+
16
+ def sequence
17
+ request.sequence(client)
18
+ end
19
+ end
9
20
 
10
21
  include Support::HandlerManager::Setup
11
22
  include Connection::Status
@@ -14,9 +25,16 @@ module HTTPkit
14
25
  Connection::EventMachine.start_client(config, self)
15
26
  end
16
27
 
28
+ def self.request(*args)
29
+ uri = URI(args[1])
30
+ client = start(address: uri.host, port: uri.port)
31
+ client.request(*args)
32
+ end
33
+
17
34
  attr_reader :config
18
35
 
19
36
  def initialize(config, connection)
37
+ @sequence = 0
20
38
  @config = config
21
39
  @requests = []
22
40
 
@@ -32,12 +50,8 @@ module HTTPkit
32
50
  def perform(request)
33
51
  served = Promise.new
34
52
 
35
- if closed?
36
- served.reject(@connection.closed.reason)
37
- else
38
- @requests << [request, served]
39
- perform!(request)
40
- end
53
+ request.sequence(self, @sequence += 1)
54
+ perform!(request, served).resume if validate(request, served)
41
55
 
42
56
  served
43
57
  end
@@ -46,39 +60,49 @@ module HTTPkit
46
60
  request, served = find_request
47
61
 
48
62
  if request
63
+ # XXX: fulfillment should happen after invoke(:receive), so that all
64
+ # handlers have run when the application code resumes
49
65
  served.fulfill(response)
50
- @handlers.notify(:receive, request, response)
66
+ request, response = @handlers.invoke(:receive, request, response)
51
67
  response.closed { finish(request) }
52
68
  end
53
69
  end
54
70
 
55
71
  def finish(request)
56
- @requests.delete_if { |(req)| req == request }
57
- @handlers.notify(:finish, request)
72
+ @requests.delete_if do |(other)|
73
+ request.sequence(self) == other.sequence(self)
74
+ end
75
+ @handlers.invoke(:finish, request)
58
76
  end
59
77
 
60
78
  def teardown(reason)
61
79
  @requests.each do |_, served|
62
80
  served.reject(reason)
63
- if (response = served.value)
64
- response.reject_closed(reason)
65
- end
81
+ next unless (response = served.value)
82
+ response.reject_closed(reason)
66
83
  end
67
84
  end
68
85
 
69
86
  private
70
87
 
71
- def add_extra_headers(request)
72
- host = sprintf(HOST_VALUE, config[:address], config[:port])
73
- request.add_extra_headers(USER_AGENT => USER_AGENT_VALUE, HOST => host)
88
+ def validate(request, served)
89
+ if closed?
90
+ served.reject(@connection.closed.reason)
91
+ false
92
+ elsif request.sequence(self) != @sequence
93
+ served.reject(DoubleRequestError.new(self, request))
94
+ false
95
+ else
96
+ true
97
+ end
74
98
  end
75
99
 
76
- def perform!(request)
100
+ def perform!(request, served)
77
101
  Fiber.new do
78
- add_extra_headers(request)
79
- @handlers.notify(:perform, request)
102
+ request, _ = @handlers.invoke(:perform, request)
103
+ @requests << [request, served]
80
104
  @connection.serialize(request)
81
- end.resume
105
+ end
82
106
  end
83
107
 
84
108
  def find_request
@@ -90,5 +114,10 @@ module HTTPkit
90
114
  @connection.on_message = method(:receive)
91
115
  @connection.closed.then(method(:teardown), method(:teardown))
92
116
  end
117
+
118
+ def setup_handlers
119
+ (@config[:handlers] ||= []).push(MandatoryHandler.new, BodyHandler.new)
120
+ super
121
+ end
93
122
  end
94
123
  end
@@ -0,0 +1,21 @@
1
+ # encoding: utf-8
2
+
3
+ module HTTPkit
4
+ class Client::BodyHandler
5
+ def perform(request)
6
+ request = request.with_headers(request.body_headers)
7
+
8
+ yield banana_request(request)
9
+ end
10
+
11
+ private
12
+
13
+ def banana_request(request)
14
+ if !request.body_included?
15
+ request.with_body('')
16
+ else
17
+ request
18
+ end
19
+ end
20
+ end
21
+ end
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module HTTPkit
4
- class Client::KeepAlive
4
+ class Client::KeepAliveHandler
5
5
  def perform(request)
6
6
  @previous, previous = request, @previous
7
7
  previous.closed! if previous
@@ -0,0 +1,29 @@
1
+ # encoding: utf-8
2
+
3
+ module HTTPkit
4
+ class Client::MandatoryHandler
5
+ USER_AGENT = 'User-Agent'.freeze
6
+ USER_AGENT_VALUE = "httpkit/#{HTTPkit::VERSION}".freeze
7
+ HOST = 'Host'.freeze
8
+ HOST_VALUE = '%s:%d'.freeze
9
+
10
+ def setup(config, _, _)
11
+ @config = config
12
+ end
13
+
14
+ def perform(request)
15
+ yield request.with_headers(missing_headers(request))
16
+ end
17
+
18
+ private
19
+
20
+ def missing_headers(request)
21
+ headers.reject { |k, _| request.headers.key?(k) }
22
+ end
23
+
24
+ def headers
25
+ host = sprintf(HOST_VALUE, @config[:address], @config[:port])
26
+ { USER_AGENT => USER_AGENT_VALUE, HOST => host }
27
+ end
28
+ end
29
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module HTTPkit
4
4
  # @see EM.heartbeat_interval
5
- class Client::Timeouts
5
+ class Client::TimeoutsHandler
6
6
  def setup(config, _, connection)
7
7
  @config = config
8
8
  @connection = connection
@@ -24,7 +24,6 @@ module HTTPkit
24
24
 
25
25
  def serialize(message)
26
26
  serializer = Serializer.new(message, method(:send_data))
27
- serializer.setup_body
28
27
  serializer.serialize
29
28
  end
30
29
 
@@ -33,12 +32,13 @@ module HTTPkit
33
32
  # p data
34
33
  @parser << data
35
34
  rescue => ex
35
+ # puts [ex.message, *ex.backtrace].join("\n\t")
36
36
  close(ex)
37
37
  end
38
38
 
39
39
  # def send_data(data)
40
- # # p [__id__, :send, data]
41
- # p data
40
+ # p [__id__, :send, data]
41
+ # # p data
42
42
  # super
43
43
  # end
44
44
 
@@ -61,7 +61,7 @@ module HTTPkit
61
61
  end
62
62
 
63
63
  def on_body(chunk)
64
- @message.body.closed.progress(chunk)
64
+ @message.body.write(chunk)
65
65
  end
66
66
 
67
67
  def on_message_complete
@@ -1,19 +1,55 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module HTTPkit
4
- Request = Struct.new(:http_method, :uri, :headers, :body, :http_version)
5
-
6
4
  class Request
7
- private :http_method=, :uri=, :headers=, :body=
8
-
9
5
  include Support::Message
6
+ include Adamantium
7
+
8
+ attr_reader :http_method, :uri, :headers, :body, :http_version
9
+
10
+ def initialize(http_method, uri, headers = {}, body = '',
11
+ http_version = 1.1)
12
+ @http_method = http_method
13
+ # @uri = URI('http://' + uri)
14
+ @uri = URI.join('http:///', uri).request_uri
15
+ @headers = headers
16
+ @body = Body.build(body)
17
+ @http_version = http_version
18
+ @sequences = SequenceHash.new
19
+ end
20
+
21
+ def with_headers(new_headers)
22
+ with(2, headers.merge(new_headers))
23
+ end
24
+
25
+ def with_body(new_body)
26
+ with(3, Body.build(new_body))
27
+ end
10
28
 
11
- # TODO: URI.join is really slow
12
- def initialize(http_method, uri, headers = {}, body = '')
13
- super(http_method,
14
- # URI('http://' + uri)
15
- URI.join('http:///', uri).request_uri,
16
- headers, Body.build(body), 1.1)
29
+ def with(index, argument)
30
+ args = [http_method, uri, headers, body, http_version]
31
+ args[index] = argument
32
+ new_self(args)
17
33
  end
34
+
35
+ def new_self(args)
36
+ other = self.class.new(*args)
37
+ @sequences.each { |(obj, id)| other.sequence(obj, id) }
38
+ other
39
+ end
40
+
41
+ def sequence(obj, id = nil)
42
+ @sequences[obj.object_id] ||= id
43
+ end
44
+
45
+ def body_included?
46
+ body_present?
47
+ end
48
+
49
+ def body_present?
50
+ !body.length_known? || !body.length.zero?
51
+ end
52
+
53
+ SequenceHash = Class.new(Hash) { include Adamantium::Mutable }
18
54
  end
19
55
  end
@@ -1,15 +1,46 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  module HTTPkit
4
- Response = Struct.new(:status, :headers, :body, :http_version)
5
-
6
4
  class Response
7
- private :status=, :headers=, :body=
5
+ NOT_INCLUDED = [204, 304]
6
+ NOT_PRESENT = [205]
8
7
 
9
8
  include Support::Message
9
+ include Adamantium
10
+
11
+ attr_reader :status, :headers, :body, :http_version
12
+
13
+ def initialize(status, headers = {}, body = '', http_version = 1.1)
14
+ @status = status
15
+ @headers = headers
16
+ @body = Body.build(body)
17
+ @http_version = http_version
18
+ end
19
+
20
+ def with_headers(new_headers)
21
+ with(1, headers.merge(new_headers))
22
+ end
23
+
24
+ def with_body(new_body)
25
+ with(2, Body.build(new_body))
26
+ end
27
+
28
+ def with(index, argument)
29
+ args = [status, headers, body, http_version]
30
+ args[index] = argument
31
+ new_self(args)
32
+ end
33
+
34
+ def new_self(args)
35
+ self.class.new(*args)
36
+ end
37
+
38
+ def body_included?
39
+ !informational? && !NOT_INCLUDED.include?(status)
40
+ end
10
41
 
11
- def initialize(status, headers = {}, body = '')
12
- super(status, headers, Body.build(body), 1.1)
42
+ def body_present?
43
+ !informational? && !NOT_PRESENT.include?(status)
13
44
  end
14
45
 
15
46
  def status_name
@@ -42,6 +73,7 @@ module HTTPkit
42
73
 
43
74
  UNKNOWN_STATUS = 'Unknown Status'.freeze
44
75
 
76
+ # TODO: double-check with RFC7230
45
77
  STATUS_NAMES =
46
78
  { 100 => 'Continue'.freeze,
47
79
  101 => 'Switching Protocols'.freeze,
@@ -1,48 +1,36 @@
1
1
  # encoding: utf-8
2
2
 
3
- require 'forwardable'
4
-
5
3
  module HTTPkit
4
+ # Streaming HTTP message serializer.
5
+ #
6
+ # @api public
7
+ #
6
8
  class Serializer
7
9
  REQUEST_LINE = "%s %s HTTP/%.1f\r\n".freeze
8
10
  RESPONSE_LINE = "HTTP/%.1f %d %s\r\n".freeze
9
11
  HEADER = "%s: %s\r\n".freeze
12
+ TRANSFER_ENCODING = 'Transfer-Encoding'.freeze
13
+ CHUNKED = 'chunked'.freeze
14
+ CONTENT_LENGTH = 'Content-Length'.freeze
10
15
  CRLF = "\r\n".freeze
11
16
 
12
- include Encoding
13
-
14
- extend Forwardable
15
- def_delegator :@message, :headers
16
- def_delegator :@message, :body
17
-
18
17
  def initialize(message, writer)
19
- @message = message
20
- @writer = writer
18
+ @message, @writer = message, writer
21
19
  end
22
20
 
23
21
  def serialize
24
- write(banana_line)
22
+ write(first_line)
25
23
  write(header_block)
26
-
27
24
  write_body
28
25
  end
29
26
 
30
- def setup_body
31
- if body.closed.pending?
32
- setup_chunked_encoding
33
- setup_chunked_streaming
34
- else
35
- setup_identity_encoding
36
- end
37
- end
38
-
39
27
  private
40
28
 
41
29
  def write(*data)
42
- @writer.call(data.join(''))
30
+ @writer.call(data.join)
43
31
  end
44
32
 
45
- def banana_line
33
+ def first_line
46
34
  if Request === @message
47
35
  request_line
48
36
  else
@@ -61,15 +49,35 @@ module HTTPkit
61
49
  end
62
50
 
63
51
  def header_block
64
- headers.reduce('') { |a, e| a << header_line(*e) } + CRLF
52
+ @message.headers
53
+ .reduce('') { |a, e| a << header_line(*e) } + CRLF
65
54
  end
66
55
 
67
56
  def header_line(key, value)
68
57
  if value.to_s.empty?
69
58
  ''
70
59
  else
71
- sprintf(HEADER, key, value)
60
+ sprintf(HEADER, key,
61
+ value.respond_to?(:join) ? value.join(', ') : value)
72
62
  end
73
63
  end
64
+
65
+ def write_body
66
+ body = @message.body
67
+ if !body.length_known?
68
+ body.each { |chunk| write_chunk(chunk) }
69
+ write_chunk!('')
70
+ else
71
+ write(body)
72
+ end
73
+ end
74
+
75
+ def write_chunk(chunk)
76
+ write_chunk!(chunk) unless chunk.empty?
77
+ end
78
+
79
+ def write_chunk!(chunk)
80
+ write(chunk.bytesize.to_s(16), CRLF, chunk, CRLF)
81
+ end
74
82
  end
75
83
  end