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,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Middleware
5
+ # A middleware that resolves identities for signing requests.
6
+ # @api private
7
+ class Auth
8
+ include Middleware::Logging
9
+
10
+ # @param [Class] app The next middleware in the stack.
11
+ # @param [#resolve(auth_params)] auth_resolver A class that responds to a
12
+ # `resolve(auth_params)` method where `auth_params` is a struct with an
13
+ # operation_name. For a given operation_name, the method must return an
14
+ # ordered list of {Hearth::AuthOption} objects to be considered for
15
+ # authentication.
16
+ # @param [Struct] auth_params A struct with an operation_name and other
17
+ # parameters that may be used to resolve auth options.
18
+ # @param [Array<Hearth::AuthScheme::Base>] auth_schemes A list of
19
+ # auth schemes to consider for authentication.
20
+ def initialize(app, auth_resolver:, auth_params:, auth_schemes:, **kwargs)
21
+ @app = app
22
+ @auth_resolver = auth_resolver
23
+ @auth_params = auth_params
24
+ @auth_schemes = auth_schemes.to_h { |s| [s.scheme_id, s] }
25
+
26
+ @identity_resolvers = {}
27
+ kwargs.each do |key, value|
28
+ next unless key.superclass == Hearth::Identities::Base
29
+
30
+ @identity_resolvers[key] = value
31
+ end
32
+ end
33
+
34
+ # @param input
35
+ # @param context
36
+ # @return [Output]
37
+ def call(input, context)
38
+ log_debug(context, 'Resolving auth')
39
+ auth_options = @auth_resolver.resolve(@auth_params)
40
+ log_debug(context, "Resolved auth options: #{auth_options}")
41
+ context.auth = resolve_auth(auth_options)
42
+ log_debug(context, "Resolved auth: #{context.auth}")
43
+ @app.call(input, context)
44
+ end
45
+
46
+ private
47
+
48
+ ResolvedAuth = Struct.new(
49
+ :signer,
50
+ :signer_properties,
51
+ :identity,
52
+ :identity_properties,
53
+ keyword_init: true
54
+ )
55
+
56
+ def resolve_auth(auth_options)
57
+ failures = []
58
+
59
+ auth_options.each do |auth_option|
60
+ auth_scheme = @auth_schemes[auth_option.scheme_id]
61
+ resolved_auth = try_load_auth_scheme(
62
+ auth_option,
63
+ auth_scheme,
64
+ failures
65
+ )
66
+
67
+ return resolved_auth if resolved_auth
68
+ end
69
+
70
+ raise failures.join("\n")
71
+ end
72
+
73
+ def try_load_auth_scheme(auth_option, auth_scheme, failures)
74
+ scheme_id = auth_option.scheme_id
75
+ unless auth_scheme
76
+ failures << "Auth scheme #{scheme_id} was not enabled " \
77
+ 'for this request'
78
+ return
79
+ end
80
+
81
+ identity_resolver = auth_scheme.identity_resolver(@identity_resolvers)
82
+ unless identity_resolver
83
+ failures << "Auth scheme #{scheme_id} did not have an " \
84
+ 'identity resolver configured'
85
+ return
86
+ end
87
+
88
+ identity_properties = auth_option.identity_properties
89
+ identity = identity_resolver.identity(identity_properties)
90
+
91
+ ResolvedAuth.new(
92
+ identity: identity,
93
+ identity_properties: auth_option.identity_properties,
94
+ signer: auth_scheme.signer,
95
+ signer_properties: auth_option.signer_properties
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
@@ -5,6 +5,8 @@ module Hearth
5
5
  # A middleware that builds a request object.
6
6
  # @api private
7
7
  class Build
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] app The next middleware in the stack.
9
11
  # @param [Class] builder A builder object responsible for building the
10
12
  # request. It must respond to #build and take the request and input as
@@ -18,7 +20,37 @@ module Hearth
18
20
  # @param context
19
21
  # @return [Output]
20
22
  def call(input, context)
23
+ interceptor_error = Interceptors.invoke(
24
+ hook: Interceptor::MODIFY_BEFORE_SERIALIZATION,
25
+ input: input,
26
+ context: context,
27
+ output: nil,
28
+ aggregate_errors: false
29
+ )
30
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
31
+
32
+ interceptor_error = Interceptors.invoke(
33
+ hook: Interceptor::READ_BEFORE_SERIALIZATION,
34
+ input: input,
35
+ context: context,
36
+ output: nil,
37
+ aggregate_errors: false
38
+ )
39
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
40
+
41
+ log_debug(context, "Building request with: #{input}")
21
42
  @builder.build(context.request, input: input)
