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,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