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