43
+ log_debug(context, "Built request: #{context.request.inspect}")
44
+
45
+ interceptor_error = Interceptors.invoke(
46
+ hook: Interceptor::READ_AFTER_SERIALIZATION,
47
+ input: input,
48
+ context: context,
49
+ output: nil,
50
+ aggregate_errors: false
51
+ )
52
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
53
+
22
54
  @app.call(input, context)
23
55
  end
24
56
  end
@@ -5,6 +5,8 @@ module Hearth
5
5
  # A middleware that prefixes the host.
6
6
  # @api private
7
7
  class HostPrefix
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] app The next middleware in the stack.
9
11
  # @param [Boolean] disable_host_prefix If true, this option will not
10
12
  # modify the host url.
@@ -21,25 +23,27 @@ module Hearth
21
23
  # @param context
22
24
  # @return [Output]
23
25
  def call(input, context)
24
- unless @disable_host_prefix
25
- prefix = apply_labels(@host_prefix, input)
26
- context.request.prefix_host(prefix)
27
- end
26
+ prefix_host(input, context) unless @disable_host_prefix
28
27
  @app.call(input, context)
29
28
  end
30
29
 
31
30
  private
32
31
 
32
+ def prefix_host(input, context)
33
+ log_debug(context, "Prefixing host with #{@host_prefix}")
34
+ prefix = apply_labels(@host_prefix, input)
35
+ context.request.prefix_host(prefix)
36
+ log_debug(context, "Prefixed host: #{context.request.uri.host}")
37
+ end
38
+
33
39
  def apply_labels(host_prefix, input)
34
40
  host_prefix.gsub(/\{.+?\}/) do |host_label|
35
41
  key = host_label.delete('{}')
36
42
  value = input[key.to_sym]
37
-
38
43
  if value.nil? || value.empty?
39
44
  raise ArgumentError,
40
45
  "Host label #{key} cannot be nil or empty."
41
46
  end
42
-
43
47
  value
44
48
  end
45
49
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hearth
4
+ module Middleware
5
+ # A middleware used to initialize the request, called first in the stack
6
+ # @api private
7
+ class Initialize
8
+ include Middleware::Logging
9
+
10
+ # @param [Class] app The next middleware in the stack.
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
+ interceptor_error = Interceptors.invoke(
20
+ hook: Interceptor::READ_BEFORE_EXECUTION,
21
+ input: input,
22
+ context: context,
23
+ output: nil,
24
+ aggregate_errors: true
25
+ )
26
+
27
+ output =
28
+ if interceptor_error
29
+ Hearth::Output.new(error: interceptor_error)
30
+ else
31
+ log_debug(context, "Initializing request with #{input}")
32
+ @app.call(input, context)
33
+ end
34
+ log_debug(context, 'Finished request')
35
+
36
+ interceptor_error = Interceptors.invoke(
37
+ hook: Interceptor::MODIFY_BEFORE_COMPLETION,
38
+ input: input,
39
+ context: context,
40
+ output: output,
41
+ aggregate_errors: false
42
+ )
43
+ output.error = interceptor_error if interceptor_error
44
+
45
+ interceptor_error = Interceptors.invoke(
46
+ hook: Interceptor::READ_AFTER_EXECUTION,
47
+ input: input,
48
+ context: context,
49
+ output: output,
50
+ aggregate_errors: true
51
+ )
52
+ output.error = interceptor_error if interceptor_error
53
+
54
+ output
55
+ end
56
+ end
57
+ end
58
+ end
@@ -5,6 +5,8 @@ module Hearth
5
5
  # A middleware that parses a response object.
6
6
  # @api private
7
7
  class Parse
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] app The next middleware in the stack.
9
11
  # @param [Class] error_parser A parser object responsible for parsing the
10
12
  # response if there is an error. It must respond to #parse and take the
@@ -23,19 +25,56 @@ module Hearth
23
25
  # @return [Output]
24
26
  def call(input, context)
25
27
  output = @app.call(input, context)
