hearth 1.0.0.pre1

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 (57) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +12 -0
  3. data/VERSION +1 -0
  4. data/lib/hearth/api_error.rb +15 -0
  5. data/lib/hearth/block_io.rb +24 -0
  6. data/lib/hearth/context.rb +34 -0
  7. data/lib/hearth/http/api_error.rb +29 -0
  8. data/lib/hearth/http/client.rb +152 -0
  9. data/lib/hearth/http/error_parser.rb +105 -0
  10. data/lib/hearth/http/headers.rb +70 -0
  11. data/lib/hearth/http/middleware/content_length.rb +29 -0
  12. data/lib/hearth/http/networking_error.rb +20 -0
  13. data/lib/hearth/http/request.rb +132 -0
  14. data/lib/hearth/http/response.rb +29 -0
  15. data/lib/hearth/http.rb +36 -0
  16. data/lib/hearth/json/parse_error.rb +18 -0
  17. data/lib/hearth/json.rb +30 -0
  18. data/lib/hearth/middleware/around_handler.rb +24 -0
  19. data/lib/hearth/middleware/build.rb +26 -0
  20. data/lib/hearth/middleware/host_prefix.rb +48 -0
  21. data/lib/hearth/middleware/parse.rb +42 -0
  22. data/lib/hearth/middleware/request_handler.rb +24 -0
  23. data/lib/hearth/middleware/response_handler.rb +25 -0
  24. data/lib/hearth/middleware/retry.rb +43 -0
  25. data/lib/hearth/middleware/send.rb +62 -0
  26. data/lib/hearth/middleware/validate.rb +29 -0
  27. data/lib/hearth/middleware.rb +16 -0
  28. data/lib/hearth/middleware_builder.rb +246 -0
  29. data/lib/hearth/middleware_stack.rb +73 -0
  30. data/lib/hearth/number_helper.rb +33 -0
  31. data/lib/hearth/output.rb +20 -0
  32. data/lib/hearth/structure.rb +40 -0
  33. data/lib/hearth/stubbing/client_stubs.rb +115 -0
  34. data/lib/hearth/stubbing/stubs.rb +32 -0
  35. data/lib/hearth/time_helper.rb +35 -0
  36. data/lib/hearth/union.rb +10 -0
  37. data/lib/hearth/validator.rb +20 -0
  38. data/lib/hearth/waiters/errors.rb +15 -0
  39. data/lib/hearth/waiters/poller.rb +132 -0
  40. data/lib/hearth/waiters/waiter.rb +79 -0
  41. data/lib/hearth/xml/formatter.rb +68 -0
  42. data/lib/hearth/xml/node.rb +123 -0
  43. data/lib/hearth/xml/node_matcher.rb +24 -0
  44. data/lib/hearth/xml/parse_error.rb +18 -0
  45. data/lib/hearth/xml.rb +58 -0
  46. data/lib/hearth.rb +26 -0
  47. data/sig/lib/seahorse/api_error.rbs +10 -0
  48. data/sig/lib/seahorse/document.rbs +2 -0
  49. data/sig/lib/seahorse/http/api_error.rbs +21 -0
  50. data/sig/lib/seahorse/http/headers.rbs +47 -0
  51. data/sig/lib/seahorse/http/response.rbs +21 -0
  52. data/sig/lib/seahorse/simple_delegator.rbs +3 -0
  53. data/sig/lib/seahorse/structure.rbs +18 -0
  54. data/sig/lib/seahorse/stubbing/client_stubs.rbs +103 -0
  55. data/sig/lib/seahorse/stubbing/stubs.rbs +14 -0
  56. data/sig/lib/seahorse/union.rbs +6 -0
  57. metadata +111 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b43b47b8abef04f52e1f151032c376b90b3112e23c3084fbba6dd777e3f95180
