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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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