26
- parse_error(context.response, output) unless output.error
27
- parse_data(context.response, output) unless output.error
28
+
29
+ interceptor_error = Interceptors.invoke(
30
+ hook: Interceptor::MODIFY_BEFORE_DESERIALIZATION,
31
+ input: input,
32
+ context: context,
33
+ output: output,
34
+ aggregate_errors: false
35
+ )
36
+ if interceptor_error
37
+ output.error = interceptor_error
38
+ return output
39
+ end
40
+
41
+ interceptor_error = Interceptors.invoke(
42
+ hook: Interceptor::READ_BEFORE_DESERIALIZATION,
43
+ input: input,
44
+ context: context,
45
+ output: output,
46
+ aggregate_errors: false
47
+ )
48
+ if interceptor_error
49
+ output.error = interceptor_error
50
+ return output
51
+ end
52
+
53
+ parse_error(context, output) unless output.error
54
+ parse_data(context, output) unless output.error
55
+
56
+ interceptor_error = Interceptors.invoke(
57
+ hook: Interceptor::READ_AFTER_DESERIALIZATION,
58
+ input: input,
59
+ context: context,
60
+ output: output,
61
+ aggregate_errors: false
62
+ )
63
+ output.error = interceptor_error if interceptor_error
64
+
28
65
  output
29
66
  end
30
67
 
31
68
  private
32
69
 
33
- def parse_error(response, output)
34
- output.error = @error_parser.parse(response)
70
+ def parse_error(context, output)
71
+ output.error = @error_parser.parse(context.response, output.metadata)
72
+ log_debug(context, "Parsed error: #{output.error}") if output.error
35
73
  end
36
74
 
37
- def parse_data(response, output)
38
- output.data = @data_parser.parse(response)
75
+ def parse_data(context, output)
76
+ output.data = @data_parser.parse(context.response)
77
+ log_debug(context, "Parsed data: #{output.data}") if output.data
39
78
  end
40
79
  end
41
80
  end
@@ -2,41 +2,115 @@
2
2
 
3
3
  module Hearth
4
4
  module Middleware
5
- # A middleware that retries the request.
5
+ # A middleware that retries the request using a retry strategy.
6
6
  # @api private
7
7
  class Retry
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] app The next middleware in the stack.
9
- # @param [Integer] max_attempts The maximum number of attempts to make
10
- # before giving up.
11
- # @param [Integer] max_delay The maximum delay between attempts.
12
- def initialize(app, max_attempts:, max_delay:)
11
+ # @param [Strategy] retry_strategy (Standard) The retry strategy
12
+ # to use. Hearth has two built in classes, Standard and Adaptive.
13
+ # * `Retry::Standard` - A standardized set of retry rules across
14
+ # the AWS SDKs. This includes support for retry quotas, which limit
15
+ # the number of unsuccessful retries a client can make.
16
+ # * `Retry::Adaptive` - An experimental retry mode that includes
17
+ # all the functionality of `standard` mode along with automatic
18
+ # client side throttling. This is a provisional mode that may change
19
+ # behavior in the future.
20
+ def initialize(app, retry_strategy:, error_inspector_class:)
13
21
  @app = app
14
- @max_attempts = max_attempts
15
- @max_delay = max_delay
22
+ @retry_strategy = retry_strategy
23
+ @error_inspector_class = error_inspector_class
24
+
25
+ @retries = 0
16
26
  end
17
27
 
18
- # @param input
19
- # @param context
20
- # @return [Output]
21
28
  def call(input, context)
22
- attempt = 1
23
- begin
24
- @app.call(input, context)
25
- rescue Hearth::HTTP::NetworkingError => e
26
- raise e if attempt >= @max_attempts
27
-
28
- Kernel.sleep(backoff_with_jitter(attempt))
29
- attempt += 1
30
- retry
29
+ interceptor_error = Interceptors.invoke(
30
+ hook: Interceptor::MODIFY_BEFORE_RETRY_LOOP,
31
+ input: input,
32
+ context: context,
33
+ output: nil,
34
+ aggregate_errors: false
35
+ )
36
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
37
+
38
+ output = nil
39
+ token = @retry_strategy.acquire_initial_retry_token(nil)
40
+ log_debug(context, "Starting retry loop with token: #{token}")
41
+ loop do
42
+ interceptor_error = Interceptors.invoke(
43
+ hook: Interceptor::READ_BEFORE_ATTEMPT,
44
+ input: input,
45
+ context: context,
46
+ output: nil,
47
+ aggregate_errors: true
48
+ )
49
+
50
+ output =
51
+ if interceptor_error
52
+ Hearth::Output.new(error: interceptor_error)
53
+ else
54
+ log_debug(context, 'Attempting request in retry loop')
55
+ @app.call(input, context)
56
+ end
57
+
58
+ interceptor_error = Interceptors.invoke(
59
+ hook: Interceptor::MODIFY_BEFORE_ATTEMPT_COMPLETION,
60
+ input: input,
61
+ context: context,
62
+ output: output,
63
+ aggregate_errors: false
64
+ )
65
+ output.error = interceptor_error if interceptor_error
66
+
67
+ interceptor_error = Interceptors.invoke(
68
+ hook: Interceptor::READ_AFTER_ATTEMPT,
69
+ input: input,
70
+ context: context,
71
+ output: output,
72
+ aggregate_errors: true
73
+ )
74
+ output.error = interceptor_error if interceptor_error
75
+
76
+ if (error = output.error)
77
+ log_debug(context, "Request failed with error: #{error}")
78
+ error_info = @error_inspector_class.new(error, context.response)
79
+ token = @retry_strategy.refresh_retry_token(token, error_info)
80
+ break unless token
81
+
82
+ log_debug(context, "Retry token refreshed: #{token}")
83
+ log_debug(context, "Sleeping for #{token.retry_delay} seconds")
84
+ Kernel.sleep(token.retry_delay)
85
+ else
86
+ @retry_strategy.record_success(token)
87
+ log_debug(context, 'Request succeeded')
88
+ break
89
+ end
90
+
91
+ reset_request(context)
92
+ reset_response(context, output)
93
+ @retries += 1
31
94
  end