4
+ data.tar.gz: a902f668849d1c28d0afd66ce2c3b80eac6a5626187e3c44a1d13d99234576e0
5
+ SHA512:
6
+ metadata.gz: 4958477705f77ead625d1bbb49b2c11b0e44542af63bf9aaa95d87bba82a73cdbed65b40461f56e6bc9388cb612cf4cb4c341e7ba1a9b2502076ffa809e3cf14
7
+ data.tar.gz: 44f0d91c07b0f431b9660f459307daebbbbe8824ad90b4009cfdff63fa139c340201ed88667f80909bbd973bdc40216c74efd4601ce9fbbfb5d3c81e2f2bed2d
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ Unreleased Changes
2
+ ------------------
3
+
4
+ 1.0.0.pre1 (2022-01-10)
5
+ ------------------
6
+
7
+ * Feature - Initial public pre-release for Smithy Ruby SDKs.
8
+
9
+ 0.1.0 (2013-29-04)
10
+ ------------------
11
+
12
+ Not intended for public usage. Used as an internal detail of AWS SDK For Ruby v2 and v3.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0.pre1
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Base class for errors returned from an API. This excludes networking
5
+ # errors and errors generated on the client-side.
6
+ class ApiError < StandardError
7
+ def initialize(error_code:, message: nil)
8
+ @error_code = error_code
9
+ super(message)
10
+ end
11
+
12
+ # @return [String]
13
+ attr_reader :error_code
14
+ end
15
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Given a block, BlockIO will call it with data to write and return the
5
+ # bytesize written. BlockIO keeps track of all bytes yielded.
6
+ class BlockIO
7
+ # @param [Proc] block
8
+ def initialize(block)
9
+ @block = block
10
+ @bytes_yielded = 0
11
+ end
12
+
13
+ # @return [Integer]
14
+ attr_reader :bytes_yielded
15
+
16
+ # @param [String] data
17
+ # @return [Integer] Returns the number of bytes written.
18
+ def write(data)
19
+ @block.call(data)
20
+ @bytes_yielded += data.bytesize
21
+ data.bytesize
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ # Stores request and response objects, and other useful things used by
5
+ # multiple Middleware.
6
+ class Context
7
+ def initialize(options = {})
8
+ @operation_name = options[:operation_name]
9
+ @request = options[:request]
10
+ @response = options[:response]
11
+ @logger = options[:logger]
12
+ @params = options[:params]
13
+ @metadata = options[:metadata] || {}
14
+ end
15
+
16
+ # @return [Symbol] Name of the API operation called.
17
+ attr_reader :operation_name
18
+
19
+ # @return [Hearth::HTTP::Request]
20
+ attr_reader :request
21
+
22
+ # @return [Hearth::HTTP::Response]
23
+ attr_reader :response
24
+
25
+ # @return [Logger] An instance of the logger configured for the Client.
26
+ attr_reader :logger
27
+
28
+ # @return [Hash] The hash of the original request parameters.
29
+ attr_reader :params
30
+
31
+ # @return [Hash]
32
+ attr_reader :metadata
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # Base class for HTTP errors returned from an API. Inherits from
6
+ # {Hearth::ApiError}.
7
+ class ApiError < Hearth::ApiError
8
+ def initialize(http_resp:, **kwargs)
9
+ @http_status = http_resp.status
10
+ @http_headers = http_resp.headers
11
+ @http_body = http_resp.body
12
+ @request_id = http_resp.headers['x-request-id']
13
+ super(**kwargs)
14
+ end
15
+
16
+ # @return [Integer]
17
+ attr_reader :http_status
18
+
19
+ # @return [Hash<String, String>]
20
+ attr_reader :http_headers
21
+
22
+ # @return [String]
23
+ attr_reader :http_body
24
+
25
+ # @return [String]
26
+ attr_reader :request_id
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'logger'
5
+ require 'openssl'
6
+
7
+ module Hearth
8
+ module HTTP
9
+ # Transmits an HTTP {Request} object, returning an HTTP {Response}.
10
+ # @api private
11
+ class Client
12
+ # Initialize an instance of this HTTP client.
13
+ #
14
+ # @param [Hash] options The options for this HTTP Client
15
+ #
16
+ # @option options [Boolean] :http_wire_trace (false) When `true`,
17
+ # HTTP debug output will be sent to the `:logger`.
18
+ #
19
+ # @option options [Logger] :logger A logger where debug output is sent.
20
+ #
21
+ # @option options [URI::HTTP,String] :http_proxy A proxy to send
22
+ # requests through. Formatted like 'http://proxy.com:123'.
23
+ #
24
+ # @option options [Boolean] :ssl_verify_peer (true) When `true`,
25
+ # SSL peer certificates are verified when establishing a
26
+ # connection.
27
+ #
28
+ # @option options [String] :ssl_ca_bundle Full path to the SSL
29
+ # certificate authority bundle file that should be used when
30
+ # verifying peer certificates. If you do not pass
31
+ # `:ssl_ca_bundle` or `:ssl_ca_directory` the system default
32
+ # will be used if available.
33
+ #
34
+ # @option options [String] :ssl_ca_directory Full path of the
35
+ # directory that contains the unbundled SSL certificate
36
+ # authority files for verifying peer certificates. If you do
37
+ # not pass `:ssl_ca_bundle` or `:ssl_ca_directory` the
38
+ # system default will be used if available.
39
+ def initialize(options = {})
40
+ @http_wire_trace = options[:http_wire_trace]
41
+ @logger = options[:logger]
42
+ @http_proxy = options[:http_proxy]
43
+ @http_proxy = URI.parse(@http_proxy.to_s) if @http_proxy
44
+ @ssl_verify_peer = options[:ssl_verify_peer]
45
+ @ssl_ca_bundle = options[:ssl_ca_bundle]
46
+ @ssl_ca_directory = options[:ssl_ca_directory]
47
+ @ssl_ca_store = options[:ssl_ca_store]
48
+ end
49
+
50
+ # @param [Request] request
51
+ # @param [Response] response
52
+ # @return [Response]
53
+ def transmit(request:, response:)
54
+ uri = URI.parse(request.url)
55
+ http = create_http(uri)
56
+ http.set_debug_output(@logger) if @http_wire_trace
57
+
58
+ if uri.scheme == 'https'
59
+ configure_ssl(http)
60
+ else
61
+ http.use_ssl = false
62
+ end
63
+
64
+ _transmit(http, request, response)
65
+ response.body.rewind if response.body.respond_to?(:rewind)
66
+ response
67
+ rescue ArgumentError => e
68
+ # Invalid verb, ArgumentError is a StandardError
69
+ raise e
70
+ rescue StandardError => e
71
+ raise Hearth::HTTP::NetworkingError, e
72
+ end
73
+
74
+ private
75
+
76
+ def _transmit(http, request, response)
77
+ http.start do |conn|
78
+ conn.request(build_net_request(request)) do |net_resp|
79
+ response.status = net_resp.code.to_i
80
+ response.headers = extract_headers(net_resp)
81
+ net_resp.read_body do |chunk|
82
+ response.body.write(chunk)
83
+ end
84
+ end
85
+ end
86
+ end
87
+
88
+ # Creates an HTTP connection to the endpoint
89
+ # Applies proxy if set
90
+ def create_http(endpoint)
91
+ args = []
92
+ args << endpoint.host
93
+ args << endpoint.port
94
+ args += http_proxy_parts if @http_proxy
95
+ # Net::HTTP.new uses positional arguments: host, port, proxy_args....
96
+ Net::HTTP.new(*args.compact)
97
+ end
98
+
99
+ # applies ssl settings to the HTTP object
100
+ def configure_ssl(http)
101
+ http.use_ssl = true
102
+ if @ssl_verify_peer
103
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
104
+ http.ca_file = @ssl_ca_bundle if @ssl_ca_bundle
105
+ http.ca_path = @ssl_ca_directory if @ssl_ca_directory
106
+ http.cert_store = @ssl_ca_store if @ssl_ca_store
107
+ else
108
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
109
+ end
110
+ end
111
+
112
+ # Constructs and returns a Net::HTTP::Request object from
113
+ # a {Http::Request}.
114
+ # @param [Http::Request] request
115
+ # @return [Net::HTTP::Request]
116
+ def build_net_request(request)
117
+ request_class = net_http_request_class(request)
118
+ req = request_class.new(request.url, request.headers.to_h)
119
+ req.body_stream = request.body
120
+ req
121
+ end
122
+
123
+ # @param [Net::HTTP::Response] response
124
+ # @return [Hash<String, String>]
125
+ def extract_headers(response)
126
+ response.to_hash.transform_values(&:first)
127
+ end
128
+
129
+ # @param [Http::Request] request
130
+ # @raise [InvalidHttpVerbError]
131
+ # @return Returns a base `Net::HTTP::Request` class, e.g.,
132
+ # `Net::HTTP::Get`, `Net::HTTP::Post`, etc.
133
+ def net_http_request_class(request)
134
+ Net::HTTP.const_get(request.http_method.capitalize)
135
+ rescue NameError
136
+ msg = "`#{request.http_method}` is not a valid http verb"
137
+ raise ArgumentError, msg
138
+ end
139
+
140
+ # Extract the parts of the http_proxy URI
141
+ # @return [Array(String)]
142
+ def http_proxy_parts
143
+ [
144
+ @http_proxy.host,
145
+ @http_proxy.port,
146
+ (@http_proxy.user && CGI.unescape(@http_proxy.user)),
147
+ (@http_proxy.password && CGI.unescape(@http_proxy.password))
148
+ ]
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # Uses HTTP specific logic + Protocol defined Errors and
6
+ # Error Code function to determine if a response should
7
+ # be parsed as an Error. Contains generic Error parsing
8
+ # logic as well.
9
+ # @api private
10
+ class ErrorParser
11
+ # @api private
12
+ HTTP_3XX = (300..399).freeze
13
+
14
+ # @api private
15
+ HTTP_4XX = (400..499).freeze
16
+
17
+ # @api private
18
+ HTTP_5XX = (500..599).freeze
19
+
20
+ # @param [Module] error_module The code generated Errors module.
21
+ # Must contain service specific implementations of
22
+ # ApiRedirectError, ApiClientError, and ApiServerError
23
+ #
24
+ # @param [Integer] success_status The status code of a
25
+ # successful response as defined by the model for
26
+ # this operation. If this is a non 2XX value,
27
+ # the request will be considered successful if
28
+ # it has the success_status and does not
29
+ # have an error code.
30
+ #
31
+ # @param [Array<Class<ApiError>>] errors Array of Error classes
32
+ # modeled for the operation.
33
+ #
34
+ # @param [callable] error_code_fn Protocol specific function
35
+ # that will return the error code from a response, or nil if
36
+ # there is none.
37
+ def initialize(error_module:, success_status:, errors:, error_code_fn:)
38
+ @error_module = error_module
39
+ @success_status = success_status
40
+ @errors = errors
41
+ @error_code_fn = error_code_fn
42
+ end
43
+
44
+ # Parse and return the error if the response is not successful.
45
+ #
46
+ # @param [Response] response The HTTP response
47
+ def parse(response)
48
+ extract_error(response) if error?(response)
49
+ end
50
+
51
+ private
52
+
53
+ # Implements the following order of precedence
54
+ # 1. Response has error_code -> error
55
+ # 2. Response code == http trait status code? -> success
56
+ # 3. Response code matches any error status codes? -> error
57
+ # [EXCLUDED, covered by error_code]
58
+ # 4. Response code is 2xx? -> success
59
+ # 6. Response code 5xx -> unknown server error
60
+ # [MODIFIED, 3xx, 4xx, 5xx mapped, everything else is Generic ApiError]
61
+ # 7. Everything else -> unknown client error
62
+ def error?(http_resp)
63
+ return true if @error_code_fn.call(http_resp)
64
+ return false if http_resp.status == @success_status
65
+
66
+ !(200..299).cover?(http_resp.status)
67
+ end
68
+
69
+ def extract_error(http_resp)
70
+ error_code = @error_code_fn.call(http_resp)
71
+ error_class = error_class(error_code) if error_code
72
+
73
+ error_opts = {
74
+ http_resp: http_resp,
75
+ error_code: error_code,
76
+ message: error_code # default message
77
+ }
78
+
79
+ if error_class
80
+ error_class.new(**error_opts)
81
+ else
82
+ generic_error(error_opts)
83
+ end
84
+ end
85
+
86
+ def error_class(error_code)
87
+ @errors.find do |e|
88
+ e.name.include? error_code
89
+ end
90
+ end
91
+
92
+ def generic_error(error_opts)
93
+ http_resp = error_opts[:http_resp]
94
+ case http_resp.status
95
+ when HTTP_3XX then @error_module::ApiRedirectError.new(
96
+ location: http_resp.headers['location'], **error_opts
97
+ )
98
+ when HTTP_4XX then @error_module::ApiClientError.new(**error_opts)
99
+ when HTTP_5XX then @error_module::ApiServerError.new(**error_opts)
100
+ else @error_module::ApiError.new(**error_opts)
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ # Provides Hash like access for Headers with key normalization
6
+ # @api private
7
+ class Headers
8
+ # @param [Hash<String,String>] headers
9
+ def initialize(headers: {})
10
+ @headers = {}
11
+ headers.each_pair do |key, value|
12
+ self[key] = value
13
+ end
14
+ end
15
+
16
+ # @param [String] key
17
+ def [](key)
18
+ @headers[normalize(key)]
19
+ end
20
+
21
+ # @param [String] key
22
+ # @param [String] value
23
+ def []=(key, value)
24
+ @headers[normalize(key)] = value.to_s
25
+ end
26
+
27
+ # @param [String] key
28
+ # @return [Boolean] Returns `true` if there is a header with
29
+ # the given key.
30
+ def key?(key)
31
+ @headers.key?(normalize(key))
32
+ end
33
+
34
+ # @return [Array<String>]
35
+ def keys
36
+ @headers.keys
37
+ end
38
+
39
+ # @param [String] key
40
+ # @return [String, nil] Returns the value for the deleted key.
41
+ def delete(key)
42
+ @headers.delete(normalize(key))
43
+ end
44
+
45
+ # @return [Enumerable<String,String>]
46
+ def each_pair(&block)
47
+ @headers.each(&block)
48
+ end
49
+ alias each each_pair
50
+
51
+ # @return [Hash]
52
+ def to_hash
53
+ @headers.dup
54
+ end
55
+ alias to_h to_hash
56
+
57
+ # @return [Integer] Returns the number of entries in the headers
58
+ # hash.
59
+ def size
60
+ @headers.size
61
+ end
62
+
63
+ private
64
+
65
+ def normalize(key)
66
+ key.to_s.gsub(/[^-]+/, &:capitalize)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module HTTP
5
+ module Middleware
6
+ # A middleware that sets Content-Length for any body that has a size.
7
+ # @api private
8
+ class ContentLength
9
+ def initialize(app, _ = {})
10
+ @app = app
11
+ end
12
+
13
+ # @param input
14
+ # @param context
15
+ # @return [Output]
16
+ def call(input, context)
17
+ request = context.request
18
+ if request&.body.respond_to?(:size) &&
19
+ !request.headers.key?('Content-Length')
20
+ length = request.body.size
21
+ request.headers['Content-Length'] = length
22
+ end
23
+
24
+ @app.call(input, context)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
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
19
+ end
20
+ end
@@ -0,0 +1,132 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+ require 'uri'
5
+
6
+ module Hearth
7
+ module HTTP
8
+ # Represents an HTTP request.
9
+ # @api private
10
+ class Request
11
+ # @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)
17
+ @http_method = http_method
18
+ @url = url
19
+ @headers = headers
20
+ @body = body
21
+ end
22
+
23
+ # @return [String]
24
+ attr_accessor :http_method
25
+
26
+ # @return [String]
27
+ attr_accessor :url
28
+
29
+ # @return [Headers]
30
+ attr_accessor :headers
31
+
32
+ # @return [IO]
33
+ attr_accessor :body
34
+
35
+ # Append a path to the HTTP request URL.
36
+ #
37
+ # http_req.url = "https://example.com"
38
+ # http_req.append_path('/')
39
+ # http_req.url
40
+ # #=> "https://example.com/"
41
+ #
42
+ # Paths will be joined by a single '/':
43
+ #
44
+ # http_req.url = "https://example.com/path-prefix/"
45
+ # http_req.append_path('/path-suffix')
46
+ # http_req.url
47
+ # #=> "https://example.com/path-prefix/path-suffix"
48
+ #
49
+ # Resultant URL preserves the querystring:
50
+ #
51
+ # http_req.url = "https://example.com/path-prefix?querystring
52
+ # http_req.append_path('/path-suffix')
53
+ # http_req.url
54
+ # #=> "https://example.com/path-prefix/path-suffix?querystring"
55
+ #
56
+ # The provided path should be URI escaped before being passed.
57
+ #
58
+ # http_req.url = "https://example.com
59
+ # http_req.append_path(
60
+ # Hearth::HTTP.uri_escape_path('/part 1/part 2')
61
+ # )
62
+ # http_req.url
63
+ # #=> "https://example.com/part%201/part%202"
64
+ #
65
+ # @param [String] path A URI escaped path.
66
+ def append_path(path)
67
+ uri = URI.parse(@url)
68
+ base_path = uri.path.sub(%r{/$}, '') # remove trailing slash
69
+ path = path.sub(%r{^/}, '') # remove prefix slash
70
+ uri.path = "#{base_path}/#{path}" # join on single slash
71
+ @url = uri.to_s
72
+ end
73
+
74
+ # Append querystring parameter to the HTTP request URL.
75
+ #
76
+ # http_req.url = "https://example.com"
77
+ # http_req.append_query_param('query')
78
+ # http_req.append_query_param('key 1', 'value 1')
79
+ #
80
+ # http_req.url
81
+ # #=> "https://example.com?query&key%201=value%201
82
+ #
83
+ # @overload append_query_param(name)
84
+ # @param [String] name
85
+ # The name of the querystring parameter to add. This name
86
+ # will be URI escaped.
87
+ #
88
+ # @overload append_query_param(name, value)
89
+ # @param [String] name
90
+ # The name of the querystring parameter to add. This name
91
+ # will be URI escaped.
92
+ # @param [String] value
93
+ # The value of the querystring parameter to add. This value
94
+ # will be URI escaped.
95
+ #
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
107
+ end
108
+
109
+ # Append a host prefix to the HTTP request URL.
110
+ #
111
+ # http_req.url = "https://example.com"
112
+ # http_req.prefix_host('data.')
113
+ #
114
+ # http_req.url
115
+ # #=> "https://data.foo.com
116
+ #
117
+ # @param [String] prefix A dot (.) terminated prefix for the host.
118
+ #
119
+ def prefix_host(prefix)
120
+ uri = URI.parse(@url)
121
+ 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
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stringio'
4
+
5
+ module Hearth
6
+ module HTTP
7
+ # Represents an HTTP Response.
8
+ # @api private
9
+ class Response
10
+ # @param [Integer] status
11
+ # @param [Headers] headers
12
+ # @param [IO] body
13
+ def initialize(status: 200, headers: Headers.new, body: StringIO.new)
14
+ @status = status
15
+ @headers = headers
16
+ @body = body
17
+ end
18
+
19
+ # @return [Integer]
20
+ attr_accessor :status
21
+
22
+ # @return [Headers]
23
+ attr_accessor :headers
24
+
25
+ # @return [IO]
26
+ attr_accessor :body
27
+ end
28
+ end
29
+ end