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,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ # Represents a request for a service operation call.
6
+ class Request
7
+ include HandlerBuilder
8
+
9
+ # @option options [HandlerList] :handlers (nil)
10
+ # @option options [HandlerContext] :context (nil)
11
+ def initialize(options = {})
12
+ @handlers = options[:handlers] || HandlerList.new
13
+ @context = options[:context] || HandlerContext.new
14
+ end
15
+
16
+ # @return [HandlerList]
17
+ attr_reader :handlers
18
+
19
+ # @return [HandlerContext]
20
+ attr_reader :context
21
+
22
+ # Sends the request, returning an {Response} object.
23
+ #
24
+ # response = request.send_request
25
+ #
26
+ # # Streaming Responses
27
+ #
28
+ # By default, responses are buffered into memory. This can be
29
+ # bad if you are downloading large responses, e.g. large files.
30
+ # You can avoid this by streaming the response to a block or some other
31
+ # target.
32
+ #
33
+ # ## Streaming to a File
34
+ #
35
+ # You can stream the raw response bodies to a File, or any IO-like
36
+ # object, by passing the `:target` option.
37
+ #
38
+ # # create a new file at the given path
39
+ # request.send_request(target: '/path/to/target/file')
40
+ #
41
+ # # or provide an IO object to write to
42
+ # File.open('photo.jpg', 'wb') do |file|
43
+ # request.send_request(target: file)
44
+ # end
45
+ #
46
+ # **Please Note**: The target IO object may receive `#truncate(0)`
47
+ # if the request generates a networking error and bytes have already
48
+ # been written to the target.
49
+ #
50
+ # ## Block Streaming
51
+ #
52
+ # Pass a block to `#send_request` and the response will be yielded in
53
+ # chunks to the given block.
54
+ #
55
+ # # stream the response data
56
+ # request.send_request do |chunk|
57
+ # file.write(chunk)
58
+ # end
59
+ #
60
+ # **Please Note**: When streaming to a block, it is not possible to
61
+ # retry failed requests.
62
+ #
63
+ # @option options [String, IO] :target When specified, the response
64
+ # body is written to the target. This is helpful when you are sending
65
+ # a request that may return a large payload that you don't want to
66
+ # load into memory.
67
+ #
68
+ # @return [Response, nil]
69
+ #
70
+ def send_request(options = {}, &block)
71
+ @context[:response_target] = options[:target] || block
72
+ @handlers.to_stack&.call(@context)
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'delegate'
4
+
5
+ module Smithy
6
+ module Client
7
+ # Represents the response for a service operation call.
8
+ class Response < Delegator
9
+ # @option options [HandlerContext] :context (nil)
10
+ # @option options [Structure] :data (nil)
11
+ # @option options [StandardError] :error (nil)
12
+ def initialize(options = {})
13
+ @context = options[:context] || HandlerContext.new
14
+ @data = options[:data]
15
+ @error = options[:error]
16
+ @context.http_response.on_error { |error| @error = error }
17
+ super(@error || @data)
18
+ end
19
+
20
+ # @return [HandlerContext]
21
+ attr_reader :context
22
+
23
+ # @return [Structure, nil] The response data. This may be `nil` if the
24
+ # response contains an {#error}.
25
+ attr_accessor :data
26
+
27
+ # @return [StandardError, nil] The error that occurred during the
28
+ # operation. This will be `nil` if the operation was successful.
29
+ attr_accessor :error
30
+
31
+ # Necessary to define as a subclass of Delegator
32
+ # @api private
33
+ def __getobj__(&)
34
+ @error || @data
35
+ end
36
+
37
+ # Necessary to define as a subclass of Delegator
38
+ # @api private
39
+ def __setobj__(obj)
40
+ if obj.is_a?(StandardError)
41
+ @error = obj
42
+ else
43
+ @data = obj
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module Retry
6
+ # Adaptive retry strategy for retrying requests.
7
+ class Adaptive
8
+ # @option [#call] :backoff (EXPONENTIAL_BACKOFF) A callable object that
9
+ # calculates a backoff delay for a retry attempt.
10
+ # @option [Integer] :max_attempts (3) The maximum number of attempts that
11
+ # will be made for a single request, including the initial attempt.
12
+ # @option [Boolean] :wait_to_fill When true, the request will sleep until
13
+ # there is sufficient client side capacity to retry the request. When
14
+ # false, the request will raise a `CapacityNotAvailableError` and will
15
+ # not retry instead of sleeping.
16
+ def initialize(options = {})
17
+ super()
18
+ @backoff = options[:backoff] || EXPONENTIAL_BACKOFF
19
+ @max_attempts = options[:max_attempts] || 3
20
+ @wait_to_fill = options[:wait_to_fill] || true
21
+ @client_rate_limiter = ClientRateLimiter.new
22
+ @quota = Quota.new
23
+ @capacity_amount = 0
24
+ end
25
+
26
+ # @return [#call]
27
+ attr_reader :backoff
28
+
29
+ # @return [Integer]
30
+ attr_reader :max_attempts
31
+
32
+ # @return [Boolean]
33
+ attr_reader :wait_to_fill
34
+
35
+ def acquire_initial_retry_token(_token_scope = nil)
36
+ @client_rate_limiter.token_bucket_acquire(1, wait_to_fill: @wait_to_fill)
37
+ Token.new
38
+ end
39
+
40
+ def refresh_retry_token(retry_token, error_info)
41
+ return unless error_info.retryable?
42
+
43
+ @client_rate_limiter.update_sending_rate(
44
+ error_info.error_type == 'Throttling'
45
+ )
46
+ return if retry_token.retry_count >= @max_attempts - 1
47
+
48
+ @capacity_amount = @quota.checkout_capacity(error_info)
49
+ return unless @capacity_amount.positive?
50
+
51
+ delay = error_info.hints[:retry_after]
52
+ delay ||= @backoff.call(retry_token.retry_count)
53
+ retry_token.retry_count += 1
54
+ retry_token.retry_delay = delay
55
+ retry_token
56
+ end
57
+
58
+ def record_success(retry_token)
59
+ @client_rate_limiter.update_sending_rate(false)
60
+ @quota.release(@capacity_amount)
61
+ retry_token
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module Retry
6
+ # Raised when the adaptive retry strategy is unable to acquire capacity.
7
+ class CapacityNotAvailableError < RuntimeError; end
8
+
9
+ # Used only in 'adaptive' retry mode
10
+ # @api private
11
+ class ClientRateLimiter
12
+ MIN_CAPACITY = 1
13
+ MIN_FILL_RATE = 0.5
14
+ SMOOTH = 0.8
15
+ # How much to scale back after a throttling response
16
+ BETA = 0.7
17
+ # Controls how aggressively we scale up after being throttled
18
+ SCALE_CONSTANT = 0.4
19
+
20
+ def initialize
21
+ @mutex = Mutex.new
22
+ @fill_rate = nil
23
+ @max_capacity = nil
24
+ @current_capacity = 0
25
+ @last_timestamp = nil
26
+ @enabled = false
27
+ @measured_tx_rate = 0
28
+ @last_tx_rate_bucket = monotonic_seconds
29
+ @request_count = 0
30
+ @last_max_rate = 0
31
+ @last_throttle_time = monotonic_seconds
32
+ @calculated_rate = nil
33
+ end
34
+
35
+ def token_bucket_acquire(amount, wait_to_fill: true)
36
+ # Client side throttling is not enabled until we see a
37
+ # throttling error
38
+ return unless @enabled
39
+
40
+ @mutex.synchronize do
41
+ token_bucket_refill
42
+
43
+ # Next see if we have enough capacity for the requested amount
44
+ while @current_capacity < amount
45
+ raise CapacityNotAvailableError unless wait_to_fill
46
+
47
+ @mutex.sleep((amount - @current_capacity) / @fill_rate)
48
+ token_bucket_refill
49
+ end
50
+ @current_capacity -= amount
51
+ end
52
+ end
53
+
54
+ def update_sending_rate(is_throttling_error)
55
+ @mutex.synchronize do
56
+ update_measured_rate
57
+
58
+ if is_throttling_error
59
+ # The fill_rate is from the token bucket
60
+ @last_max_rate = rate_to_use
61
+ calculate_time_window
62
+ @last_throttle_time = monotonic_seconds
63
+ @calculated_rate = cubic_throttle(rate_to_use)
64
+ @enabled = true
65
+ else
66
+ calculate_time_window
67
+ @calculated_rate = cubic_success(monotonic_seconds)
68
+ end
69
+
70
+ new_rate = [@calculated_rate, 2 * @measured_tx_rate].min
71
+ token_bucket_update_rate(new_rate)
72
+ end
73
+ end
74
+
75
+ private
76
+
77
+ def monotonic_seconds
78
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
79
+ end
80
+
81
+ def token_bucket_refill
82
+ timestamp = monotonic_seconds
83
+ unless @last_timestamp
84
+ @last_timestamp = timestamp
85
+ return
86
+ end
87
+
88
+ fill_amount = (timestamp - @last_timestamp) * @fill_rate
89
+ @current_capacity = [@max_capacity, @current_capacity + fill_amount].min
90
+ @last_timestamp = timestamp
91
+ end
92
+
93
+ def token_bucket_update_rate(new_rps)
94
+ # Refill based on our current rate before we update to the
95
+ # new fill rate
96
+ token_bucket_refill
97
+ @fill_rate = [new_rps, MIN_FILL_RATE].max
98
+ @max_capacity = [new_rps, MIN_CAPACITY].max
99
+ # When we scale down we can't have a current capacity that exceeds our
100
+ # max_capacity.
101
+ @current_capacity = [@current_capacity, @max_capacity].min
102
+ end
103
+
104
+ def update_measured_rate
105
+ t = monotonic_seconds
106
+ time_bucket = (t * 2).floor / 2.0
107
+ @request_count += 1
108
+ return unless time_bucket > @last_tx_rate_bucket
109
+
110
+ current_rate = @request_count / (time_bucket - @last_tx_rate_bucket)
111
+ @measured_tx_rate = (current_rate * SMOOTH) + (@measured_tx_rate * (1 - SMOOTH))
112
+ @request_count = 0
113
+ @last_tx_rate_bucket = time_bucket
114
+ end
115
+
116
+ def calculate_time_window
117
+ # This is broken out into a separate calculation because it only
118
+ # gets updated when @last_max_rate changes so it can be cached.
119
+ base = ((@last_max_rate * (1 - BETA)) / SCALE_CONSTANT)
120
+ @time_window = base**(1.0 / 3)
121
+ end
122
+
123
+ def cubic_success(timestamp)
124
+ dt = timestamp - @last_throttle_time
125
+ (SCALE_CONSTANT * ((dt - @time_window)**3)) + @last_max_rate
126
+ end
127
+
128
+ def cubic_throttle(rate_to_use)
129
+ rate_to_use * BETA
130
+ end
131
+
132
+ def rate_to_use
133
+ if @enabled
134
+ [@measured_tx_rate, @fill_rate].min
135
+ else
136
+ @measured_tx_rate
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module Retry
6
+ # @api private
7
+ # Used in 'standard' and 'adaptive' retry modes.
8
+ class Quota
9
+ INITIAL_RETRY_TOKENS = 500
10
+ RETRY_COST = 5
11
+ NO_RETRY_INCREMENT = 1
12
+ TIMEOUT_RETRY_COST = 10
13
+
14
+ def initialize
15
+ @mutex = Mutex.new
16
+ @max_capacity = INITIAL_RETRY_TOKENS
17
+ @available_capacity = @max_capacity
18
+ end
19
+
20
+ # Check if there is sufficient capacity to retry and return it.
21
+ # If there is insufficient capacity, return 0
22
+ # @return [Integer] The amount of capacity checked out
23
+ def checkout_capacity(error_info)
24
+ @mutex.synchronize do
25
+ capacity_amount =
26
+ if error_info.error_type == 'Transient'
27
+ TIMEOUT_RETRY_COST
28
+ else
29
+ RETRY_COST
30
+ end
31
+
32
+ # unable to acquire capacity
33
+ return 0 if capacity_amount > @available_capacity
34
+
35
+ @available_capacity -= capacity_amount
36
+ capacity_amount
37
+ end
38
+ end
39
+
40
+ # capacity_amount refers to the amount of capacity requested from
41
+ # the last retry. It can either be RETRY_COST, TIMEOUT_RETRY_COST,
42
+ # or unset.
43
+ def release(capacity_amount)
44
+ # Implementation note: The release() method is called for
45
+ # every API call. In the common case where the request is
46
+ # successful and we're at full capacity, we can avoid locking.
47
+ # We can't exceed max capacity so there's no work we have to do.
48
+ return if @available_capacity == @max_capacity
49
+
50
+ @mutex.synchronize do
51
+ @available_capacity += capacity_amount || NO_RETRY_INCREMENT
52
+ @available_capacity = [@available_capacity, @max_capacity].min
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module Retry
6
+ # Standard retry strategy for retrying requests.
7
+ class Standard
8
+ # @option [#call] :backoff (EXPONENTIAL_BACKOFF) A callable object that
9
+ # calculates a backoff delay for a retry attempt.
10
+ # @option [Integer] :max_attempts (3) The maximum number of attempts that
11
+ # will be made for a single request, including the initial attempt.
12
+ def initialize(options = {})
13
+ super()
14
+ @backoff = options[:backoff] || EXPONENTIAL_BACKOFF
15
+ @max_attempts = options[:max_attempts] || 3
16
+ @quota = Quota.new
17
+ @capacity_amount = 0
18
+ end
19
+
20
+ # @return [#call]
21
+ attr_reader :backoff
22
+
23
+ # @return [Integer]
24
+ attr_reader :max_attempts
25
+
26
+ def acquire_initial_retry_token(_token_scope = nil)
27
+ Token.new
28
+ end
29
+
30
+ def refresh_retry_token(retry_token, error_info)
31
+ return unless error_info.retryable?
32
+
33
+ return if retry_token.retry_count >= @max_attempts - 1
34
+
35
+ @capacity_amount = @quota.checkout_capacity(error_info)
36
+ return unless @capacity_amount.positive?
37
+
38
+ delay = error_info.hints[:retry_after]
39
+ delay ||= @backoff.call(retry_token.retry_count)
40
+ retry_token.retry_count += 1
41
+ retry_token.retry_delay = delay
42
+ retry_token
43
+ end
44
+
45
+ def record_success(retry_token)
46
+ @quota.release(@capacity_amount)
47
+ retry_token
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'retry/adaptive'
4
+ require_relative 'retry/client_rate_limiter'
5
+ require_relative 'retry/quota'
6
+ require_relative 'retry/standard'
7
+
8
+ module Smithy
9
+ module Client
10
+ module Retry
11
+ # The maximum backoff delay for retrying requests.
12
+ MAX_BACKOFF = 20
13
+
14
+ # The default backoff for retrying requests.
15
+ EXPONENTIAL_BACKOFF = lambda do |attempts|
16
+ [Kernel.rand * (2**attempts), MAX_BACKOFF].min || 0
17
+ end
18
+
19
+ # Represents a token that can be used to retry an operation.
20
+ class Token
21
+ def initialize
22
+ @retry_count = 0
23
+ @retry_delay = 0
24
+ end
25
+
26
+ # The number of times the operation has been retried.
27
+ # @return [Integer]
28
+ attr_accessor :retry_count
29
+
30
+ # The delay before the next retry.
31
+ # @return [Numeric]
32
+ attr_accessor :retry_delay
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'request_builder'
4
+ require_relative 'response_parser'
5
+ require_relative 'response_stubber'
6
+
7
+ module Smithy
8
+ module Client
9
+ module RPCv2CBOR
10
+ # @api private
11
+ class Protocol
12
+ def initialize(options = {})
13
+ @options = options
14
+ end
15
+
16
+ def build_request(context)
17
+ RequestBuilder.new(@options).build(context)
18
+ end
19
+
20
+ def parse_error(response)
21
+ ResponseParser.new(@options).parse_error(response.context)
22
+ end
23
+
24
+ def parse_data(response)
25
+ ResponseParser.new(@options).parse_data(response.context)
26
+ end
27
+
28
+ def stub_data(service, operation, data)
29
+ ResponseStubber.new(@options).stub_data(service, operation, data)
30
+ end
31
+
32
+ def stub_error(service, error_code)
33
+ ResponseStubber.new(@options).stub_error(service, error_code)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module RPCv2CBOR
6
+ # @api private
7
+ class RequestBuilder
8
+ include Schema::Shapes
9
+
10
+ def initialize(options = {})
11
+ @codec = CBOR::Codec.new(options)
12
+ end
13
+
14
+ def build(context)
15
+ apply_http_method(context)
16
+ apply_headers(context)
17
+ apply_body(context)
18
+ apply_url_path(context)
19
+ end
20
+
21
+ private
22
+
23
+ def apply_http_method(context)
24
+ context.http_request.http_method = 'POST'
25
+ end
26
+
27
+ def apply_headers(context)
28
+ context.http_request.headers['Smithy-Protocol'] = 'rpc-v2-cbor'
29
+ apply_content_type_header(context)
30
+ apply_accept_header(context)
31
+ end
32
+
33
+ def apply_content_type_header(context)
34
+ input = context.operation.input
35
+ content_type =
36
+ if event_stream?(input)
37
+ 'application/vnd.amazon.eventstream'
38
+ elsif input.shape != Prelude::Unit
39
+ 'application/cbor'
40
+ end
41
+
42
+ context.http_request.headers['Content-Type'] ||= content_type if content_type
43
+ end
44
+
45
+ def apply_accept_header(context)
46
+ accept =
47
+ if event_stream?(context.operation.output)
48
+ 'application/vnd.amazon.eventstream'
49
+ else
50
+ 'application/cbor'
51
+ end
52
+
53
+ context.http_request.headers['Accept'] ||= accept
54
+ end
55
+
56
+ def apply_body(context)
57
+ context.http_request.body = @codec.serialize(context.operation.input, context.params)
58
+ end
59
+
60
+ def apply_url_path(context)
61
+ base = context.http_request.endpoint
62
+ service_name = context.config.service.name
63
+ base.path += "/service/#{service_name}/operation/#{context.operation.name}"
64
+ end
65
+
66
+ def event_stream?(ref)
67
+ ref.shape.members.each_value do |member_ref|
68
+ shape = member_ref.shape
69
+ return true if shape.traits.key?('smithy.api#streaming') && shape.is_a?(UnionShape)
70
+ end
71
+ false
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Smithy
4
+ module Client
5
+ module RPCv2CBOR
6
+ # @api private
7
+ class ResponseParser
8
+ def initialize(options = {})
9
+ @codec = CBOR::Codec.new(options)
10
+ end
11
+
12
+ def parse_error(context)
13
+ if !valid_response?(context)
14
+ code, message, data = http_status_error(context)
15
+ build_error(context, code, message, data)
16
+ elsif (300..599).cover?(context.http_response.status_code)
17
+ error(context)
18
+ end
19
+ end
20
+
21
+ def parse_data(context)
22
+ @codec.deserialize(context.operation.output, context.http_response.body.read)
23
+ end
24
+
25
+ private
26
+
27
+ def valid_response?(context)
28
+ req_header = context.http_request.headers['smithy-protocol']
29
+ resp_header = context.http_request.headers['smithy-protocol']
30
+ req_header == resp_header
31
+ end
32
+
33
+ def error(context)
34
+ body = context.http_response.body.read
35
+ if body.empty?
36
+ code, message, data = http_status_error(context)
37
+ else
38
+ code, message, data = extract_error(body, context)
39
+ end
40
+ build_error(context, code, message, data)
41
+ end
42
+
43
+ def extract_error(body, context)
44
+ data = CBOR.decode(body)
45
+ type = data['__type']
46
+ code = error_code(type, context)
47
+ message = data['message']
48
+ data = parse_error_data(context, body, type)
49
+ [code, message, data]
50
+ rescue CBOR::Error
51
+ [http_status_error_code(context), '', Schema::EmptyStructure.new]
52
+ end
53
+
54
+ def parse_error_data(context, body, code)
55
+ data = Schema::EmptyStructure.new
56
+ context.operation.errors.each do |ref|
57
+ next unless ref.shape.id == code
58
+
59
+ data = @codec.deserialize(ref, body, ref.shape.type.new)
60
+ end
61
+ data
62
+ end
63
+
64
+ def error_code(type, context)
65
+ return type.split('#').last if type
66
+
67
+ http_status_error_code(context)
68
+ end
69
+
70
+ def build_error(context, code, message, data)
71
+ errors_module = context.client.class.errors_module
72
+ errors_module.error_class(code).new(context, message, data)
73
+ end
74
+
75
+ def http_status_error(context)
76
+ [http_status_error_code(context), '', Schema::EmptyStructure.new]
77
+ end
78
+
79
+ def http_status_error_code(context)
80
+ status_code = context.http_response.status_code
81
+ "HTTP#{status_code}Error"
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end