hearth 1.0.0.pre1 → 1.0.0.pre2

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 (157) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -4
  3. data/VERSION +1 -1
  4. data/lib/hearth/api_error.rb +15 -1
  5. data/lib/hearth/auth_option.rb +21 -0
  6. data/lib/hearth/auth_schemes/anonymous.rb +21 -0
  7. data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
  8. data/lib/hearth/auth_schemes/http_basic.rb +16 -0
  9. data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
  10. data/lib/hearth/auth_schemes/http_digest.rb +16 -0
  11. data/lib/hearth/auth_schemes.rb +32 -0
  12. data/lib/hearth/checksums.rb +31 -0
  13. data/lib/hearth/client_stubs.rb +130 -0
  14. data/lib/hearth/config/env_provider.rb +53 -0
  15. data/lib/hearth/config/resolver.rb +52 -0
  16. data/lib/hearth/configuration.rb +15 -0
  17. data/lib/hearth/connection_pool.rb +77 -0
  18. data/lib/hearth/context.rb +28 -4
  19. data/lib/hearth/dns/host_address.rb +23 -0
  20. data/lib/hearth/dns/host_resolver.rb +92 -0
  21. data/lib/hearth/dns.rb +48 -0
  22. data/lib/hearth/http/api_error.rb +4 -8
  23. data/lib/hearth/http/client.rb +208 -59
  24. data/lib/hearth/http/error_inspector.rb +85 -0
  25. data/lib/hearth/http/error_parser.rb +18 -20
  26. data/lib/hearth/http/field.rb +64 -0
  27. data/lib/hearth/http/fields.rb +117 -0
  28. data/lib/hearth/http/middleware/content_length.rb +5 -2
  29. data/lib/hearth/http/middleware/content_md5.rb +31 -0
  30. data/lib/hearth/http/middleware/request_compression.rb +157 -0
  31. data/lib/hearth/http/middleware.rb +12 -0
  32. data/lib/hearth/http/networking_error.rb +1 -14
  33. data/lib/hearth/http/request.rb +83 -56
  34. data/lib/hearth/http/response.rb +42 -13
  35. data/lib/hearth/http.rb +14 -5
  36. data/lib/hearth/identities/anonymous.rb +8 -0
  37. data/lib/hearth/identities/http_api_key.rb +16 -0
  38. data/lib/hearth/identities/http_bearer.rb +16 -0
  39. data/lib/hearth/identities/http_login.rb +20 -0
  40. data/lib/hearth/identities.rb +21 -0
  41. data/lib/hearth/identity_resolver.rb +17 -0
  42. data/lib/hearth/interceptor.rb +506 -0
  43. data/lib/hearth/interceptor_context.rb +36 -0
  44. data/lib/hearth/interceptor_list.rb +48 -0
  45. data/lib/hearth/interceptors.rb +75 -0
  46. data/lib/hearth/middleware/auth.rb +100 -0
  47. data/lib/hearth/middleware/build.rb +32 -0
  48. data/lib/hearth/middleware/host_prefix.rb +10 -6
  49. data/lib/hearth/middleware/initialize.rb +58 -0
  50. data/lib/hearth/middleware/parse.rb +45 -6
  51. data/lib/hearth/middleware/retry.rb +97 -23
  52. data/lib/hearth/middleware/send.rb +137 -25
  53. data/lib/hearth/middleware/sign.rb +65 -0
  54. data/lib/hearth/middleware/validate.rb +11 -1
  55. data/lib/hearth/middleware.rb +19 -8
  56. data/lib/hearth/middleware_stack.rb +1 -43
  57. data/lib/hearth/networking_error.rb +18 -0
  58. data/lib/hearth/number_helper.rb +2 -2
  59. data/lib/hearth/output.rb +8 -4
  60. data/lib/hearth/plugin_list.rb +53 -0
  61. data/lib/hearth/query/param.rb +52 -0
  62. data/lib/hearth/query/param_list.rb +54 -0
  63. data/lib/hearth/query/param_matcher.rb +32 -0
  64. data/lib/hearth/refreshing_identity_resolver.rb +63 -0
  65. data/lib/hearth/request.rb +22 -0
  66. data/lib/hearth/response.rb +33 -0
  67. data/lib/hearth/retry/adaptive.rb +60 -0
  68. data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
  69. data/lib/hearth/retry/client_rate_limiter.rb +143 -0
  70. data/lib/hearth/retry/exponential_backoff.rb +15 -0
  71. data/lib/hearth/retry/retry_quota.rb +56 -0
  72. data/lib/hearth/retry/standard.rb +46 -0
  73. data/lib/hearth/retry/strategy.rb +20 -0
  74. data/lib/hearth/retry.rb +16 -0
  75. data/lib/hearth/signers/anonymous.rb +16 -0
  76. data/lib/hearth/signers/http_api_key.rb +29 -0
  77. data/lib/hearth/signers/http_basic.rb +23 -0
  78. data/lib/hearth/signers/http_bearer.rb +19 -0
  79. data/lib/hearth/signers/http_digest.rb +19 -0
  80. data/lib/hearth/signers.rb +23 -0
  81. data/lib/hearth/stubs.rb +30 -0
  82. data/lib/hearth/time_helper.rb +5 -3
  83. data/lib/hearth/validator.rb +44 -5
  84. data/lib/hearth/waiters/poller.rb +6 -7
  85. data/lib/hearth/waiters/waiter.rb +17 -4
  86. data/lib/hearth/xml/formatter.rb +11 -2
  87. data/lib/hearth/xml/node.rb +2 -2
  88. data/lib/hearth.rb +32 -5
  89. data/sig/lib/hearth/aliases.rbs +4 -0
  90. data/sig/lib/hearth/api_error.rbs +13 -0
  91. data/sig/lib/hearth/auth_option.rbs +11 -0
  92. data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
  93. data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
  94. data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
  95. data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
  96. data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
  97. data/sig/lib/hearth/auth_schemes.rbs +13 -0
  98. data/sig/lib/hearth/block_io.rbs +9 -0
  99. data/sig/lib/hearth/client_stubs.rbs +5 -0
  100. data/sig/lib/hearth/configuration.rbs +7 -0
  101. data/sig/lib/hearth/dns/host_address.rbs +13 -0
  102. data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
  103. data/sig/lib/hearth/http/api_error.rbs +13 -0
  104. data/sig/lib/hearth/http/client.rbs +9 -0
  105. data/sig/lib/hearth/http/field.rbs +19 -0
  106. data/sig/lib/hearth/http/fields.rbs +43 -0
  107. data/sig/lib/hearth/http/request.rbs +25 -0
  108. data/sig/lib/hearth/http/response.rbs +21 -0
  109. data/sig/lib/hearth/identities/anonymous.rbs +6 -0
  110. data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
  111. data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
  112. data/sig/lib/hearth/identities/http_login.rbs +11 -0
  113. data/sig/lib/hearth/identities.rbs +9 -0
  114. data/sig/lib/hearth/identity_resolver.rbs +7 -0
  115. data/sig/lib/hearth/interceptor.rbs +9 -0
  116. data/sig/lib/hearth/interceptor_context.rbs +15 -0
  117. data/sig/lib/hearth/interceptor_list.rbs +16 -0
  118. data/sig/lib/hearth/interfaces.rbs +65 -0
  119. data/sig/lib/hearth/output.rbs +11 -0
  120. data/sig/lib/hearth/plugin_list.rbs +15 -0
  121. data/sig/lib/hearth/query/param.rbs +17 -0
  122. data/sig/lib/hearth/query/param_list.rbs +25 -0
  123. data/sig/lib/hearth/request.rbs +9 -0
  124. data/sig/lib/hearth/response.rbs +11 -0
  125. data/sig/lib/hearth/retry/adaptive.rbs +13 -0
  126. data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
  127. data/sig/lib/hearth/retry/standard.rbs +13 -0
  128. data/sig/lib/hearth/retry/strategy.rbs +11 -0
  129. data/sig/lib/hearth/retry.rbs +9 -0
  130. data/sig/lib/hearth/signers/anonymous.rbs +9 -0
  131. data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
  132. data/sig/lib/hearth/signers/http_basic.rbs +9 -0
  133. data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
  134. data/sig/lib/hearth/signers/http_digest.rbs +9 -0
  135. data/sig/lib/hearth/signers.rbs +9 -0
  136. data/sig/lib/hearth/structure.rbs +7 -0
  137. data/sig/lib/hearth/union.rbs +5 -0
  138. data/sig/lib/hearth/waiters/waiter.rbs +17 -0
  139. metadata +132 -22
  140. data/lib/hearth/http/headers.rb +0 -70
  141. data/lib/hearth/middleware/around_handler.rb +0 -24
  142. data/lib/hearth/middleware/request_handler.rb +0 -24
  143. data/lib/hearth/middleware/response_handler.rb +0 -25
  144. data/lib/hearth/middleware_builder.rb +0 -246
  145. data/lib/hearth/stubbing/client_stubs.rb +0 -115
  146. data/lib/hearth/stubbing/stubs.rb +0 -32
  147. data/lib/hearth/waiters/errors.rb +0 -15
  148. data/sig/lib/seahorse/api_error.rbs +0 -10
  149. data/sig/lib/seahorse/document.rbs +0 -2
  150. data/sig/lib/seahorse/http/api_error.rbs +0 -21
  151. data/sig/lib/seahorse/http/headers.rbs +0 -47
  152. data/sig/lib/seahorse/http/response.rbs +0 -21
  153. data/sig/lib/seahorse/simple_delegator.rbs +0 -3
  154. data/sig/lib/seahorse/structure.rbs +0 -18
  155. data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
  156. data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
  157. data/sig/lib/seahorse/union.rbs +0 -6
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # Represents an HTTP field.
6
+ class Field
7
+ # @param [String] name The name of the field.
8
+ # @param [Array|#to_s] value (nil) The values for the field. It can be any
9
+ # object that responds to `#to_s` or an Array of objects that respond to
10
+ # `#to_s`.
11
+ # @param [Symbol] kind The kind of field, either :header or :trailer.
12
+ def initialize(name, value = nil, kind: :header)
13
+ if name.nil? || name.empty?
14
+ raise ArgumentError, 'Field name must be a non-empty String'
15
+ end
16
+
17
+ @name = name
18
+ @value = value
19
+ @kind = kind
20
+ end
21
+
22
+ # @return [String]
23
+ attr_reader :name
24
+
25
+ # @return [Symbol]
26
+ attr_reader :kind
27
+
28
+ # Returns an escaped string representation of the field.
29
+ # @return [String]
30
+ def value(encoding = nil)
31
+ value =
32
+ if @value.is_a?(Array)
33
+ @value.compact.map { |v| escape_value(v.to_s) }.join(', ')
34
+ else
35
+ @value.to_s
36
+ end
37
+ value = value.encode(encoding) if encoding
38
+ value
39
+ end
40
+
41
+ # @return [Boolean]
42
+ def header?
43
+ @kind == :header
44
+ end
45
+
46
+ # @return [Boolean]
47
+ def trailer?
48
+ @kind == :trailer
49
+ end
50
+
51
+ # @return [Hash]
52
+ def to_h
53
+ { @name => value }
54
+ end
55
+
56
+ private
57
+
58
+ def escape_value(str)
59
+ s = str
60
+ s.include?('"') || s.include?(',') ? "\"#{s.gsub('"', '\"')}\"" : s
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # Provides Hash like access for Headers and Trailers with key normalization
6
+ class Fields
7
+ include Enumerable
8
+
9
+ # @param [Array<Field>] fields
10
+ # @param [String] encoding
11
+ def initialize(fields = [], encoding: 'utf-8')
12
+ unless fields.is_a?(Enumerable)
13
+ raise ArgumentError, 'fields must be an Enumerable of Field'
14
+ end
15
+
16
+ @entries = {}
17
+ fields.each { |field| self[field.name] = field }
18
+ @encoding = encoding
19
+ end
20
+
21
+ # @return [String]
22
+ attr_reader :encoding
23
+
24
+ # @param [String] key
25
+ def [](key)
26
+ @entries[key.downcase]
27
+ end
28
+
29
+ # @param [String] key
30
+ # @param [Field] value
31
+ def []=(key, value)
32
+ raise ArgumentError, 'value must be a Field' unless value.is_a?(Field)
33
+
34
+ @entries[key.downcase] = value
35
+ end
36
+
37
+ # @param [String] key
38
+ # @return [Boolean] Returns `true` if there is a Field with the given key.
39
+ def key?(key)
40
+ @entries.key?(key.downcase)
41
+ end
42
+
43
+ # @param [String] key
44
+ # @return [Field, nil] Returns the Field for the deleted Field key.
45
+ def delete(key)
46
+ @entries.delete(key.downcase)
47
+ end
48
+
49
+ # @return [Enumerable<Field>]
50
+ def each(&block)
51
+ @entries.values.each(&block)
52
+ end
53
+
54
+ # @return [Integer] Returns the number of Field entries.
55
+ def size
56
+ @entries.size
57
+ end
58
+
59
+ # @return [Hash]
60
+ def clear
61
+ @entries = {}
62
+ end
63
+
64
+ # @api private
65
+ def inspect
66
+ super.gsub(/ @entries={.*},/, '')
67
+ end
68
+
69
+ # Proxy class that wraps Fields to create Headers and Trailers
70
+ class Proxy
71
+ include Enumerable
72
+
73
+ def initialize(fields, kind)
74
+ @fields = fields
75
+ @kind = kind
76
+ end
77
+
78
+ # @param [String] key
79
+ def [](key)
80
+ @fields[key].value(@fields.encoding) if key?(key)
81
+ end
82
+
83
+ # @param [String] key
84
+ # @param [#to_s, Array<#to_s>] value
85
+ def []=(key, value)
86
+ @fields[key] = Field.new(key, value, kind: @kind)
87
+ end
88
+
89
+ # @param [String] key
90
+ # @return [Boolean] Returns `true` if there is a Field with the given
91
+ # key and kind.
92
+ def key?(key)
93
+ @fields.key?(key) && @fields[key].kind == @kind
94
+ end
95
+
96
+ # @param [String] key
97
+ # @return [Field, nil] Returns the value for the deleted Field key.
98
+ def delete(key)
99
+ @fields.delete(key).value(@fields.encoding) if key?(key)
100
+ end
101
+
102
+ # @return [Enumerable<String,String>]
103
+ def each(&block)
104
+ @fields.filter { |f| f.kind == @kind }
105
+ .to_h { |f| [f.name, f.value(@fields.encoding)] }
106
+ .each(&block)
107
+ end
108
+ alias each_pair each
109
+
110
+ # @api private
111
+ def inspect
112
+ to_h
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
@@ -6,6 +6,8 @@ module Hearth
6
6
  # A middleware that sets Content-Length for any body that has a size.