95
+ log_debug(context, 'Finished retry loop')
96
+ output
32
97
  end
33
98
 
34
99
  private
35
100
 
36
- # https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
37
- def backoff_with_jitter(attempt)
38
- # scales like 1,2,4,8
39
- Kernel.rand * [@max_delay, 2**(attempt - 1)].min
101
+ def reset_request(context)
102
+ request = context.request
103
+ request.body.rewind if request.body.respond_to?(:rewind)
104
+
105
+ context.auth.signer.reset(
106
+ request: request,
107
+ properties: context.auth.signer_properties
108
+ )
109
+ end
110
+
111
+ def reset_response(context, output)
112
+ context.response.reset
113
+ output.error = nil
40
114
  end
41
115
  end
42
116
  end
@@ -5,58 +5,170 @@ module Hearth
5
5
  # A middleware used to send the request.
6
6
  # @api private
7
7
  class Send
8
+ include Middleware::Logging
9
+
8
10
  # @param [Class] _app The next middleware in the stack.
9
11
  # @param [Boolean] stub_responses If true, a request is not sent and a
10
12
  # stubbed response is returned.
11
- # @param [Class] stub_class A stub object that is responsible for creating
12
- # a stubbed response. It must respond to #stub and take the response
13
- # and stub data as arguments.
14
- # @param [Stubs] stubs A {Hearth::Stubbing:Stubs} object containing
13
+ # @param [Class] stub_data_class A stub object that is responsible for
14
+ # creating a stubbed data response. It must respond to #stub and take
15
+ # the response and stub data as arguments.
16
+ # @param [Array<Class>] stub_error_classes An array of error classes
17
+ # that are responsible for creating a stubbed error response. They
18
+ # must respond to #stub and take the response and stub data as
19
+ # arguments.
20
+ # @param [Stubs] stubs A {Hearth::Stubs} object containing
15
21
  # stubbed data for any given operation.
16
- def initialize(_app, client:, stub_responses:, stub_class:, stubs:)
22
+ def initialize(_app, client:, stub_responses:,
23
+ stub_data_class:, stub_error_classes:, stubs:)
17
24
  @client = client
18
25
  @stub_responses = stub_responses
19
- @stub_class = stub_class
26
+ @stub_data_class = stub_data_class
27
+ @stub_error_classes = stub_error_classes
20
28
  @stubs = stubs
21
29
  end
22
30
 
23
- # @param _input
31
+ # @param input
24
32
  # @param context
25
33
  # @return [Output]
26
- def call(_input, context)
34
+ def call(input, context)
35
+ interceptor_error = Interceptors.invoke(
36
+ hook: Interceptor::MODIFY_BEFORE_TRANSMIT,
37
+ input: input,
38
+ context: context,
39
+ output: nil,
40
+ aggregate_errors: false
41
+ )
42
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
43
+
44
+ interceptor_error = Interceptors.invoke(
45
+ hook: Interceptor::READ_BEFORE_TRANSMIT,
46
+ input: input,
47
+ context: context,
48
+ output: nil,
49
+ aggregate_errors: false
50
+ )
51
+ return Hearth::Output.new(error: interceptor_error) if interceptor_error
52
+
53
+ output = Output.new
27
54
  if @stub_responses
28
- stub = @stubs.next(context.operation_name)
29
- output = Output.new
30
- apply_stub(stub, context, output)
31
- output
55
+ stub_response(input, context, output)
32
56
  else
33
- @client.transmit(
34
- request: context.request,
35
- response: context.response
36
- )
37
- Output.new
57
+ send_request(context, output)
38
58
  end
