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.
Files changed (145) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/LICENSE +201 -0
  4. data/README.md +169 -0
  5. data/SECURITY.md +19 -0
  6. data/SKILL.md +83 -0
  7. data/api.md +188 -0
  8. data/lib/galaxy-ruby/client.rb +254 -0
  9. data/lib/galaxy-ruby/errors.rb +227 -0
  10. data/lib/galaxy-ruby/file_part.rb +58 -0
  11. data/lib/galaxy-ruby/internal/transport/base_client.rb +576 -0
  12. data/lib/galaxy-ruby/internal/transport/pooled_net_requester.rb +224 -0
  13. data/lib/galaxy-ruby/internal/type/array_of.rb +164 -0
  14. data/lib/galaxy-ruby/internal/type/base_model.rb +529 -0
  15. data/lib/galaxy-ruby/internal/type/base_page.rb +55 -0
  16. data/lib/galaxy-ruby/internal/type/boolean.rb +77 -0
  17. data/lib/galaxy-ruby/internal/type/converter.rb +333 -0
  18. data/lib/galaxy-ruby/internal/type/enum.rb +130 -0
  19. data/lib/galaxy-ruby/internal/type/file_input.rb +111 -0
  20. data/lib/galaxy-ruby/internal/type/hash_of.rb +184 -0
  21. data/lib/galaxy-ruby/internal/type/request_parameters.rb +42 -0
  22. data/lib/galaxy-ruby/internal/type/union.rb +234 -0
  23. data/lib/galaxy-ruby/internal/type/unknown.rb +81 -0
  24. data/lib/galaxy-ruby/internal/util.rb +960 -0
  25. data/lib/galaxy-ruby/internal.rb +15 -0
  26. data/lib/galaxy-ruby/models/authentication_create_token_params.rb +14 -0
  27. data/lib/galaxy-ruby/models/authentication_create_token_response.rb +16 -0
  28. data/lib/galaxy-ruby/models/authentication_create_user_params.rb +32 -0
  29. data/lib/galaxy-ruby/models/authentication_list_me_params.rb +14 -0
  30. data/lib/galaxy-ruby/models/celestial_body.rb +81 -0
  31. data/lib/galaxy-ruby/models/celestial_body_create_params.rb +20 -0
  32. data/lib/galaxy-ruby/models/credentials.rb +25 -0
  33. data/lib/galaxy-ruby/models/new_planet_webhook_event.rb +216 -0
  34. data/lib/galaxy-ruby/models/planet.rb +218 -0
  35. data/lib/galaxy-ruby/models/planet_create_params.rb +14 -0
  36. data/lib/galaxy-ruby/models/planet_delete_params.rb +20 -0
  37. data/lib/galaxy-ruby/models/planet_delte_image_params.rb +29 -0
  38. data/lib/galaxy-ruby/models/planet_delte_image_response.rb +48 -0
  39. data/lib/galaxy-ruby/models/planet_list_all_data_params.rb +30 -0
  40. data/lib/galaxy-ruby/models/planet_list_all_data_response.rb +51 -0
  41. data/lib/galaxy-ruby/models/planet_retrieve_params.rb +20 -0
  42. data/lib/galaxy-ruby/models/planet_update_params.rb +20 -0
  43. data/lib/galaxy-ruby/models/unwrap_webhook_event.rb +14 -0
  44. data/lib/galaxy-ruby/models/user.rb +26 -0
  45. data/lib/galaxy-ruby/models/webhook_unwrap_params.rb +14 -0
  46. data/lib/galaxy-ruby/models.rb +78 -0
  47. data/lib/galaxy-ruby/request_options.rb +77 -0
  48. data/lib/galaxy-ruby/resources/authentication.rb +80 -0
  49. data/lib/galaxy-ruby/resources/celestial_bodies.rb +37 -0
  50. data/lib/galaxy-ruby/resources/planets.rb +215 -0
  51. data/lib/galaxy-ruby/resources/webhooks.rb +34 -0
  52. data/lib/galaxy-ruby/version.rb +5 -0
  53. data/lib/galaxy-ruby.rb +78 -0
  54. data/rbi/galaxy-ruby/client.rbi +128 -0
  55. data/rbi/galaxy-ruby/errors.rbi +205 -0
  56. data/rbi/galaxy-ruby/file_part.rbi +37 -0
  57. data/rbi/galaxy-ruby/internal/transport/base_client.rbi +305 -0
  58. data/rbi/galaxy-ruby/internal/transport/pooled_net_requester.rbi +84 -0
  59. data/rbi/galaxy-ruby/internal/type/array_of.rbi +104 -0
  60. data/rbi/galaxy-ruby/internal/type/base_model.rbi +314 -0
  61. data/rbi/galaxy-ruby/internal/type/base_page.rbi +43 -0
  62. data/rbi/galaxy-ruby/internal/type/boolean.rbi +58 -0
  63. data/rbi/galaxy-ruby/internal/type/converter.rbi +222 -0
  64. data/rbi/galaxy-ruby/internal/type/enum.rbi +82 -0
  65. data/rbi/galaxy-ruby/internal/type/file_input.rbi +59 -0
  66. data/rbi/galaxy-ruby/internal/type/hash_of.rbi +104 -0
  67. data/rbi/galaxy-ruby/internal/type/request_parameters.rbi +31 -0
  68. data/rbi/galaxy-ruby/internal/type/union.rbi +128 -0
  69. data/rbi/galaxy-ruby/internal/type/unknown.rbi +58 -0
  70. data/rbi/galaxy-ruby/internal/util.rbi +515 -0
  71. data/rbi/galaxy-ruby/internal.rbi +18 -0
  72. data/rbi/galaxy-ruby/models/authentication_create_token_params.rbi +32 -0
  73. data/rbi/galaxy-ruby/models/authentication_create_token_response.rbi +29 -0
  74. data/rbi/galaxy-ruby/models/authentication_create_user_params.rbi +54 -0
  75. data/rbi/galaxy-ruby/models/authentication_list_me_params.rbi +32 -0
  76. data/rbi/galaxy-ruby/models/celestial_body.rbi +140 -0
  77. data/rbi/galaxy-ruby/models/celestial_body_create_params.rbi +53 -0
  78. data/rbi/galaxy-ruby/models/credentials.rbi +27 -0
  79. data/rbi/galaxy-ruby/models/new_planet_webhook_event.rbi +396 -0
  80. data/rbi/galaxy-ruby/models/planet.rbi +350 -0
  81. data/rbi/galaxy-ruby/models/planet_create_params.rbi +32 -0
  82. data/rbi/galaxy-ruby/models/planet_delete_params.rbi +38 -0
  83. data/rbi/galaxy-ruby/models/planet_delte_image_params.rbi +55 -0
  84. data/rbi/galaxy-ruby/models/planet_delte_image_response.rbi +85 -0
  85. data/rbi/galaxy-ruby/models/planet_list_all_data_params.rbi +60 -0
  86. data/rbi/galaxy-ruby/models/planet_list_all_data_response.rbi +110 -0
  87. data/rbi/galaxy-ruby/models/planet_retrieve_params.rbi +38 -0
  88. data/rbi/galaxy-ruby/models/planet_update_params.rbi +38 -0
  89. data/rbi/galaxy-ruby/models/unwrap_webhook_event.rbi +17 -0
  90. data/rbi/galaxy-ruby/models/user.rbi +33 -0
  91. data/rbi/galaxy-ruby/models/webhook_unwrap_params.rbi +32 -0
  92. data/rbi/galaxy-ruby/models.rbi +39 -0
  93. data/rbi/galaxy-ruby/request_options.rbi +59 -0
  94. data/rbi/galaxy-ruby/resources/authentication.rbi +47 -0
  95. data/rbi/galaxy-ruby/resources/celestial_bodies.rbi +28 -0
  96. data/rbi/galaxy-ruby/resources/planets.rbi +176 -0
  97. data/rbi/galaxy-ruby/resources/webhooks.rbi +29 -0
  98. data/rbi/galaxy-ruby/version.rbi +5 -0
  99. data/sig/galaxy-ruby/client.rbs +62 -0
  100. data/sig/galaxy-ruby/errors.rbs +117 -0
  101. data/sig/galaxy-ruby/file_part.rbs +21 -0
  102. data/sig/galaxy-ruby/internal/transport/base_client.rbs +133 -0
  103. data/sig/galaxy-ruby/internal/transport/pooled_net_requester.rbs +48 -0
  104. data/sig/galaxy-ruby/internal/type/array_of.rbs +48 -0
  105. data/sig/galaxy-ruby/internal/type/base_model.rbs +102 -0
  106. data/sig/galaxy-ruby/internal/type/base_page.rbs +24 -0
  107. data/sig/galaxy-ruby/internal/type/boolean.rbs +26 -0
  108. data/sig/galaxy-ruby/internal/type/converter.rbs +79 -0
  109. data/sig/galaxy-ruby/internal/type/enum.rbs +32 -0
  110. data/sig/galaxy-ruby/internal/type/file_input.rbs +25 -0
  111. data/sig/galaxy-ruby/internal/type/hash_of.rbs +48 -0
  112. data/sig/galaxy-ruby/internal/type/request_parameters.rbs +17 -0
  113. data/sig/galaxy-ruby/internal/type/union.rbs +52 -0
  114. data/sig/galaxy-ruby/internal/type/unknown.rbs +26 -0
  115. data/sig/galaxy-ruby/internal/util.rbs +199 -0
  116. data/sig/galaxy-ruby/internal.rbs +9 -0
  117. data/sig/galaxy-ruby/models/authentication_create_token_params.rbs +15 -0
  118. data/sig/galaxy-ruby/models/authentication_create_token_response.rbs +15 -0
  119. data/sig/galaxy-ruby/models/authentication_create_user_params.rbs +34 -0
  120. data/sig/galaxy-ruby/models/authentication_list_me_params.rbs +15 -0
  121. data/sig/galaxy-ruby/models/celestial_body.rbs +76 -0
  122. data/sig/galaxy-ruby/models/celestial_body_create_params.rbs +24 -0
  123. data/sig/galaxy-ruby/models/credentials.rbs +15 -0
  124. data/sig/galaxy-ruby/models/new_planet_webhook_event.rbs +211 -0
  125. data/sig/galaxy-ruby/models/planet.rbs +211 -0
  126. data/sig/galaxy-ruby/models/planet_create_params.rbs +15 -0
  127. data/sig/galaxy-ruby/models/planet_delete_params.rbs +23 -0
  128. data/sig/galaxy-ruby/models/planet_delte_image_params.rbs +32 -0
  129. data/sig/galaxy-ruby/models/planet_delte_image_response.rbs +50 -0
  130. data/sig/galaxy-ruby/models/planet_list_all_data_params.rbs +32 -0
  131. data/sig/galaxy-ruby/models/planet_list_all_data_response.rbs +64 -0
  132. data/sig/galaxy-ruby/models/planet_retrieve_params.rbs +23 -0
  133. data/sig/galaxy-ruby/models/planet_update_params.rbs +25 -0
  134. data/sig/galaxy-ruby/models/unwrap_webhook_event.rbs +11 -0
  135. data/sig/galaxy-ruby/models/user.rbs +19 -0
  136. data/sig/galaxy-ruby/models/webhook_unwrap_params.rbs +15 -0
  137. data/sig/galaxy-ruby/models.rbs +35 -0
  138. data/sig/galaxy-ruby/request_options.rbs +34 -0
  139. data/sig/galaxy-ruby/resources/authentication.rbs +24 -0
  140. data/sig/galaxy-ruby/resources/celestial_bodies.rbs +12 -0
  141. data/sig/galaxy-ruby/resources/planets.rbs +68 -0
  142. data/sig/galaxy-ruby/resources/webhooks.rbs +13 -0
  143. data/sig/galaxy-ruby/version.rbs +3 -0
  144. data/sig/manifest.yaml +6 -0
  145. 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