test-test-sink-test-test2 0.0.5
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 +7 -0
- data/README.md +151 -0
- data/lib/sink/base_client.rb +404 -0
- data/lib/sink/base_model.rb +226 -0
- data/lib/sink/client.rb +391 -0
- data/lib/sink/util.rb +78 -0
- data/lib/sink/version.rb +5 -0
- data/lib/sink.rb +13 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 3c0f9378f1675d98710fa5ec56bd73650d25ca22789e0916102b29c485a47c28
|
4
|
+
data.tar.gz: 3051b450c74b423e9ba5561f1c15748619e819d4ee871b024122d8f7331772d4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 420b8a6a0e53b0b8f4604b9c1d72eb0b03ddac8998a49fd7aefe34d4c8ecfa00dda11487ecfe25e3b1946058a4fa28a769b053fb37801f992733fd9ee7662062
|
7
|
+
data.tar.gz: f3a704f7fcd330785b1cea5d2c74ff80e12eecc472f4e1427f64635e5ee6553de922ef3a7c223e0d4d2a1331c936e9fc4ef009c2e5f280d14ccd96fe63d30e82
|
data/README.md
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
# Sink Ruby API library
|
2
|
+
|
3
|
+
The Sink Ruby library provides convenient access to the Sink REST API from any Ruby 3.0+
|
4
|
+
application.
|
5
|
+
|
6
|
+
It is generated with [Stainless](https://www.stainlessapi.com/).
|
7
|
+
|
8
|
+
## Documentation
|
9
|
+
|
10
|
+
Documentation for the most recent version of this gem can be found [on RubyDoc](https://rubydoc.info/github/stainless-sdks/sink-ruby-public).
|
11
|
+
|
12
|
+
The underlying REST API documentation can be found on [stainlessapi.com](https://stainlessapi.com).
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
To use this gem during the beta, install directly from GitHub with Bundler by
|
17
|
+
adding the following to your application's `Gemfile`:
|
18
|
+
|
19
|
+
```ruby
|
20
|
+
gem "sink", git: "https://github.com/stainless-sdks/sink-ruby-public", branch: "main"
|
21
|
+
```
|
22
|
+
|
23
|
+
To fetch an initial copy of the gem:
|
24
|
+
|
25
|
+
```sh
|
26
|
+
bundle install
|
27
|
+
```
|
28
|
+
|
29
|
+
To update the version used by your application when updates are pushed to
|
30
|
+
GitHub:
|
31
|
+
|
32
|
+
```sh
|
33
|
+
bundle update sink
|
34
|
+
```
|
35
|
+
|
36
|
+
## Usage
|
37
|
+
|
38
|
+
```ruby
|
39
|
+
require "sink"
|
40
|
+
|
41
|
+
sink = Sink::Client.new(
|
42
|
+
user_token: "My User Token", # defaults to ENV["SINK_CUSTOM_API_KEY_ENV"]
|
43
|
+
environment: "sandbox", # defaults to "production"
|
44
|
+
username: "Robert",
|
45
|
+
some_number_arg_required_no_default: 0,
|
46
|
+
some_number_arg_required_no_default_no_env: 0,
|
47
|
+
required_arg_no_env: "<example>"
|
48
|
+
)
|
49
|
+
|
50
|
+
card = sink.cards.create(
|
51
|
+
type: "SINGLE_USE",
|
52
|
+
exp_month: "08",
|
53
|
+
not_: "TEST",
|
54
|
+
shipping_address: {
|
55
|
+
"address1" => "180 Varick St",
|
56
|
+
"city" => "New York",
|
57
|
+
"country" => "USA",
|
58
|
+
"first_name" => "Jason",
|
59
|
+
"last_name" => "Mimosa",
|
60
|
+
"state" => "NY",
|
61
|
+
"postal_code" => "H0H0H0"
|
62
|
+
}
|
63
|
+
)
|
64
|
+
|
65
|
+
puts(card.token)
|
66
|
+
```
|
67
|
+
|
68
|
+
### Errors
|
69
|
+
|
70
|
+
When the library is unable to connect to the API, or if the API returns a
|
71
|
+
non-success status code (i.e., 4xx or 5xx response), a subclass of
|
72
|
+
`Sink::HTTP::Error` will be thrown:
|
73
|
+
|
74
|
+
```ruby
|
75
|
+
begin
|
76
|
+
sink.cards.create(type: "an_incorrect_type")
|
77
|
+
rescue Sink::HTTP::Error => e
|
78
|
+
puts(e.code) # 400
|
79
|
+
end
|
80
|
+
```
|
81
|
+
|
82
|
+
Error codes are as followed:
|
83
|
+
|
84
|
+
| Cause | Error Type |
|
85
|
+
| ---------------- | -------------------------- |
|
86
|
+
| HTTP 400 | `BadRequestError` |
|
87
|
+
| HTTP 401 | `AuthenticationError` |
|
88
|
+
| HTTP 403 | `PermissionDeniedError` |
|
89
|
+
| HTTP 404 | `NotFoundError` |
|
90
|
+
| HTTP 409 | `ConflictError` |
|
91
|
+
| HTTP 422 | `UnprocessableEntityError` |
|
92
|
+
| HTTP 429 | `RateLimitError` |
|
93
|
+
| HTTP >=500 | `InternalServerError` |
|
94
|
+
| Other HTTP error | `APIStatusError` |
|
95
|
+
| Timeout | `APITimeoutError` |
|
96
|
+
| Network error | `APIConnectionError` |
|
97
|
+
|
98
|
+
### Retries
|
99
|
+
|
100
|
+
Certain errors will be automatically retried 1 times by default, with a short
|
101
|
+
exponential backoff. Connection errors (for example, due to a network
|
102
|
+
connectivity problem), 408 Request Timeout, 409 Conflict, 429 Rate Limit, >=500 Internal errors,
|
103
|
+
and timeouts will all be retried by default.
|
104
|
+
|
105
|
+
You can use the `max_retries` option to configure or disable this:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# Configure the default for all requests:
|
109
|
+
sink = Sink::Client.new(
|
110
|
+
max_retries: 0, # default is 1
|
111
|
+
username: "Robert",
|
112
|
+
some_number_arg_required_no_default: 0,
|
113
|
+
some_number_arg_required_no_default_no_env: 0,
|
114
|
+
required_arg_no_env: "<example>"
|
115
|
+
)
|
116
|
+
|
117
|
+
# Or, configure per-request:
|
118
|
+
sink.cards.provision_foo("my card token", digital_wallet: "GOOGLE_PAY", max_retries: 5)
|
119
|
+
```
|
120
|
+
|
121
|
+
### Timeouts
|
122
|
+
|
123
|
+
By default, requests will time out after 60 seconds.
|
124
|
+
Timeouts are applied separately to the initial connection and the overall request time,
|
125
|
+
so in some cases a request could wait 2\*timeout seconds before it fails.
|
126
|
+
|
127
|
+
You can use the `timeout` option to configure or disable this:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# Configure the default for all requests:
|
131
|
+
sink = Sink::Client.new(
|
132
|
+
timeout: nil, # default is 60
|
133
|
+
username: "Robert",
|
134
|
+
some_number_arg_required_no_default: 0,
|
135
|
+
some_number_arg_required_no_default_no_env: 0,
|
136
|
+
required_arg_no_env: "<example>"
|
137
|
+
)
|
138
|
+
|
139
|
+
# Or, configure per-request:
|
140
|
+
sink.cards.create(type: "DIGITAL", timeout: 5)
|
141
|
+
```
|
142
|
+
|
143
|
+
## Versioning
|
144
|
+
|
145
|
+
This package follows [SemVer](https://semver.org/spec/v2.0.0.html) conventions. As the
|
146
|
+
library is in initial development and has a major version of `0`, APIs may change
|
147
|
+
at any time.
|
148
|
+
|
149
|
+
## Requirements
|
150
|
+
|
151
|
+
Ruby 3.0 or higher.
|
@@ -0,0 +1,404 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sink
|
4
|
+
# @!visibility private
|
5
|
+
class BaseClient
|
6
|
+
attr_accessor :requester
|
7
|
+
|
8
|
+
# @param base_url [String]
|
9
|
+
# @param timeout [Integer, nil]
|
10
|
+
# @param headers [Hash{String => String}]
|
11
|
+
# @param max_retries [Integer]
|
12
|
+
# @param idempotency_header [String, nil]
|
13
|
+
def initialize(
|
14
|
+
base_url:,
|
15
|
+
timeout: nil,
|
16
|
+
headers: {},
|
17
|
+
max_retries: 0,
|
18
|
+
idempotency_header: nil
|
19
|
+
)
|
20
|
+
self.requester = PooledNetRequester.new
|
21
|
+
base_url_parsed = URI.parse(base_url)
|
22
|
+
@headers = Util.normalized_headers(
|
23
|
+
{
|
24
|
+
"X-Stainless-Lang" => "ruby",
|
25
|
+
"X-Stainless-Package-Version" => Sink::VERSION,
|
26
|
+
"X-Stainless-Runtime" => RUBY_ENGINE,
|
27
|
+
"X-Stainless-Runtime-Version" => RUBY_ENGINE_VERSION,
|
28
|
+
"Accept" => "application/json"
|
29
|
+
},
|
30
|
+
headers
|
31
|
+
)
|
32
|
+
@host = base_url_parsed.host
|
33
|
+
@scheme = base_url_parsed.scheme
|
34
|
+
@port = base_url_parsed.port
|
35
|
+
@base_path = self.class.normalize_path(base_url_parsed.path)
|
36
|
+
@max_retries = max_retries
|
37
|
+
@timeout = timeout
|
38
|
+
@idempotency_header = idempotency_header
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Hash{String => String}]
|
42
|
+
def auth_headers
|
43
|
+
{}
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_request(req, opts)
|
47
|
+
if (body = req[:body])
|
48
|
+
# Body can be at least a Hash or Array, just check for Hash shape for now.
|
49
|
+
if body.is_a?(Hash)
|
50
|
+
body.each_key do |k|
|
51
|
+
unless k.is_a?(Symbol)
|
52
|
+
raise ArgumentError, "Request body keys must be Symbols, got #{k.inspect}"
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
unless opts.is_a?(Hash) || opts.is_a?(Sink::RequestOption)
|
59
|
+
raise ArgumentError, "Request `opts` must be a Hash or RequestOptions, got #{opts.inspect}"
|
60
|
+
end
|
61
|
+
opts.to_h.each_key do |k|
|
62
|
+
unless k.is_a?(Symbol)
|
63
|
+
raise ArgumentError, "Request `opts` keys must be Symbols, got #{k.inspect}"
|
64
|
+
end
|
65
|
+
unless (valid_keys = Sink::RequestOptions.options).include?(k)
|
66
|
+
raise ArgumentError, "Request `opts` keys must be one of #{valid_keys}, got #{k.inspect}"
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def self.normalize_path(path)
|
72
|
+
path.gsub(/\/+/, "/")
|
73
|
+
end
|
74
|
+
|
75
|
+
def resolve_uri_elements(req)
|
76
|
+
from_args =
|
77
|
+
if req[:url]
|
78
|
+
uri = req[:url].is_a?(URI::Generic) ? req[:url] : URI.parse(req[:url])
|
79
|
+
{
|
80
|
+
host: uri.host,
|
81
|
+
scheme: uri.scheme,
|
82
|
+
path: uri.path,
|
83
|
+
query: CGI.parse(uri.query || ""),
|
84
|
+
port: uri.port
|
85
|
+
}
|
86
|
+
else
|
87
|
+
from_req = req.slice(:host, :scheme, :path, :port, :query)
|
88
|
+
from_req[:path] = self.class.normalize_path("/#{@base_path}/#{from_req[:path]}")
|
89
|
+
from_req
|
90
|
+
end
|
91
|
+
|
92
|
+
uri_components = {host: @host, scheme: @scheme, port: @port}.merge(from_args)
|
93
|
+
|
94
|
+
if req[:extra_query]
|
95
|
+
uri_components[:query] = Util.deep_merge(uri_components[:query], req[:extra_query], concat: true)
|
96
|
+
end
|
97
|
+
|
98
|
+
uri_components
|
99
|
+
end
|
100
|
+
|
101
|
+
def prep_request(options)
|
102
|
+
method = options.fetch(:method)
|
103
|
+
|
104
|
+
headers = Util.normalized_headers(@headers, auth_headers, options[:headers], options[:extra_headers])
|
105
|
+
if @idempotency_header && !headers[@idempotency_header] && ![:get, :head, :options].include?(method)
|
106
|
+
headers[@idempotency_header.to_s.downcase] = options[:idempotency_key] || generate_idempotency_key
|
107
|
+
end
|
108
|
+
if !headers.key?("x-stainless-retry-count")
|
109
|
+
headers["x-stainless-retry-count"] = "0"
|
110
|
+
end
|
111
|
+
headers.compact!
|
112
|
+
headers.transform_values!(&:to_s)
|
113
|
+
|
114
|
+
body =
|
115
|
+
case method
|
116
|
+
when :post, :put, :patch, :delete
|
117
|
+
body = options[:body]
|
118
|
+
if body
|
119
|
+
if headers["content-type"] == "application/json"
|
120
|
+
JSON.dump(body)
|
121
|
+
else
|
122
|
+
body
|
123
|
+
end
|
124
|
+
end
|
125
|
+
else
|
126
|
+
nil
|
127
|
+
end
|
128
|
+
if options[:extra_body]
|
129
|
+
body = Util.deep_merge(body, options[:extra_body])
|
130
|
+
end
|
131
|
+
|
132
|
+
url_elements = resolve_uri_elements(options)
|
133
|
+
|
134
|
+
{method: method, headers: headers, body: body}.merge(url_elements)
|
135
|
+
end
|
136
|
+
|
137
|
+
def generate_idempotency_key
|
138
|
+
"stainless-ruby-retry-#{SecureRandom.uuid}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def should_retry?(response)
|
142
|
+
should_retry_header = response["x-should-retry"]
|
143
|
+
|
144
|
+
case should_retry_header
|
145
|
+
when "true"
|
146
|
+
true
|
147
|
+
when "false"
|
148
|
+
false
|
149
|
+
else
|
150
|
+
response_code = response.code.to_i
|
151
|
+
# retry on:
|
152
|
+
# 408: timeouts
|
153
|
+
# 409: locks
|
154
|
+
# 429: rate limits
|
155
|
+
# 500+: unknown errors
|
156
|
+
[408, 409, 429].include?(response_code) || response_code >= 500
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def make_status_error(message:, body:, response:)
|
161
|
+
raise NotImplementedError
|
162
|
+
end
|
163
|
+
|
164
|
+
def make_status_error_from_response(response)
|
165
|
+
err_body =
|
166
|
+
begin
|
167
|
+
JSON.parse(response.body)
|
168
|
+
rescue StandardError
|
169
|
+
response
|
170
|
+
end
|
171
|
+
|
172
|
+
# We include the body in the error message as well as returning it
|
173
|
+
# since logging error messages is a common and quick way to assess what's
|
174
|
+
# wrong with a response.
|
175
|
+
message = "Error code: #{response.code}; Response: #{response.body}"
|
176
|
+
|
177
|
+
make_status_error(message: message, body: err_body, response: response)
|
178
|
+
end
|
179
|
+
|
180
|
+
def header_based_retry(response)
|
181
|
+
# Note the `retry-after-ms` header may not be standard, but is a good idea and we'd like proactive support for it.
|
182
|
+
retry_after_millis = Float(response["retry-after-ms"], exception: false)
|
183
|
+
if retry_after_millis
|
184
|
+
retry_after = retry_after_millis / 1000.0
|
185
|
+
elsif response["retry-after"]
|
186
|
+
retry_after = Float(response["retry-after"], exception: false)
|
187
|
+
if retry_after.nil?
|
188
|
+
begin
|
189
|
+
base = Time.now
|
190
|
+
if response["x-stainless-mock-sleep-base"]
|
191
|
+
base = Time.httpdate(response["x-stainless-mock-sleep-base"])
|
192
|
+
end
|
193
|
+
retry_after = Time.httpdate(response["retry-after"]) - base
|
194
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
retry_after
|
199
|
+
rescue StandardError # rubocop:disable Lint/SuppressedException
|
200
|
+
end
|
201
|
+
|
202
|
+
def send_request(request, max_retries:, timeout:, redirect_count:)
|
203
|
+
delay = 0.6
|
204
|
+
max_delay = 8.0
|
205
|
+
# Don't send the current retry count in the headers if the caller modified the header defaults.
|
206
|
+
should_send_retry_count = request[:headers]["x-stainless-retry-count"] == "0"
|
207
|
+
retries = 0
|
208
|
+
loop do # rubocop:disable Metrics/BlockLength
|
209
|
+
if should_send_retry_count
|
210
|
+
request[:headers]["x-stainless-retry-count"] = retries.to_s
|
211
|
+
end
|
212
|
+
|
213
|
+
begin
|
214
|
+
response = @requester.execute(request, timeout: timeout)
|
215
|
+
status = response.code.to_i
|
216
|
+
|
217
|
+
if status < 300
|
218
|
+
return response
|
219
|
+
elsif status < 400
|
220
|
+
begin
|
221
|
+
prev_uri = URI.parse(Util.uri_from_req(request, absolute: true))
|
222
|
+
location = URI.join(prev_uri, response["location"])
|
223
|
+
rescue ArgumentError
|
224
|
+
message = "server responded with status #{status} but no valid location header"
|
225
|
+
raise HTTP::APIConnectionError.new(message: message, request: request)
|
226
|
+
end
|
227
|
+
# from whatwg fetch spec
|
228
|
+
if redirect_count == 20
|
229
|
+
message = "failed to complete the request within 20 redirects"
|
230
|
+
raise HTTP::APIConnectionError.new(message: message, request: request)
|
231
|
+
end
|
232
|
+
if location.scheme != "http" && location.scheme != "https"
|
233
|
+
message = "tried to redirect to a non-http URL"
|
234
|
+
raise HTTP::APIConnectionError.new(message: message, request: request)
|
235
|
+
end
|
236
|
+
request = request.merge(resolve_uri_elements({url: location}))
|
237
|
+
# from whatwg fetch spec
|
238
|
+
if ([301, 302].include?(status) && request[:method] == :post) || (status == 303)
|
239
|
+
request[:method] = request[:method] == :head ? :head : :get
|
240
|
+
request[:body] = nil
|
241
|
+
request[:headers] = request[:headers].reject do |k|
|
242
|
+
%w[content-encoding content-language content-location content-type content-length].include?(k)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
# from undici
|
246
|
+
if Sink::Util.uri_origin(prev_uri) != Sink::Util.uri_origin(location)
|
247
|
+
request[:headers] = request[:headers].reject do |k|
|
248
|
+
%w[authorization cookie proxy-authorization host].include?(k)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
return send_request(
|
252
|
+
request,
|
253
|
+
max_retries: max_retries,
|
254
|
+
timeout: timeout,
|
255
|
+
redirect_count: redirect_count + 1
|
256
|
+
)
|
257
|
+
end
|
258
|
+
rescue Net::HTTPBadResponse
|
259
|
+
if retries >= max_retries
|
260
|
+
message = "failed to complete the request within #{max_retries} retries"
|
261
|
+
raise HTTP::APIConnectionError.new(message: message, request: request)
|
262
|
+
end
|
263
|
+
rescue Timeout::Error
|
264
|
+
if retries >= max_retries
|
265
|
+
message = "failed to complete the request within #{max_retries} retries"
|
266
|
+
raise HTTP::APITimeoutError.new(message: message, request: request)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
if (response && !should_retry?(response)) || retries >= max_retries
|
271
|
+
raise make_status_error_from_response(response)
|
272
|
+
end
|
273
|
+
|
274
|
+
retries += 1
|
275
|
+
base_delay = header_based_retry(response)
|
276
|
+
if base_delay
|
277
|
+
delay = base_delay
|
278
|
+
else
|
279
|
+
base_delay = (delay * (2**retries))
|
280
|
+
jitter_factor = 1 - (0.25 * rand)
|
281
|
+
delay = (base_delay * jitter_factor).clamp(0, max_delay)
|
282
|
+
end
|
283
|
+
|
284
|
+
if response&.key?("x-stainless-mock-sleep")
|
285
|
+
request[:headers]["x-stainless-mock-slept"] = delay
|
286
|
+
else
|
287
|
+
sleep(delay)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
# Execute the request specified by req + opts. This is the method that all
|
293
|
+
# resource methods call into.
|
294
|
+
# Params req & opts are kept separate up until this point so that we can
|
295
|
+
# validate opts as it was given to us by the user.
|
296
|
+
def request(req, opts)
|
297
|
+
validate_request(req, opts)
|
298
|
+
options = Util.deep_merge(req, opts)
|
299
|
+
request_args = prep_request(options)
|
300
|
+
response = send_request(
|
301
|
+
request_args,
|
302
|
+
max_retries: opts.fetch(:max_retries, @max_retries),
|
303
|
+
timeout: opts.fetch(:timeout, @timeout),
|
304
|
+
redirect_count: 0
|
305
|
+
)
|
306
|
+
raw_data =
|
307
|
+
case response.content_type
|
308
|
+
when "application/json"
|
309
|
+
begin
|
310
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
311
|
+
req[:unwrap] ? data[req[:unwrap]] : data
|
312
|
+
rescue JSON::ParserError
|
313
|
+
response.body
|
314
|
+
end
|
315
|
+
# TODO: parsing other response types
|
316
|
+
else
|
317
|
+
response.body
|
318
|
+
end
|
319
|
+
|
320
|
+
if (page = req[:page])
|
321
|
+
model = req.fetch(:model)
|
322
|
+
page.new(model, raw_data, response, self, req, opts)
|
323
|
+
elsif (model = req[:model])
|
324
|
+
Converter.convert(model, raw_data)
|
325
|
+
else
|
326
|
+
raw_data
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
# @return [String]
|
331
|
+
def inspect
|
332
|
+
base_url = Util.uri_from_req(
|
333
|
+
{host: @host, scheme: @scheme, port: @port, path: @base_path},
|
334
|
+
absolute: true
|
335
|
+
)
|
336
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{base_url.inspect} max_retries=#{@max_retries.inspect} timeout=#{@timeout.inspect}>"
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
class Error < StandardError
|
341
|
+
end
|
342
|
+
|
343
|
+
module HTTP
|
344
|
+
class Error < Sink::Error
|
345
|
+
end
|
346
|
+
|
347
|
+
class ResponseError < Error
|
348
|
+
attr_reader :response, :body
|
349
|
+
|
350
|
+
# @!attribute [r] code
|
351
|
+
# @return [Integer]
|
352
|
+
attr_reader :code
|
353
|
+
|
354
|
+
def initialize(message:, response:, body:)
|
355
|
+
super(message)
|
356
|
+
@response = response
|
357
|
+
@body = body
|
358
|
+
@code = response.code.to_i
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
class RequestError < Error
|
363
|
+
attr_reader :request
|
364
|
+
|
365
|
+
def initialize(message:, request:)
|
366
|
+
super(message)
|
367
|
+
@request = request
|
368
|
+
end
|
369
|
+
end
|
370
|
+
|
371
|
+
class BadRequestError < ResponseError
|
372
|
+
end
|
373
|
+
|
374
|
+
class AuthenticationError < ResponseError
|
375
|
+
end
|
376
|
+
|
377
|
+
class PermissionDeniedError < ResponseError
|
378
|
+
end
|
379
|
+
|
380
|
+
class NotFoundError < ResponseError
|
381
|
+
end
|
382
|
+
|
383
|
+
class ConflictError < ResponseError
|
384
|
+
end
|
385
|
+
|
386
|
+
class UnprocessableEntityError < ResponseError
|
387
|
+
end
|
388
|
+
|
389
|
+
class RateLimitError < ResponseError
|
390
|
+
end
|
391
|
+
|
392
|
+
class InternalServerError < ResponseError
|
393
|
+
end
|
394
|
+
|
395
|
+
class APIStatusError < ResponseError
|
396
|
+
end
|
397
|
+
|
398
|
+
class APIConnectionError < RequestError
|
399
|
+
end
|
400
|
+
|
401
|
+
class APITimeoutError < RequestError
|
402
|
+
end
|
403
|
+
end
|
404
|
+
end
|
@@ -0,0 +1,226 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sink
|
4
|
+
# @!visibility private
|
5
|
+
module Converter
|
6
|
+
# Based on `value`, returns a value that conforms to `type`, to the extent possible:
|
7
|
+
# - If the given `value` conforms to `type` already, the given `value`.
|
8
|
+
# - If it's possible and safe to convert the given `value` to `type`, such a converted value.
|
9
|
+
# - Otherwise, the given `value` unaltered.
|
10
|
+
def self.convert(type, value)
|
11
|
+
# If `type.is_a?(Converter)`, `type` is an instance of a class that mixes
|
12
|
+
# in `Converter`, indicating that the type should define `#convert` on this
|
13
|
+
# instance. This is used for Enums and ArrayOfs, which are parameterized.
|
14
|
+
# If `type.include?(Converter)`, `type` is a class that mixes in `Converter`
|
15
|
+
# which we use to signal that the class should define `.convert`. This is
|
16
|
+
# used where the class itself fully specifies the type, like model classes.
|
17
|
+
# We don't monkey-patch Ruby-native types, so those need to be handled
|
18
|
+
# directly.
|
19
|
+
if type.is_a?(Converter) || type.include?(Converter)
|
20
|
+
type.convert(value)
|
21
|
+
elsif type == Date
|
22
|
+
Date.parse(value)
|
23
|
+
elsif type == Time
|
24
|
+
Time.parse(value)
|
25
|
+
elsif type == NilClass
|
26
|
+
nil
|
27
|
+
elsif type == Float
|
28
|
+
value.is_a?(Numeric) ? value.to_f : value
|
29
|
+
elsif [Object, Integer, String, Hash].include?(type)
|
30
|
+
value
|
31
|
+
else
|
32
|
+
raise StandardError, "Unexpected type #{type}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# When we don't know what to expect for the value.
|
38
|
+
# @!visibility private
|
39
|
+
class Unknown
|
40
|
+
include Converter
|
41
|
+
|
42
|
+
def self.convert(value)
|
43
|
+
value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Ruby has no Boolean class; this is something for models to refer to.
|
48
|
+
# @!visibility private
|
49
|
+
class BooleanModel
|
50
|
+
include Converter
|
51
|
+
|
52
|
+
def self.convert(value)
|
53
|
+
value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# A value from among a specified list of options. OpenAPI enum values map to
|
58
|
+
# Ruby values in the SDK as follows:
|
59
|
+
# boolean => true|false
|
60
|
+
# integer => Integer
|
61
|
+
# float => Float
|
62
|
+
# string => Symbol
|
63
|
+
# We can therefore convert string values to Symbols, but can't convert other
|
64
|
+
# values safely.
|
65
|
+
# @!visibility private
|
66
|
+
class Enum
|
67
|
+
include Converter
|
68
|
+
|
69
|
+
def self.convert(value)
|
70
|
+
if value.is_a?(String)
|
71
|
+
value.to_sym
|
72
|
+
else
|
73
|
+
value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [Array<Symbol>] All of the valid Symbol values for this enum.
|
78
|
+
def self.values
|
79
|
+
@values ||= constants.map { |c| const_get(c) }
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Array of items of a given type.
|
84
|
+
# @!visibility private
|
85
|
+
class ArrayOf
|
86
|
+
include Converter
|
87
|
+
|
88
|
+
def initialize(items_type_info = nil, enum: nil)
|
89
|
+
@items_type_fn = enum || (items_type_info.is_a?(Proc) ? items_type_info : -> { items_type_info })
|
90
|
+
end
|
91
|
+
|
92
|
+
def convert(value)
|
93
|
+
items_type = @items_type_fn.call
|
94
|
+
value.map { |item| Converter.convert(items_type, item) }
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
class BaseModel
|
99
|
+
include Converter
|
100
|
+
|
101
|
+
# @!visibility private
|
102
|
+
# Assumes superclass fields are totally defined before fields are accessed / defined on subclasses.
|
103
|
+
# @return [Hash{Symbol => Hash{Symbol => Object}}]
|
104
|
+
def self.fields
|
105
|
+
@fields ||= (superclass == BaseModel ? {} : superclass.fields.dup)
|
106
|
+
end
|
107
|
+
|
108
|
+
# @!visibility private
|
109
|
+
# @param name_sym [Symbol]
|
110
|
+
# @param api_name [Symbol, nil]
|
111
|
+
# @param type_info [Proc, Object]
|
112
|
+
# @param mode [Symbol]
|
113
|
+
# @return [void]
|
114
|
+
def self.add_field(name_sym, api_name:, type_info:, mode:)
|
115
|
+
type_fn = type_info.is_a?(Proc) ? type_info : -> { type_info }
|
116
|
+
key = api_name || name_sym
|
117
|
+
fields[name_sym] = {type_fn: type_fn, mode: mode, key: key}
|
118
|
+
|
119
|
+
define_method(name_sym) do
|
120
|
+
field_type = type_fn.call
|
121
|
+
Converter.convert(field_type, @data[key])
|
122
|
+
rescue StandardError
|
123
|
+
name = self.class.name.split("::").last
|
124
|
+
raise ConversionError,
|
125
|
+
"Failed to parse #{name}.#{name_sym} as #{field_type.inspect}. " \
|
126
|
+
"To get the unparsed API response, use #{name}[:#{key}]."
|
127
|
+
end
|
128
|
+
define_method("#{name_sym}=") { |val| @data[key] = val }
|
129
|
+
end
|
130
|
+
|
131
|
+
# @!visibility private
|
132
|
+
# NB `required` is just a signal to the reader. We don't do runtime validation anyway.
|
133
|
+
def self.required(name_sym, type_info = nil, mode = :rw, api_name: nil, enum: nil)
|
134
|
+
add_field(name_sym, api_name: api_name, type_info: enum || type_info, mode: mode)
|
135
|
+
end
|
136
|
+
|
137
|
+
# @!visibility private
|
138
|
+
# NB `optional` is just a signal to the reader. We don't do runtime validation anyway.
|
139
|
+
def self.optional(name_sym, type_info = nil, mode = :rw, api_name: nil, enum: nil)
|
140
|
+
add_field(name_sym, api_name: api_name, type_info: enum || type_info, mode: mode)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @!visibility private
|
144
|
+
def self.convert(data)
|
145
|
+
new(data)
|
146
|
+
end
|
147
|
+
|
148
|
+
# Create a new instance of a model.
|
149
|
+
# @param data [Hash{Symbol => Object}] Raw data to initialize the model with.
|
150
|
+
def initialize(data = {})
|
151
|
+
@data = {}
|
152
|
+
# TODO: what if data isn't a hash?
|
153
|
+
data.each do |field_name, value|
|
154
|
+
next if value.nil?
|
155
|
+
|
156
|
+
field = self.class.fields[field_name.to_sym]
|
157
|
+
if field
|
158
|
+
next if field[:mode] == :w
|
159
|
+
end
|
160
|
+
@data[field_name.to_sym] = value
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
# Returns a Hash of the data underlying this object.
|
165
|
+
# Keys are Symbols and values are the raw values from the response.
|
166
|
+
# The return value indicates which values were ever set on the object -
|
167
|
+
# i.e. there will be a key in this hash if they ever were, even if the
|
168
|
+
# set value was nil.
|
169
|
+
# This method is not recursive.
|
170
|
+
# The returned value is shared by the object, so it should not be mutated.
|
171
|
+
#
|
172
|
+
# @return [Hash{Symbol => Object}] Data for this object.
|
173
|
+
def to_h
|
174
|
+
@data
|
175
|
+
end
|
176
|
+
|
177
|
+
alias_method :to_hash, :to_h
|
178
|
+
|
179
|
+
# Returns the raw value associated with the given key, if found. Otherwise, nil is returned.
|
180
|
+
# It is valid to lookup keys that are not in the API spec, for example to access
|
181
|
+
# undocumented features.
|
182
|
+
# This method does not parse response data into higher-level types.
|
183
|
+
# Lookup by anything other than a Symbol is an ArgumentError.
|
184
|
+
#
|
185
|
+
# @param key [Symbol] Key to look up by.
|
186
|
+
#
|
187
|
+
# @return [Object, nil] The raw value at the given key.
|
188
|
+
def [](key)
|
189
|
+
if !key.instance_of?(Symbol)
|
190
|
+
raise ArgumentError, "Expected symbol key for lookup, got #{key.inspect}"
|
191
|
+
end
|
192
|
+
@data[key]
|
193
|
+
end
|
194
|
+
|
195
|
+
# @param keys [Array<Symbol>, nil]
|
196
|
+
# @return [Hash{Symbol => Object}]
|
197
|
+
def deconstruct_keys(keys)
|
198
|
+
(keys || self.class.fields.keys).to_h do |k|
|
199
|
+
if !k.instance_of?(Symbol)
|
200
|
+
raise ArgumentError, "Expected symbol key for lookup, got #{k.inspect}"
|
201
|
+
end
|
202
|
+
|
203
|
+
if !self.class.fields.key?(k)
|
204
|
+
raise KeyError, "Expected one of #{self.class.fields.keys}, got #{k.inspect}"
|
205
|
+
end
|
206
|
+
|
207
|
+
[k, method(k).call]
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
# @return [String]
|
212
|
+
def inspect
|
213
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} #{deconstruct_keys(nil).map do |k, v|
|
214
|
+
"#{k}=#{v.inspect}"
|
215
|
+
end.join(' ')}>"
|
216
|
+
end
|
217
|
+
|
218
|
+
# @return [String]
|
219
|
+
def to_s
|
220
|
+
@data.to_s
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class ConversionError < Error
|
225
|
+
end
|
226
|
+
end
|
data/lib/sink/client.rb
ADDED
@@ -0,0 +1,391 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sink
|
4
|
+
class Client < BaseClient
|
5
|
+
# Default max number of retries to attempt after a failed retryable request.
|
6
|
+
DEFAULT_MAX_RETRIES = 1
|
7
|
+
|
8
|
+
# Client option
|
9
|
+
# @return [String]
|
10
|
+
attr_reader :user_token
|
11
|
+
|
12
|
+
# Client option
|
13
|
+
# @return [String]
|
14
|
+
attr_reader :api_key_header
|
15
|
+
|
16
|
+
# Client option
|
17
|
+
# @return [String]
|
18
|
+
attr_reader :api_key_query
|
19
|
+
|
20
|
+
# Client option
|
21
|
+
# @return [String]
|
22
|
+
attr_reader :username
|
23
|
+
|
24
|
+
# Client option
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :client_id
|
27
|
+
|
28
|
+
# Client option
|
29
|
+
# @return [String]
|
30
|
+
attr_reader :client_secret
|
31
|
+
|
32
|
+
# Client option
|
33
|
+
# @return [Boolean]
|
34
|
+
attr_reader :some_boolean_arg
|
35
|
+
|
36
|
+
# Client option
|
37
|
+
# @return [Integer]
|
38
|
+
attr_reader :some_integer_arg
|
39
|
+
|
40
|
+
# Client option
|
41
|
+
# @return [Float]
|
42
|
+
attr_reader :some_number_arg
|
43
|
+
|
44
|
+
# Client option
|
45
|
+
# @return [Float]
|
46
|
+
attr_reader :some_number_arg_required
|
47
|
+
|
48
|
+
# Client option
|
49
|
+
# @return [Float]
|
50
|
+
attr_reader :some_number_arg_required_no_default
|
51
|
+
|
52
|
+
# Client option
|
53
|
+
# @return [Float]
|
54
|
+
attr_reader :some_number_arg_required_no_default_no_env
|
55
|
+
|
56
|
+
# Client option
|
57
|
+
# @return [String]
|
58
|
+
attr_reader :required_arg_no_env
|
59
|
+
|
60
|
+
# Client option
|
61
|
+
# @return [String]
|
62
|
+
attr_reader :required_arg_no_env_with_default
|
63
|
+
|
64
|
+
# Client option
|
65
|
+
# @return [String]
|
66
|
+
attr_reader :client_path_param
|
67
|
+
|
68
|
+
# Client option
|
69
|
+
# @return [String]
|
70
|
+
attr_reader :camel_case_path
|
71
|
+
|
72
|
+
# Client option
|
73
|
+
# @return [String]
|
74
|
+
attr_reader :client_query_param
|
75
|
+
|
76
|
+
# Client option
|
77
|
+
# @return [String]
|
78
|
+
attr_reader :client_path_or_query_param
|
79
|
+
|
80
|
+
# @return [Sink::Resources::Testing]
|
81
|
+
attr_reader :testing
|
82
|
+
|
83
|
+
# @return [Sink::Resources::ComplexQueries]
|
84
|
+
attr_reader :complex_queries
|
85
|
+
|
86
|
+
# @return [Sink::Resources::Casing]
|
87
|
+
attr_reader :casing
|
88
|
+
|
89
|
+
# @return [Sink::Resources::Tools]
|
90
|
+
attr_reader :tools
|
91
|
+
|
92
|
+
# @return [Sink::Resources::UndocumentedResource]
|
93
|
+
attr_reader :undocumented_resource
|
94
|
+
|
95
|
+
# @return [Sink::Resources::MethodConfig]
|
96
|
+
attr_reader :method_config
|
97
|
+
|
98
|
+
# @return [Sink::Resources::Streaming]
|
99
|
+
attr_reader :streaming
|
100
|
+
|
101
|
+
# @return [Sink::Resources::PaginationTests]
|
102
|
+
attr_reader :pagination_tests
|
103
|
+
|
104
|
+
# @return [Sink::Resources::Docstrings]
|
105
|
+
attr_reader :docstrings
|
106
|
+
|
107
|
+
# @return [Sink::Resources::InvalidSchemas]
|
108
|
+
attr_reader :invalid_schemas
|
109
|
+
|
110
|
+
# @return [Sink::Resources::ResourceRefs]
|
111
|
+
attr_reader :resource_refs
|
112
|
+
|
113
|
+
# @return [Sink::Resources::Cards]
|
114
|
+
attr_reader :cards
|
115
|
+
|
116
|
+
# @return [Sink::Resources::Files]
|
117
|
+
attr_reader :files
|
118
|
+
|
119
|
+
# @return [Sink::Resources::Resources]
|
120
|
+
attr_reader :resources
|
121
|
+
|
122
|
+
# @return [Sink::Resources::ConfigTools]
|
123
|
+
attr_reader :config_tools
|
124
|
+
|
125
|
+
# Stainless API company
|
126
|
+
# @return [Sink::Resources::Company]
|
127
|
+
attr_reader :company
|
128
|
+
|
129
|
+
# @return [Sink::Resources::OpenAPIFormats]
|
130
|
+
attr_reader :openapi_formats
|
131
|
+
|
132
|
+
# @return [Sink::Resources::Parent]
|
133
|
+
attr_reader :parent
|
134
|
+
|
135
|
+
# @return [Sink::Resources::Envelopes]
|
136
|
+
attr_reader :envelopes
|
137
|
+
|
138
|
+
# @return [Sink::Resources::Types]
|
139
|
+
attr_reader :types
|
140
|
+
|
141
|
+
# @return [Sink::Resources::Clients]
|
142
|
+
attr_reader :clients
|
143
|
+
|
144
|
+
# @return [Sink::Resources::Names]
|
145
|
+
attr_reader :names
|
146
|
+
|
147
|
+
# Widget is love
|
148
|
+
# Widget is life
|
149
|
+
# @return [Sink::Resources::Widgets]
|
150
|
+
attr_reader :widgets
|
151
|
+
|
152
|
+
# @return [Sink::Resources::Responses]
|
153
|
+
attr_reader :responses
|
154
|
+
|
155
|
+
# @return [Sink::Resources::PathParams]
|
156
|
+
attr_reader :path_params
|
157
|
+
|
158
|
+
# @return [Sink::Resources::PositionalParams]
|
159
|
+
attr_reader :positional_params
|
160
|
+
|
161
|
+
# @return [Sink::Resources::EmptyBody]
|
162
|
+
attr_reader :empty_body
|
163
|
+
|
164
|
+
# @return [Sink::Resources::QueryParams]
|
165
|
+
attr_reader :query_params
|
166
|
+
|
167
|
+
# @return [Sink::Resources::BodyParams]
|
168
|
+
attr_reader :body_params
|
169
|
+
|
170
|
+
# @return [Sink::Resources::HeaderParams]
|
171
|
+
attr_reader :header_params
|
172
|
+
|
173
|
+
# @return [Sink::Resources::MixedParams]
|
174
|
+
attr_reader :mixed_params
|
175
|
+
|
176
|
+
# @return [Sink::Resources::MakeAmbiguousSchemasLooser]
|
177
|
+
attr_reader :make_ambiguous_schemas_looser
|
178
|
+
|
179
|
+
# @return [Sink::Resources::MakeAmbiguousSchemasExplicit]
|
180
|
+
attr_reader :make_ambiguous_schemas_explicit
|
181
|
+
|
182
|
+
# @return [Sink::Resources::DecoratorTests]
|
183
|
+
attr_reader :decorator_tests
|
184
|
+
|
185
|
+
# @return [Sink::Resources::Tests]
|
186
|
+
attr_reader :tests
|
187
|
+
|
188
|
+
# @return [Sink::Resources::DeeplyNested]
|
189
|
+
attr_reader :deeply_nested
|
190
|
+
|
191
|
+
# @return [Sink::Resources::Version1_30Names]
|
192
|
+
attr_reader :version_1_30_names
|
193
|
+
|
194
|
+
# @return [Sink::Resources::Recursion]
|
195
|
+
attr_reader :recursion
|
196
|
+
|
197
|
+
# @return [Sink::Resources::SharedQueryParams]
|
198
|
+
attr_reader :shared_query_params
|
199
|
+
|
200
|
+
# @return [Sink::Resources::ModelReferencedInParentAndChild]
|
201
|
+
attr_reader :model_referenced_in_parent_and_child
|
202
|
+
|
203
|
+
# Creates and returns a new client for interacting with the API.
|
204
|
+
#
|
205
|
+
# @param environment ["production", "sandbox", nil] Specifies the environment to use for the API.
|
206
|
+
#
|
207
|
+
# Each environment maps to a different base URL:
|
208
|
+
#
|
209
|
+
# - `production` corresponds to `https://demo.stainlessapi.com/`
|
210
|
+
# - `sandbox` corresponds to `https://demo-sanbox.stainlessapi.com/`
|
211
|
+
# @param base_url [String, nil] Override the default base URL for the API, e.g., `"https://api.example.com/v2/"`
|
212
|
+
# @param user_token [String, nil] The API Key for the SINK API, sent as a bearer token Defaults to
|
213
|
+
# `ENV["SINK_CUSTOM_API_KEY_ENV"]`
|
214
|
+
# @param api_key_header [String, nil] The API Key for the SINK API, sent as an api key header Defaults to
|
215
|
+
# `ENV["SINK_CUSTOM_API_KEY_HEADER_ENV"]`
|
216
|
+
# @param api_key_query [String, nil] The API Key for the SINK API, sent as an api key query Defaults to
|
217
|
+
# `ENV["SINK_CUSTOM_API_KEY_QUERY_ENV"]`
|
218
|
+
# @param username [String, nil] Defaults to `ENV["SINK_USER"]`
|
219
|
+
# @param client_id [String, nil] Defaults to `ENV["SINK_CLIENT_ID"]`
|
220
|
+
# @param client_secret [String, nil] Defaults to `ENV["SINK_CLIENT_SECRET"]`
|
221
|
+
# @param some_boolean_arg [Boolean, nil] Defaults to `ENV["SINK_SOME_BOOLEAN_ARG"]`
|
222
|
+
# @param some_integer_arg [Integer, nil] Defaults to `ENV["SINK_SOME_INTEGER_ARG"]`
|
223
|
+
# @param some_number_arg [Float, nil] Defaults to `ENV["SINK_SOME_NUMBER_ARG"]`
|
224
|
+
# @param some_number_arg_required [Float, nil] Defaults to `ENV["SINK_SOME_NUMBER_ARG"]`
|
225
|
+
# @param some_number_arg_required_no_default [Float, nil] Defaults to `ENV["SINK_SOME_NUMBER_ARG"]`
|
226
|
+
# @param some_number_arg_required_no_default_no_env [Float, nil]
|
227
|
+
# @param required_arg_no_env [String, nil]
|
228
|
+
# @param required_arg_no_env_with_default [String, nil]
|
229
|
+
# @param client_path_param [String, nil]
|
230
|
+
# @param camel_case_path [String, nil]
|
231
|
+
# @param client_query_param [String, nil]
|
232
|
+
# @param client_path_or_query_param [String, nil]
|
233
|
+
# @param max_retries [Integer] Max number of retries to attempt after a failed retryable request.
|
234
|
+
#
|
235
|
+
# @return [Sink::Client]
|
236
|
+
def initialize(
|
237
|
+
environment: nil,
|
238
|
+
base_url: nil,
|
239
|
+
user_token: nil,
|
240
|
+
api_key_header: nil,
|
241
|
+
api_key_query: nil,
|
242
|
+
username: nil,
|
243
|
+
client_id: nil,
|
244
|
+
client_secret: nil,
|
245
|
+
some_boolean_arg: nil,
|
246
|
+
some_integer_arg: nil,
|
247
|
+
some_number_arg: nil,
|
248
|
+
some_number_arg_required: nil,
|
249
|
+
some_number_arg_required_no_default: nil,
|
250
|
+
some_number_arg_required_no_default_no_env: nil,
|
251
|
+
required_arg_no_env: nil,
|
252
|
+
required_arg_no_env_with_default: nil,
|
253
|
+
client_path_param: nil,
|
254
|
+
camel_case_path: nil,
|
255
|
+
client_query_param: nil,
|
256
|
+
client_path_or_query_param: nil,
|
257
|
+
max_retries: DEFAULT_MAX_RETRIES,
|
258
|
+
timeout: 60
|
259
|
+
)
|
260
|
+
environments = {"production" => "https://demo.stainlessapi.com/", "sandbox" => "https://demo-sanbox.stainlessapi.com/"}
|
261
|
+
if environment && base_url
|
262
|
+
raise ArgumentError, "both environment and base_url given, expected only one"
|
263
|
+
elsif environment
|
264
|
+
if !environments.key?(environment.to_s)
|
265
|
+
raise ArgumentError, "environment must be one of #{environments.keys}, got #{environment}"
|
266
|
+
end
|
267
|
+
base_url = environments[environment.to_s]
|
268
|
+
elsif !base_url
|
269
|
+
base_url = "https://demo.stainlessapi.com/"
|
270
|
+
end
|
271
|
+
|
272
|
+
username_header = [username, ENV["SINK_USER"]].find { |v| !v.nil? }
|
273
|
+
if username_header.nil?
|
274
|
+
raise ArgumentError, "username is required"
|
275
|
+
end
|
276
|
+
client_secret_header = [client_secret, ENV["SINK_CLIENT_SECRET"], "hellosecret"].find { |v| !v.nil? }
|
277
|
+
some_integer_arg_header = [some_integer_arg, ENV["SINK_SOME_INTEGER_ARG"], 123].find { |v| !v.nil? }
|
278
|
+
headers = {
|
279
|
+
"My-Api-Version" => "11",
|
280
|
+
"X-Enable-Metrics" => "1",
|
281
|
+
"X-Client-UserName" => username_header,
|
282
|
+
"X-Client-Secret" => client_secret_header,
|
283
|
+
"X-Integer" => some_integer_arg_header
|
284
|
+
}
|
285
|
+
|
286
|
+
idempotency_header = "Idempotency-Key"
|
287
|
+
|
288
|
+
@user_token = [user_token, ENV["SINK_CUSTOM_API_KEY_ENV"]].find { |v| !v.nil? }
|
289
|
+
@api_key_header = [api_key_header, ENV["SINK_CUSTOM_API_KEY_HEADER_ENV"]].find { |v| !v.nil? }
|
290
|
+
@api_key_query = [api_key_query, ENV["SINK_CUSTOM_API_KEY_QUERY_ENV"]].find { |v| !v.nil? }
|
291
|
+
@client_id = [client_id, ENV["SINK_CLIENT_ID"]].find { |v| !v.nil? }
|
292
|
+
@some_boolean_arg = [some_boolean_arg, ENV["SINK_SOME_BOOLEAN_ARG"], true].find { |v| !v.nil? }
|
293
|
+
@some_number_arg = [some_number_arg, ENV["SINK_SOME_NUMBER_ARG"], 1.2].find { |v| !v.nil? }
|
294
|
+
@some_number_arg_required = [some_number_arg_required, ENV["SINK_SOME_NUMBER_ARG"], 1.2].find do |v|
|
295
|
+
!v.nil?
|
296
|
+
end
|
297
|
+
@some_number_arg_required_no_default = [
|
298
|
+
some_number_arg_required_no_default,
|
299
|
+
ENV["SINK_SOME_NUMBER_ARG"]
|
300
|
+
].find do |v|
|
301
|
+
!v.nil?
|
302
|
+
end
|
303
|
+
if @some_number_arg_required_no_default.nil?
|
304
|
+
raise ArgumentError, "some_number_arg_required_no_default is required"
|
305
|
+
end
|
306
|
+
@some_number_arg_required_no_default_no_env = some_number_arg_required_no_default_no_env
|
307
|
+
if @some_number_arg_required_no_default_no_env.nil?
|
308
|
+
raise ArgumentError, "some_number_arg_required_no_default_no_env is required"
|
309
|
+
end
|
310
|
+
@required_arg_no_env = required_arg_no_env
|
311
|
+
if @required_arg_no_env.nil?
|
312
|
+
raise ArgumentError, "required_arg_no_env is required"
|
313
|
+
end
|
314
|
+
@required_arg_no_env_with_default = [required_arg_no_env_with_default, "hi!"].find { |v| !v.nil? }
|
315
|
+
@client_query_param = client_query_param
|
316
|
+
|
317
|
+
super(
|
318
|
+
base_url: base_url,
|
319
|
+
max_retries: max_retries,
|
320
|
+
timeout: timeout,
|
321
|
+
headers: headers,
|
322
|
+
idempotency_header: idempotency_header
|
323
|
+
)
|
324
|
+
|
325
|
+
@testing = Sink::Resources::Testing.new(client: self)
|
326
|
+
@complex_queries = Sink::Resources::ComplexQueries.new(client: self)
|
327
|
+
@casing = Sink::Resources::Casing.new(client: self)
|
328
|
+
@tools = Sink::Resources::Tools.new(client: self)
|
329
|
+
@undocumented_resource = Sink::Resources::UndocumentedResource.new(client: self)
|
330
|
+
@method_config = Sink::Resources::MethodConfig.new(client: self)
|
331
|
+
@streaming = Sink::Resources::Streaming.new(client: self)
|
332
|
+
@pagination_tests = Sink::Resources::PaginationTests.new(client: self)
|
333
|
+
@docstrings = Sink::Resources::Docstrings.new(client: self)
|
334
|
+
@invalid_schemas = Sink::Resources::InvalidSchemas.new(client: self)
|
335
|
+
@resource_refs = Sink::Resources::ResourceRefs.new(client: self)
|
336
|
+
@cards = Sink::Resources::Cards.new(client: self)
|
337
|
+
@files = Sink::Resources::Files.new(client: self)
|
338
|
+
@resources = Sink::Resources::Resources.new(client: self)
|
339
|
+
@config_tools = Sink::Resources::ConfigTools.new(client: self)
|
340
|
+
@company = Sink::Resources::Company.new(client: self)
|
341
|
+
@openapi_formats = Sink::Resources::OpenAPIFormats.new(client: self)
|
342
|
+
@parent = Sink::Resources::Parent.new(client: self)
|
343
|
+
@envelopes = Sink::Resources::Envelopes.new(client: self)
|
344
|
+
@types = Sink::Resources::Types.new(client: self)
|
345
|
+
@clients = Sink::Resources::Clients.new(client: self)
|
346
|
+
@names = Sink::Resources::Names.new(client: self)
|
347
|
+
@widgets = Sink::Resources::Widgets.new(client: self)
|
348
|
+
@responses = Sink::Resources::Responses.new(client: self)
|
349
|
+
@path_params = Sink::Resources::PathParams.new(client: self)
|
350
|
+
@positional_params = Sink::Resources::PositionalParams.new(client: self)
|
351
|
+
@empty_body = Sink::Resources::EmptyBody.new(client: self)
|
352
|
+
@query_params = Sink::Resources::QueryParams.new(client: self)
|
353
|
+
@body_params = Sink::Resources::BodyParams.new(client: self)
|
354
|
+
@header_params = Sink::Resources::HeaderParams.new(client: self)
|
355
|
+
@mixed_params = Sink::Resources::MixedParams.new(client: self)
|
356
|
+
@make_ambiguous_schemas_looser = Sink::Resources::MakeAmbiguousSchemasLooser.new(client: self)
|
357
|
+
@make_ambiguous_schemas_explicit = Sink::Resources::MakeAmbiguousSchemasExplicit.new(client: self)
|
358
|
+
@decorator_tests = Sink::Resources::DecoratorTests.new(client: self)
|
359
|
+
@tests = Sink::Resources::Tests.new(client: self)
|
360
|
+
@deeply_nested = Sink::Resources::DeeplyNested.new(client: self)
|
361
|
+
@version_1_30_names = Sink::Resources::Version1_30Names.new(client: self)
|
362
|
+
@recursion = Sink::Resources::Recursion.new(client: self)
|
363
|
+
@shared_query_params = Sink::Resources::SharedQueryParams.new(client: self)
|
364
|
+
@model_referenced_in_parent_and_child = Sink::Resources::ModelReferencedInParentAndChild.new(client: self)
|
365
|
+
end
|
366
|
+
|
367
|
+
# @!visibility private
|
368
|
+
def make_status_error(message:, body:, response:)
|
369
|
+
case response.code.to_i
|
370
|
+
when 400
|
371
|
+
Sink::HTTP::BadRequestError.new(message: message, response: response, body: body)
|
372
|
+
when 401
|
373
|
+
Sink::HTTP::AuthenticationError.new(message: message, response: response, body: body)
|
374
|
+
when 403
|
375
|
+
Sink::HTTP::PermissionDeniedError.new(message: message, response: response, body: body)
|
376
|
+
when 404
|
377
|
+
Sink::HTTP::NotFoundError.new(message: message, response: response, body: body)
|
378
|
+
when 409
|
379
|
+
Sink::HTTP::ConflictError.new(message: message, response: response, body: body)
|
380
|
+
when 422
|
381
|
+
Sink::HTTP::UnprocessableEntityError.new(message: message, response: response, body: body)
|
382
|
+
when 429
|
383
|
+
Sink::HTTP::RateLimitError.new(message: message, response: response, body: body)
|
384
|
+
when 500..599
|
385
|
+
Sink::HTTP::InternalServerError.new(message: message, response: response, body: body)
|
386
|
+
else
|
387
|
+
Sink::HTTP::APIStatusError.new(message: message, response: response, body: body)
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
end
|
data/lib/sink/util.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sink
|
4
|
+
# @!visibility private
|
5
|
+
class Util
|
6
|
+
# Use this to indicate that a value should be explicitly removed from a data structure
|
7
|
+
# when using `Sink::Util.deep_merge`.
|
8
|
+
# E.g. merging `{a: 1}` and `{a: OMIT}` should produce `{}`, where merging `{a: 1}` and
|
9
|
+
# `{}` would produce `{a: 1}`.
|
10
|
+
OMIT = Object.new.freeze
|
11
|
+
|
12
|
+
# Recursively merge one hash with another.
|
13
|
+
# If the values at a given key are not both hashes, just take the new value.
|
14
|
+
# @param concat [true, false] whether to merge sequences by concatenation
|
15
|
+
def self.deep_merge(left, right, concat: false)
|
16
|
+
right_cleaned = if right.is_a?(Hash)
|
17
|
+
right.reject { |_, value| value == OMIT }
|
18
|
+
else
|
19
|
+
right
|
20
|
+
end
|
21
|
+
|
22
|
+
if left.is_a?(Hash) && right_cleaned.is_a?(Hash)
|
23
|
+
left
|
24
|
+
.reject { |key, _| right[key] == OMIT }
|
25
|
+
.merge(right_cleaned) do |_k, old_val, new_val|
|
26
|
+
deep_merge(old_val, new_val, concat: concat)
|
27
|
+
end
|
28
|
+
elsif left.is_a?(Array) && right_cleaned.is_a?(Array) && concat
|
29
|
+
left.concat(right_cleaned)
|
30
|
+
else
|
31
|
+
right_cleaned
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.coerce_integer(str)
|
36
|
+
Integer(str, exception: false) || str
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.coerce_float(str)
|
40
|
+
Float(str, exception: false) || str
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.coerce_boolean(input)
|
44
|
+
case input
|
45
|
+
in "true"
|
46
|
+
true
|
47
|
+
in "false"
|
48
|
+
false
|
49
|
+
else
|
50
|
+
input
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.uri_from_req(req, absolute:)
|
55
|
+
query_string = ("?#{URI.encode_www_form(req[:query])}" if req[:query])
|
56
|
+
uri = String.new
|
57
|
+
if absolute
|
58
|
+
uri << "#{req[:scheme]}://#{req[:host]}"
|
59
|
+
if req[:port] && !(req[:scheme] == "https" && req[:port] == 443) && !(req[:scheme] == "http" && req[:port] == 80)
|
60
|
+
uri << ":#{req[:port]}"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
uri << ((req[:path] || "/") + (query_string || ""))
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.uri_origin(uri)
|
67
|
+
if uri.respond_to?(:origin)
|
68
|
+
uri.origin
|
69
|
+
else
|
70
|
+
"#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? '' : ":#{uri.port}"}"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.normalized_headers(*headers)
|
75
|
+
{}.merge(*headers.compact).transform_keys(&:downcase)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/sink/version.rb
ADDED
data/lib/sink.rb
ADDED
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: test-test-sink-test-test2
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sink
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-11-04 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: connection_pool
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description:
|
28
|
+
email: dev@stainlessapi.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.md
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- lib/sink.rb
|
36
|
+
- lib/sink/base_client.rb
|
37
|
+
- lib/sink/base_model.rb
|
38
|
+
- lib/sink/client.rb
|
39
|
+
- lib/sink/util.rb
|
40
|
+
- lib/sink/version.rb
|
41
|
+
homepage: https://rubydoc.info/github/ms-jpq/test-publishing-yard
|
42
|
+
licenses: []
|
43
|
+
metadata:
|
44
|
+
homepage_uri: https://rubydoc.info/github/ms-jpq/test-publishing-yard
|
45
|
+
source_code_uri: https://github.com/ms-jpq/test-publishing-yard
|
46
|
+
rubygems_mfa_required: 'true'
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 3.0.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.3.27
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Ruby library to access the Sink API
|
66
|
+
test_files: []
|