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,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module NetHTTP
|
6
|
+
# The default HTTP handler for Smithy::Client. This is based on Ruby's `Net::HTTP`.
|
7
|
+
# @api private
|
8
|
+
class Handler < Client::Handler
|
9
|
+
# @api private
|
10
|
+
class TruncatedBodyError < IOError
|
11
|
+
def initialize(bytes_expected, bytes_received)
|
12
|
+
msg = "http response body truncated, expected #{bytes_expected} " \
|
13
|
+
"bytes, received #{bytes_received} bytes"
|
14
|
+
super(msg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param [HandlerContext] context
|
19
|
+
# @return [Output]
|
20
|
+
def call(context)
|
21
|
+
transmit(context.config, context.http_request, context.http_response)
|
22
|
+
Response.new(context: context)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param [Configuration] config
|
28
|
+
# @param [HTTP::Request] req
|
29
|
+
# @param [HTTP::Response] resp
|
30
|
+
# @return [void]
|
31
|
+
def transmit(config, req, resp)
|
32
|
+
# Monkey patch default content-type set by Net::HTTP
|
33
|
+
Thread.current[:net_http_skip_default_content_type] = true
|
34
|
+
session(config, req) do |http|
|
35
|
+
http.request(build_net_request(req)) do |net_resp|
|
36
|
+
bytes_received = signal_response(net_resp, resp)
|
37
|
+
complete_response(req, resp, bytes_received)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
rescue ArgumentError => e
|
41
|
+
# Invalid verb, ArgumentError is a StandardError
|
42
|
+
# Not retryable.
|
43
|
+
resp.signal_error(e)
|
44
|
+
rescue StandardError => e
|
45
|
+
error = NetworkingError.new(e)
|
46
|
+
resp.signal_error(error)
|
47
|
+
ensure
|
48
|
+
# ensure we turn off monkey patch in case of error
|
49
|
+
Thread.current[:net_http_skip_default_content_type] = nil
|
50
|
+
end
|
51
|
+
|
52
|
+
def signal_response(net_resp, resp)
|
53
|
+
status_code = net_resp.code.to_i
|
54
|
+
headers = extract_headers(net_resp)
|
55
|
+
|
56
|
+
bytes_received = 0
|
57
|
+
resp.signal_headers(status_code, headers)
|
58
|
+
net_resp.read_body do |chunk|
|
59
|
+
bytes_received += chunk.bytesize
|
60
|
+
resp.signal_data(chunk)
|
61
|
+
end
|
62
|
+
bytes_received
|
63
|
+
end
|
64
|
+
|
65
|
+
def complete_response(req, resp, bytes_received)
|
66
|
+
if should_verify_bytes?(req, resp)
|
67
|
+
verify_bytes_received(resp, bytes_received)
|
68
|
+
else
|
69
|
+
resp.signal_done
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def should_verify_bytes?(req, resp)
|
74
|
+
req.http_method != 'HEAD' && resp.headers['content-length']
|
75
|
+
end
|
76
|
+
|
77
|
+
def verify_bytes_received(resp, bytes_received)
|
78
|
+
bytes_expected = resp.headers['content-length'].to_i
|
79
|
+
if bytes_expected == bytes_received
|
80
|
+
resp.signal_done
|
81
|
+
else
|
82
|
+
error = TruncatedBodyError.new(bytes_expected, bytes_received)
|
83
|
+
resp.signal_error(NetworkingError.new(error))
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param [Configuration] config
|
88
|
+
# @param [HTTP::Request] req
|
89
|
+
# @yieldparam [Net::HTTP] http
|
90
|
+
def session(config, req, &)
|
91
|
+
pool_for(config).session_for(req.endpoint, &)
|
92
|
+
end
|
93
|
+
|
94
|
+
# @param [Configuration] config
|
95
|
+
# @return [ConnectionPool]
|
96
|
+
def pool_for(config)
|
97
|
+
ConnectionPool.for(pool_options(config))
|
98
|
+
end
|
99
|
+
|
100
|
+
# Extracts the {ConnectionPool} configuration options.
|
101
|
+
# @param [Configuration] config
|
102
|
+
# @return [Hash]
|
103
|
+
def pool_options(config)
|
104
|
+
ConnectionPool::OPTIONS.keys.each_with_object({}) do |opt, opts|
|
105
|
+
opts[opt] = config.send(opt)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# Constructs and returns a Net::HTTP::Request object from a {HTTP::Request}.
|
110
|
+
# @param [HTTP::Request] request
|
111
|
+
# @return [Net::HTTP::Request]
|
112
|
+
def build_net_request(request)
|
113
|
+
request_class = net_http_request_class(request)
|
114
|
+
req = request_class.new(request.endpoint.request_uri, net_headers_for(request))
|
115
|
+
|
116
|
+
# Net::HTTP adds a default Content-Type when a body is present.
|
117
|
+
# Set the body stream when it has an unknown size or when it is > 0.
|
118
|
+
if !request.body.respond_to?(:size) ||
|
119
|
+
(request.body.respond_to?(:size) && request.body.size.positive?)
|
120
|
+
req.body_stream = request.body
|
121
|
+
end
|
122
|
+
req
|
123
|
+
end
|
124
|
+
|
125
|
+
# @param [HTTP::Request] request
|
126
|
+
# @raise [ArgumentError]
|
127
|
+
# @return Returns a base `Net::HTTP::Request` class, e.g.,
|
128
|
+
# `Net::HTTP::Get`, `Net::HTTP::Post`, etc.
|
129
|
+
def net_http_request_class(request)
|
130
|
+
Net::HTTP.const_get(request.http_method.capitalize)
|
131
|
+
rescue NameError
|
132
|
+
msg = "`#{request.http_method}` is not a valid http verb"
|
133
|
+
raise ArgumentError, msg
|
134
|
+
end
|
135
|
+
|
136
|
+
# Validate that fields are not trailers and return a hash of headers.
|
137
|
+
# @param [HTTP::Request] request
|
138
|
+
# @return [Hash<String, String>]
|
139
|
+
def net_headers_for(request)
|
140
|
+
# Net::HTTP adds a default header for accept-encoding (2.0.0+).
|
141
|
+
# Setting a default empty value defeats this.
|
142
|
+
# Removing this is necessary for most services to not break request
|
143
|
+
# signatures as well as dynamodb crc32 checks (these fail if the
|
144
|
+
# response is gzipped).
|
145
|
+
headers = { 'accept-encoding' => '' }
|
146
|
+
request.headers.each_pair do |key, value|
|
147
|
+
headers[key] = value
|
148
|
+
end
|
149
|
+
headers
|
150
|
+
end
|
151
|
+
|
152
|
+
# @param [Net::HTTP::Response] response
|
153
|
+
# @return [Hash<String, String>]
|
154
|
+
def extract_headers(response)
|
155
|
+
response.to_hash.transform_values(&:first)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
module NetHTTP
|
6
|
+
# @api private
|
7
|
+
module Patches
|
8
|
+
def self.apply!
|
9
|
+
Net::HTTPGenericRequest.prepend(PatchDefaultContentType)
|
10
|
+
end
|
11
|
+
|
12
|
+
# For requests with bodies, Net::HTTP sets a default content type of:
|
13
|
+
# 'application/x-www-form-urlencoded'
|
14
|
+
# There are cases where we should not send content type at all.
|
15
|
+
# Even when no body is supplied, Net::HTTP uses a default empty body
|
16
|
+
# and sets it anyway. This patch disables the behavior when a Thread
|
17
|
+
# local variable is set.
|
18
|
+
module PatchDefaultContentType
|
19
|
+
def supply_default_content_type
|
20
|
+
return if Thread.current[:net_http_skip_default_content_type]
|
21
|
+
|
22
|
+
super
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# Raised when a networking error occurs.
|
6
|
+
class NetworkingError < StandardError
|
7
|
+
# @param [StandardError] error
|
8
|
+
def initialize(error)
|
9
|
+
super(error.message)
|
10
|
+
@original_error = error
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :original_error
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Smithy
|
4
|
+
module Client
|
5
|
+
# Raised when calling {PageableResponse#next_page} on a paginator that
|
6
|
+
# is on the last page of results. You can call {PageableResponse#last_page?}
|
7
|
+
# or {PageableResponse#next_page?} to know if there are more pages.
|
8
|
+
class LastPageError < RuntimeError
|
9
|
+
# @param [Response] response
|
10
|
+
def initialize(response)
|
11
|
+
@response = response
|
12
|
+
super('unable to fetch next page, end of results reached')
|
13
|
+
end
|
14
|
+
|
15
|
+
# @return [Response]
|
16
|
+
attr_reader :response
|
17
|
+
end
|
18
|
+
|
19
|
+
# Decorates a {Smithy::Client::Response} with paging convenience methods.
|
20
|
+
# Most API calls provide paged responses to limit the amount of data returned
|
21
|
+
# with each response. To optimize for latency, some APIs may return an
|
22
|
+
# inconsistent number of responses per page. You should rely on the values of
|
23
|
+
# the `next_page?` method or using enumerable methods such as `each_page` rather
|
24
|
+
# than the number of items returned to iterate through results. See below for
|
25
|
+
# examples.
|
26
|
+
#
|
27
|
+
# # Enumerator Methods
|
28
|
+
# The simplest way to handle paged response data is to use the built-in
|
29
|
+
# `each_page` enumerator on the response object:
|
30
|
+
#
|
31
|
+
# weather = Weather::Client.new
|
32
|
+
# weather.list_cities.each_page do |page|
|
33
|
+
# puts page.items.map(&:name)
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# This yields one response object per API call made. The SDK retrieves additional
|
37
|
+
# pages of data to complete the request.
|
38
|
+
#
|
39
|
+
# If the operation allows for it, a selected item can be enumerated using
|
40
|
+
# `each_item`:
|
41
|
+
#
|
42
|
+
# weather = Weather::Client.new
|
43
|
+
# weather.list_cities.each_item do |item|
|
44
|
+
# puts item.name
|
45
|
+
# end
|
46
|
+
#
|
47
|
+
# # Handling Paged Responses Manually
|
48
|
+
# To handle paging yourself, use the Response's `next_page?` method to verify
|
49
|
+
# there are more pages to retrieve, or use the `last_page?` method to verify
|
50
|
+
# there are no more pages to retrieve.
|
51
|
+
#
|
52
|
+
# If there are more pages, use the `next_page` method to retrieve the
|
53
|
+
# next page of results, as shown in the following example.
|
54
|
+
#
|
55
|
+
# weather = Weather::Client.new
|
56
|
+
#
|
57
|
+
# # Get the first page of data
|
58
|
+
# response = weather.list_cities
|
59
|
+
#
|
60
|
+
# # Get additional pages
|
61
|
+
# while response.next_page?
|
62
|
+
# response = response.next_page
|
63
|
+
# # Use the response data here...
|
64
|
+
# puts response.items.map(&:name)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
module PageableResponse
|
68
|
+
# @api private
|
69
|
+
attr_accessor :paginator
|
70
|
+
|
71
|
+
# Returns `true` if there are no more results. Calling {#next_page}
|
72
|
+
# when this method returns `false` will raise an error.
|
73
|
+
# @return [Boolean]
|
74
|
+
def last_page?
|
75
|
+
return @last_page if @last_page
|
76
|
+
|
77
|
+
@last_page = !truncated?
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns `true` if there are more results. Calling {#next_page} will
|
81
|
+
# return the next response.
|
82
|
+
# @return [Boolean]
|
83
|
+
def next_page?
|
84
|
+
return @next_page if @next_page
|
85
|
+
|
86
|
+
@next_page = truncated?
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param [Hash] params A hash of additional request params.
|
90
|
+
# @return [Response] Returns the next page of results.
|
91
|
+
def next_page(params = {})
|
92
|
+
raise LastPageError, self if last_page?
|
93
|
+
|
94
|
+
params = next_page_params(params)
|
95
|
+
context.client.send(context.operation_name, params)
|
96
|
+
end
|
97
|
+
|
98
|
+
# Yields the current and each following response to the given block.
|
99
|
+
# @yieldparam [Response] response
|
100
|
+
# @return [Enumerable, nil] Returns a new Enumerable if no block is given.
|
101
|
+
def each_page(&)
|
102
|
+
response = self
|
103
|
+
yield(response)
|
104
|
+
until response.last_page?
|
105
|
+
response = response.next_page
|
106
|
+
yield(response)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# Yields the current and each following item to the given block.
|
111
|
+
# @yieldparam [Object] item
|
112
|
+
# @return [Enumerable, nil] Returns a new Enumerable if no block is given.
|
113
|
+
def each_item(&)
|
114
|
+
response = self
|
115
|
+
@paginator.items(response.data).each(&)
|
116
|
+
until response.last_page?
|
117
|
+
response = response.next_page
|
118
|
+
@paginator.items(response.data).each(&)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def truncated?
|
125
|
+
next_t = @paginator.next_tokens(data)
|
126
|
+
!(next_t.empty? || next_t == @paginator.prev_tokens(context.params))
|
127
|
+
end
|
128
|
+
|
129
|
+
def next_page_params(params)
|
130
|
+
prev_tokens = @paginator.prev_tokens(context.params)
|
131
|
+
# Remove all previous tokens from original params
|
132
|
+
# Sometimes a token can be nil and merge would not include it.
|
133
|
+
new_params = context.params.except(*prev_tokens)
|
134
|
+
new_params.merge!(@paginator.next_tokens(data).merge(params))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bigdecimal'
|
4
|
+
require 'stringio'
|
5
|
+
require 'date'
|
6
|
+
require 'time'
|
7
|
+
require 'tempfile'
|
8
|
+
|
9
|
+
module Smithy
|
10
|
+
module Client
|
11
|
+
# @api private
|
12
|
+
class ParamConverter
|
13
|
+
include Schema::Shapes
|
14
|
+
|
15
|
+
@mutex = Mutex.new
|
16
|
+
@converters = Hash.new { |h, k| h[k] = {} }
|
17
|
+
|
18
|
+
def initialize(ref)
|
19
|
+
@ref = ref
|
20
|
+
@opened_files = []
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_reader :opened_files
|
24
|
+
|
25
|
+
# @param [Hash] params
|
26
|
+
# @return [Hash]
|
27
|
+
def convert(params)
|
28
|
+
structure(@ref, params)
|
29
|
+
end
|
30
|
+
|
31
|
+
def close_opened_files
|
32
|
+
@opened_files.each(&:close)
|
33
|
+
@opened_files = []
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def c(ref, value)
|
39
|
+
self.class.c(ref.shape.class, value, self)
|
40
|
+
end
|
41
|
+
|
42
|
+
def shape(ref, value)
|
43
|
+
case ref.shape
|
44
|
+
when ListShape then list(ref, value)
|
45
|
+
when MapShape then map(ref, value)
|
46
|
+
when StructureShape then structure(ref, value)
|
47
|
+
when UnionShape then union(ref, value)
|
48
|
+
else c(ref, value)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def list(ref, values)
|
53
|
+
values = c(ref, values)
|
54
|
+
return values unless values.is_a?(Array)
|
55
|
+
|
56
|
+
values.collect { |v| shape(ref.shape.member, v) }
|
57
|
+
end
|
58
|
+
|
59
|
+
def map(ref, values)
|
60
|
+
values = c(ref, values)
|
61
|
+
return values unless values.is_a?(Hash)
|
62
|
+
|
63
|
+
values.each.with_object({}) do |(key, value), hash|
|
64
|
+
hash[shape(ref.shape.key, key)] = shape(ref.shape.value, value)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def structure(ref, values)
|
69
|
+
values = c(ref, values)
|
70
|
+
return values unless values.respond_to?(:each_pair)
|
71
|
+
|
72
|
+
values.each_pair do |k, v|
|
73
|
+
next if v.nil?
|
74
|
+
next unless ref.shape.member?(k)
|
75
|
+
|
76
|
+
values[k] = shape(ref.shape.member(k), v)
|
77
|
+
end
|
78
|
+
values
|
79
|
+
end
|
80
|
+
|
81
|
+
def union(ref, values)
|
82
|
+
values = c(ref, values)
|
83
|
+
|
84
|
+
if values.is_a?(Schema::Union)
|
85
|
+
_name, member_ref = ref.shape.member_by_type(values.class)
|
86
|
+
values = shape(member_ref, values)
|
87
|
+
else
|
88
|
+
key, value = values.first
|
89
|
+
values[key] = shape(ref.shape.member(key), value)
|
90
|
+
end
|
91
|
+
values
|
92
|
+
end
|
93
|
+
|
94
|
+
class << self
|
95
|
+
# Registers a new value converter. Converters run in the context
|
96
|
+
# of a shape and value class.
|
97
|
+
#
|
98
|
+
# # add a converter that stringifies integers
|
99
|
+
# shape_class = Shapes::StringShape
|
100
|
+
# ParamConverter.add(shape_class, Integer) { |i| i.to_s }
|
101
|
+
#
|
102
|
+
# @param [Class<Shapes::Shape>] shape_class
|
103
|
+
# @param [Class] value_class
|
104
|
+
# @param [#call] block An object that responds to `#call`
|
105
|
+
# accepting a single argument. This function should perform
|
106
|
+
# the value conversion if possible, returning the result.
|
107
|
+
# If the conversion is not possible, the original value should
|
108
|
+
# be returned.
|
109
|
+
# @return [void]
|
110
|
+
def add(shape_class, value_class, &block)
|
111
|
+
@converters[shape_class][value_class] = block
|
112
|
+
end
|
113
|
+
|
114
|
+
def ensure_open(file, converter)
|
115
|
+
if file.closed?
|
116
|
+
new_file = File.open(file.path, 'rb')
|
117
|
+
converter.opened_files << new_file
|
118
|
+
new_file
|
119
|
+
else
|
120
|
+
file
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def c(shape, value, instance = nil)
|
125
|
+
if (converter = converter_for(shape, value))
|
126
|
+
converter.call(value, instance)
|
127
|
+
else
|
128
|
+
value
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def converter_for(shape_class, value)
|
135
|
+
unless @converters[shape_class].key?(value.class)
|
136
|
+
@mutex.synchronize do
|
137
|
+
unless @converters[shape_class].key?(value.class)
|
138
|
+
@converters[shape_class][value.class] = find(shape_class, value)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
@converters[shape_class][value.class]
|
143
|
+
end
|
144
|
+
|
145
|
+
def find(shape_class, value)
|
146
|
+
converter = nil
|
147
|
+
each_base_class(shape_class) do |klass|
|
148
|
+
@converters[klass].each do |value_class, block|
|
149
|
+
if value.is_a?(value_class)
|
150
|
+
converter = block
|
151
|
+
break
|
152
|
+
end
|
153
|
+
end
|
154
|
+
break if converter
|
155
|
+
end
|
156
|
+
converter
|
157
|
+
end
|
158
|
+
|
159
|
+
def each_base_class(shape_class, &)
|
160
|
+
shape_class.ancestors.each do |ancestor|
|
161
|
+
yield(ancestor) if @converters.key?(ancestor)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
add(BigDecimalShape, BigDecimal)
|
167
|
+
add(BigDecimalShape, Integer) { |i| BigDecimal(i) }
|
168
|
+
add(BigDecimalShape, Float) { |f| BigDecimal(f.to_s) }
|
169
|
+
add(BigDecimalShape, String) do |str|
|
170
|
+
BigDecimal(str)
|
171
|
+
rescue ArgumentError
|
172
|
+
str
|
173
|
+
end
|
174
|
+
|
175
|
+
add(BlobShape, IO)
|
176
|
+
add(BlobShape, File) { |file, converter| ensure_open(file, converter) }
|
177
|
+
add(BlobShape, Tempfile) { |tmpfile, converter| ensure_open(tmpfile, converter) }
|
178
|
+
add(BlobShape, StringIO)
|
179
|
+
add(BlobShape, String)
|
180
|
+
|
181
|
+
add(BooleanShape, TrueClass)
|
182
|
+
add(BooleanShape, FalseClass)
|
183
|
+
add(BooleanShape, String) do |str|
|
184
|
+
{ 'true' => true, 'false' => false }[str]
|
185
|
+
end
|
186
|
+
|
187
|
+
add(EnumShape, String)
|
188
|
+
add(EnumShape, Symbol) { |sym, _| sym.to_s }
|
189
|
+
|
190
|
+
add(IntegerShape, Integer)
|
191
|
+
add(IntegerShape, Float) { |f, _| f.to_i }
|
192
|
+
add(IntegerShape, String) do |str|
|
193
|
+
Integer(str)
|
194
|
+
rescue ArgumentError
|
195
|
+
str
|
196
|
+
end
|
197
|
+
|
198
|
+
add(IntEnumShape, Integer)
|
199
|
+
add(IntEnumShape, Float) { |f, _| f.to_i }
|
200
|
+
add(IntEnumShape, String) do |str|
|
201
|
+
Integer(str)
|
202
|
+
rescue ArgumentError
|
203
|
+
str
|
204
|
+
end
|
205
|
+
|
206
|
+
add(FloatShape, Float)
|
207
|
+
add(FloatShape, Integer) { |i, _| i.to_f }
|
208
|
+
add(FloatShape, String) do |str|
|
209
|
+
Float(str)
|
210
|
+
rescue ArgumentError
|
211
|
+
str
|
212
|
+
end
|
213
|
+
|
214
|
+
add(ListShape, Array) { |a, _| a.dup }
|
215
|
+
add(ListShape, Enumerable) { |v, _| v.to_a }
|
216
|
+
|
217
|
+
add(MapShape, Hash) { |h, _| h.dup }
|
218
|
+
add(MapShape, ::Struct) do |s|
|
219
|
+
s.members.each.with_object({}) { |k, h| h[k] = s[k] }
|
220
|
+
end
|
221
|
+
|
222
|
+
add(StringShape, String)
|
223
|
+
add(StringShape, Symbol) { |sym, _| sym.to_s }
|
224
|
+
|
225
|
+
add(StructureShape, Hash) { |h, _| h.dup }
|
226
|
+
add(StructureShape, ::Struct)
|
227
|
+
|
228
|
+
add(TimestampShape, Time)
|
229
|
+
add(TimestampShape, Date) { |d, _| d.to_time }
|
230
|
+
add(TimestampShape, DateTime) { |dt, _| dt.to_time }
|
231
|
+
add(TimestampShape, Integer) { |i| Time.at(i) }
|
232
|
+
add(TimestampShape, Float) { |f| Time.at(f) }
|
233
|
+
add(TimestampShape, String) do |str|
|
234
|
+
Time.parse(str)
|
235
|
+
rescue ArgumentError
|
236
|
+
str
|
237
|
+
end
|
238
|
+
|
239
|
+
add(UnionShape, Hash) { |h, _| h.dup }
|
240
|
+
add(UnionShape, Schema::Union)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|