smithy-client 1.0.0.pre0 → 1.0.0.pre1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2 -0
- data/VERSION +1 -1
- data/lib/smithy-client/anonymous_provider.rb +12 -0
- data/lib/smithy-client/auth_option.rb +23 -0
- data/lib/smithy-client/auth_scheme.rb +25 -0
- data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
- data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
- data/lib/smithy-client/base.rb +200 -0
- data/lib/smithy-client/block_io.rb +36 -0
- data/lib/smithy-client/configuration.rb +222 -0
- data/lib/smithy-client/default_params.rb +91 -0
- data/lib/smithy-client/dynamic_errors.rb +82 -0
- data/lib/smithy-client/endpoint_rules.rb +186 -0
- data/lib/smithy-client/handler.rb +29 -0
- data/lib/smithy-client/handler_builder.rb +33 -0
- data/lib/smithy-client/handler_context.rb +67 -0
- data/lib/smithy-client/handler_list.rb +197 -0
- data/lib/smithy-client/handler_list_entry.rb +102 -0
- data/lib/smithy-client/http/error_inspector.rb +87 -0
- data/lib/smithy-client/http/headers.rb +122 -0
- data/lib/smithy-client/http/request.rb +57 -0
- data/lib/smithy-client/http/response.rb +178 -0
- data/lib/smithy-client/http_api_key_provider.rb +18 -0
- data/lib/smithy-client/http_bearer_provider.rb +18 -0
- data/lib/smithy-client/http_login_provider.rb +19 -0
- data/lib/smithy-client/identities/anonymous.rb +10 -0
- data/lib/smithy-client/identities/http_api_key.rb +18 -0
- data/lib/smithy-client/identities/http_bearer.rb +18 -0
- data/lib/smithy-client/identities/http_login.rb +22 -0
- data/lib/smithy-client/identity.rb +15 -0
- data/lib/smithy-client/log_formatter.rb +215 -0
- data/lib/smithy-client/log_param_filter.rb +88 -0
- data/lib/smithy-client/log_param_formatter.rb +65 -0
- data/lib/smithy-client/managed_file.rb +14 -0
- data/lib/smithy-client/net_http/connection_pool.rb +297 -0
- data/lib/smithy-client/net_http/handler.rb +160 -0
- data/lib/smithy-client/net_http/patches.rb +28 -0
- data/lib/smithy-client/networking_error.rb +16 -0
- data/lib/smithy-client/pageable_response.rb +138 -0
- data/lib/smithy-client/param_converter.rb +243 -0
- data/lib/smithy-client/param_validator.rb +213 -0
- data/lib/smithy-client/plugin.rb +144 -0
- data/lib/smithy-client/plugin_list.rb +141 -0
- data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
- data/lib/smithy-client/plugins/checksum_required.rb +51 -0
- data/lib/smithy-client/plugins/content_length.rb +26 -0
- data/lib/smithy-client/plugins/default_params.rb +22 -0
- data/lib/smithy-client/plugins/host_prefix.rb +69 -0
- data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
- data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
- data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
- data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
- data/lib/smithy-client/plugins/logging.rb +56 -0
- data/lib/smithy-client/plugins/net_http.rb +163 -0
- data/lib/smithy-client/plugins/pageable_response.rb +37 -0
- data/lib/smithy-client/plugins/param_converter.rb +32 -0
- data/lib/smithy-client/plugins/param_validator.rb +30 -0
- data/lib/smithy-client/plugins/protocol.rb +66 -0
- data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
- data/lib/smithy-client/plugins/request_compression.rb +200 -0
- data/lib/smithy-client/plugins/response_target.rb +71 -0
- data/lib/smithy-client/plugins/retry_errors.rb +125 -0
- data/lib/smithy-client/plugins/sign_requests.rb +24 -0
- data/lib/smithy-client/plugins/stub_responses.rb +102 -0
- data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
- data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
- data/lib/smithy-client/request.rb +76 -0
- data/lib/smithy-client/response.rb +48 -0
- data/lib/smithy-client/retry/adaptive.rb +66 -0
- data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
- data/lib/smithy-client/retry/quota.rb +58 -0
- data/lib/smithy-client/retry/standard.rb +52 -0
- data/lib/smithy-client/retry.rb +36 -0
- data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
- data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
- data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
- data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
- data/lib/smithy-client/service_error.rb +57 -0
- data/lib/smithy-client/signer.rb +16 -0
- data/lib/smithy-client/signers/anonymous.rb +13 -0
- data/lib/smithy-client/signers/http_api_key.rb +52 -0
- data/lib/smithy-client/signers/http_basic.rb +23 -0
- data/lib/smithy-client/signers/http_bearer.rb +19 -0
- data/lib/smithy-client/signers/http_digest.rb +21 -0
- data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
- data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
- data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
- data/lib/smithy-client/stubbing/protocol.rb +29 -0
- data/lib/smithy-client/stubbing/stub_data.rb +25 -0
- data/lib/smithy-client/stubbing.rb +14 -0
- data/lib/smithy-client/stubs.rb +212 -0
- data/lib/smithy-client/util.rb +15 -0
- data/lib/smithy-client/waiters/poller.rb +93 -0
- data/lib/smithy-client/waiters/waiter.rb +113 -0
- data/lib/smithy-client.rb +66 -1
- metadata +163 -9
@@ -0,0 +1,200 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'zlib'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module Client
|
7
|
+
module Plugins
|
8
|
+
# @api private
|
9
|
+
class RequestCompression < Plugin
|
10
|
+
DEFAULT_MIN_COMPRESSION_SIZE = 10_240
|
11
|
+
MIN_COMPRESSION_SIZE_LIMIT = 10_485_760
|
12
|
+
SUPPORTED_ENCODINGS = %w[gzip].freeze
|
13
|
+
CHUNK_SIZE = 1 * 1024 * 1024 # one MB
|
14
|
+
|
15
|
+
option(
|
16
|
+
:disable_request_compression,
|
17
|
+
default: false,
|
18
|
+
doc_type: 'Boolean',
|
19
|
+
docstring: 'When `true`, the request body will not be compressed for supported operations.'
|
20
|
+
) do |_config|
|
21
|
+
value = ENV['DISABLE_REQUEST_COMPRESSION'] || 'false'
|
22
|
+
Util.str_to_bool(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
option(
|
26
|
+
:request_min_compression_size_bytes,
|
27
|
+
default: DEFAULT_MIN_COMPRESSION_SIZE,
|
28
|
+
doc_type: Integer,
|
29
|
+
docstring: <<~DOCS) do |_config|
|
30
|
+
The minimum size in bytes that triggers compression for request bodies.
|
31
|
+
The value must be non-negative integer value between 0 and 10,485,780 bytes inclusive.
|
32
|
+
DOCS
|
33
|
+
value = ENV['REQUEST_MIN_COMPRESSION_SIZE_BYTES'] || DEFAULT_MIN_COMPRESSION_SIZE
|
34
|
+
Integer(value)
|
35
|
+
end
|
36
|
+
|
37
|
+
def after_initialize(client)
|
38
|
+
validate_disable_request_compression(client.config)
|
39
|
+
validate_request_min_compression_size_bytes(client.config)
|
40
|
+
end
|
41
|
+
|
42
|
+
def validate_disable_request_compression(config)
|
43
|
+
return if [true, false].include?(config.disable_request_compression)
|
44
|
+
|
45
|
+
raise ArgumentError,
|
46
|
+
':disable_request_compression must be either `true` or `false`'
|
47
|
+
end
|
48
|
+
|
49
|
+
def validate_request_min_compression_size_bytes(config)
|
50
|
+
begin
|
51
|
+
value = Integer(config.request_min_compression_size_bytes)
|
52
|
+
return if value.between?(0, MIN_COMPRESSION_SIZE_LIMIT)
|
53
|
+
rescue ArgumentError
|
54
|
+
# handled below
|
55
|
+
end
|
56
|
+
|
57
|
+
raise ArgumentError,
|
58
|
+
':request_min_compression_size_bytes must be a non-negative integer ' \
|
59
|
+
'value between `0` and `10,485,760` bytes inclusive'
|
60
|
+
end
|
61
|
+
|
62
|
+
def add_handlers(handlers, config)
|
63
|
+
# Ensure compression is performed BEFORE calculating a checksum
|
64
|
+
handlers.add(Handler, priority: 25) unless config.disable_request_compression
|
65
|
+
end
|
66
|
+
|
67
|
+
# @api private
|
68
|
+
class Handler < Client::Handler
|
69
|
+
def call(context)
|
70
|
+
if request_compression_trait?(context)
|
71
|
+
selected_encoding = request_encoding_selection(context)
|
72
|
+
if selected_encoding
|
73
|
+
if streaming?(context.operation.input)
|
74
|
+
process_streaming_compression(selected_encoding, context)
|
75
|
+
elsif context.http_request.body.size >= context.config.request_min_compression_size_bytes
|
76
|
+
process_compression(selected_encoding, context)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
@handler.call(context)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def request_compression_trait?(context)
|
86
|
+
context.operation.traits.key?('smithy.api#requestCompression')
|
87
|
+
end
|
88
|
+
|
89
|
+
def request_encoding_selection(context)
|
90
|
+
encodings = context.operation.traits['smithy.api#requestCompression']['encodings']
|
91
|
+
encodings.find { |encoding| RequestCompression::SUPPORTED_ENCODINGS.include?(encoding) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def streaming?(input)
|
95
|
+
input.shape.members.any? do |_, member_ref|
|
96
|
+
member_ref.shape.traits.key?('smithy.api#streaming') &&
|
97
|
+
!member_ref.shape.traits.key?('smithy.api#requiresLength')
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def process_streaming_compression(encoding, context)
|
102
|
+
case encoding
|
103
|
+
when 'gzip'
|
104
|
+
context.http_request.body = GzipIO.new(context.http_request.body)
|
105
|
+
else
|
106
|
+
raise StandardError, "Encoding #{encoding} is not supported"
|
107
|
+
end
|
108
|
+
update_content_encoding(encoding, context)
|
109
|
+
end
|
110
|
+
|
111
|
+
def process_compression(encoding, context)
|
112
|
+
case encoding
|
113
|
+
when 'gzip'
|
114
|
+
gzip_compress(context)
|
115
|
+
else
|
116
|
+
raise StandardError, "We currently do not support #{encoding} encoding"
|
117
|
+
end
|
118
|
+
update_content_encoding(encoding, context)
|
119
|
+
end
|
120
|
+
|
121
|
+
def gzip_compress(context) # rubocop:disable Metrics/AbcSize
|
122
|
+
compressed = StringIO.new
|
123
|
+
compressed.binmode
|
124
|
+
gzip_writer = Zlib::GzipWriter.new(compressed)
|
125
|
+
if context.http_request.body.respond_to?(:read)
|
126
|
+
update_in_chunks(gzip_writer, context.http_request.body)
|
127
|
+
else
|
128
|
+
gzip_writer.write(context.http_request.body)
|
129
|
+
end
|
130
|
+
gzip_writer.close
|
131
|
+
context.http_request.body = StringIO.new(compressed.string)
|
132
|
+
end
|
133
|
+
|
134
|
+
def update_in_chunks(compressor, io)
|
135
|
+
loop do
|
136
|
+
chunk = io.read(CHUNK_SIZE)
|
137
|
+
break unless chunk
|
138
|
+
|
139
|
+
compressor.write(chunk)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def update_content_encoding(encoding, context)
|
144
|
+
headers = context.http_request.headers
|
145
|
+
if headers['Content-Encoding']
|
146
|
+
headers['Content-Encoding'] += ", #{encoding}"
|
147
|
+
else
|
148
|
+
headers['Content-Encoding'] = encoding
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
# @api private
|
153
|
+
class GzipIO
|
154
|
+
def initialize(body)
|
155
|
+
@body = body
|
156
|
+
@buffer = ChunkBuffer.new
|
157
|
+
@gzip_writer = Zlib::GzipWriter.new(@buffer)
|
158
|
+
end
|
159
|
+
|
160
|
+
def read(length, buff = nil)
|
161
|
+
if @gzip_writer.closed?
|
162
|
+
# an empty string to signify an end as
|
163
|
+
# there will be nothing remaining to be read
|
164
|
+
StringIO.new('').read(length, buff)
|
165
|
+
return
|
166
|
+
end
|
167
|
+
|
168
|
+
chunk = @body.read(length)
|
169
|
+
if !chunk || chunk.empty?
|
170
|
+
# closing the writer will write one last chunk
|
171
|
+
# with a trailer (to be read from the @buffer)
|
172
|
+
@gzip_writer.close
|
173
|
+
else
|
174
|
+
# flush happens first to ensure that header fields
|
175
|
+
# are being sent over since write will override
|
176
|
+
@gzip_writer.flush
|
177
|
+
@gzip_writer.write(chunk)
|
178
|
+
end
|
179
|
+
|
180
|
+
StringIO.new(@buffer.last_chunk).read(length, buff)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# @api private
|
185
|
+
class ChunkBuffer
|
186
|
+
def initialize
|
187
|
+
@last_chunk = nil
|
188
|
+
end
|
189
|
+
|
190
|
+
attr_reader :last_chunk
|
191
|
+
|
192
|
+
def write(data)
|
193
|
+
@last_chunk = data
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Smithy
|
6
|
+
module Client
|
7
|
+
module Plugins
|
8
|
+
# @api private
|
9
|
+
class ResponseTarget < Plugin
|
10
|
+
# This handler is responsible for replacing the response body IO
|
11
|
+
# object with custom targets, such as a block, or a file. It is important
|
12
|
+
# to not write data to the custom target in the case of a non-success
|
13
|
+
# response. We do not want to write an XML error message to someone's
|
14
|
+
# file.
|
15
|
+
class Handler < Client::Handler
|
16
|
+
def call(context)
|
17
|
+
target = context[:response_target]
|
18
|
+
add_event_listeners(context.http_response, target) if target
|
19
|
+
@handler.call(context)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def add_event_listeners(response, target)
|
25
|
+
add_header_listener(response, target)
|
26
|
+
add_success_listener(response)
|
27
|
+
add_error_listener(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_header_listener(response, target)
|
31
|
+
response.on_headers(200..299) do
|
32
|
+
# In a fresh response, the body will be a StringIO. However, when a request
|
33
|
+
# is retried we may have an existing ManagedFile or BlockIO,
|
34
|
+
# and those should be reused.
|
35
|
+
response.body = io(target, response.headers) if response.body.is_a?(StringIO)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_success_listener(response)
|
40
|
+
response.on_success(200..299) do
|
41
|
+
body = response.body
|
42
|
+
body.close if body.is_a?(ManagedFile) && body.open?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_error_listener(response)
|
47
|
+
response.on_error do
|
48
|
+
body = response.body
|
49
|
+
# When using a File response_target, we do not want to write
|
50
|
+
# error messages to the file. So set the body to a new StringIO
|
51
|
+
if body.is_a?(ManagedFile)
|
52
|
+
File.unlink(body)
|
53
|
+
response.body = StringIO.new
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def io(target, headers)
|
59
|
+
case target
|
60
|
+
when Proc then BlockIO.new(headers, &target)
|
61
|
+
when String, Pathname then ManagedFile.new(target, 'w+b')
|
62
|
+
else target
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
handler(Handler, step: :initialize, priority: 90)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Plugins
|
6
|
+
# @api private
|
7
|
+
class RetryErrors < Plugin
|
8
|
+
option(
|
9
|
+
:retry_strategy,
|
10
|
+
default: 'standard',
|
11
|
+
doc_type: 'String, Class',
|
12
|
+
docstring: <<~DOCS)
|
13
|
+
The retry strategy to use when retrying errors. This can be one of the following:
|
14
|
+
* `standard` - A standardized retry strategy used by the AWS SDKs. This includes support
|
15
|
+
for retry quotas, which limit the number of unsuccessful retries a client can make.
|
16
|
+
* `adaptive` - An experimental retry strategy that includes all the functionality of the
|
17
|
+
`standard` strategy along with automatic client side throttling. This is a provisional
|
18
|
+
strategy that may change behavior in the future.
|
19
|
+
* Any instance of a class that implements the following methods:
|
20
|
+
- `acquire_initial_retry_token(token_scope)`
|
21
|
+
- `refresh_retry_token(retry_token, error_info)`
|
22
|
+
- `record_success(retry_token)`
|
23
|
+
DOCS
|
24
|
+
|
25
|
+
option(
|
26
|
+
:retry_max_attempts,
|
27
|
+
default: 3,
|
28
|
+
doc_type: Integer,
|
29
|
+
docstring: <<~DOCS)
|
30
|
+
The maximum number attempts that will be made for a single request, including
|
31
|
+
the initial attempt. Used in the `standard` and `adaptive` retry strategies.
|
32
|
+
DOCS
|
33
|
+
|
34
|
+
option(
|
35
|
+
:retry_backoff,
|
36
|
+
default: Retry::EXPONENTIAL_BACKOFF,
|
37
|
+
doc_default: 'Smithy::Client::Retry::EXPONENTIAL_BACKOFF',
|
38
|
+
doc_type: 'lambda',
|
39
|
+
docstring: <<~DOCS)
|
40
|
+
A callable object that calculates a backoff delay for a retry attempt. The callable
|
41
|
+
should accept a single argument, `attempts`, that represents the number of attempts
|
42
|
+
that have been made. Used in the `standard` and `adaptive` retry strategies.
|
43
|
+
DOCS
|
44
|
+
|
45
|
+
option(
|
46
|
+
:adaptive_retry_wait_to_fill,
|
47
|
+
default: true,
|
48
|
+
doc_type: 'Boolean',
|
49
|
+
docstring: <<~DOCS)
|
50
|
+
When true, the request will sleep until there is sufficient client side capacity to retry
|
51
|
+
the request. When false, the request will raise a `CapacityNotAvailableError` and will
|
52
|
+
not retry instead of sleeping.
|
53
|
+
DOCS
|
54
|
+
|
55
|
+
def after_initialize(client)
|
56
|
+
config = client.config
|
57
|
+
config.retry_strategy =
|
58
|
+
case config.retry_strategy
|
59
|
+
when 'standard'
|
60
|
+
Retry::Standard.new(
|
61
|
+
max_attempts: config.retry_max_attempts,
|
62
|
+
backoff: config.retry_backoff
|
63
|
+
)
|
64
|
+
when 'adaptive'
|
65
|
+
Retry::Adaptive.new(
|
66
|
+
max_attempts: config.retry_max_attempts,
|
67
|
+
backoff: config.retry_backoff,
|
68
|
+
wait_to_fill: config.adaptive_retry_wait_to_fill
|
69
|
+
)
|
70
|
+
else
|
71
|
+
config.retry_strategy
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# @api private
|
76
|
+
class Handler < Client::Handler
|
77
|
+
def call(context)
|
78
|
+
retry_strategy = context.config.retry_strategy
|
79
|
+
token = retry_strategy.acquire_initial_retry_token(nil)
|
80
|
+
handle(context, retry_strategy, token)
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def handle(context, retry_strategy, token)
|
86
|
+
response = @handler.call(context)
|
87
|
+
if (error = response.error)
|
88
|
+
return response unless retryable?(context.http_request)
|
89
|
+
|
90
|
+
error_info = HTTP::ErrorInspector.new(error, context.http_response)
|
91
|
+
token = retry_strategy.refresh_retry_token(token, error_info)
|
92
|
+
return response unless token
|
93
|
+
|
94
|
+
Kernel.sleep(token.retry_delay)
|
95
|
+
else
|
96
|
+
retry_strategy.record_success(token)
|
97
|
+
return response
|
98
|
+
end
|
99
|
+
|
100
|
+
reset_request(context)
|
101
|
+
reset_response(context, response)
|
102
|
+
context.retries += 1
|
103
|
+
handle(context, retry_strategy, token)
|
104
|
+
end
|
105
|
+
|
106
|
+
def retryable?(request)
|
107
|
+
# IO responds to #rewind however it returns an illegal seek error
|
108
|
+
request.body.respond_to?(:rewind) && !request.body.instance_of?(IO)
|
109
|
+
end
|
110
|
+
|
111
|
+
def reset_request(context)
|
112
|
+
context.http_request.body.rewind
|
113
|
+
end
|
114
|
+
|
115
|
+
def reset_response(context, response)
|
116
|
+
context.http_response.reset
|
117
|
+
response.error = nil
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
handler(Handler, step: :retry)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Plugins
|
6
|
+
# @api private
|
7
|
+
class SignRequests < Plugin
|
8
|
+
# @api private
|
9
|
+
class Handler < Client::Handler
|
10
|
+
def call(context)
|
11
|
+
context[:auth].signer.sign(
|
12
|
+
request: context.http_request,
|
13
|
+
identity: context[:auth].identity,
|
14
|
+
properties: context[:auth].signer_properties
|
15
|
+
)
|
16
|
+
@handler.call(context)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
handler(Handler, step: :sign)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module Plugins
|
6
|
+
# @api private
|
7
|
+
class StubResponses < Plugin
|
8
|
+
option(
|
9
|
+
:stub_responses,
|
10
|
+
default: false,
|
11
|
+
doc_type: 'Boolean',
|
12
|
+
docstring: <<~DOCS)
|
13
|
+
When `true`, the client will return stubbed responses instead of networking requests.
|
14
|
+
By default fake responses are generated and returned. You can specify the response data
|
15
|
+
to return or errors to raise by calling {Stubs#stub_responses}.
|
16
|
+
@see Stubs
|
17
|
+
DOCS
|
18
|
+
|
19
|
+
option(:stubs) { {} }
|
20
|
+
# @api private
|
21
|
+
option(:stubs_mutex) { Mutex.new }
|
22
|
+
# @api private
|
23
|
+
option(:api_requests) { [] }
|
24
|
+
# @api private
|
25
|
+
option(:api_requests_mutex) { Mutex.new }
|
26
|
+
|
27
|
+
def add_handlers(handlers, config)
|
28
|
+
return unless config.stub_responses
|
29
|
+
|
30
|
+
handlers.add(APIRequestsHandler)
|
31
|
+
handlers.add(StubHandler, step: :send)
|
32
|
+
end
|
33
|
+
|
34
|
+
def before_initialize(_client_class, options)
|
35
|
+
return unless options[:stub_responses]
|
36
|
+
|
37
|
+
options[:endpoint_provider] ||= Stubbing::EndpointProvider.new
|
38
|
+
end
|
39
|
+
|
40
|
+
def after_initialize(client)
|
41
|
+
return unless client.config.stub_responses
|
42
|
+
|
43
|
+
client.handlers.remove(RetryErrors::Handler)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Returns a registered stubbed response instead of a real response.
|
47
|
+
# @api private
|
48
|
+
class StubHandler < Client::Handler
|
49
|
+
def call(context)
|
50
|
+
response = Smithy::Client::Response.new(context: context)
|
51
|
+
stub = context.client.next_stub(context)
|
52
|
+
stub[:mutex].synchronize { apply_stub(stub, response) }
|
53
|
+
response
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def apply_stub(stub, response)
|
59
|
+
http_response = response.context.http_response
|
60
|
+
if stub[:error]
|
61
|
+
signal_error(stub[:error], http_response)
|
62
|
+
elsif stub[:http]
|
63
|
+
signal_http(stub[:http], http_response)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def signal_error(error, http_response)
|
68
|
+
if error.is_a?(Exception)
|
69
|
+
http_response.signal_error(error)
|
70
|
+
else
|
71
|
+
http_response.signal_error(error.new)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def signal_http(stub, http_response)
|
76
|
+
http_response.signal_headers(stub.status_code, stub.headers)
|
77
|
+
signal_data(stub, http_response)
|
78
|
+
http_response.signal_done
|
79
|
+
end
|
80
|
+
|
81
|
+
def signal_data(stub, http_response)
|
82
|
+
while (chunk = stub.body.read(1024 * 1024))
|
83
|
+
http_response.signal_data(chunk)
|
84
|
+
end
|
85
|
+
stub.body.rewind
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Tracks API requests made by the client.
|
90
|
+
# @api private
|
91
|
+
class APIRequestsHandler < Client::Handler
|
92
|
+
def call(context)
|
93
|
+
context.config.api_requests_mutex.synchronize do
|
94
|
+
context.config.api_requests << context
|
95
|
+
end
|
96
|
+
@handler.call(context)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/expectations'
|
4
|
+
|
5
|
+
# Provides an RSpec matcher for protocol specs.
|
6
|
+
# rubocop:disable Metrics/BlockLength
|
7
|
+
RSpec::Matchers.define :match_data do |expected|
|
8
|
+
match do |actual|
|
9
|
+
# identical values don't need more comparison
|
10
|
+
return true if actual == expected
|
11
|
+
|
12
|
+
expect(actual.class).to eq(expected.class)
|
13
|
+
|
14
|
+
def match_hash(actual, expected)
|
15
|
+
expected.each do |key, value|
|
16
|
+
expect(actual).to include(key)
|
17
|
+
match_data(actual[key], value)
|
18
|
+
end
|
19
|
+
|
20
|
+
actual.each_key do |key|
|
21
|
+
expect(expected).to include(key)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def match_array(actual, expected)
|
26
|
+
actual.each_with_index do |value, index|
|
27
|
+
match_data(value, expected[index])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def match_float(actual, expected)
|
32
|
+
return if actual.nan? && expected.nan?
|
33
|
+
return if actual.infinite? && expected.infinite?
|
34
|
+
|
35
|
+
expect(actual).to be_within(0.0001).of(expected)
|
36
|
+
end
|
37
|
+
|
38
|
+
def match_data(actual, expected) # rubocop:disable Metrics/AbcSize
|
39
|
+
case actual
|
40
|
+
when Hash
|
41
|
+
match_hash(actual, expected)
|
42
|
+
when Array
|
43
|
+
match_array(actual, expected)
|
44
|
+
when Float
|
45
|
+
match_float(actual, expected)
|
46
|
+
when Time
|
47
|
+
expect(actual.utc.iso8601).to eq(expected.utc.iso8601)
|
48
|
+
when StringIO
|
49
|
+
expect(actual.string).to eq(expected)
|
50
|
+
else
|
51
|
+
expect(actual).to eq(expected)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
match_data(actual, expected)
|
56
|
+
end
|
57
|
+
|
58
|
+
diffable
|
59
|
+
end
|
60
|
+
# rubocop:enable Metrics/BlockLength
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# A module that can be included in a class to provide a #identity method.
|
6
|
+
# The class must implement #refresh(properties) that sets @identity. The
|
7
|
+
# refresh method will be called when #identity is called and the identity
|
8
|
+
# is nil or near expiration.
|
9
|
+
module RefreshingIdentityProvider
|
10
|
+
SYNC_EXPIRATION_LENGTH = 300 # 5 minutes
|
11
|
+
ASYNC_EXPIRATION_LENGTH = 600 # 10 minutes
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@mutex = Mutex.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Identities::Base]
|
18
|
+
def identity(properties = {})
|
19
|
+
if @identity
|
20
|
+
refresh_if_near_expiration!(properties)
|
21
|
+
else # initialization
|
22
|
+
@mutex.synchronize { refresh(properties) }
|
23
|
+
end
|
24
|
+
@identity
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def sync_expiration_length
|
30
|
+
self.class::SYNC_EXPIRATION_LENGTH
|
31
|
+
end
|
32
|
+
|
33
|
+
def async_expiration_length
|
34
|
+
self.class::ASYNC_EXPIRATION_LENGTH
|
35
|
+
end
|
36
|
+
|
37
|
+
# Refreshes identity asynchronously and synchronously.
|
38
|
+
# If we are near to expiration, block while refreshing the identity.
|
39
|
+
# Otherwise, if we're approaching expiration, use the existing identity
|
40
|
+
# but attempt a refresh in the background.
|
41
|
+
def refresh_if_near_expiration!(properties)
|
42
|
+
# NOTE: This check is an optimization. Rather than acquire the mutex on
|
43
|
+
# every #refresh_if_near_expiration call, we check before doing so, and
|
44
|
+
# then we check within the mutex to avoid a race condition.
|
45
|
+
if near_expiration?(sync_expiration_length)
|
46
|
+
@mutex.synchronize do
|
47
|
+
refresh(properties) if near_expiration?(sync_expiration_length)
|
48
|
+
end
|
49
|
+
elsif @async_refresh && near_expiration?(async_expiration_length)
|
50
|
+
unless @mutex.locked?
|
51
|
+
Thread.new do
|
52
|
+
@mutex.synchronize do
|
53
|
+
refresh(properties) if near_expiration?(async_expiration_length)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def near_expiration?(expiration_length)
|
61
|
+
(Time.now.to_i + expiration_length) > @identity.expiration.to_i
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|