59
+
60
+ interceptor_error = Interceptors.invoke(
61
+ hook: Interceptor::READ_AFTER_TRANSMIT,
62
+ input: input,
63
+ context: context,
64
+ output: output,
65
+ aggregate_errors: false
66
+ )
67
+ output.error = interceptor_error if interceptor_error
68
+
69
+ output
39
70
  end
40
71
 
41
72
  private
42
73
 
43
- def apply_stub(stub, context, output)
74
+ def stub_response(input, context, output)
75
+ stub = @stubs.next(context.operation_name)
76
+ log_debug(context, "Stubbing response with stub: #{stub}")
77
+ apply_stub(stub, input, context, output)
78
+ log_debug(context, "Stubbed response: #{context.response.inspect}")
79
+ return unless context.response.body.respond_to?(:rewind)
80
+
81
+ context.response.body.rewind
82
+ end
83
+
84
+ def send_request(context, output)
85
+ log_debug(context, "Sending request: #{context.request.inspect}")
86
+ @client.transmit(
87
+ request: context.request,
88
+ response: context.response,
89
+ logger: context.logger
90
+ )
91
+ log_debug(context, "Received response: #{context.response.inspect}")
92
+ rescue Hearth::NetworkingError => e
93
+ output.error = e
94
+ end
95
+
96
+ def apply_stub(stub, input, context, output)
44
97
  case stub
45
98
  when Proc
46
- stub = stub.call(context)
47
- apply_stub(stub, context, output) if stub
48
- when Exception
99
+ stub = stub.call(input)
100
+ apply_stub(stub, input, context, output)
101
+ when Exception, ApiError
49
102
  output.error = stub
50
- when Class
51
- output.error = stub.new
52
103
  when Hash
53
- @stub_class.stub(context.response, stub: stub)
104
+ apply_stub_hash(stub, context)
54
105
  when NilClass
55
- @stub_class.stub(context.response, stub: @stub_class.default)
106
+ apply_stub_nil(context)
107
+ when Hearth::Structure
108
+ apply_stub_hearth_structure(stub, context)
109
+ when Hearth::Response
110
+ context.response.replace(stub)
56
111
  else
57
112
  raise ArgumentError, 'Unsupported stub type'
58
113
  end
59
114
  end
115
+
116
+ def apply_stub_hash(stub, context)
117
+ if stub.key?(:error) && !stub.key?(:data)
118
+ apply_stub_hash_error(stub, context)
119
+ elsif stub.key?(:data) && !stub.key?(:error)
120
+ apply_stub_hash_data(stub, context)
121
+ else
122
+ raise ArgumentError, 'Unsupported stub hash, must be :data or :error'
123
+ end
124
+ end
125
+
126
+ def apply_stub_hash_data(stub, context)
127
+ output = @stub_data_class.build(stub[:data], context: 'stub')
128
+ @stub_data_class.validate!(output, context: 'stub')
129
+ @stub_data_class.stub(context.response, stub: output)
130
+ end
131
+
132
+ def apply_stub_hash_error(stub, context)
133
+ stub_error_class = stub_error_class(stub[:error][:class])
134
+ output = stub_error_class.build(
135
+ stub[:error][:data] || {},
136
+ context: 'stub'
137
+ )
138
+ stub_error_class.validate!(output, context: 'stub')
139
+ stub_error_class.stub(context.response, stub: output)
140
+ end
141
+
142
+ def stub_error_class(error_class)
143
+ raise ArgumentError, 'Missing stub error class' unless error_class
144
+
145
+ unless error_class.is_a?(Class)
146
+ raise ArgumentError, 'Stub error class must be a class'
147
+ end
148
+
149
+ error_base_name = error_class.name.split('::').last
150
+ stub_class = @stub_error_classes.find do |stub_error_class|
151
+ stub_base_name = stub_error_class.name.split('::').last
152
+ error_base_name == stub_base_name
153
+ end
154
+ raise ArgumentError, 'Unsupported stub error class' unless stub_class
155
+
156
+ stub_class
157
+ end
158
+
159
+ def apply_stub_nil(context)
160
+ output = @stub_data_class.build(
161
+ @stub_data_class.default,
162
+ context: 'stub'
163
+ )
164
+ @stub_data_class.validate!(output, context: 'stub')
165
+ @stub_data_class.stub(context.response, stub: output)
166
+ end
167
+
168
+ def apply_stub_hearth_structure(stub, context)
169
+ @stub_data_class.validate!(stub, context: 'stub')
170
+ @stub_data_class.stub(context.response, stub: stub)
171
+ end
60
172
  end
61
173
  end
62
174
  end