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,960 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ScalarGalaxy
4
+ module Internal
5
+ # @api private
6
+ module Util
7
+ # @api private
8
+ #
9
+ # @return [Float]
10
+ def self.monotonic_secs = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
+
12
+ # @api private
13
+ #
14
+ # @param ns [Module, Class]
15
+ #
16
+ # @return [Enumerable<Module, Class>]
17
+ def self.walk_namespaces(ns)
18
+ ns
19
+ .constants(false)
20
+ .lazy
21
+ .flat_map do
22
+ case (c = ns.const_get(_1, false))
23
+ in Module | Class
24
+ walk_namespaces(c)
25
+ else
26
+ []
27
+ end
28
+ end
29
+ .chain([ns])
30
+ end
31
+
32
+ class << self
33
+ # @api private
34
+ #
35
+ # @return [String]
36
+ def arch
37
+ case (arch = RbConfig::CONFIG["arch"])&.downcase
38
+ in nil
39
+ "unknown"
40
+ in /aarch64|arm64/
41
+ "arm64"
42
+ in /x86_64/
43
+ "x64"
44
+ in /arm/
45
+ "arm"
46
+ else
47
+ "other:#{arch}"
48
+ end
49
+ end
50
+
51
+ # @api private
52
+ #
53
+ # @return [String]
54
+ def os
55
+ case (host = RbConfig::CONFIG["host_os"])&.downcase
56
+ in nil
57
+ "Unknown"
58
+ in /linux/
59
+ "Linux"
60
+ in /darwin/
61
+ "MacOS"
62
+ in /freebsd/
63
+ "FreeBSD"
64
+ in /openbsd/
65
+ "OpenBSD"
66
+ in /mswin|mingw|cygwin|ucrt/
67
+ "Windows"
68
+ else
69
+ "Other:#{host}"
70
+ end
71
+ end
72
+ end
73
+
74
+ class << self
75
+ # @api private
76
+ #
77
+ # @param input [Object]
78
+ #
79
+ # @return [Boolean]
80
+ def primitive?(input)
81
+ case input
82
+ in true | false | Numeric | Symbol | String
83
+ true
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ # @api private
90
+ #
91
+ # @param input [String, Boolean]
92
+ #
93
+ # @return [Boolean, Object]
94
+ def coerce_boolean(input)
95
+ case input.is_a?(String) ? input.downcase : input
96
+ in "true"
97
+ true
98
+ in "false"
99
+ false
100
+ else
101
+ input
102
+ end
103
+ end
104
+
105
+ # @api private
106
+ #
107
+ # @param input [String, Boolean]
108
+ #
109
+ # @raise [ArgumentError]
110
+ # @return [Boolean, nil]
111
+ def coerce_boolean!(input)
112
+ case coerce_boolean(input)
113
+ in true | false | nil => coerced
114
+ coerced
115
+ else
116
+ raise ArgumentError.new("Unable to coerce #{input.inspect} into boolean value")
117
+ end
118
+ end
119
+
120
+ # @api private
121
+ #
122
+ # @param input [String, Integer]
123
+ #
124
+ # @return [Integer, Object]
125
+ def coerce_integer(input)
126
+ Integer(input, exception: false) || input
127
+ end
128
+
129
+ # @api private
130
+ #
131
+ # @param input [String, Integer, Float]
132
+ #
133
+ # @return [Float, Object]
134
+ def coerce_float(input)
135
+ Float(input, exception: false) || input
136
+ end
137
+
138
+ # @api private
139
+ #
140
+ # @param input [Object]
141
+ #
142
+ # @return [Hash{Object=>Object}, Object]
143
+ def coerce_hash(input)
144
+ case input
145
+ in NilClass | Array | Set | Enumerator | StringIO | IO
146
+ input
147
+ else
148
+ input.respond_to?(:to_h) ? input.to_h : input
149
+ end
150
+ end
151
+
152
+ # @api private
153
+ #
154
+ # @param input [Object]
155
+ #
156
+ # @raise [ArgumentError]
157
+ # @return [Hash{Object=>Object}, nil]
158
+ def coerce_hash!(input)
159
+ case coerce_hash(input)
160
+ in Hash | nil => coerced
161
+ coerced
162
+ else
163
+ message = "Expected a #{Hash} or #{ScalarGalaxy::Internal::Type::BaseModel}, got #{input.inspect}"
164
+ raise ArgumentError.new(message)
165
+ end
166
+ end
167
+ end
168
+
169
+ class << self
170
+ # @api private
171
+ #
172
+ # @param lhs [Object]
173
+ # @param rhs [Object]
174
+ # @param concat [Boolean]
175
+ #
176
+ # @return [Object]
177
+ private def deep_merge_lr(lhs, rhs, concat: false)
178
+ case [lhs, rhs, concat]
179
+ in [Hash, Hash, _]
180
+ lhs.merge(rhs) { deep_merge_lr(_2, _3, concat: concat) }
181
+ in [Array, Array, true]
182
+ lhs.concat(rhs)
183
+ else
184
+ rhs
185
+ end
186
+ end
187
+
188
+ # @api private
189
+ #
190
+ # Recursively merge one hash with another. If the values at a given key are not
191
+ # both hashes, just take the new value.
192
+ #
193
+ # @param values [Array<Object>]
194
+ #
195
+ # @param sentinel [Object, nil] the value to return if no values are provided.
196
+ #
197
+ # @param concat [Boolean] whether to merge sequences by concatenation.
198
+ #
199
+ # @return [Object]
200
+ def deep_merge(*values, sentinel: nil, concat: false)
201
+ case values
202
+ in [value, *values]
203
+ values.reduce(value) { |acc, val| deep_merge_lr(acc, val, concat: concat) }
204
+ else
205
+ sentinel
206
+ end
207
+ end
208
+
209
+ # @api private
210
+ #
211
+ # @param data [Hash{Symbol=>Object}, Array<Object>, Object]
212
+ # @param pick [Symbol, Integer, Array<Symbol, Integer>, Proc, nil]
213
+ # @param blk [Proc, nil]
214
+ #
215
+ # @return [Object, nil]
216
+ def dig(data, pick, &blk)
217
+ case [data, pick]
218
+ in [_, nil]
219
+ data
220
+ in [Hash, Symbol] | [Array, Integer]
221
+ data.fetch(pick) { blk&.call }
222
+ in [Hash | Array, Array]
223
+ pick.reduce(data) do |acc, key|
224
+ case acc
225
+ in Hash if acc.key?(key)
226
+ acc.fetch(key)
227
+ in Array if key.is_a?(Integer) && key < acc.length
228
+ acc[key]
229
+ else
230
+ return blk&.call
231
+ end
232
+ end
233
+ in [_, Proc]
234
+ pick.call(data)
235
+ else
236
+ blk&.call
237
+ end
238
+ end
239
+ end
240
+
241
+ # @type [Regexp]
242
+ #
243
+ # https://www.rfc-editor.org/rfc/rfc3986.html#section-3.3
244
+ RFC_3986_NOT_PCHARS = /[^A-Za-z0-9\-._~!$&'()*+,;=:@]+/
245
+
246
+ class << self
247
+ # @api private
248
+ #
249
+ # @param uri [URI::Generic]
250
+ #
251
+ # @return [String]
252
+ def uri_origin(uri)
253
+ "#{uri.scheme}://#{uri.host}#{":#{uri.port}" unless uri.port == uri.default_port}"
254
+ end
255
+
256
+ # @api private
257
+ #
258
+ # @param path [String, Integer]
259
+ #
260
+ # @return [String]
261
+ def encode_path(path)
262
+ path.to_s.gsub(ScalarGalaxy::Internal::Util::RFC_3986_NOT_PCHARS) { ERB::Util.url_encode(_1) }
263
+ end
264
+
265
+ # @api private
266
+ #
267
+ # @param path [String, Array<String>]
268
+ #
269
+ # @return [String]
270
+ def interpolate_path(path)
271
+ case path
272
+ in String
273
+ path
274
+ in []
275
+ ""
276
+ in [String => p, *interpolations]
277
+ encoded = interpolations.map { encode_path(_1) }
278
+ format(p, *encoded)
279
+ end
280
+ end
281
+ end
282
+
283
+ class << self
284
+ # @api private
285
+ #
286
+ # @param query [String, nil]
287
+ #
288
+ # @return [Hash{String=>Array<String>}]
289
+ def decode_query(query)
290
+ CGI.parse(query.to_s)
291
+ end
292
+
293
+ # @api private
294
+ #
295
+ # @param query [Hash{String=>Array<String>, String, nil}, nil]
296
+ #
297
+ # @return [String, nil]
298
+ def encode_query(query)
299
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
300
+ end
301
+ end
302
+
303
+ class << self
304
+ # @api private
305
+ #
306
+ # @param url [URI::Generic, String]
307
+ #
308
+ # @return [Hash{Symbol=>String, Integer, nil}]
309
+ def parse_uri(url)
310
+ parsed = URI::Generic.component.zip(URI.split(url)).to_h
311
+ {**parsed, query: decode_query(parsed.fetch(:query))}
312
+ end
313
+
314
+ # @api private
315
+ #
316
+ # @param parsed [Hash{Symbol=>String, Integer, nil}] .
317
+ #
318
+ # @option parsed [String, nil] :scheme
319
+ #
320
+ # @option parsed [String, nil] :host
321
+ #
322
+ # @option parsed [Integer, nil] :port
323
+ #
324
+ # @option parsed [String, nil] :path
325
+ #
326
+ # @option parsed [Hash{String=>Array<String>}] :query
327
+ #
328
+ # @return [URI::Generic]
329
+ def unparse_uri(parsed)
330
+ URI::Generic.build(**parsed, query: encode_query(parsed.fetch(:query)))
331
+ end
332
+
333
+ # @api private
334
+ #
335
+ # @param lhs [Hash{Symbol=>String, Integer, nil}] .
336
+ #
337
+ # @option lhs [String, nil] :scheme
338
+ #
339
+ # @option lhs [String, nil] :host
340
+ #
341
+ # @option lhs [Integer, nil] :port
342
+ #
343
+ # @option lhs [String, nil] :path
344
+ #
345
+ # @option lhs [Hash{String=>Array<String>}] :query
346
+ #
347
+ # @param rhs [Hash{Symbol=>String, Integer, nil}] .
348
+ #
349
+ # @option rhs [String, nil] :scheme
350
+ #
351
+ # @option rhs [String, nil] :host
352
+ #
353
+ # @option rhs [Integer, nil] :port
354
+ #
355
+ # @option rhs [String, nil] :path
356
+ #
357
+ # @option rhs [Hash{String=>Array<String>}] :query
358
+ #
359
+ # @return [URI::Generic]
360
+ def join_parsed_uri(lhs, rhs)
361
+ base_path, base_query = lhs.fetch_values(:path, :query)
362
+ slashed = base_path.end_with?("/") ? base_path : "#{base_path}/"
363
+
364
+ merged = {**parse_uri(rhs.fetch(:path)), **rhs.except(:path, :query)}
365
+ parsed_path, parsed_query = merged.fetch_values(:path, :query)
366
+ override = URI::Generic.build(**merged.slice(:scheme, :host, :port), path: parsed_path)
367
+
368
+ joined = URI.join(URI::Generic.build(lhs.except(:path, :query)), slashed, override)
369
+ query =
370
+ deep_merge(
371
+ joined.path == base_path ? base_query : {},
372
+ parsed_query,
373
+ rhs[:query].to_h,
374
+ concat: true
375
+ )
376
+
377
+ joined.query = encode_query(query)
378
+ joined
379
+ end
380
+ end
381
+
382
+ class << self
383
+ # @api private
384
+ #
385
+ # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
386
+ #
387
+ # @return [Hash{String=>String}]
388
+ def normalized_headers(*headers)
389
+ {}.merge(*headers.compact)
390
+ .to_h do |key, val|
391
+ value =
392
+ case val
393
+ in Array
394
+ val.filter_map { _1&.to_s&.strip }.join(", ")
395
+ else
396
+ val&.to_s&.strip
397
+ end
398
+ [key.downcase, value]
399
+ end
400
+ end
401
+ end
402
+
403
+ # @api private
404
+ #
405
+ # An adapter that satisfies the IO interface required by `::IO.copy_stream`
406
+ class ReadIOAdapter
407
+ # @api private
408
+ #
409
+ # @return [Boolean, nil]
410
+ def close? = @closing
411
+
412
+ # @api private
413
+ def close
414
+ case @stream
415
+ in Enumerator
416
+ ScalarGalaxy::Internal::Util.close_fused!(@stream)
417
+ in IO if close?
418
+ @stream.close
419
+ else
420
+ end
421
+ end
422
+
423
+ # @api private
424
+ #
425
+ # @param max_len [Integer, nil]
426
+ #
427
+ # @return [String]
428
+ private def read_enum(max_len)
429
+ case max_len
430
+ in nil
431
+ @stream.to_a.join
432
+ in Integer
433
+ @buf << @stream.next while @buf.length < max_len
434
+ @buf.slice!(..max_len)
435
+ end
436
+ rescue StopIteration
437
+ @stream = nil
438
+ @buf.slice!(0..)
439
+ end
440
+
441
+ # @api private
442
+ #
443
+ # @param max_len [Integer, nil]
444
+ # @param out_string [String, nil]
445
+ #
446
+ # @return [String, nil]
447
+ def read(max_len = nil, out_string = nil)
448
+ case @stream
449
+ in nil
450
+ nil
451
+ in IO | StringIO
452
+ @stream.read(max_len, out_string)
453
+ in Enumerator
454
+ read = read_enum(max_len)
455
+ case out_string
456
+ in String
457
+ out_string.replace(read)
458
+ in nil
459
+ read
460
+ end
461
+ end.tap(&@blk)
462
+ end
463
+
464
+ # @api private
465
+ #
466
+ # @param src [String, Pathname, StringIO, Enumerable<String>]
467
+ # @param blk [Proc]
468
+ #
469
+ # @yieldparam [String]
470
+ def initialize(src, &blk)
471
+ @stream =
472
+ case src
473
+ in String
474
+ StringIO.new(src)
475
+ in Pathname
476
+ @closing = true
477
+ src.open(binmode: true)
478
+ else
479
+ src
480
+ end
481
+ @buf = String.new
482
+ @blk = blk
483
+ end
484
+ end
485
+
486
+ class << self
487
+ # @param blk [Proc]
488
+ #
489
+ # @yieldparam [Enumerator::Yielder]
490
+ # @return [Enumerable<String>]
491
+ def writable_enum(&blk)
492
+ Enumerator.new do |y|
493
+ y.define_singleton_method(:write) do
494
+ self << _1.dup
495
+ _1.bytesize
496
+ end
497
+
498
+ blk.call(y)
499
+ end
500
+ end
501
+ end
502
+
503
+ # @type [Regexp]
504
+ JSON_CONTENT = %r{^application/(?:[a-zA-Z0-9.-]+\+)?json(?!l)}
505
+ # @type [Regexp]
506
+ JSONL_CONTENT = %r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)}
507
+
508
+ class << self
509
+ # @api private
510
+ #
511
+ # @param query [Hash{Symbol=>Object}]
512
+ #
513
+ # @return [Hash{Symbol=>Object}]
514
+ def encode_query_params(query)
515
+ out = {}
516
+ query.each { write_query_param_element!(out, _1, _2) }
517
+ out
518
+ end
519
+
520
+ # @api private
521
+ #
522
+ # @param collection [Hash{Symbol=>Object}]
523
+ # @param key [String]
524
+ # @param element [Object]
525
+ #
526
+ # @return [nil]
527
+ private def write_query_param_element!(collection, key, element)
528
+ case element
529
+ in Hash
530
+ element.each { |name, value| write_query_param_element!(collection, "#{key}[#{name}]", value) }
531
+ in Array
532
+ collection[key] = element.map(&:to_s).join(",")
533
+ else
534
+ collection[key] = element.to_s
535
+ end
536
+ end
537
+
538
+ # @api private
539
+ #
540
+ # @param y [Enumerator::Yielder]
541
+ # @param val [Object]
542
+ # @param closing [Array<Proc>]
543
+ # @param content_type [String, nil]
544
+ private def write_multipart_content(y, val:, closing:, content_type: nil)
545
+ content_line = "Content-Type: %s\r\n\r\n"
546
+
547
+ case val
548
+ in ScalarGalaxy::FilePart
549
+ return (
550
+ write_multipart_content(y, val: val.content, closing: closing, content_type: val.content_type)
551
+ )
552
+ in Pathname
553
+ y << format(content_line, content_type || "application/octet-stream")
554
+ io = val.open(binmode: true)
555
+ closing << io.method(:close)
556
+ IO.copy_stream(io, y)
557
+ in IO
558
+ y << format(content_line, content_type || "application/octet-stream")
559
+ IO.copy_stream(val, y)
560
+ in StringIO
561
+ y << format(content_line, content_type || "application/octet-stream")
562
+ y << val.string
563
+ in -> { primitive?(_1) }
564
+ y << format(content_line, content_type || "text/plain")
565
+ y << val.to_s
566
+ else
567
+ y << format(content_line, content_type || "application/json")
568
+ y << JSON.generate(val)
569
+ end
570
+ y << "\r\n"
571
+ end
572
+
573
+ # @api private
574
+ #
575
+ # @param y [Enumerator::Yielder]
576
+ # @param boundary [String]
577
+ # @param key [Symbol, String]
578
+ # @param val [Object]
579
+ # @param closing [Array<Proc>]
580
+ private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
581
+ y << "--#{boundary}\r\n"
582
+ y << "Content-Disposition: form-data"
583
+
584
+ y << "; name=\"#{key}\"" unless key.nil?
585
+
586
+ case val
587
+ in ScalarGalaxy::FilePart unless val.filename.nil?
588
+ filename = encode_path(val.filename)
589
+ y << "; filename=\"#{filename}\""
590
+ in Pathname | IO
591
+ filename = encode_path(::File.basename(val.to_path))
592
+ y << "; filename=\"#{filename}\""
593
+ else
594
+ end
595
+ y << "\r\n"
596
+
597
+ write_multipart_content(y, val: val, closing: closing)
598
+ end
599
+
600
+ # @api private
601
+ #
602
+ # https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#special-considerations-for-multipart-content
603
+ #
604
+ # @param body [Object]
605
+ #
606
+ # @return [Array(String, Enumerable<String>)]
607
+ private def encode_multipart_streaming(body)
608
+ # rubocop:disable Style/CaseEquality
609
+ # RFC 1521 Section 7.2.1 says we should have 70 char maximum for boundary length
610
+ boundary = SecureRandom.urlsafe_base64(46)
611
+
612
+ closing = []
613
+ strio =
614
+ writable_enum do |y|
615
+ case body
616
+ in Hash
617
+ body.each do |key, val|
618
+ case val
619
+ in Array if val.all? { primitive?(_1) || ScalarGalaxy::Internal::Type::FileInput === _1 }
620
+ val.each do |v|
621
+ write_multipart_chunk(y, boundary: boundary, key: key, val: v, closing: closing)
622
+ end
623
+ else
624
+ write_multipart_chunk(y, boundary: boundary, key: key, val: val, closing: closing)
625
+ end
626
+ end
627
+ else
628
+ write_multipart_chunk(y, boundary: boundary, key: nil, val: body, closing: closing)
629
+ end
630
+ y << "--#{boundary}--\r\n"
631
+ end
632
+
633
+ fused_io = fused_enum(strio) { closing.each(&:call) }
634
+ [boundary, fused_io]
635
+ # rubocop:enable Style/CaseEquality
636
+ end
637
+
638
+ # @api private
639
+ #
640
+ # @param headers [Hash{String=>String}]
641
+ # @param body [Object]
642
+ #
643
+ # @return [Object]
644
+ def encode_content(headers, body)
645
+ # rubocop:disable Style/CaseEquality
646
+ content_type = headers["content-type"]
647
+ case [content_type, body]
648
+ in [ScalarGalaxy::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }]
649
+ [headers, JSON.generate(body)]
650
+ in [
651
+ ScalarGalaxy::Internal::Util::JSONL_CONTENT,
652
+ Enumerable
653
+ ] unless ScalarGalaxy::Internal::Type::FileInput === body
654
+ [headers, body.lazy.map { JSON.generate(_1) }]
655
+ in [%r{^multipart/form-data}, Hash | ScalarGalaxy::Internal::Type::FileInput]
656
+ boundary, strio = encode_multipart_streaming(body)
657
+ headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"}
658
+ [headers, strio]
659
+ in [_, Symbol | Numeric]
660
+ [headers, body.to_s]
661
+ in [_, StringIO]
662
+ [headers, body.string]
663
+ in [_, ScalarGalaxy::FilePart]
664
+ [headers, body.content]
665
+ else
666
+ [headers, body]
667
+ end
668
+ # rubocop:enable Style/CaseEquality
669
+ end
670
+
671
+ # @api private
672
+ #
673
+ # https://www.iana.org/assignments/character-sets/character-sets.xhtml
674
+ #
675
+ # @param content_type [String]
676
+ # @param text [String]
677
+ def force_charset!(content_type, text:)
678
+ charset = /charset=([^;\s]+)/.match(content_type)&.captures&.first
679
+
680
+ return unless charset
681
+
682
+ begin
683
+ encoding = Encoding.find(charset)
684
+ text.force_encoding(encoding)
685
+ rescue ArgumentError
686
+ nil
687
+ end
688
+ end
689
+
690
+ # @api private
691
+ #
692
+ # Assumes each chunk in stream has `Encoding::BINARY`.
693
+ #
694
+ # @param headers [Hash{String=>String}]
695
+ # @param stream [Enumerable<String>]
696
+ # @param suppress_error [Boolean]
697
+ #
698
+ # @raise [JSON::ParserError]
699
+ # @return [Object]
700
+ def decode_content(headers, stream:, suppress_error: false)
701
+ case (content_type = headers["content-type"])
702
+ in ScalarGalaxy::Internal::Util::JSON_CONTENT
703
+ return nil if (json = stream.to_a.join).empty?
704
+
705
+ begin
706
+ JSON.parse(json, symbolize_names: true)
707
+ rescue JSON::ParserError => e
708
+ raise e unless suppress_error
709
+ json
710
+ end
711
+ in ScalarGalaxy::Internal::Util::JSONL_CONTENT
712
+ lines = decode_lines(stream)
713
+ chain_fused(lines) do |y|
714
+ lines.each do
715
+ next if _1.empty?
716
+
717
+ y << JSON.parse(_1, symbolize_names: true)
718
+ end
719
+ end
720
+ in %r{^text/event-stream}
721
+ lines = decode_lines(stream)
722
+ decode_sse(lines)
723
+ else
724
+ text = stream.to_a.join
725
+ force_charset!(content_type, text: text)
726
+ StringIO.new(text)
727
+ end
728
+ end
729
+ end
730
+
731
+ class << self
732
+ # @api private
733
+ #
734
+ # https://doc.rust-lang.org/std/iter/trait.FusedIterator.html
735
+ #
736
+ # @param enum [Enumerable<Object>]
737
+ # @param external [Boolean]
738
+ # @param close [Proc]
739
+ #
740
+ # @return [Enumerable<Object>]
741
+ def fused_enum(enum, external: false, &close)
742
+ fused = false
743
+ iter =
744
+ Enumerator.new do |y|
745
+ next if fused
746
+
747
+ fused = true
748
+ external ? loop { y << enum.next } : enum.each(&y)
749
+ ensure
750
+ close&.call
751
+ close = nil
752
+ end
753
+
754
+ iter.define_singleton_method(:rewind) do
755
+ fused = true
756
+ self
757
+ end
758
+ iter
759
+ end
760
+
761
+ # @api private
762
+ #
763
+ # @param enum [Enumerable<Object>, nil]
764
+ def close_fused!(enum)
765
+ return unless enum.is_a?(Enumerator)
766
+
767
+ # rubocop:disable Lint/UnreachableLoop
768
+ enum.rewind.each { break }
769
+ # rubocop:enable Lint/UnreachableLoop
770
+ end
771
+
772
+ # @api private
773
+ #
774
+ # @param enum [Enumerable<Object>, nil]
775
+ # @param blk [Proc]
776
+ #
777
+ # @yieldparam [Enumerator::Yielder]
778
+ # @return [Enumerable<Object>]
779
+ def chain_fused(enum, &blk)
780
+ iter = Enumerator.new { blk.call(_1) }
781
+ fused_enum(iter) { close_fused!(enum) }
782
+ end
783
+ end
784
+
785
+ class << self
786
+ # @api private
787
+ #
788
+ # Assumes Strings have been forced into having `Encoding::BINARY`.
789
+ #
790
+ # This decoder is responsible for reassembling lines split across multiple
791
+ # fragments.
792
+ #
793
+ # @param enum [Enumerable<String>]
794
+ #
795
+ # @return [Enumerable<String>]
796
+ def decode_lines(enum)
797
+ re = /(\r\n|\r|\n)/
798
+ buffer = String.new
799
+ cr_seen = nil
800
+
801
+ chain_fused(enum) do |y|
802
+ enum.each do |row|
803
+ offset = buffer.bytesize
804
+ buffer << row
805
+ while (match = re.match(buffer, cr_seen&.to_i || offset))
806
+ case [match.captures.first, cr_seen]
807
+ in ["\r", nil]
808
+ cr_seen = match.end(1)
809
+ next
810
+ in ["\r" | "\r\n", Integer]
811
+ y << buffer.slice!(..(cr_seen.pred))
812
+ else
813
+ y << buffer.slice!(..(match.end(1).pred))
814
+ end
815
+ offset = 0
816
+ cr_seen = nil
817
+ end
818
+ end
819
+
820
+ y << buffer.slice!(..(cr_seen.pred)) unless cr_seen.nil?
821
+ y << buffer unless buffer.empty?
822
+ end
823
+ end
824
+
825
+ # @api private
826
+ #
827
+ # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
828
+ #
829
+ # Assumes that `lines` has been decoded with `#decode_lines`.
830
+ #
831
+ # @param lines [Enumerable<String>]
832
+ #
833
+ # @return [Enumerable<Hash{Symbol=>Object}>]
834
+ def decode_sse(lines)
835
+ # rubocop:disable Metrics/BlockLength
836
+ chain_fused(lines) do |y|
837
+ blank = {event: nil, data: nil, id: nil, retry: nil}
838
+ current = {}
839
+
840
+ lines.each do |line|
841
+ case line.sub(/\R$/, "")
842
+ in ""
843
+ next if current.empty?
844
+ y << {**blank, **current}
845
+ current = {}
846
+ in /^:/
847
+ next
848
+ in /^([^:]+):\s?(.*)$/
849
+ field, value = Regexp.last_match.captures
850
+ case field
851
+ in "event"
852
+ current.merge!(event: value)
853
+ in "data"
854
+ (current[:data] ||= String.new) << (value << "\n")
855
+ in "id" unless value.include?("\0")
856
+ current.merge!(id: value)
857
+ in "retry" if /^\d+$/ =~ value
858
+ current.merge!(retry: Integer(value))
859
+ else
860
+ end
861
+ else
862
+ end
863
+ end
864
+ # rubocop:enable Metrics/BlockLength
865
+
866
+ y << {**blank, **current} unless current.empty?
867
+ end
868
+ end
869
+ end
870
+
871
+ # @api private
872
+ module SorbetRuntimeSupport
873
+ class MissingSorbetRuntimeError < ::RuntimeError
874
+ end
875
+
876
+ # @api private
877
+ #
878
+ # @return [Hash{Symbol=>Object}]
879
+ private def sorbet_runtime_constants = @sorbet_runtime_constants ||= {}
880
+
881
+ # @api private
882
+ #
883
+ # @param name [Symbol]
884
+ def const_missing(name)
885
+ super unless sorbet_runtime_constants.key?(name)
886
+
887
+ unless Object.const_defined?(:T)
888
+ message = "Trying to access a Sorbet constant #{name.inspect} without `sorbet-runtime`."
889
+ raise MissingSorbetRuntimeError.new(message)
890
+ end
891
+
892
+ sorbet_runtime_constants.fetch(name).call
893
+ end
894
+
895
+ # @api private
896
+ #
897
+ # @param name [Symbol]
898
+ #
899
+ # @return [Boolean]
900
+ def sorbet_constant_defined?(name) = sorbet_runtime_constants.key?(name)
901
+
902
+ # @api private
903
+ #
904
+ # @param name [Symbol]
905
+ # @param blk [Proc]
906
+ def define_sorbet_constant!(name, &blk) = sorbet_runtime_constants.store(name, blk)
907
+
908
+ # @api private
909
+ #
910
+ # @return [Object]
911
+ def to_sorbet_type = raise NotImplementedError
912
+
913
+ class << self
914
+ # @api private
915
+ #
916
+ # @param type [ScalarGalaxy::Internal::Util::SorbetRuntimeSupport, Object]
917
+ #
918
+ # @return [Object]
919
+ def to_sorbet_type(type)
920
+ case type
921
+ in ScalarGalaxy::Internal::Util::SorbetRuntimeSupport
922
+ type.to_sorbet_type
923
+ in Class | Module
924
+ type
925
+ in true | false
926
+ T::Boolean
927
+ else
928
+ type.class
929
+ end
930
+ end
931
+ end
932
+ end
933
+
934
+ extend ScalarGalaxy::Internal::Util::SorbetRuntimeSupport
935
+
936
+ define_sorbet_constant!(:ParsedUri) do
937
+ T.type_alias do
938
+ {
939
+ scheme: T.nilable(String),
940
+ host: T.nilable(String),
941
+ port: T.nilable(Integer),
942
+ path: T.nilable(String),
943
+ query: T::Hash[String, T::Array[String]]
944
+ }
945
+ end
946
+ end
947
+
948
+ define_sorbet_constant!(:ServerSentEvent) do
949
+ T.type_alias do
950
+ {
951
+ event: T.nilable(String),
952
+ data: T.nilable(String),
953
+ id: T.nilable(String),
954
+ retry: T.nilable(Integer)
955
+ }
956
+ end
957
+ end
958
+ end
959
+ end
960
+ end