hearth 1.0.0.pre1 → 1.0.0.pre3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (176) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/anonymous_auth_resolver.rb +11 -0
  5. data/lib/hearth/api_error.rb +15 -1
  6. data/lib/hearth/auth_option.rb +21 -0
  7. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  8. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  11. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  12. data/lib/hearth/auth_schemes.rb +32 -0
  13. data/lib/hearth/checksums.rb +31 -0
  14. data/lib/hearth/client.rb +66 -0
  15. data/lib/hearth/client_stubs.rb +128 -0
  16. data/lib/hearth/config/env_provider.rb +53 -0
  17. data/lib/hearth/config/resolver.rb +53 -0
  18. data/lib/hearth/configuration.rb +15 -0
  19. data/lib/hearth/connection_pool.rb +77 -0
  20. data/lib/hearth/context.rb +29 -4
  21. data/lib/hearth/dns/host_address.rb +27 -0
  22. data/lib/hearth/dns/host_resolver.rb +92 -0
  23. data/lib/hearth/dns.rb +48 -0
  24. data/lib/hearth/endpoint_rules.rb +154 -0
  25. data/lib/hearth/http/api_error.rb +4 -8
  26. data/lib/hearth/http/client.rb +206 -59
  27. data/lib/hearth/http/error_inspector.rb +85 -0
  28. data/lib/hearth/http/error_parser.rb +18 -20
  29. data/lib/hearth/http/field.rb +49 -0
  30. data/lib/hearth/http/fields.rb +117 -0
  31. data/lib/hearth/http/header_list_builder.rb +42 -0
  32. data/lib/hearth/http/header_list_parser.rb +92 -0
  33. data/lib/hearth/http/middleware/content_length.rb +7 -4
  34. data/lib/hearth/http/middleware/content_md5.rb +30 -0
  35. data/lib/hearth/http/middleware/request_compression.rb +154 -0
  36. data/lib/hearth/http/middleware.rb +12 -0
  37. data/lib/hearth/http/networking_error.rb +1 -14
  38. data/lib/hearth/http/request.rb +83 -56
  39. data/lib/hearth/http/response.rb +42 -13
  40. data/lib/hearth/http.rb +16 -5
  41. data/lib/hearth/identities/anonymous.rb +8 -0
  42. data/lib/hearth/identities/http_api_key.rb +16 -0
  43. data/lib/hearth/identities/http_bearer.rb +16 -0
  44. data/lib/hearth/identities/http_login.rb +20 -0
  45. data/lib/hearth/identities.rb +21 -0
  46. data/lib/hearth/identity_provider.rb +17 -0
  47. data/lib/hearth/interceptor.rb +506 -0
  48. data/lib/hearth/interceptor_context.rb +40 -0
  49. data/lib/hearth/interceptor_list.rb +48 -0
  50. data/lib/hearth/interceptors.rb +76 -0
  51. data/lib/hearth/json.rb +4 -4
  52. data/lib/hearth/middleware/auth.rb +103 -0
  53. data/lib/hearth/middleware/build.rb +32 -1
  54. data/lib/hearth/middleware/endpoint.rb +79 -0
  55. data/lib/hearth/middleware/host_prefix.rb +11 -8
  56. data/lib/hearth/middleware/initialize.rb +57 -0
  57. data/lib/hearth/middleware/parse.rb +45 -7
  58. data/lib/hearth/middleware/retry.rb +105 -24
  59. data/lib/hearth/middleware/send.rb +137 -26
  60. data/lib/hearth/middleware/sign.rb +65 -0
  61. data/lib/hearth/middleware/validate.rb +11 -1
  62. data/lib/hearth/middleware.rb +20 -8
  63. data/lib/hearth/middleware_stack.rb +2 -44
  64. data/lib/hearth/networking_error.rb +18 -0
  65. data/lib/hearth/number_helper.rb +3 -3
  66. data/lib/hearth/output.rb +8 -4
  67. data/lib/hearth/plugin_list.rb +53 -0
  68. data/lib/hearth/query/param.rb +56 -0
  69. data/lib/hearth/query/param_list.rb +54 -0
  70. data/lib/hearth/query/param_matcher.rb +31 -0
  71. data/lib/hearth/refreshing_identity_provider.rb +63 -0
  72. data/lib/hearth/request.rb +22 -0
  73. data/lib/hearth/response.rb +36 -0
  74. data/lib/hearth/retry/adaptive.rb +60 -0
  75. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  76. data/lib/hearth/retry/client_rate_limiter.rb +145 -0
  77. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  78. data/lib/hearth/retry/retry_quota.rb +56 -0
  79. data/lib/hearth/retry/standard.rb +46 -0
  80. data/lib/hearth/retry.rb +29 -0
  81. data/lib/hearth/signers/anonymous.rb +16 -0
  82. data/lib/hearth/signers/http_api_key.rb +29 -0
  83. data/lib/hearth/signers/http_basic.rb +23 -0
  84. data/lib/hearth/signers/http_bearer.rb +19 -0
  85. data/lib/hearth/signers/http_digest.rb +19 -0
  86. data/lib/hearth/signers.rb +23 -0
  87. data/lib/hearth/structure.rb +7 -3
  88. data/lib/hearth/stubs.rb +38 -0
  89. data/lib/hearth/time_helper.rb +6 -5
  90. data/lib/hearth/validator.rb +60 -5
  91. data/lib/hearth/waiters/poller.rb +10 -9
  92. data/lib/hearth/waiters/waiter.rb +23 -9
  93. data/lib/hearth/xml/formatter.rb +11 -2
  94. data/lib/hearth/xml/node.rb +2 -3
  95. data/lib/hearth/xml/node_matcher.rb +0 -1
  96. data/lib/hearth.rb +37 -6
  97. data/sig/lib/hearth/aliases.rbs +6 -0
  98. data/sig/lib/hearth/anonymous_auth_resolver.rbs +5 -0
  99. data/sig/lib/hearth/api_error.rbs +13 -0
  100. data/sig/lib/hearth/auth_option.rbs +11 -0
  101. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  102. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  103. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  104. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  105. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  106. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  107. data/sig/lib/hearth/block_io.rbs +9 -0
  108. data/sig/lib/hearth/client.rbs +9 -0
  109. data/sig/lib/hearth/client_stubs.rbs +5 -0
  110. data/sig/lib/hearth/configuration.rbs +7 -0
  111. data/sig/lib/hearth/dns/host_address.rbs +11 -0
  112. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  113. data/sig/lib/hearth/endpoint_rules.rbs +17 -0
  114. data/sig/lib/hearth/http/api_error.rbs +13 -0
  115. data/sig/lib/hearth/http/client.rbs +9 -0
  116. data/sig/lib/hearth/http/field.rbs +19 -0
  117. data/sig/lib/hearth/http/fields.rbs +43 -0
  118. data/sig/lib/hearth/http/header_list_builder.rbs +15 -0
  119. data/sig/lib/hearth/http/header_list_parser.rbs +19 -0
  120. data/sig/lib/hearth/http/networking_error.rbs +6 -0
  121. data/sig/lib/hearth/http/request.rbs +25 -0
  122. data/sig/lib/hearth/http/response.rbs +21 -0
  123. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  124. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  125. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  126. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  127. data/sig/lib/hearth/identities.rbs +9 -0
  128. data/sig/lib/hearth/identity_provider.rbs +7 -0
  129. data/sig/lib/hearth/interceptor.rbs +9 -0
  130. data/sig/lib/hearth/interceptor_context.rbs +17 -0
  131. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  132. data/sig/lib/hearth/interfaces.rbs +87 -0
  133. data/sig/lib/hearth/json/parse_error.rbs +9 -0
  134. data/sig/lib/hearth/networking_error.rbs +7 -0
  135. data/sig/lib/hearth/output.rbs +11 -0
  136. data/sig/lib/hearth/plugin_list.rbs +13 -0
  137. data/sig/lib/hearth/query/param.rbs +17 -0
  138. data/sig/lib/hearth/query/param_list.rbs +25 -0
  139. data/sig/lib/hearth/refreshing_identity_provider.rbs +10 -0
  140. data/sig/lib/hearth/request.rbs +9 -0
  141. data/sig/lib/hearth/response.rbs +11 -0
  142. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  143. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  144. data/sig/lib/hearth/retry/standard.rbs +13 -0
  145. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  146. data/sig/lib/hearth/retry.rbs +9 -0
  147. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  148. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  149. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  150. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  151. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  152. data/sig/lib/hearth/signers.rbs +9 -0
  153. data/sig/lib/hearth/structure.rbs +6 -0
  154. data/sig/lib/hearth/stubs.rbs +9 -0
  155. data/sig/lib/hearth/union.rbs +5 -0
  156. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  157. data/sig/lib/hearth/xml/parse_error.rbs +9 -0
  158. metadata +151 -25
  159. data/lib/hearth/http/headers.rb +0 -70
  160. data/lib/hearth/middleware/around_handler.rb +0 -24
  161. data/lib/hearth/middleware/request_handler.rb +0 -24
  162. data/lib/hearth/middleware/response_handler.rb +0 -25
  163. data/lib/hearth/middleware_builder.rb +0 -246
  164. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  165. data/lib/hearth/stubbing/stubs.rb +0 -32
  166. data/lib/hearth/waiters/errors.rb +0 -15
  167. data/sig/lib/seahorse/api_error.rbs +0 -10
  168. data/sig/lib/seahorse/document.rbs +0 -2
  169. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  170. data/sig/lib/seahorse/http/headers.rbs +0 -47
  171. data/sig/lib/seahorse/http/response.rbs +0 -21
  172. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  173. data/sig/lib/seahorse/structure.rbs +0 -18
  174. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  175. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  176. data/sig/lib/seahorse/union.rbs +0 -6
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # @api private
6
+ module HeaderListBuilder
7
+ class << self
8
+ def build_list(value)
9
+ value.compact.join(', ')
10
+ end
11
+
12
+ # builds a string from a list of possibly quoted values
13
+ # ensures that quoted values are escaped
14
+ def build_string_list(value)
15
+ value.compact.map { |s| escape_value(s) }.join(', ')
16
+ end
17
+
18
+ def build_http_date_list(value)
19
+ value.compact.map { |t| Hearth::TimeHelper.to_http_date(t) }
20
+ .join(', ')
21
+ end
22
+
23
+ def build_date_time_list(value)
24
+ value.compact.map { |t| Hearth::TimeHelper.to_date_time(t) }
25
+ .join(', ')
26
+ end
27
+
28
+ def build_epoch_seconds_list(value)
29
+ value.compact.map { |t| Hearth::TimeHelper.to_epoch_seconds(t) }
30
+ .join(', ')
31
+ end
32
+
33
+ private
34
+
35
+ def escape_value(str)
36
+ s = str
37
+ s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'strscan'
4
+
5
+ module Hearth
6
+ module HTTP
7
+ # @api private
8
+ module HeaderListParser
9
+ class << self
10
+ def parse_boolean_list(value)
11
+ value.split(', ').map { |s| s == 'true' }
12
+ end
13
+
14
+ def parse_integer_list(value)
15
+ value.split(', ').map(&:to_i)
16
+ end
17
+
18
+ def parse_float_list(value)
19
+ value.split(', ').map(&:to_f)
20
+ end
21
+
22
+ # parse a list of possibly quoted and escaped string values
23
+ # Follows:
24
+ # # [RFC-7230's specification of header values](https://datatracker.ietf.org/doc/html/rfc7230#section-3.2.6).
25
+ def parse_string_list(value)
26
+ buffer = StringScanner.new(value)
27
+ parsed = []
28
+
29
+ parsed << read_value(buffer) until buffer.eos?
30
+
31
+ parsed
32
+ end
33
+
34
+ # rfc822/http-date has a comma after day but is NOT escaped.
35
+ # # eg: Mon, 16 Dec 2019 23:48:18 GMT, Mon, 16 Dec 2019 23:48:18 GMT
36
+ def parse_http_date_list(value)
37
+ value.split(',').each_slice(2).map { |v| Time.parse(v[0] + v[1]) }
38
+ end
39
+
40
+ def parse_date_time_list(value)
41
+ value.split(',').map { |v| Time.parse(v) }
42
+ end
43
+
44
+ def parse_epoch_seconds_list(value)
45
+ value.split(',').map { |v| Time.at(v.to_i) }
46
+ end
47
+
48
+ private
49
+
50
+ def read_value(buffer)
51
+ until buffer.eos?
52
+ case buffer.peek(1)
53
+ when ' ', "\t"
54
+ # drop leading whitespace
55
+ buffer.getch
56
+ next
57
+ when '"'
58
+ buffer.getch # drop the quote and advance
59
+ return read_quoted_value(buffer)
60
+ else
61
+ return read_unquoted_value(buffer)
62
+ end
63
+ end
64
+ # buffer is only whitespace
65
+ nil
66
+ end
67
+
68
+ def read_unquoted_value(buffer)
69
+ # there cannot be any escaped values
70
+ value = buffer.scan_until(/,|$/)
71
+ # drop the comma if we matched it
72
+ buffer.matched == ',' ? value.chop : value
73
+ end
74
+
75
+ def read_quoted_value(buffer)
76
+ # scan until we have an unescaped double quote
77
+ value = buffer.scan_until(/[^\\]"/)
78
+ unless value
79
+ raise ArgumentError,
80
+ 'Invalid String list: No closing quote found'
81
+ end
82
+
83
+ # drop any remaining whitespace/commas
84
+ buffer.scan_until(/[\s,]*/)
85
+ # the last character will always be the closing quote.
86
+ # Add a starting quote and then unescape (undump)
87
+ "\"#{value}".undump
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -4,8 +4,9 @@ module Hearth
4
4
  module HTTP
5
5
  module Middleware
6
6
  # A middleware that sets Content-Length for any body that has a size.
7
- # @api private
8
7
  class ContentLength
8
+ include Hearth::Middleware::Logging
9
+
9
10
  def initialize(app, _ = {})
10
11
  @app = app
11
12
  end
@@ -15,10 +16,12 @@ module Hearth
15
16
  # @return [Output]
16
17
  def call(input, context)
17
18
  request = context.request
18
- if request&.body.respond_to?(:size) &&
19
- !request.headers.key?('Content-Length')
20
- length = request.body.size
19
+ body = request.body
20
+ if !request.headers.key?('Content-Length') &&
21
+ (body.respond_to?(:size) && body.size.positive?)
22
+ length = body.size
21
23
  request.headers['Content-Length'] = length
24
+ log_debug(context, "Set Content-Length to #{length}")
22
25
  end
23
26
 
24
27
  @app.call(input, context)
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ module Middleware
6
+ # A middleware that sets Content-MD5 for any body.
7
+ class ContentMD5
8
+ include Hearth::Middleware::Logging
9
+
10
+ def initialize(app, _ = {})
11
+ @app = app
12
+ end
13
+
14
+ # @param input
15
+ # @param context
16
+ # @return [Output]
17
+ def call(input, context)
18
+ request = context.request
19
+ unless request.headers.key?('Content-MD5')
20
+ md5 = Hearth::Checksums.md5(request.body)
21
+ request.headers['Content-MD5'] = md5
22
+ log_debug(context, "Set Content-MD5 to #{md5}")
23
+ end
24
+
25
+ @app.call(input, context)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ module Middleware
6
+ # A middleware that compresses the request body and
7
+ # adds the Content-Encoding header
8
+ class RequestCompression
9
+ include Hearth::Middleware::Logging
10
+
11
+ SUPPORTED_ENCODINGS = %w[gzip].freeze
12
+ CHUNK_SIZE = 1 * 1024 * 1024 # one MB
13
+
14
+ # @param [Class] app The next middleware in the stack.
15
+ # @param [Boolean] disable_request_compression If true, the request
16
+ # body is not compressed.
17
+ # @param [Integer] request_min_compression_size_bytes The minimum size
18
+ # of the request body to be compressed.
19
+ # @param [Array<String>] encodings The encodings to be used for
20
+ # compression.
21
+ # @param [Boolean] streaming If true, the request body is compressed
22
+ # in chunks.
23
+ def initialize(app, disable_request_compression:,
24
+ request_min_compression_size_bytes:, encodings:,
25
+ streaming:)
26
+ @app = app
27
+ @disable_request_compression = disable_request_compression
28
+ @request_min_compression_size_bytes =
29
+ request_min_compression_size_bytes
30
+ @encodings = encodings
31
+ @streaming = streaming
32
+ end
33
+
34
+ # @param input
35
+ # @param context
36
+ # @return [Output]
37
+ def call(input, context)
38
+ compress_request(context) unless @disable_request_compression
39
+ @app.call(input, context)
40
+ end
41
+
42
+ private
43
+
44
+ def compress_request(context)
45
+ selected_encoding = @encodings.find do |encoding|
46
+ SUPPORTED_ENCODINGS.include?(encoding)
47
+ end
48
+ return unless selected_encoding
49
+
50
+ log_debug(context, "Compressing request with: #{selected_encoding}")
51
+ request = context.request
52
+ if @streaming
53
+ compress_streaming_body(selected_encoding, request)
54
+ log_debug(context, 'Compressed request body in chunks')
55
+ elsif request.body.size >= @request_min_compression_size_bytes
56
+ compress_body(selected_encoding, request)
57
+ log_debug(context, 'Compressed request body')
58
+ end
59
+ end
60
+
61
+ def update_content_encoding(encoding, request)
62
+ headers = request.headers
63
+ if headers['Content-Encoding']
64
+ headers['Content-Encoding'] += ", #{encoding}"
65
+ else
66
+ headers['Content-Encoding'] = encoding
67
+ end
68
+ end
69
+
70
+ def compress_body(encoding, request)
71
+ return unless encoding == 'gzip'
72
+
73
+ gzip_compress(request)
74
+ update_content_encoding(encoding, request)
75
+ end
76
+
77
+ def gzip_compress(request)
78
+ compressed = StringIO.new
79
+ compressed.binmode
80
+ gzip_writer = Zlib::GzipWriter.new(compressed)
81
+ if request.body.respond_to?(:read)
82
+ update_in_chunks(gzip_writer, request.body)
83
+ else
84
+ gzip_writer.write(request.body)
85
+ end
86
+ gzip_writer.close
87
+ new_body = StringIO.new(compressed.string)
88
+ request.body = new_body
89
+ end
90
+
91
+ def update_in_chunks(compressor, io)
92
+ loop do
93
+ chunk = io.read(CHUNK_SIZE)
94
+ break unless chunk
95
+
96
+ compressor.write(chunk)
97
+ end
98
+ end
99
+
100
+ def compress_streaming_body(encoding, request)
101
+ return unless encoding == 'gzip'
102
+
103
+ request.body = GzipIO.new(request.body)
104
+ update_content_encoding(encoding, request)
105
+ end
106
+
107
+ # @api private
108
+ class GzipIO
109
+ def initialize(body)
110
+ @body = body
111
+ @buffer = ChunkBuffer.new
112
+ @gzip_writer = Zlib::GzipWriter.new(@buffer)
113
+ end
114
+
115
+ def read(length, buff = nil)
116
+ if @gzip_writer.closed?
117
+ # an empty string to signify an end as
118
+ # there will be nothing remaining to be read
119
+ StringIO.new('').read(length, buff)
120
+ return
121
+ end
122
+
123
+ chunk = @body.read(length)
124
+ if !chunk || chunk.empty?
125
+ # closing the writer will write one last chunk
126
+ # with a trailer (to be read from the @buffer)
127
+ @gzip_writer.close
128
+ else
129
+ # flush happens first to ensure that header fields
130
+ # are being sent over since write will override
131
+ @gzip_writer.flush
132
+ @gzip_writer.write(chunk)
133
+ end
134
+
135
+ StringIO.new(@buffer.last_chunk).read(length, buff)
136
+ end
137
+ end
138
+
139
+ # @api private
140
+ class ChunkBuffer
141
+ def initialize
142
+ @last_chunk = nil
143
+ end
144
+
145
+ attr_reader :last_chunk
146
+
147
+ def write(data)
148
+ @last_chunk = data
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'middleware/content_length'
4
+ require_relative 'middleware/content_md5'
5
+ require_relative 'middleware/request_compression'
6
+
7
+ module Hearth
8
+ module HTTP
9
+ # @api private
10
+ module Middleware; end
11
+ end
12
+ end
@@ -2,19 +2,6 @@
2
2
 
3
3
  module Hearth
4
4
  module HTTP
5
- # Thrown by a Client when encountering a networking error while transmitting
6
- # a request or receiving a response. You can access the original error
7
- # by calling {#original_error}.
8
- class NetworkingError < StandardError
9
- MSG = 'Encountered an error while transmitting the request: %<message>s'
10
-
11
- def initialize(original_error)
12
- @original_error = original_error
13
- super(format(MSG, message: original_error.message))
14
- end
15
-
16
- # @return [StandardError]
17
- attr_reader :original_error
18
- end
5
+ class NetworkingError < Hearth::NetworkingError; end
19
6
  end
20
7
  end
@@ -1,83 +1,77 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'stringio'
4
- require 'uri'
5
-
6
3
  module Hearth
7
4
  module HTTP
8
5
  # Represents an HTTP request.
9
- # @api private
10
- class Request
6
+ class Request < Hearth::Request
11
7
  # @param [String] http_method
12
- # @param [String] url
13
- # @param [Headers] headers
14
- # @param [IO] body
15
- def initialize(http_method: nil, url: nil, headers: Headers.new,
16
- body: StringIO.new)
8
+ # @param [Fields] fields
9
+ # @param (see Hearth::Request#initialize)
10
+ def initialize(http_method: nil, fields: Fields.new, **kwargs)
11
+ super(**kwargs)
17
12
  @http_method = http_method
18
- @url = url
19
- @headers = headers
20
- @body = body
13
+ @fields = fields
14
+ @headers = Fields::Proxy.new(@fields, :header)
15
+ @trailers = Fields::Proxy.new(@fields, :trailer)
21
16
  end
22
17
 
23
18
  # @return [String]
24
19
  attr_accessor :http_method
25
20
 
26
- # @return [String]
27
- attr_accessor :url
21
+ # @return [Fields]
22
+ attr_reader :fields
28
23
 
29
- # @return [Headers]
30
- attr_accessor :headers
24
+ # @return [Fields::Proxy]
25
+ attr_reader :headers
31
26
 
32
- # @return [IO]
33
- attr_accessor :body
27
+ # @return [Fields::Proxy]
28
+ attr_reader :trailers
34
29
 
35
- # Append a path to the HTTP request URL.
30
+ # Append a path to the HTTP request URI.
36
31
  #
37
- # http_req.url = "https://example.com"
32
+ # http_req.uri = "https://example.com"
38
33
  # http_req.append_path('/')
39
- # http_req.url
34
+ # http_req.uri.to_s
40
35
  # #=> "https://example.com/"
41
36
  #
42
37
  # Paths will be joined by a single '/':
43
38
  #
44
- # http_req.url = "https://example.com/path-prefix/"
39
+ # http_req.uri = "https://example.com/path-prefix/"
45
40
  # http_req.append_path('/path-suffix')
46
- # http_req.url
41
+ # http_req.uri.to_s
47
42
  # #=> "https://example.com/path-prefix/path-suffix"
48
43
  #
49
- # Resultant URL preserves the querystring:
44
+ # Resultant URI preserves the querystring:
50
45
  #
51
- # http_req.url = "https://example.com/path-prefix?querystring
46
+ # http_req.uri = "https://example.com/path-prefix?querystring
52
47
  # http_req.append_path('/path-suffix')
53
- # http_req.url
48
+ # http_req.uri.to_s
54
49
  # #=> "https://example.com/path-prefix/path-suffix?querystring"
55
50
  #
56
51
  # The provided path should be URI escaped before being passed.
57
52
  #
58
- # http_req.url = "https://example.com
53
+ # http_req.uri = "https://example.com
59
54
  # http_req.append_path(
60
55
  # Hearth::HTTP.uri_escape_path('/part 1/part 2')
61
56
  # )
62
- # http_req.url
57
+ # http_req.uri.to_s
63
58
  # #=> "https://example.com/part%201/part%202"
64
59
  #
65
60
  # @param [String] path A URI escaped path.
61
+ #
66
62
  def append_path(path)
67
- uri = URI.parse(@url)
68
63
  base_path = uri.path.sub(%r{/$}, '') # remove trailing slash
69
64
  path = path.sub(%r{^/}, '') # remove prefix slash
70
65
  uri.path = "#{base_path}/#{path}" # join on single slash
71
- @url = uri.to_s
72
66
  end
73
67
 
74
- # Append querystring parameter to the HTTP request URL.
68
+ # Append querystring parameter to the HTTP request URI.
75
69
  #
76
- # http_req.url = "https://example.com"
70
+ # http_req.uri = "https://example.com"
77
71
  # http_req.append_query_param('query')
78
72
  # http_req.append_query_param('key 1', 'value 1')
79
73
  #
80
- # http_req.url
74
+ # http_req.uri.to_s
81
75
  # #=> "https://example.com?query&key%201=value%201
82
76
  #
83
77
  # @overload append_query_param(name)
@@ -93,39 +87,72 @@ module Hearth
93
87
  # The value of the querystring parameter to add. This value
94
88
  # will be URI escaped.
95
89
  #
96
- def append_query_param(*args)
97
- param =
98
- case args.size
99
- when 1 then escape(args[0])
100
- when 2 then "#{escape(args[0])}=#{escape(args[1])}"
101
- else raise ArgumentError, 'wrong number of arguments ' \
102
- "(given #{args.size}, expected 1 or 2)"
103
- end
104
- uri = URI.parse(@url)
105
- uri.query = uri.query ? "#{uri.query}&#{param}" : param
106
- @url = uri.to_s
90
+ def append_query_param(name, value = nil)
91
+ param = Hearth::Query::Param.new(name, value)
92
+ uri.query = uri.query ? "#{uri.query}&#{param}" : param.to_s
107
93
  end
108
94
 
109
- # Append a host prefix to the HTTP request URL.
95
+ # Append querystring parameter list to the HTTP request URI.
96
+ #
97
+ # http_req.uri = "https://example.com"
98
+ # query_params = Hearth::Query::ParamList.new
99
+ # query_params['key 1'] = nil
100
+ # query_params['key 2'] = 'value 2'
101
+ # http_req.append_query_param_list(query_params)
110
102
  #
111
- # http_req.url = "https://example.com"
103
+ # http_req.uri.to_s
104
+ # #=> "https://example.com?key%201=&key%202=value%202"
105
+ #
106
+ # @param [ParamList] param_list
107
+ # An instance of Hearth::Query::ParamList containing the list of
108
+ # querystring parameters to add. The names and values are URI escaped.
109
+ #
110
+ def append_query_param_list(param_list)
111
+ return if param_list.empty?
112
+
113
+ uri.query = uri.query ? "#{uri.query}&#{param_list}" : param_list.to_s
114
+ end
115
+
116
+ # Remove querystring parameter from the HTTP request URI.
117
+ #
118
+ # http_req.uri = "https://example.com"
119
+ # http_req.append_query_param('query')
120
+ # http_req.append_query_param('empty', '')
121
+ # http_req.append_query_param('deleteme', 'true')
122
+ # http_req.append_query_param('key', 'value')
123
+ # #=> "https://example.com?query&empty=&deleteme=true&key=value"
124
+ #
125
+ # http_req.remove_query_param('deleteme')
126
+ # #=> "query&empty=&key=value"
127
+ #
128
+ # http_req.uri.to_s
129
+ # #=> "https://example.com?query&empty=&key=value"
130
+ #
131
+ # @param [String] name The name of the querystring parameter to remove.
132
+ #
133
+ def remove_query_param(name)
134
+ parsed = CGI.parse(uri.query)
135
+ parsed.delete(name)
136
+ # encode_www_form ignores query params without values
137
+ # (CGI parses these as empty lists)
138
+ parsed.each do |key, values|
139
+ parsed[key] = values.empty? ? nil : values
140
+ end
141
+ uri.query = URI.encode_www_form(parsed)
142
+ end
143
+
144
+ # Append a host prefix to the HTTP request URI.
145
+ #
146
+ # http_req.uri = "https://example.com"
112
147
  # http_req.prefix_host('data.')
113
148
  #
114
- # http_req.url
149
+ # http_req.uri.to_s
115
150
  # #=> "https://data.foo.com
116
151
  #
117
152
  # @param [String] prefix A dot (.) terminated prefix for the host.
118
153
  #
119
154
  def prefix_host(prefix)
120
- uri = URI.parse(@url)
121
155
  uri.host = prefix + uri.host
122
- @url = uri.to_s
123
- end
124
-
125
- private
126
-
127
- def escape(value)
128
- Hearth::HTTP.uri_escape(value.to_s)
129
156
  end
130
157
  end
131
158
  end
@@ -1,29 +1,58 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'stringio'
4
-
5
3
  module Hearth
6
4
  module HTTP
7
5
  # Represents an HTTP Response.
8
- # @api private
9
- class Response
6
+ class Response < Hearth::Response
10
7
  # @param [Integer] status
11
- # @param [Headers] headers
12
- # @param [IO] body
13
- def initialize(status: 200, headers: Headers.new, body: StringIO.new)
8
+ # @param [String, nil] reason
9
+ # @param [Fields] fields
10
+ # @param (see Hearth::Response#initialize)
11
+ def initialize(status: 0, reason: nil, fields: Fields.new, **kwargs)
12
+ super(**kwargs)
14
13
  @status = status
15
- @headers = headers
16
- @body = body
14
+ @reason = reason
15
+ @fields = fields
16
+ @headers = Fields::Proxy.new(@fields, :header)
17
+ @trailers = Fields::Proxy.new(@fields, :trailer)
17
18
  end
18
19
 
19
20
  # @return [Integer]
20
21
  attr_accessor :status
21
22
 
22
- # @return [Headers]
23
- attr_accessor :headers
23
+ # @return [String, nil]
24
+ attr_accessor :reason
25
+
26
+ # @return [Fields]
27
+ attr_reader :fields
28
+
29
+ # @return [Fields::Proxy]
30
+ attr_reader :headers
31
+
32
+ # @return [Fields::Proxy]
33
+ attr_reader :trailers
24
34
 
25
- # @return [IO]
26
- attr_accessor :body
35
+ # Replace attributes from other response
36
+ # @param [Response] other
37
+ # @return [Response]
38
+ def replace(other)
39
+ @status = other.status
40
+ @reason = other.reason
41
+ @fields = other.fields
42
+ @headers = Fields::Proxy.new(@fields, :header)
43
+ @trailers = Fields::Proxy.new(@fields, :trailer)
44
+
45
+ super
46
+ end
47
+
48
+ # Resets the HTTP response.
49
+ # @return [Response]
50
+ def reset
51
+ @status = 0
52
+ @reason = nil
53
+ @fields.clear
54
+ super
55
+ end
27
56
  end
28
57
  end
29
58
  end