hearth 1.0.0.pre1

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