7
7
  # @api private
8
8
  class ContentLength
9
+ include Hearth::Middleware::Logging
10
+
9
11
  def initialize(app, _ = {})
10
12
  @app = app
11
13
  end
@@ -15,10 +17,11 @@ module Hearth
15
17
  # @return [Output]
16
18
  def call(input, context)
17
19
  request = context.request
18
- if request&.body.respond_to?(:size) &&
19
- !request.headers.key?('Content-Length')
20
+ if !request.headers.key?('Content-Length') &&
21
+ request.body.respond_to?(:size)
20
22
  length = request.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,31 @@
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
+ # @api private
8
+ class ContentMD5
9
+ include Hearth::Middleware::Logging
10
+
11
+ def initialize(app, _ = {})
12
+ @app = app
13
+ end
14
+
15
+ # @param input
16
+ # @param context
17
+ # @return [Output]
18
+ def call(input, context)
19
+ request = context.request
20
+ unless request.headers.key?('Content-MD5')
21
+ md5 = Hearth::Checksums.md5(request.body)
22
+ request.headers['Content-MD5'] = md5
23
+ log_debug(context, "Set Content-MD5 to #{md5}")
24
+ end
25
+
26
+ @app.call(input, context)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,157 @@
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
+ # @api private
9
+ class RequestCompression
10
+ include Hearth::Middleware::Logging
11
+
12
+ SUPPORTED_ENCODINGS = %w[gzip].freeze
13
+ CHUNK_SIZE = 1 * 1024 * 1024 # one MB
14
+
15
+ # @param [Class] app The next middleware in the stack.
16
+ # @param [Boolean] disable_request_compression If true, the request
17
+ # body is not compressed.
18
+ # @param [Integer] request_min_compression_size_bytes The minimum size
19
+ # of the request body to be compressed.
20
+ # @param [Array<String>] encodings The encodings to be used for
21
+ # compression.
22
+ # @param [Boolean] streaming If true, the request body is compressed
23
+ # in chunks.
24
+ def initialize(app, disable_request_compression:,
25
+ request_min_compression_size_bytes:, encodings:,
26
+ streaming:)
27
+ @app = app
28
+ @disable_request_compression = disable_request_compression
29
+ @request_min_compression_size_bytes =
30
+ request_min_compression_size_bytes
31
+ @encodings = encodings
32
+ @streaming = streaming
33
+ end
34
+
35
+ # @param input
36
+ # @param context
37
+ # @return [Output]
38
+ def call(input, context)
39
+ compress_request(context) unless @disable_request_compression
40
+ @app.call(input, context)
41
+ end
42
+
43
+ private
44
+
45
+ def compress_request(context)
46
+ selected_encoding = @encodings.find do |encoding|
47
+ SUPPORTED_ENCODINGS.include?(encoding)
48
+ end
49
+ return unless selected_encoding
50
+
51
+ log_debug(context, "Compressing request with: #{selected_encoding}")
52
+ request = context.request
53
+ if @streaming
54
+ compress_streaming_body(selected_encoding, request)
55
+ log_debug(context, 'Compressed request body in chunks')
56
+ elsif request.body.size >= @request_min_compression_size_bytes
57
+ compress_body(selected_encoding, request)
58
+ log_debug(context, 'Compressed request body')
59
+ end
60
+ end
61
+
62
+ def update_content_encoding(encoding, request)
63
+ headers = request.headers
64
+ if headers['Content-Encoding']
65
+ headers['Content-Encoding'] += ",#{encoding}"
66
+ else
67
+ headers['Content-Encoding'] = encoding
68
+ end
69
+ end
70
+
71
+ def compress_body(encoding, request)
72
+ case encoding
73
+ when 'gzip'
74
+ gzip_compress(request)
75
+ end
76
+ update_content_encoding(encoding, request)
77
+ end
78
+
79
+ def gzip_compress(request)
80
+ compressed = StringIO.new
81
+ compressed.binmode
82
+ gzip_writer = Zlib::GzipWriter.new(compressed)
83
+ if request.body.respond_to?(:read)
84
+ update_in_chunks(gzip_writer, request.body)
85
+ else
86
+ gzip_writer.write(request.body)
87
+ end
88
+ gzip_writer.close
89
+ new_body = StringIO.new(compressed.string)
90
+ request.body = new_body
91
+ end
92
+
93
+ def update_in_chunks(compressor, io)
94
+ loop do
95
+ chunk = io.read(CHUNK_SIZE)
96
+ break unless chunk
97
+
98
+ compressor.write(chunk)
99
+ end
100
+ end
101
+
102
+ def compress_streaming_body(encoding, request)
103
+ case encoding
104
+ when 'gzip'
105
+ request.body = GzipIO.new(request.body)
106
+ end
107
+ update_content_encoding(encoding, request)
108
+ end
109
+
110
+ # @api private
111
+ class GzipIO
112
+ def initialize(body)
113
+ @body = body
114
+ @buffer = ChunkBuffer.new
115
+ @gzip_writer = Zlib::GzipWriter.new(@buffer)
116
+ end
117
+
118
+ def read(length, buff = nil)
119
+ if @gzip_writer.closed?
120
+ # an empty string to signify an end as
121
+ # there will be nothing remaining to be read
122
+ StringIO.new('').read(length, buff)
123
+ return
124
+ end
125
+
126
+ chunk = @body.read(length)
127
+ if !chunk || chunk.empty?
128
+ # closing the writer will write one last chunk
129
+ # with a trailer (to be read from the @buffer)
130
+ @gzip_writer.close
131
+ else
132
+ # flush happens first to ensure that header fields
133
+ # are being sent over since write will override
134
+ @gzip_writer.flush
135
+ @gzip_writer.write(chunk)
136
+ end
137
+
138
+ StringIO.new(@buffer.last_chunk).read(length, buff)
139
+ end
140
+ end
141
+
142
+ # @api private
143
+ class ChunkBuffer
144
+ def initialize
145
+ @last_chunk = nil
146
+ end
147
+
148
+ attr_reader :last_chunk
149
+
150
+ def write(data)
151
+ @last_chunk = data
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
157
+ 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