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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -4
- data/VERSION +1 -1
- data/lib/hearth/api_error.rb +15 -1
- data/lib/hearth/auth_option.rb +21 -0
- data/lib/hearth/auth_schemes/anonymous.rb +21 -0
- data/lib/hearth/auth_schemes/http_api_key.rb +16 -0
- data/lib/hearth/auth_schemes/http_basic.rb +16 -0
- data/lib/hearth/auth_schemes/http_bearer.rb +16 -0
- data/lib/hearth/auth_schemes/http_digest.rb +16 -0
- data/lib/hearth/auth_schemes.rb +32 -0
- data/lib/hearth/checksums.rb +31 -0
- data/lib/hearth/client_stubs.rb +130 -0
- data/lib/hearth/config/env_provider.rb +53 -0
- data/lib/hearth/config/resolver.rb +52 -0
- data/lib/hearth/configuration.rb +15 -0
- data/lib/hearth/connection_pool.rb +77 -0
- data/lib/hearth/context.rb +28 -4
- data/lib/hearth/dns/host_address.rb +23 -0
- data/lib/hearth/dns/host_resolver.rb +92 -0
- data/lib/hearth/dns.rb +48 -0
- data/lib/hearth/http/api_error.rb +4 -8
- data/lib/hearth/http/client.rb +208 -59
- data/lib/hearth/http/error_inspector.rb +85 -0
- data/lib/hearth/http/error_parser.rb +18 -20
- data/lib/hearth/http/field.rb +64 -0
- data/lib/hearth/http/fields.rb +117 -0
- data/lib/hearth/http/middleware/content_length.rb +5 -2
- data/lib/hearth/http/middleware/content_md5.rb +31 -0
- data/lib/hearth/http/middleware/request_compression.rb +157 -0
- data/lib/hearth/http/middleware.rb +12 -0
- data/lib/hearth/http/networking_error.rb +1 -14
- data/lib/hearth/http/request.rb +83 -56
- data/lib/hearth/http/response.rb +42 -13
- data/lib/hearth/http.rb +14 -5
- data/lib/hearth/identities/anonymous.rb +8 -0
- data/lib/hearth/identities/http_api_key.rb +16 -0
- data/lib/hearth/identities/http_bearer.rb +16 -0
- data/lib/hearth/identities/http_login.rb +20 -0
- data/lib/hearth/identities.rb +21 -0
- data/lib/hearth/identity_resolver.rb +17 -0
- data/lib/hearth/interceptor.rb +506 -0
- data/lib/hearth/interceptor_context.rb +36 -0
- data/lib/hearth/interceptor_list.rb +48 -0
- data/lib/hearth/interceptors.rb +75 -0
- data/lib/hearth/middleware/auth.rb +100 -0
- data/lib/hearth/middleware/build.rb +32 -0
- data/lib/hearth/middleware/host_prefix.rb +10 -6
- data/lib/hearth/middleware/initialize.rb +58 -0
- data/lib/hearth/middleware/parse.rb +45 -6
- data/lib/hearth/middleware/retry.rb +97 -23
- data/lib/hearth/middleware/send.rb +137 -25
- data/lib/hearth/middleware/sign.rb +65 -0
- data/lib/hearth/middleware/validate.rb +11 -1
- data/lib/hearth/middleware.rb +19 -8
- data/lib/hearth/middleware_stack.rb +1 -43
- data/lib/hearth/networking_error.rb +18 -0
- data/lib/hearth/number_helper.rb +2 -2
- data/lib/hearth/output.rb +8 -4
- data/lib/hearth/plugin_list.rb +53 -0
- data/lib/hearth/query/param.rb +52 -0
- data/lib/hearth/query/param_list.rb +54 -0
- data/lib/hearth/query/param_matcher.rb +32 -0
- data/lib/hearth/refreshing_identity_resolver.rb +63 -0
- data/lib/hearth/request.rb +22 -0
- data/lib/hearth/response.rb +33 -0
- data/lib/hearth/retry/adaptive.rb +60 -0
- data/lib/hearth/retry/capacity_not_available_error.rb +9 -0
- data/lib/hearth/retry/client_rate_limiter.rb +143 -0
- data/lib/hearth/retry/exponential_backoff.rb +15 -0
- data/lib/hearth/retry/retry_quota.rb +56 -0
- data/lib/hearth/retry/standard.rb +46 -0
- data/lib/hearth/retry/strategy.rb +20 -0
- data/lib/hearth/retry.rb +16 -0
- data/lib/hearth/signers/anonymous.rb +16 -0
- data/lib/hearth/signers/http_api_key.rb +29 -0
- data/lib/hearth/signers/http_basic.rb +23 -0
- data/lib/hearth/signers/http_bearer.rb +19 -0
- data/lib/hearth/signers/http_digest.rb +19 -0
- data/lib/hearth/signers.rb +23 -0
- data/lib/hearth/stubs.rb +30 -0
- data/lib/hearth/time_helper.rb +5 -3
- data/lib/hearth/validator.rb +44 -5
- data/lib/hearth/waiters/poller.rb +6 -7
- data/lib/hearth/waiters/waiter.rb +17 -4
- data/lib/hearth/xml/formatter.rb +11 -2
- data/lib/hearth/xml/node.rb +2 -2
- data/lib/hearth.rb +32 -5
- data/sig/lib/hearth/aliases.rbs +4 -0
- data/sig/lib/hearth/api_error.rbs +13 -0
- data/sig/lib/hearth/auth_option.rbs +11 -0
- data/sig/lib/hearth/auth_schemes/anonymous.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_api_key.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_basic.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_bearer.rbs +7 -0
- data/sig/lib/hearth/auth_schemes/http_digest.rbs +7 -0
- data/sig/lib/hearth/auth_schemes.rbs +13 -0
- data/sig/lib/hearth/block_io.rbs +9 -0
- data/sig/lib/hearth/client_stubs.rbs +5 -0
- data/sig/lib/hearth/configuration.rbs +7 -0
- data/sig/lib/hearth/dns/host_address.rbs +13 -0
- data/sig/lib/hearth/dns/host_resolver.rbs +19 -0
- data/sig/lib/hearth/http/api_error.rbs +13 -0
- data/sig/lib/hearth/http/client.rbs +9 -0
- data/sig/lib/hearth/http/field.rbs +19 -0
- data/sig/lib/hearth/http/fields.rbs +43 -0
- data/sig/lib/hearth/http/request.rbs +25 -0
- data/sig/lib/hearth/http/response.rbs +21 -0
- data/sig/lib/hearth/identities/anonymous.rbs +6 -0
- data/sig/lib/hearth/identities/http_api_key.rbs +9 -0
- data/sig/lib/hearth/identities/http_bearer.rbs +9 -0
- data/sig/lib/hearth/identities/http_login.rbs +11 -0
- data/sig/lib/hearth/identities.rbs +9 -0
- data/sig/lib/hearth/identity_resolver.rbs +7 -0
- data/sig/lib/hearth/interceptor.rbs +9 -0
- data/sig/lib/hearth/interceptor_context.rbs +15 -0
- data/sig/lib/hearth/interceptor_list.rbs +16 -0
- data/sig/lib/hearth/interfaces.rbs +65 -0
- data/sig/lib/hearth/output.rbs +11 -0
- data/sig/lib/hearth/plugin_list.rbs +15 -0
- data/sig/lib/hearth/query/param.rbs +17 -0
- data/sig/lib/hearth/query/param_list.rbs +25 -0
- data/sig/lib/hearth/request.rbs +9 -0
- data/sig/lib/hearth/response.rbs +11 -0
- data/sig/lib/hearth/retry/adaptive.rbs +13 -0
- data/sig/lib/hearth/retry/exponential_backoff.rbs +7 -0
- data/sig/lib/hearth/retry/standard.rbs +13 -0
- data/sig/lib/hearth/retry/strategy.rbs +11 -0
- data/sig/lib/hearth/retry.rbs +9 -0
- data/sig/lib/hearth/signers/anonymous.rbs +9 -0
- data/sig/lib/hearth/signers/http_api_key.rbs +9 -0
- data/sig/lib/hearth/signers/http_basic.rbs +9 -0
- data/sig/lib/hearth/signers/http_bearer.rbs +9 -0
- data/sig/lib/hearth/signers/http_digest.rbs +9 -0
- data/sig/lib/hearth/signers.rbs +9 -0
- data/sig/lib/hearth/structure.rbs +7 -0
- data/sig/lib/hearth/union.rbs +5 -0
- data/sig/lib/hearth/waiters/waiter.rbs +17 -0
- metadata +132 -22
- data/lib/hearth/http/headers.rb +0 -70
- data/lib/hearth/middleware/around_handler.rb +0 -24
- data/lib/hearth/middleware/request_handler.rb +0 -24
- data/lib/hearth/middleware/response_handler.rb +0 -25
- data/lib/hearth/middleware_builder.rb +0 -246
- data/lib/hearth/stubbing/client_stubs.rb +0 -115
- data/lib/hearth/stubbing/stubs.rb +0 -32
- data/lib/hearth/waiters/errors.rb +0 -15
- data/sig/lib/seahorse/api_error.rbs +0 -10
- data/sig/lib/seahorse/document.rbs +0 -2
- data/sig/lib/seahorse/http/api_error.rbs +0 -21
- data/sig/lib/seahorse/http/headers.rbs +0 -47
- data/sig/lib/seahorse/http/response.rbs +0 -21
- data/sig/lib/seahorse/simple_delegator.rbs +0 -3
- data/sig/lib/seahorse/structure.rbs +0 -18
- data/sig/lib/seahorse/stubbing/client_stubs.rbs +0 -103
- data/sig/lib/seahorse/stubbing/stubs.rbs +0 -14
- data/sig/lib/seahorse/union.rbs +0 -6
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Middleware
|
|
5
|
+
# A middleware that signs requests using the resolved identity.
|
|
6
|
+
# @api private
|
|
7
|
+
class Sign
|
|
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::MODIFY_BEFORE_SIGNING,
|
|
21
|
+
input: input,
|
|
22
|
+
context: context,
|
|
23
|
+
output: nil,
|
|
24
|
+
aggregate_errors: false
|
|
25
|
+
)
|
|
26
|
+
return Hearth::Output.new(error: interceptor_error) if interceptor_error
|
|
27
|
+
|
|
28
|
+
interceptor_error = Interceptors.invoke(
|
|
29
|
+
hook: Interceptor::READ_BEFORE_SIGNING,
|
|
30
|
+
input: input,
|
|
31
|
+
context: context,
|
|
32
|
+
output: nil,
|
|
33
|
+
aggregate_errors: false
|
|
34
|
+
)
|
|
35
|
+
return Hearth::Output.new(error: interceptor_error) if interceptor_error
|
|
36
|
+
|
|
37
|
+
sign_request(context)
|
|
38
|
+
output = @app.call(input, context)
|
|
39
|
+
|
|
40
|
+
interceptor_error = Interceptors.invoke(
|
|
41
|
+
hook: Interceptor::READ_AFTER_SIGNING,
|
|
42
|
+
input: input,
|
|
43
|
+
context: context,
|
|
44
|
+
output: output,
|
|
45
|
+
aggregate_errors: false
|
|
46
|
+
)
|
|
47
|
+
output.error = interceptor_error if interceptor_error
|
|
48
|
+
|
|
49
|
+
output
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def sign_request(context)
|
|
55
|
+
log_debug(context, "Signing request with: #{context.auth.signer}")
|
|
56
|
+
context.auth.signer.sign(
|
|
57
|
+
request: context.request,
|
|
58
|
+
identity: context.auth.identity,
|
|
59
|
+
properties: context.auth.signer_properties
|
|
60
|
+
)
|
|
61
|
+
log_debug(context, "Signed request: #{context.request.inspect}")
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -5,6 +5,8 @@ module Hearth
|
|
|
5
5
|
# A middleware used to validate input.
|
|
6
6
|
# @api private
|
|
7
7
|
class Validate
|
|
8
|
+
include Middleware::Logging
|
|
9
|
+
|
|
8
10
|
# @param [Class] app The next middleware in the stack.
|
|
9
11
|
# @param [Boolean] validate_input If true, the input is validated against
|
|
10
12
|
# the model and an error is raised for unexpected types.
|
|
@@ -21,9 +23,17 @@ module Hearth
|
|
|
21
23
|
# @param context
|
|
22
24
|
# @return [Output]
|
|
23
25
|
def call(input, context)
|
|
24
|
-
|
|
26
|
+
validate_input(input, context) if @validate_input
|
|
25
27
|
@app.call(input, context)
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def validate_input(input, context)
|
|
33
|
+
log_debug(context, "Validating input with: #{input}")
|
|
34
|
+
@validator.validate!(input, context: 'input')
|
|
35
|
+
log_debug(context, 'Validated input')
|
|
36
|
+
end
|
|
27
37
|
end
|
|
28
38
|
end
|
|
29
39
|
end
|
data/lib/hearth/middleware.rb
CHANGED
|
@@ -1,16 +1,27 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
module Hearth
|
|
4
|
+
# @api private
|
|
5
|
+
module Middleware
|
|
6
|
+
# Helper methods for logging to middleware.
|
|
7
|
+
module Logging
|
|
8
|
+
# Logs a debug message to the logger on context. The message is prefixed
|
|
9
|
+
# with the invocation id and the class name.
|
|
10
|
+
def log_debug(context, message)
|
|
11
|
+
context.logger.debug(
|
|
12
|
+
"[#{context.invocation_id}] [#{self.class}] #{message}"
|
|
13
|
+
)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require_relative 'middleware/auth'
|
|
4
20
|
require_relative 'middleware/build'
|
|
5
21
|
require_relative 'middleware/host_prefix'
|
|
6
22
|
require_relative 'middleware/parse'
|
|
7
|
-
require_relative 'middleware/request_handler'
|
|
8
|
-
require_relative 'middleware/response_handler'
|
|
9
23
|
require_relative 'middleware/retry'
|
|
10
24
|
require_relative 'middleware/send'
|
|
25
|
+
require_relative 'middleware/sign'
|
|
11
26
|
require_relative 'middleware/validate'
|
|
12
|
-
|
|
13
|
-
module Hearth
|
|
14
|
-
# @api private
|
|
15
|
-
module Middleware; end
|
|
16
|
-
end
|
|
27
|
+
require_relative 'middleware/initialize'
|
|
@@ -11,52 +11,10 @@ module Hearth
|
|
|
11
11
|
@middleware.push([middleware, middleware_kwargs])
|
|
12
12
|
end
|
|
13
13
|
|
|
14
|
-
def use_before(before, middleware, **middleware_kwargs)
|
|
15
|
-
new_middleware = []
|
|
16
|
-
@middleware.each do |klass, args|
|
|
17
|
-
new_middleware << [middleware, middleware_kwargs] if before == klass
|
|
18
|
-
new_middleware << [klass, args]
|
|
19
|
-
end
|
|
20
|
-
unless new_middleware.size == @middleware.size + 1
|
|
21
|
-
raise ArgumentError,
|
|
22
|
-
"Failed to insert #{middleware} before #{before}"
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
@middleware = new_middleware
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def use_after(after, middleware, **middleware_kwargs)
|
|
29
|
-
new_middleware = []
|
|
30
|
-
@middleware.each do |klass, args|
|
|
31
|
-
new_middleware << [klass, args]
|
|
32
|
-
new_middleware << [middleware, middleware_kwargs] if after == klass
|
|
33
|
-
end
|
|
34
|
-
unless new_middleware.size == @middleware.size + 1
|
|
35
|
-
raise ArgumentError,
|
|
36
|
-
"Failed to insert #{middleware} after #{after}"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
@middleware = new_middleware
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def remove(remove)
|
|
43
|
-
new_middleware = []
|
|
44
|
-
@middleware.each do |klass, args|
|
|
45
|
-
new_middleware << [klass, args] unless klass == remove
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
unless new_middleware.size == @middleware.size - 1
|
|
49
|
-
raise ArgumentError,
|
|
50
|
-
"Failed to remove #{remove}"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
@middleware = new_middleware
|
|
54
|
-
end
|
|
55
|
-
|
|
56
14
|
# @param input
|
|
57
15
|
# @param context
|
|
58
16
|
# @return [Output]
|
|
59
|
-
def run(input
|
|
17
|
+
def run(input, context)
|
|
60
18
|
stack.call(input, context)
|
|
61
19
|
end
|
|
62
20
|
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
# Thrown by a Client when encountering a networking error while transmitting
|
|
5
|
+
# a request or receiving a response. You can access the original error
|
|
6
|
+
# by calling {#original_error}.
|
|
7
|
+
class NetworkingError < StandardError
|
|
8
|
+
MSG = 'Encountered an error while transmitting the request: %<message>s'
|
|
9
|
+
|
|
10
|
+
def initialize(original_error)
|
|
11
|
+
@original_error = original_error
|
|
12
|
+
super(format(MSG, message: original_error.message))
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @return [StandardError]
|
|
16
|
+
attr_reader :original_error
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/hearth/number_helper.rb
CHANGED
|
@@ -6,8 +6,8 @@ module Hearth
|
|
|
6
6
|
# @api private
|
|
7
7
|
module NumberHelper
|
|
8
8
|
class << self
|
|
9
|
-
# @param [Number
|
|
10
|
-
# @return [String] The serialized number
|
|
9
|
+
# @param [Number] input
|
|
10
|
+
# @return [Number, String] The serialized number
|
|
11
11
|
def serialize(input)
|
|
12
12
|
if input == ::Float::INFINITY then 'Infinity'
|
|
13
13
|
elsif input == -::Float::INFINITY then '-Infinity'
|
data/lib/hearth/output.rb
CHANGED
|
@@ -2,19 +2,23 @@
|
|
|
2
2
|
|
|
3
3
|
module Hearth
|
|
4
4
|
# A wrapper class that contains an error or data from the response.
|
|
5
|
-
# @api private
|
|
6
5
|
class Output
|
|
7
|
-
# @param [
|
|
6
|
+
# @param [ApiError] error The error class to be raised.
|
|
8
7
|
# @param [Struct] data The data returned by a client.
|
|
9
|
-
|
|
8
|
+
# @param [Hash] metadata Response metadata set by client middleware.
|
|
9
|
+
def initialize(error: nil, data: nil, metadata: {})
|
|
10
10
|
@error = error
|
|
11
11
|
@data = data
|
|
12
|
+
@metadata = metadata
|
|
12
13
|
end
|
|
13
14
|
|
|
14
|
-
# @return [
|
|
15
|
+
# @return [ApiError, nil]
|
|
15
16
|
attr_accessor :error
|
|
16
17
|
|
|
17
18
|
# @return [Struct, nil]
|
|
18
19
|
attr_accessor :data
|
|
20
|
+
|
|
21
|
+
# @return [Hash]
|
|
22
|
+
attr_accessor :metadata
|
|
19
23
|
end
|
|
20
24
|
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
# A list of plugins that can be applied to config. Plugins are callables
|
|
5
|
+
# that take one argument (config) and are called during client initialization
|
|
6
|
+
# or operation invocation to modify config.
|
|
7
|
+
class PluginList
|
|
8
|
+
include Enumerable
|
|
9
|
+
|
|
10
|
+
# Initialize a PluginList.
|
|
11
|
+
#
|
|
12
|
+
# @param [Array] plugins ([]) A list of plugins to initialize with
|
|
13
|
+
def initialize(plugins = [])
|
|
14
|
+
unless plugins.respond_to?(:each)
|
|
15
|
+
raise ArgumentError, 'Plugins must be an enumerable'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
@plugins = []
|
|
19
|
+
plugins.each { |p| add(p) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Add a plugin
|
|
23
|
+
#
|
|
24
|
+
# @param [Callable] plugin
|
|
25
|
+
def add(plugin)
|
|
26
|
+
unless valid_plugin?(plugin)
|
|
27
|
+
raise ArgumentError,
|
|
28
|
+
'Plugin must be callable and take one argument (config)'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
@plugins << plugin
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
alias << add
|
|
35
|
+
|
|
36
|
+
def each(&block)
|
|
37
|
+
@plugins.each(&block)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
# plugins must be callable and take exactly 1 argument (config)
|
|
43
|
+
def valid_plugin?(plugin)
|
|
44
|
+
case plugin
|
|
45
|
+
when Proc
|
|
46
|
+
# the arity of a proc.method(:call) is -1, need to special case
|
|
47
|
+
plugin.arity == 1
|
|
48
|
+
else
|
|
49
|
+
plugin.respond_to?(:call) && plugin.method(:call).arity == 1
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Query
|
|
5
|
+
# A class used to represent a query parameter before serialization.
|
|
6
|
+
class Param
|
|
7
|
+
# @param [String] name
|
|
8
|
+
# @param [String, Array<String>] value (nil)
|
|
9
|
+
def initialize(name, value = nil)
|
|
10
|
+
@name = name
|
|
11
|
+
@value = value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# @return [String]
|
|
15
|
+
attr_reader :name
|
|
16
|
+
|
|
17
|
+
# @return [String, Array<String>, nil]
|
|
18
|
+
attr_reader :value
|
|
19
|
+
|
|
20
|
+
# @return [String]
|
|
21
|
+
def to_s
|
|
22
|
+
if value.is_a?(Array)
|
|
23
|
+
value.map { |v| serialize(name, v) }.join('&')
|
|
24
|
+
else
|
|
25
|
+
serialize(name, value)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# @return [Boolean]
|
|
30
|
+
def ==(other)
|
|
31
|
+
other.is_a?(Param) &&
|
|
32
|
+
other.name == name &&
|
|
33
|
+
other.value == value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# @return [Boolean]
|
|
37
|
+
def <=>(other)
|
|
38
|
+
name <=> other.name
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def serialize(name, value)
|
|
44
|
+
value.nil? ? escape(name) : "#{escape(name)}=#{escape(value)}"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def escape(value)
|
|
48
|
+
Hearth::HTTP.uri_escape(value.to_s)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Query
|
|
5
|
+
# A class used to collect query params before serialization.
|
|
6
|
+
class ParamList
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@params = {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @param [String] param_name
|
|
14
|
+
# @param [String, nil] param_value
|
|
15
|
+
# @return [Param]
|
|
16
|
+
def set(param_name, param_value = nil)
|
|
17
|
+
param = Param.new(param_name, param_value)
|
|
18
|
+
@params[param.name] = param
|
|
19
|
+
end
|
|
20
|
+
alias []= set
|
|
21
|
+
|
|
22
|
+
# @return [Param, nil]
|
|
23
|
+
def [](param_name)
|
|
24
|
+
@params[param_name]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @param [String] param_name
|
|
28
|
+
# @return [Param, nil]
|
|
29
|
+
def delete(param_name)
|
|
30
|
+
@params.delete(param_name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Enumerable]
|
|
34
|
+
def each(&block)
|
|
35
|
+
to_a.each(&block)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# @return [Boolean]
|
|
39
|
+
def empty?
|
|
40
|
+
@params.empty?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @return [Array<Param>] Returns an array of sorted {Param} objects.
|
|
44
|
+
def to_a
|
|
45
|
+
@params.values.sort
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# @return [String]
|
|
49
|
+
def to_s
|
|
50
|
+
to_a.map(&:to_s).join('&')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rspec/expectations'
|
|
4
|
+
|
|
5
|
+
# Provides an rspec matcher for CGI.parse to check Float precision.
|
|
6
|
+
# @api private
|
|
7
|
+
RSpec::Matchers.define :match_query_params do |expected|
|
|
8
|
+
match do |actual|
|
|
9
|
+
return true if actual == expected
|
|
10
|
+
return false unless actual.instance_of?(expected.class)
|
|
11
|
+
|
|
12
|
+
expect(actual.keys.length).to eq(expected.keys.length)
|
|
13
|
+
expect(actual.values.length).to eq(expected.values.length)
|
|
14
|
+
|
|
15
|
+
expected.each do |ek, e|
|
|
16
|
+
expect(actual.keys).to include(ek)
|
|
17
|
+
a = actual[ek]
|
|
18
|
+
expect(e.length).to eq(a.length)
|
|
19
|
+
|
|
20
|
+
a.zip(e).each do |a0, e0|
|
|
21
|
+
# Timestamps can have optional precision.
|
|
22
|
+
if (float = Float(a0) rescue false)
|
|
23
|
+
expect(float).to eq(e0.to_f)
|
|
24
|
+
else
|
|
25
|
+
expect(a0).to eq(e0)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
diffable
|
|
32
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
# A module that can be included in a class to provide a #identity method.
|
|
5
|
+
# The class must implement #refresh(properties) that sets @identity. The
|
|
6
|
+
# refresh method will be called when #identity is called and the identity
|
|
7
|
+
# is nil or near expiration.
|
|
8
|
+
module RefreshingIdentityResolver
|
|
9
|
+
SYNC_EXPIRATION_LENGTH = 300 # 5 minutes
|
|
10
|
+
ASYNC_EXPIRATION_LENGTH = 600 # 10 minutes
|
|
11
|
+
|
|
12
|
+
def initialize
|
|
13
|
+
@mutex = Mutex.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [Identity]
|
|
17
|
+
def identity(properties = {})
|
|
18
|
+
if @identity
|
|
19
|
+
refresh_if_near_expiration!(properties)
|
|
20
|
+
else # initialization
|
|
21
|
+
@mutex.synchronize { refresh(properties) }
|
|
22
|
+
end
|
|
23
|
+
@identity
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def sync_expiration_length
|
|
29
|
+
self.class::SYNC_EXPIRATION_LENGTH
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def async_expiration_length
|
|
33
|
+
self.class::ASYNC_EXPIRATION_LENGTH
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Refreshes identity asynchronously and synchronously.
|
|
37
|
+
# If we are near to expiration, block while refreshing the identity.
|
|
38
|
+
# Otherwise, if we're approaching expiration, use the existing identity
|
|
39
|
+
# but attempt a refresh in the background.
|
|
40
|
+
def refresh_if_near_expiration!(properties)
|
|
41
|
+
# NOTE: This check is an optimization. Rather than acquire the mutex on
|
|
42
|
+
# every #refresh_if_near_expiration call, we check before doing so, and
|
|
43
|
+
# then we check within the mutex to avoid a race condition.
|
|
44
|
+
if near_expiration?(sync_expiration_length)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
refresh(properties) if near_expiration?(sync_expiration_length)
|
|
47
|
+
end
|
|
48
|
+
elsif @async_refresh && near_expiration?(async_expiration_length)
|
|
49
|
+
unless @mutex.locked?
|
|
50
|
+
Thread.new do
|
|
51
|
+
@mutex.synchronize do
|
|
52
|
+
refresh(properties) if near_expiration?(async_expiration_length)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def near_expiration?(expiration_length)
|
|
60
|
+
(Time.now.to_i + expiration_length) > @identity.expiration.to_i
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Hearth
|
|
7
|
+
# Represents a base request.
|
|
8
|
+
class Request
|
|
9
|
+
# @param [URI] uri (URI(''))
|
|
10
|
+
# @param [IO] body (StringIO.new)
|
|
11
|
+
def initialize(uri: URI(''), body: StringIO.new)
|
|
12
|
+
@uri = uri
|
|
13
|
+
@body = body
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [URI]
|
|
17
|
+
attr_accessor :uri
|
|
18
|
+
|
|
19
|
+
# @return [IO]
|
|
20
|
+
attr_accessor :body
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'stringio'
|
|
4
|
+
|
|
5
|
+
module Hearth
|
|
6
|
+
# Represents a base response.
|
|
7
|
+
class Response
|
|
8
|
+
# @param [IO] body (StringIO.new)
|
|
9
|
+
def initialize(body: StringIO.new)
|
|
10
|
+
@body = body
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [IO]
|
|
14
|
+
attr_accessor :body
|
|
15
|
+
|
|
16
|
+
# Replace attributes from other response
|
|
17
|
+
# @param [Response] other
|
|
18
|
+
# @return [Response]
|
|
19
|
+
def replace(other)
|
|
20
|
+
@body.truncate(0)
|
|
21
|
+
IO.copy_stream(other.body, @body)
|
|
22
|
+
@body.rewind
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Resets the response.
|
|
27
|
+
# @return [Response]
|
|
28
|
+
def reset
|
|
29
|
+
@body.truncate(0) if @body.respond_to?(:truncate)
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Hearth
|
|
4
|
+
module Retry
|
|
5
|
+
# Adaptive retry strategy for retrying requests.
|
|
6
|
+
class Adaptive < Strategy
|
|
7
|
+
# @param [#call] backoff (ExponentialBackoff) A callable object that
|
|
8
|
+
# calculates a backoff delay for a retry attempt.
|
|
9
|
+
# @param [Integer] max_attempts (3) The maximum number of attempts that
|
|
10
|
+
# will be made for a single request, including the initial attempt.
|
|
11
|
+
# @param [Boolean] wait_to_fill When true, the request will sleep until
|
|
12
|
+
# there is sufficient client side capacity to retry the request. When
|
|
13
|
+
# false, the request will raise a `CapacityNotAvailableError` and will
|
|
14
|
+
# not retry instead of sleeping.
|
|
15
|
+
def initialize(backoff: ExponentialBackoff.new, max_attempts: 3,
|
|
16
|
+
wait_to_fill: true)
|
|
17
|
+
super()
|
|
18
|
+
@backoff = backoff
|
|
19
|
+
@max_attempts = max_attempts
|
|
20
|
+
@wait_to_fill = wait_to_fill
|
|
21
|
+
|
|
22
|
+
# instance state
|
|
23
|
+
@client_rate_limiter = ClientRateLimiter.new
|
|
24
|
+
@retry_quota = RetryQuota.new
|
|
25
|
+
@capacity_amount = nil
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def acquire_initial_retry_token(_token_scope = nil)
|
|
29
|
+
@client_rate_limiter.token_bucket_acquire(
|
|
30
|
+
1, wait_to_fill: @wait_to_fill
|
|
31
|
+
)
|
|
32
|
+
Token.new(retry_count: 0)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def refresh_retry_token(retry_token, error_info)
|
|
36
|
+
return unless error_info.retryable?
|
|
37
|
+
|
|
38
|
+
@client_rate_limiter.update_sending_rate(
|
|
39
|
+
error_info.error_type == 'Throttling'
|
|
40
|
+
)
|
|
41
|
+
return if retry_token.retry_count >= @max_attempts - 1
|
|
42
|
+
|
|
43
|
+
@capacity_amount = @retry_quota.checkout_capacity(error_info)
|
|
44
|
+
return unless @capacity_amount.positive?
|
|
45
|
+
|
|
46
|
+
delay = error_info.hints[:retry_after]
|
|
47
|
+
delay ||= @backoff.call(retry_token.retry_count)
|
|
48
|
+
retry_token.retry_count += 1
|
|
49
|
+
retry_token.retry_delay = delay
|
|
50
|
+
retry_token
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def record_success(retry_token)
|
|
54
|
+
@client_rate_limiter.update_sending_rate(false)
|
|
55
|
+
@retry_quota.release(@capacity_amount)
|
|
56
|
+
retry_token
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|