hearth 1.0.0.pre1 → 1.0.0.pre2

Sign up to get free protection for your applications and to get access to all the features.
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