galaxy-ruby 0.2.0
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/CHANGELOG.md +8 -0
- data/LICENSE +201 -0
- data/README.md +169 -0
- data/SECURITY.md +19 -0
- data/SKILL.md +83 -0
- data/api.md +188 -0
- data/lib/galaxy-ruby/client.rb +254 -0
- data/lib/galaxy-ruby/errors.rb +227 -0
- data/lib/galaxy-ruby/file_part.rb +58 -0
- data/lib/galaxy-ruby/internal/transport/base_client.rb +576 -0
- data/lib/galaxy-ruby/internal/transport/pooled_net_requester.rb +224 -0
- data/lib/galaxy-ruby/internal/type/array_of.rb +164 -0
- data/lib/galaxy-ruby/internal/type/base_model.rb +529 -0
- data/lib/galaxy-ruby/internal/type/base_page.rb +55 -0
- data/lib/galaxy-ruby/internal/type/boolean.rb +77 -0
- data/lib/galaxy-ruby/internal/type/converter.rb +333 -0
- data/lib/galaxy-ruby/internal/type/enum.rb +130 -0
- data/lib/galaxy-ruby/internal/type/file_input.rb +111 -0
- data/lib/galaxy-ruby/internal/type/hash_of.rb +184 -0
- data/lib/galaxy-ruby/internal/type/request_parameters.rb +42 -0
- data/lib/galaxy-ruby/internal/type/union.rb +234 -0
- data/lib/galaxy-ruby/internal/type/unknown.rb +81 -0
- data/lib/galaxy-ruby/internal/util.rb +960 -0
- data/lib/galaxy-ruby/internal.rb +15 -0
- data/lib/galaxy-ruby/models/authentication_create_token_params.rb +14 -0
- data/lib/galaxy-ruby/models/authentication_create_token_response.rb +16 -0
- data/lib/galaxy-ruby/models/authentication_create_user_params.rb +32 -0
- data/lib/galaxy-ruby/models/authentication_list_me_params.rb +14 -0
- data/lib/galaxy-ruby/models/celestial_body.rb +81 -0
- data/lib/galaxy-ruby/models/celestial_body_create_params.rb +20 -0
- data/lib/galaxy-ruby/models/credentials.rb +25 -0
- data/lib/galaxy-ruby/models/new_planet_webhook_event.rb +216 -0
- data/lib/galaxy-ruby/models/planet.rb +218 -0
- data/lib/galaxy-ruby/models/planet_create_params.rb +14 -0
- data/lib/galaxy-ruby/models/planet_delete_params.rb +20 -0
- data/lib/galaxy-ruby/models/planet_delte_image_params.rb +29 -0
- data/lib/galaxy-ruby/models/planet_delte_image_response.rb +48 -0
- data/lib/galaxy-ruby/models/planet_list_all_data_params.rb +30 -0
- data/lib/galaxy-ruby/models/planet_list_all_data_response.rb +51 -0
- data/lib/galaxy-ruby/models/planet_retrieve_params.rb +20 -0
- data/lib/galaxy-ruby/models/planet_update_params.rb +20 -0
- data/lib/galaxy-ruby/models/unwrap_webhook_event.rb +14 -0
- data/lib/galaxy-ruby/models/user.rb +26 -0
- data/lib/galaxy-ruby/models/webhook_unwrap_params.rb +14 -0
- data/lib/galaxy-ruby/models.rb +78 -0
- data/lib/galaxy-ruby/request_options.rb +77 -0
- data/lib/galaxy-ruby/resources/authentication.rb +80 -0
- data/lib/galaxy-ruby/resources/celestial_bodies.rb +37 -0
- data/lib/galaxy-ruby/resources/planets.rb +215 -0
- data/lib/galaxy-ruby/resources/webhooks.rb +34 -0
- data/lib/galaxy-ruby/version.rb +5 -0
- data/lib/galaxy-ruby.rb +78 -0
- data/rbi/galaxy-ruby/client.rbi +128 -0
- data/rbi/galaxy-ruby/errors.rbi +205 -0
- data/rbi/galaxy-ruby/file_part.rbi +37 -0
- data/rbi/galaxy-ruby/internal/transport/base_client.rbi +305 -0
- data/rbi/galaxy-ruby/internal/transport/pooled_net_requester.rbi +84 -0
- data/rbi/galaxy-ruby/internal/type/array_of.rbi +104 -0
- data/rbi/galaxy-ruby/internal/type/base_model.rbi +314 -0
- data/rbi/galaxy-ruby/internal/type/base_page.rbi +43 -0
- data/rbi/galaxy-ruby/internal/type/boolean.rbi +58 -0
- data/rbi/galaxy-ruby/internal/type/converter.rbi +222 -0
- data/rbi/galaxy-ruby/internal/type/enum.rbi +82 -0
- data/rbi/galaxy-ruby/internal/type/file_input.rbi +59 -0
- data/rbi/galaxy-ruby/internal/type/hash_of.rbi +104 -0
- data/rbi/galaxy-ruby/internal/type/request_parameters.rbi +31 -0
- data/rbi/galaxy-ruby/internal/type/union.rbi +128 -0
- data/rbi/galaxy-ruby/internal/type/unknown.rbi +58 -0
- data/rbi/galaxy-ruby/internal/util.rbi +515 -0
- data/rbi/galaxy-ruby/internal.rbi +18 -0
- data/rbi/galaxy-ruby/models/authentication_create_token_params.rbi +32 -0
- data/rbi/galaxy-ruby/models/authentication_create_token_response.rbi +29 -0
- data/rbi/galaxy-ruby/models/authentication_create_user_params.rbi +54 -0
- data/rbi/galaxy-ruby/models/authentication_list_me_params.rbi +32 -0
- data/rbi/galaxy-ruby/models/celestial_body.rbi +140 -0
- data/rbi/galaxy-ruby/models/celestial_body_create_params.rbi +53 -0
- data/rbi/galaxy-ruby/models/credentials.rbi +27 -0
- data/rbi/galaxy-ruby/models/new_planet_webhook_event.rbi +396 -0
- data/rbi/galaxy-ruby/models/planet.rbi +350 -0
- data/rbi/galaxy-ruby/models/planet_create_params.rbi +32 -0
- data/rbi/galaxy-ruby/models/planet_delete_params.rbi +38 -0
- data/rbi/galaxy-ruby/models/planet_delte_image_params.rbi +55 -0
- data/rbi/galaxy-ruby/models/planet_delte_image_response.rbi +85 -0
- data/rbi/galaxy-ruby/models/planet_list_all_data_params.rbi +60 -0
- data/rbi/galaxy-ruby/models/planet_list_all_data_response.rbi +110 -0
- data/rbi/galaxy-ruby/models/planet_retrieve_params.rbi +38 -0
- data/rbi/galaxy-ruby/models/planet_update_params.rbi +38 -0
- data/rbi/galaxy-ruby/models/unwrap_webhook_event.rbi +17 -0
- data/rbi/galaxy-ruby/models/user.rbi +33 -0
- data/rbi/galaxy-ruby/models/webhook_unwrap_params.rbi +32 -0
- data/rbi/galaxy-ruby/models.rbi +39 -0
- data/rbi/galaxy-ruby/request_options.rbi +59 -0
- data/rbi/galaxy-ruby/resources/authentication.rbi +47 -0
- data/rbi/galaxy-ruby/resources/celestial_bodies.rbi +28 -0
- data/rbi/galaxy-ruby/resources/planets.rbi +176 -0
- data/rbi/galaxy-ruby/resources/webhooks.rbi +29 -0
- data/rbi/galaxy-ruby/version.rbi +5 -0
- data/sig/galaxy-ruby/client.rbs +62 -0
- data/sig/galaxy-ruby/errors.rbs +117 -0
- data/sig/galaxy-ruby/file_part.rbs +21 -0
- data/sig/galaxy-ruby/internal/transport/base_client.rbs +133 -0
- data/sig/galaxy-ruby/internal/transport/pooled_net_requester.rbs +48 -0
- data/sig/galaxy-ruby/internal/type/array_of.rbs +48 -0
- data/sig/galaxy-ruby/internal/type/base_model.rbs +102 -0
- data/sig/galaxy-ruby/internal/type/base_page.rbs +24 -0
- data/sig/galaxy-ruby/internal/type/boolean.rbs +26 -0
- data/sig/galaxy-ruby/internal/type/converter.rbs +79 -0
- data/sig/galaxy-ruby/internal/type/enum.rbs +32 -0
- data/sig/galaxy-ruby/internal/type/file_input.rbs +25 -0
- data/sig/galaxy-ruby/internal/type/hash_of.rbs +48 -0
- data/sig/galaxy-ruby/internal/type/request_parameters.rbs +17 -0
- data/sig/galaxy-ruby/internal/type/union.rbs +52 -0
- data/sig/galaxy-ruby/internal/type/unknown.rbs +26 -0
- data/sig/galaxy-ruby/internal/util.rbs +199 -0
- data/sig/galaxy-ruby/internal.rbs +9 -0
- data/sig/galaxy-ruby/models/authentication_create_token_params.rbs +15 -0
- data/sig/galaxy-ruby/models/authentication_create_token_response.rbs +15 -0
- data/sig/galaxy-ruby/models/authentication_create_user_params.rbs +34 -0
- data/sig/galaxy-ruby/models/authentication_list_me_params.rbs +15 -0
- data/sig/galaxy-ruby/models/celestial_body.rbs +76 -0
- data/sig/galaxy-ruby/models/celestial_body_create_params.rbs +24 -0
- data/sig/galaxy-ruby/models/credentials.rbs +15 -0
- data/sig/galaxy-ruby/models/new_planet_webhook_event.rbs +211 -0
- data/sig/galaxy-ruby/models/planet.rbs +211 -0
- data/sig/galaxy-ruby/models/planet_create_params.rbs +15 -0
- data/sig/galaxy-ruby/models/planet_delete_params.rbs +23 -0
- data/sig/galaxy-ruby/models/planet_delte_image_params.rbs +32 -0
- data/sig/galaxy-ruby/models/planet_delte_image_response.rbs +50 -0
- data/sig/galaxy-ruby/models/planet_list_all_data_params.rbs +32 -0
- data/sig/galaxy-ruby/models/planet_list_all_data_response.rbs +64 -0
- data/sig/galaxy-ruby/models/planet_retrieve_params.rbs +23 -0
- data/sig/galaxy-ruby/models/planet_update_params.rbs +25 -0
- data/sig/galaxy-ruby/models/unwrap_webhook_event.rbs +11 -0
- data/sig/galaxy-ruby/models/user.rbs +19 -0
- data/sig/galaxy-ruby/models/webhook_unwrap_params.rbs +15 -0
- data/sig/galaxy-ruby/models.rbs +35 -0
- data/sig/galaxy-ruby/request_options.rbs +34 -0
- data/sig/galaxy-ruby/resources/authentication.rbs +24 -0
- data/sig/galaxy-ruby/resources/celestial_bodies.rbs +12 -0
- data/sig/galaxy-ruby/resources/planets.rbs +68 -0
- data/sig/galaxy-ruby/resources/webhooks.rbs +13 -0
- data/sig/galaxy-ruby/version.rbs +3 -0
- data/sig/manifest.yaml +6 -0
- metadata +245 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ScalarGalaxy
|
|
4
|
+
module Internal
|
|
5
|
+
module Transport
|
|
6
|
+
# @api private
|
|
7
|
+
#
|
|
8
|
+
# @abstract
|
|
9
|
+
class BaseClient
|
|
10
|
+
extend ScalarGalaxy::Internal::Util::SorbetRuntimeSupport
|
|
11
|
+
|
|
12
|
+
# from whatwg fetch spec
|
|
13
|
+
MAX_REDIRECTS = 20
|
|
14
|
+
|
|
15
|
+
# rubocop:disable Style/MutableConstant
|
|
16
|
+
PLATFORM_HEADERS = {
|
|
17
|
+
"x-scalar-arch" => ScalarGalaxy::Internal::Util.arch,
|
|
18
|
+
"x-scalar-lang" => "ruby",
|
|
19
|
+
"x-scalar-os" => ScalarGalaxy::Internal::Util.os,
|
|
20
|
+
"x-scalar-package-version" => ScalarGalaxy::VERSION,
|
|
21
|
+
"x-scalar-runtime" => ::RUBY_ENGINE,
|
|
22
|
+
"x-scalar-runtime-version" => ::RUBY_ENGINE_VERSION
|
|
23
|
+
}
|
|
24
|
+
# rubocop:enable Style/MutableConstant
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
# @api private
|
|
28
|
+
#
|
|
29
|
+
# @param req [Hash{Symbol=>Object}]
|
|
30
|
+
#
|
|
31
|
+
# @raise [ArgumentError]
|
|
32
|
+
def validate!(req)
|
|
33
|
+
keys = %i[method path query headers body unwrap page stream model options]
|
|
34
|
+
case req
|
|
35
|
+
in Hash
|
|
36
|
+
req.each_key do |k|
|
|
37
|
+
unless keys.include?(k)
|
|
38
|
+
raise ArgumentError.new("Request `req` keys must be one of #{keys}, got #{k.inspect}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
else
|
|
42
|
+
raise ArgumentError.new("Request `req` must be a Hash or RequestOptions, got #{req.inspect}")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @api private
|
|
47
|
+
#
|
|
48
|
+
# @param status [Integer]
|
|
49
|
+
# @param headers [Hash{String=>String}]
|
|
50
|
+
#
|
|
51
|
+
# @return [Boolean]
|
|
52
|
+
def should_retry?(status, headers:)
|
|
53
|
+
coerced = ScalarGalaxy::Internal::Util.coerce_boolean(headers["x-should-retry"])
|
|
54
|
+
case [coerced, status]
|
|
55
|
+
in [true | false, _]
|
|
56
|
+
coerced
|
|
57
|
+
in [_, 408 | 409 | 429 | 500..]
|
|
58
|
+
# retry on:
|
|
59
|
+
# 408: timeouts
|
|
60
|
+
# 409: locks
|
|
61
|
+
# 429: rate limits
|
|
62
|
+
# 500+: unknown errors
|
|
63
|
+
true
|
|
64
|
+
else
|
|
65
|
+
false
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# @api private
|
|
70
|
+
#
|
|
71
|
+
# @param request [Hash{Symbol=>Object}] .
|
|
72
|
+
#
|
|
73
|
+
# @option request [Symbol] :method
|
|
74
|
+
#
|
|
75
|
+
# @option request [URI::Generic] :url
|
|
76
|
+
#
|
|
77
|
+
# @option request [Hash{String=>String}] :headers
|
|
78
|
+
#
|
|
79
|
+
# @option request [Object] :body
|
|
80
|
+
#
|
|
81
|
+
# @option request [Integer] :max_retries
|
|
82
|
+
#
|
|
83
|
+
# @option request [Float] :timeout
|
|
84
|
+
#
|
|
85
|
+
# @param status [Integer]
|
|
86
|
+
#
|
|
87
|
+
# @param response_headers [Hash{String=>String}]
|
|
88
|
+
#
|
|
89
|
+
# @return [Hash{Symbol=>Object}]
|
|
90
|
+
def follow_redirect(request, status:, response_headers:)
|
|
91
|
+
method, url, headers = request.fetch_values(:method, :url, :headers)
|
|
92
|
+
location =
|
|
93
|
+
Kernel.then do
|
|
94
|
+
URI.join(url, response_headers["location"])
|
|
95
|
+
rescue ArgumentError
|
|
96
|
+
message = "Server responded with status #{status} but no valid location header."
|
|
97
|
+
raise ScalarGalaxy::Errors::APIConnectionError.new(
|
|
98
|
+
url: url,
|
|
99
|
+
response: response_headers,
|
|
100
|
+
message: message
|
|
101
|
+
)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
request = {**request, url: location}
|
|
105
|
+
|
|
106
|
+
case [url.scheme, location.scheme]
|
|
107
|
+
in ["https", "http"]
|
|
108
|
+
message = "Tried to redirect to a insecure URL"
|
|
109
|
+
raise ScalarGalaxy::Errors::APIConnectionError.new(
|
|
110
|
+
url: url,
|
|
111
|
+
response: response_headers,
|
|
112
|
+
message: message
|
|
113
|
+
)
|
|
114
|
+
else
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# from whatwg fetch spec
|
|
119
|
+
case [status, method]
|
|
120
|
+
in [301 | 302, :post] | [303, _]
|
|
121
|
+
drop = %w[content-encoding content-language content-length content-location content-type]
|
|
122
|
+
request = {
|
|
123
|
+
**request,
|
|
124
|
+
method: method == :head ? :head : :get,
|
|
125
|
+
headers: headers.except(*drop),
|
|
126
|
+
body: nil
|
|
127
|
+
}
|
|
128
|
+
else
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# from undici
|
|
132
|
+
if ScalarGalaxy::Internal::Util.uri_origin(url) !=
|
|
133
|
+
ScalarGalaxy::Internal::Util.uri_origin(location)
|
|
134
|
+
drop = %w[authorization cookie host proxy-authorization]
|
|
135
|
+
request = {**request, headers: request.fetch(:headers).except(*drop)}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
request
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# @api private
|
|
142
|
+
#
|
|
143
|
+
# @param status [Integer, ScalarGalaxy::Errors::APIConnectionError]
|
|
144
|
+
# @param stream [Enumerable<String>, nil]
|
|
145
|
+
def reap_connection!(status, stream:)
|
|
146
|
+
case status
|
|
147
|
+
in ..199 | 300..499
|
|
148
|
+
stream&.each { next }
|
|
149
|
+
in ScalarGalaxy::Errors::APIConnectionError | 500.. then
|
|
150
|
+
ScalarGalaxy::Internal::Util.close_fused!(stream)
|
|
151
|
+
else
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# @return [URI::Generic]
|
|
157
|
+
attr_reader :base_url
|
|
158
|
+
|
|
159
|
+
# @return [Float]
|
|
160
|
+
attr_reader :timeout
|
|
161
|
+
|
|
162
|
+
# @return [Integer]
|
|
163
|
+
attr_reader :max_retries
|
|
164
|
+
|
|
165
|
+
# @return [Float]
|
|
166
|
+
attr_reader :initial_retry_delay
|
|
167
|
+
|
|
168
|
+
# @return [Float]
|
|
169
|
+
attr_reader :max_retry_delay
|
|
170
|
+
|
|
171
|
+
# @return [Hash{String=>String}]
|
|
172
|
+
attr_reader :headers
|
|
173
|
+
|
|
174
|
+
# @return [String, nil]
|
|
175
|
+
attr_reader :idempotency_header
|
|
176
|
+
|
|
177
|
+
# @api private
|
|
178
|
+
# @return [ScalarGalaxy::Internal::Transport::PooledNetRequester]
|
|
179
|
+
attr_reader :requester
|
|
180
|
+
|
|
181
|
+
# @api private
|
|
182
|
+
#
|
|
183
|
+
# @param base_url [String]
|
|
184
|
+
# @param timeout [Float]
|
|
185
|
+
# @param max_retries [Integer]
|
|
186
|
+
# @param initial_retry_delay [Float]
|
|
187
|
+
# @param max_retry_delay [Float]
|
|
188
|
+
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
|
|
189
|
+
# @param idempotency_header [String, nil]
|
|
190
|
+
def initialize(
|
|
191
|
+
base_url:,
|
|
192
|
+
timeout: 0.0,
|
|
193
|
+
max_retries: 0,
|
|
194
|
+
initial_retry_delay: 0.0,
|
|
195
|
+
max_retry_delay: 0.0,
|
|
196
|
+
headers: {},
|
|
197
|
+
idempotency_header: nil
|
|
198
|
+
)
|
|
199
|
+
@requester = ScalarGalaxy::Internal::Transport::PooledNetRequester.new
|
|
200
|
+
@headers =
|
|
201
|
+
ScalarGalaxy::Internal::Util.normalized_headers(
|
|
202
|
+
self.class::PLATFORM_HEADERS,
|
|
203
|
+
{
|
|
204
|
+
"accept" => "application/json",
|
|
205
|
+
"content-type" => "application/json",
|
|
206
|
+
"user-agent" => user_agent
|
|
207
|
+
},
|
|
208
|
+
headers
|
|
209
|
+
)
|
|
210
|
+
@base_url_components = ScalarGalaxy::Internal::Util.parse_uri(base_url)
|
|
211
|
+
@base_url = ScalarGalaxy::Internal::Util.unparse_uri(@base_url_components)
|
|
212
|
+
@idempotency_header = idempotency_header&.to_s&.downcase
|
|
213
|
+
@timeout = timeout
|
|
214
|
+
@max_retries = max_retries
|
|
215
|
+
@initial_retry_delay = initial_retry_delay
|
|
216
|
+
@max_retry_delay = max_retry_delay
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# @api private
|
|
220
|
+
#
|
|
221
|
+
# @return [Hash{String=>String}]
|
|
222
|
+
private def auth_headers = {}
|
|
223
|
+
|
|
224
|
+
# @api private
|
|
225
|
+
#
|
|
226
|
+
# @return [String]
|
|
227
|
+
private def user_agent = "#{self.class.name}/Ruby #{ScalarGalaxy::VERSION}"
|
|
228
|
+
|
|
229
|
+
# @api private
|
|
230
|
+
#
|
|
231
|
+
# @return [String]
|
|
232
|
+
private def generate_idempotency_key = "scalar-ruby-retry-#{SecureRandom.uuid}"
|
|
233
|
+
|
|
234
|
+
# @api private
|
|
235
|
+
#
|
|
236
|
+
# @param req [Hash{Symbol=>Object}] .
|
|
237
|
+
#
|
|
238
|
+
# @option req [Symbol] :method
|
|
239
|
+
#
|
|
240
|
+
# @option req [String, Array<String>] :path
|
|
241
|
+
#
|
|
242
|
+
# @option req [Hash{String=>Array<String>, String, nil}, nil] :query
|
|
243
|
+
#
|
|
244
|
+
# @option req [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil] :headers
|
|
245
|
+
#
|
|
246
|
+
# @option req [Object, nil] :body
|
|
247
|
+
#
|
|
248
|
+
# @option req [Symbol, Integer, Array<Symbol, Integer>, Proc, nil] :unwrap
|
|
249
|
+
#
|
|
250
|
+
# @option req [Class<ScalarGalaxy::Internal::Type::BasePage>, nil] :page
|
|
251
|
+
#
|
|
252
|
+
# @option req [Class<ScalarGalaxy::Internal::Type::BaseStream>, nil] :stream
|
|
253
|
+
#
|
|
254
|
+
# @option req [ScalarGalaxy::Internal::Type::Converter, Class, nil] :model
|
|
255
|
+
#
|
|
256
|
+
# @param opts [Hash{Symbol=>Object}] .
|
|
257
|
+
#
|
|
258
|
+
# @option opts [String, nil] :idempotency_key
|
|
259
|
+
#
|
|
260
|
+
# @option opts [Hash{String=>Array<String>, String, nil}, nil] :extra_query
|
|
261
|
+
#
|
|
262
|
+
# @option opts [Hash{String=>String, nil}, nil] :extra_headers
|
|
263
|
+
#
|
|
264
|
+
# @option opts [Object, nil] :extra_body
|
|
265
|
+
#
|
|
266
|
+
# @option opts [Integer, nil] :max_retries
|
|
267
|
+
#
|
|
268
|
+
# @option opts [Float, nil] :timeout
|
|
269
|
+
#
|
|
270
|
+
# @return [Hash{Symbol=>Object}]
|
|
271
|
+
private def build_request(req, opts)
|
|
272
|
+
method, uninterpolated_path = req.fetch_values(:method, :path)
|
|
273
|
+
|
|
274
|
+
path = ScalarGalaxy::Internal::Util.interpolate_path(uninterpolated_path)
|
|
275
|
+
|
|
276
|
+
query = ScalarGalaxy::Internal::Util.deep_merge(req[:query].to_h, opts[:extra_query].to_h)
|
|
277
|
+
|
|
278
|
+
headers =
|
|
279
|
+
ScalarGalaxy::Internal::Util.normalized_headers(
|
|
280
|
+
@headers,
|
|
281
|
+
auth_headers,
|
|
282
|
+
req[:headers].to_h,
|
|
283
|
+
opts[:extra_headers].to_h
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if @idempotency_header && !headers.key?(@idempotency_header) &&
|
|
287
|
+
(!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key))
|
|
288
|
+
headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key }
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
headers["x-scalar-retry-count"] = "0" unless headers.key?("x-scalar-retry-count")
|
|
292
|
+
|
|
293
|
+
timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..)
|
|
294
|
+
headers["x-scalar-timeout"] = timeout.to_s unless headers.key?("x-scalar-timeout") || timeout.zero?
|
|
295
|
+
|
|
296
|
+
headers.reject! { |_, v| v.to_s.empty? }
|
|
297
|
+
|
|
298
|
+
body =
|
|
299
|
+
case method
|
|
300
|
+
in :get | :head | :options | :trace
|
|
301
|
+
nil
|
|
302
|
+
else
|
|
303
|
+
ScalarGalaxy::Internal::Util.deep_merge(*[req[:body], opts[:extra_body]].compact)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Generated methods always pass `req[:body]` for operations that define a
|
|
307
|
+
# request body, so only elide the content-type header when the operation
|
|
308
|
+
# has no body at all, not when an optional body param was omitted.
|
|
309
|
+
headers.delete("content-type") if body.nil? && !req.key?(:body)
|
|
310
|
+
|
|
311
|
+
url =
|
|
312
|
+
ScalarGalaxy::Internal::Util.join_parsed_uri(
|
|
313
|
+
@base_url_components,
|
|
314
|
+
{**req, path: path, query: query}
|
|
315
|
+
)
|
|
316
|
+
headers, encoded = ScalarGalaxy::Internal::Util.encode_content(headers, body)
|
|
317
|
+
{
|
|
318
|
+
method: method,
|
|
319
|
+
url: url,
|
|
320
|
+
headers: headers,
|
|
321
|
+
body: encoded,
|
|
322
|
+
max_retries: opts.fetch(:max_retries, @max_retries),
|
|
323
|
+
timeout: timeout
|
|
324
|
+
}
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# @api private
|
|
328
|
+
#
|
|
329
|
+
# @param headers [Hash{String=>String}]
|
|
330
|
+
# @param retry_count [Integer]
|
|
331
|
+
#
|
|
332
|
+
# @return [Float]
|
|
333
|
+
private def retry_delay(headers, retry_count:)
|
|
334
|
+
# Non-standard extension
|
|
335
|
+
span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
|
|
336
|
+
return span if span
|
|
337
|
+
|
|
338
|
+
retry_header = headers["retry-after"]
|
|
339
|
+
return span if (span = Float(retry_header, exception: false))
|
|
340
|
+
|
|
341
|
+
span =
|
|
342
|
+
retry_header&.then do
|
|
343
|
+
Time.httpdate(_1) - Time.now
|
|
344
|
+
rescue ArgumentError
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
return span if span
|
|
348
|
+
|
|
349
|
+
scale = retry_count**2
|
|
350
|
+
jitter = 1 - (0.25 * rand)
|
|
351
|
+
(@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# @api private
|
|
355
|
+
#
|
|
356
|
+
# @param request [Hash{Symbol=>Object}] .
|
|
357
|
+
#
|
|
358
|
+
# @option request [Symbol] :method
|
|
359
|
+
#
|
|
360
|
+
# @option request [URI::Generic] :url
|
|
361
|
+
#
|
|
362
|
+
# @option request [Hash{String=>String}] :headers
|
|
363
|
+
#
|
|
364
|
+
# @option request [Object] :body
|
|
365
|
+
#
|
|
366
|
+
# @option request [Integer] :max_retries
|
|
367
|
+
#
|
|
368
|
+
# @option request [Float] :timeout
|
|
369
|
+
#
|
|
370
|
+
# @param redirect_count [Integer]
|
|
371
|
+
#
|
|
372
|
+
# @param retry_count [Integer]
|
|
373
|
+
#
|
|
374
|
+
# @param send_retry_header [Boolean]
|
|
375
|
+
#
|
|
376
|
+
# @raise [ScalarGalaxy::Errors::APIError]
|
|
377
|
+
# @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
|
|
378
|
+
def send_request(request, redirect_count:, retry_count:, send_retry_header:)
|
|
379
|
+
url, headers, max_retries, timeout = request.fetch_values(:url, :headers, :max_retries, :timeout)
|
|
380
|
+
input = {
|
|
381
|
+
**request.except(:timeout),
|
|
382
|
+
deadline: ScalarGalaxy::Internal::Util.monotonic_secs + timeout
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
headers["x-scalar-retry-count"] = retry_count.to_s if send_retry_header
|
|
386
|
+
|
|
387
|
+
begin
|
|
388
|
+
status, response, stream = @requester.execute(input)
|
|
389
|
+
rescue ScalarGalaxy::Errors::APIConnectionError => e
|
|
390
|
+
status = e
|
|
391
|
+
end
|
|
392
|
+
headers = ScalarGalaxy::Internal::Util.normalized_headers(response&.each_header&.to_h)
|
|
393
|
+
|
|
394
|
+
case status
|
|
395
|
+
in ..299
|
|
396
|
+
[status, response, stream]
|
|
397
|
+
in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
|
|
398
|
+
self.class.reap_connection!(status, stream: stream)
|
|
399
|
+
|
|
400
|
+
message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
|
|
401
|
+
raise ScalarGalaxy::Errors::APIConnectionError.new(url: url, response: response, message: message)
|
|
402
|
+
in 300..399
|
|
403
|
+
self.class.reap_connection!(status, stream: stream)
|
|
404
|
+
|
|
405
|
+
request = self.class.follow_redirect(request, status: status, response_headers: headers)
|
|
406
|
+
send_request(
|
|
407
|
+
request,
|
|
408
|
+
redirect_count: redirect_count + 1,
|
|
409
|
+
retry_count: retry_count,
|
|
410
|
+
send_retry_header: send_retry_header
|
|
411
|
+
)
|
|
412
|
+
in ScalarGalaxy::Errors::APIConnectionError if retry_count >= max_retries
|
|
413
|
+
raise status
|
|
414
|
+
in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: headers)
|
|
415
|
+
decoded =
|
|
416
|
+
Kernel.then do
|
|
417
|
+
ScalarGalaxy::Internal::Util.decode_content(headers, stream: stream, suppress_error: true)
|
|
418
|
+
ensure
|
|
419
|
+
self.class.reap_connection!(status, stream: stream)
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
raise ScalarGalaxy::Errors::APIStatusError.for(
|
|
423
|
+
url: url,
|
|
424
|
+
status: status,
|
|
425
|
+
headers: headers,
|
|
426
|
+
body: decoded,
|
|
427
|
+
request: nil,
|
|
428
|
+
response: response
|
|
429
|
+
)
|
|
430
|
+
in 400.. | ScalarGalaxy::Errors::APIConnectionError
|
|
431
|
+
self.class.reap_connection!(status, stream: stream)
|
|
432
|
+
|
|
433
|
+
delay = retry_delay(response || {}, retry_count: retry_count)
|
|
434
|
+
sleep(delay)
|
|
435
|
+
|
|
436
|
+
send_request(
|
|
437
|
+
request,
|
|
438
|
+
redirect_count: redirect_count,
|
|
439
|
+
retry_count: retry_count + 1,
|
|
440
|
+
send_retry_header: send_retry_header
|
|
441
|
+
)
|
|
442
|
+
end
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
# Execute the request specified by `req`. This is the method that all resource
|
|
446
|
+
# methods call into.
|
|
447
|
+
#
|
|
448
|
+
# @overload request(method, path, query: {}, headers: {}, body: nil, unwrap: nil, page: nil, stream: nil, model: ScalarGalaxy::Internal::Type::Unknown, options: {})
|
|
449
|
+
#
|
|
450
|
+
# @param method [Symbol]
|
|
451
|
+
#
|
|
452
|
+
# @param path [String, Array<String>]
|
|
453
|
+
#
|
|
454
|
+
# @param query [Hash{String=>Array<String>, String, nil}, nil]
|
|
455
|
+
#
|
|
456
|
+
# @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil]
|
|
457
|
+
#
|
|
458
|
+
# @param body [Object, nil]
|
|
459
|
+
#
|
|
460
|
+
# @param unwrap [Symbol, Integer, Array<Symbol, Integer>, Proc, nil]
|
|
461
|
+
#
|
|
462
|
+
# @param page [Class<ScalarGalaxy::Internal::Type::BasePage>, nil]
|
|
463
|
+
#
|
|
464
|
+
# @param stream [Class<ScalarGalaxy::Internal::Type::BaseStream>, nil]
|
|
465
|
+
#
|
|
466
|
+
# @param model [ScalarGalaxy::Internal::Type::Converter, Class, nil]
|
|
467
|
+
#
|
|
468
|
+
# @param options [ScalarGalaxy::RequestOptions, Hash{Symbol=>Object}, nil] .
|
|
469
|
+
#
|
|
470
|
+
# @option options [String, nil] :idempotency_key
|
|
471
|
+
#
|
|
472
|
+
# @option options [Hash{String=>Array<String>, String, nil}, nil] :extra_query
|
|
473
|
+
#
|
|
474
|
+
# @option options [Hash{String=>String, nil}, nil] :extra_headers
|
|
475
|
+
#
|
|
476
|
+
# @option options [Object, nil] :extra_body
|
|
477
|
+
#
|
|
478
|
+
# @option options [Integer, nil] :max_retries
|
|
479
|
+
#
|
|
480
|
+
# @option options [Float, nil] :timeout
|
|
481
|
+
#
|
|
482
|
+
# @raise [ScalarGalaxy::Errors::APIError]
|
|
483
|
+
# @return [Object]
|
|
484
|
+
def request(req)
|
|
485
|
+
self.class.validate!(req)
|
|
486
|
+
model = req.fetch(:model) { ScalarGalaxy::Internal::Type::Unknown }
|
|
487
|
+
opts = req[:options].to_h
|
|
488
|
+
unwrap = req[:unwrap]
|
|
489
|
+
ScalarGalaxy::RequestOptions.validate!(opts)
|
|
490
|
+
request = build_request(req.except(:options), opts)
|
|
491
|
+
url = request.fetch(:url)
|
|
492
|
+
|
|
493
|
+
# Don't send the current retry count in the headers if the caller modified the header defaults.
|
|
494
|
+
send_retry_header = request.fetch(:headers)["x-scalar-retry-count"] == "0"
|
|
495
|
+
status, response, stream =
|
|
496
|
+
send_request(request, redirect_count: 0, retry_count: 0, send_retry_header: send_retry_header)
|
|
497
|
+
|
|
498
|
+
headers = ScalarGalaxy::Internal::Util.normalized_headers(response.each_header.to_h)
|
|
499
|
+
decoded = ScalarGalaxy::Internal::Util.decode_content(headers, stream: stream)
|
|
500
|
+
case req
|
|
501
|
+
in stream: Class => st
|
|
502
|
+
st.new(
|
|
503
|
+
model: model,
|
|
504
|
+
url: url,
|
|
505
|
+
status: status,
|
|
506
|
+
headers: headers,
|
|
507
|
+
response: response,
|
|
508
|
+
unwrap: unwrap,
|
|
509
|
+
stream: decoded
|
|
510
|
+
)
|
|
511
|
+
in page: Class => page
|
|
512
|
+
page.new(client: self, req: req, headers: headers, page_data: decoded)
|
|
513
|
+
else
|
|
514
|
+
unwrapped = ScalarGalaxy::Internal::Util.dig(decoded, unwrap)
|
|
515
|
+
ScalarGalaxy::Internal::Type::Converter.coerce(model, unwrapped)
|
|
516
|
+
end
|
|
517
|
+
end
|
|
518
|
+
|
|
519
|
+
# @api private
|
|
520
|
+
#
|
|
521
|
+
# @return [String]
|
|
522
|
+
def inspect
|
|
523
|
+
# rubocop:disable Layout/LineLength
|
|
524
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} max_retries=#{@max_retries} timeout=#{@timeout}>"
|
|
525
|
+
# rubocop:enable Layout/LineLength
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
define_sorbet_constant!(:RequestComponents) do
|
|
529
|
+
T.type_alias do
|
|
530
|
+
{
|
|
531
|
+
method: Symbol,
|
|
532
|
+
path: T.any(String, T::Array[String]),
|
|
533
|
+
query: T.nilable(T::Hash[String, T.nilable(T.any(T::Array[String], String))]),
|
|
534
|
+
headers:
|
|
535
|
+
T.nilable(
|
|
536
|
+
T::Hash[
|
|
537
|
+
String,
|
|
538
|
+
T.nilable(T.any(String, Integer, T::Array[T.nilable(T.any(String, Integer))]))
|
|
539
|
+
]
|
|
540
|
+
),
|
|
541
|
+
body: T.nilable(T.anything),
|
|
542
|
+
unwrap:
|
|
543
|
+
T.nilable(
|
|
544
|
+
T.any(
|
|
545
|
+
Symbol,
|
|
546
|
+
Integer,
|
|
547
|
+
T::Array[T.any(Symbol, Integer)],
|
|
548
|
+
T.proc.params(arg0: T.anything).returns(T.anything)
|
|
549
|
+
)
|
|
550
|
+
),
|
|
551
|
+
page:
|
|
552
|
+
T.nilable(
|
|
553
|
+
T::Class[ScalarGalaxy::Internal::Type::BasePage[ScalarGalaxy::Internal::Type::BaseModel]]
|
|
554
|
+
),
|
|
555
|
+
stream: T.nilable(T::Class[T.anything]),
|
|
556
|
+
model: T.nilable(ScalarGalaxy::Internal::Type::Converter::Input),
|
|
557
|
+
options: T.nilable(ScalarGalaxy::RequestOptions::OrHash)
|
|
558
|
+
}
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
define_sorbet_constant!(:RequestInput) do
|
|
562
|
+
T.type_alias do
|
|
563
|
+
{
|
|
564
|
+
method: Symbol,
|
|
565
|
+
url: URI::Generic,
|
|
566
|
+
headers: T::Hash[String, String],
|
|
567
|
+
body: T.anything,
|
|
568
|
+
max_retries: Integer,
|
|
569
|
+
timeout: Float
|
|
570
|
+
}
|
|
571
|
+
end
|
|
572
|
+
end
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|