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.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +2 -0
  3. data/VERSION +1 -1
  4. data/lib/smithy-client/anonymous_provider.rb +12 -0
  5. data/lib/smithy-client/auth_option.rb +23 -0
  6. data/lib/smithy-client/auth_scheme.rb +25 -0
  7. data/lib/smithy-client/auth_schemes/anonymous.rb +18 -0
  8. data/lib/smithy-client/auth_schemes/http_api_key.rb +18 -0
  9. data/lib/smithy-client/auth_schemes/http_basic.rb +18 -0
  10. data/lib/smithy-client/auth_schemes/http_bearer.rb +18 -0
  11. data/lib/smithy-client/auth_schemes/http_digest.rb +18 -0
  12. data/lib/smithy-client/base.rb +200 -0
  13. data/lib/smithy-client/block_io.rb +36 -0
  14. data/lib/smithy-client/configuration.rb +222 -0
  15. data/lib/smithy-client/default_params.rb +91 -0
  16. data/lib/smithy-client/dynamic_errors.rb +82 -0
  17. data/lib/smithy-client/endpoint_rules.rb +186 -0
  18. data/lib/smithy-client/handler.rb +29 -0
  19. data/lib/smithy-client/handler_builder.rb +33 -0
  20. data/lib/smithy-client/handler_context.rb +67 -0
  21. data/lib/smithy-client/handler_list.rb +197 -0
  22. data/lib/smithy-client/handler_list_entry.rb +102 -0
  23. data/lib/smithy-client/http/error_inspector.rb +87 -0
  24. data/lib/smithy-client/http/headers.rb +122 -0
  25. data/lib/smithy-client/http/request.rb +57 -0
  26. data/lib/smithy-client/http/response.rb +178 -0
  27. data/lib/smithy-client/http_api_key_provider.rb +18 -0
  28. data/lib/smithy-client/http_bearer_provider.rb +18 -0
  29. data/lib/smithy-client/http_login_provider.rb +19 -0
  30. data/lib/smithy-client/identities/anonymous.rb +10 -0
  31. data/lib/smithy-client/identities/http_api_key.rb +18 -0
  32. data/lib/smithy-client/identities/http_bearer.rb +18 -0
  33. data/lib/smithy-client/identities/http_login.rb +22 -0
  34. data/lib/smithy-client/identity.rb +15 -0
  35. data/lib/smithy-client/log_formatter.rb +215 -0
  36. data/lib/smithy-client/log_param_filter.rb +88 -0
  37. data/lib/smithy-client/log_param_formatter.rb +65 -0
  38. data/lib/smithy-client/managed_file.rb +14 -0
  39. data/lib/smithy-client/net_http/connection_pool.rb +297 -0
  40. data/lib/smithy-client/net_http/handler.rb +160 -0
  41. data/lib/smithy-client/net_http/patches.rb +28 -0
  42. data/lib/smithy-client/networking_error.rb +16 -0
  43. data/lib/smithy-client/pageable_response.rb +138 -0
  44. data/lib/smithy-client/param_converter.rb +243 -0
  45. data/lib/smithy-client/param_validator.rb +213 -0
  46. data/lib/smithy-client/plugin.rb +144 -0
  47. data/lib/smithy-client/plugin_list.rb +141 -0
  48. data/lib/smithy-client/plugins/anonymous_auth.rb +23 -0
  49. data/lib/smithy-client/plugins/checksum_required.rb +51 -0
  50. data/lib/smithy-client/plugins/content_length.rb +26 -0
  51. data/lib/smithy-client/plugins/default_params.rb +22 -0
  52. data/lib/smithy-client/plugins/host_prefix.rb +69 -0
  53. data/lib/smithy-client/plugins/http_api_key_auth.rb +37 -0
  54. data/lib/smithy-client/plugins/http_basic_auth.rb +47 -0
  55. data/lib/smithy-client/plugins/http_bearer_auth.rb +37 -0
  56. data/lib/smithy-client/plugins/http_digest_auth.rb +60 -0
  57. data/lib/smithy-client/plugins/idempotency_token.rb +34 -0
  58. data/lib/smithy-client/plugins/logging.rb +56 -0
  59. data/lib/smithy-client/plugins/net_http.rb +163 -0
  60. data/lib/smithy-client/plugins/pageable_response.rb +37 -0
  61. data/lib/smithy-client/plugins/param_converter.rb +32 -0
  62. data/lib/smithy-client/plugins/param_validator.rb +30 -0
  63. data/lib/smithy-client/plugins/protocol.rb +66 -0
  64. data/lib/smithy-client/plugins/raise_response_errors.rb +33 -0
  65. data/lib/smithy-client/plugins/request_compression.rb +200 -0
  66. data/lib/smithy-client/plugins/response_target.rb +71 -0
  67. data/lib/smithy-client/plugins/retry_errors.rb +125 -0
  68. data/lib/smithy-client/plugins/sign_requests.rb +24 -0
  69. data/lib/smithy-client/plugins/stub_responses.rb +102 -0
  70. data/lib/smithy-client/protocol_spec_matcher.rb +60 -0
  71. data/lib/smithy-client/refreshing_identity_provider.rb +65 -0
  72. data/lib/smithy-client/request.rb +76 -0
  73. data/lib/smithy-client/response.rb +48 -0
  74. data/lib/smithy-client/retry/adaptive.rb +66 -0
  75. data/lib/smithy-client/retry/client_rate_limiter.rb +142 -0
  76. data/lib/smithy-client/retry/quota.rb +58 -0
  77. data/lib/smithy-client/retry/standard.rb +52 -0
  78. data/lib/smithy-client/retry.rb +36 -0
  79. data/lib/smithy-client/rpc_v2_cbor/protocol.rb +38 -0
  80. data/lib/smithy-client/rpc_v2_cbor/request_builder.rb +76 -0
  81. data/lib/smithy-client/rpc_v2_cbor/response_parser.rb +86 -0
  82. data/lib/smithy-client/rpc_v2_cbor/response_stubber.rb +34 -0
  83. data/lib/smithy-client/service_error.rb +57 -0
  84. data/lib/smithy-client/signer.rb +16 -0
  85. data/lib/smithy-client/signers/anonymous.rb +13 -0
  86. data/lib/smithy-client/signers/http_api_key.rb +52 -0
  87. data/lib/smithy-client/signers/http_basic.rb +23 -0
  88. data/lib/smithy-client/signers/http_bearer.rb +19 -0
  89. data/lib/smithy-client/signers/http_digest.rb +21 -0
  90. data/lib/smithy-client/stubbing/data_applicator.rb +61 -0
  91. data/lib/smithy-client/stubbing/empty_stub.rb +69 -0
  92. data/lib/smithy-client/stubbing/endpoint_provider.rb +22 -0
  93. data/lib/smithy-client/stubbing/protocol.rb +29 -0
  94. data/lib/smithy-client/stubbing/stub_data.rb +25 -0
  95. data/lib/smithy-client/stubbing.rb +14 -0
  96. data/lib/smithy-client/stubs.rb +212 -0
  97. data/lib/smithy-client/util.rb +15 -0
  98. data/lib/smithy-client/waiters/poller.rb +93 -0
  99. data/lib/smithy-client/waiters/waiter.rb +113 -0
  100. data/lib/smithy-client.rb +66 -1
  101. 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