brand.dev 0.0.1.pre.alpha.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 (102) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +229 -0
  3. data/SECURITY.md +23 -0
  4. data/lib/brand_dev/client.rb +74 -0
  5. data/lib/brand_dev/errors.rb +192 -0
  6. data/lib/brand_dev/file_part.rb +55 -0
  7. data/lib/brand_dev/internal/transport/base_client.rb +555 -0
  8. data/lib/brand_dev/internal/transport/pooled_net_requester.rb +209 -0
  9. data/lib/brand_dev/internal/type/array_of.rb +162 -0
  10. data/lib/brand_dev/internal/type/base_model.rb +484 -0
  11. data/lib/brand_dev/internal/type/base_page.rb +55 -0
  12. data/lib/brand_dev/internal/type/boolean.rb +71 -0
  13. data/lib/brand_dev/internal/type/converter.rb +292 -0
  14. data/lib/brand_dev/internal/type/enum.rb +120 -0
  15. data/lib/brand_dev/internal/type/file_input.rb +103 -0
  16. data/lib/brand_dev/internal/type/hash_of.rb +182 -0
  17. data/lib/brand_dev/internal/type/request_parameters.rb +42 -0
  18. data/lib/brand_dev/internal/type/union.rb +227 -0
  19. data/lib/brand_dev/internal/type/unknown.rb +75 -0
  20. data/lib/brand_dev/internal/util.rb +915 -0
  21. data/lib/brand_dev/internal.rb +20 -0
  22. data/lib/brand_dev/models/brand_identify_from_transaction_params.rb +22 -0
  23. data/lib/brand_dev/models/brand_identify_from_transaction_response.rb +435 -0
  24. data/lib/brand_dev/models/brand_retrieve_by_ticker_params.rb +22 -0
  25. data/lib/brand_dev/models/brand_retrieve_by_ticker_response.rb +432 -0
  26. data/lib/brand_dev/models/brand_retrieve_naics_params.rb +27 -0
  27. data/lib/brand_dev/models/brand_retrieve_naics_response.rb +61 -0
  28. data/lib/brand_dev/models/brand_retrieve_params.rb +91 -0
  29. data/lib/brand_dev/models/brand_retrieve_response.rb +432 -0
  30. data/lib/brand_dev/models/brand_search_params.rb +22 -0
  31. data/lib/brand_dev/models/brand_search_response.rb +35 -0
  32. data/lib/brand_dev/models.rb +51 -0
  33. data/lib/brand_dev/request_options.rb +77 -0
  34. data/lib/brand_dev/resources/brand.rb +130 -0
  35. data/lib/brand_dev/version.rb +5 -0
  36. data/lib/brand_dev.rb +64 -0
  37. data/manifest.yaml +15 -0
  38. data/rbi/brand_dev/client.rbi +49 -0
  39. data/rbi/brand_dev/errors.rbi +162 -0
  40. data/rbi/brand_dev/file_part.rbi +37 -0
  41. data/rbi/brand_dev/internal/transport/base_client.rbi +293 -0
  42. data/rbi/brand_dev/internal/transport/pooled_net_requester.rbi +79 -0
  43. data/rbi/brand_dev/internal/type/array_of.rbi +104 -0
  44. data/rbi/brand_dev/internal/type/base_model.rbi +302 -0
  45. data/rbi/brand_dev/internal/type/base_page.rbi +42 -0
  46. data/rbi/brand_dev/internal/type/boolean.rbi +56 -0
  47. data/rbi/brand_dev/internal/type/converter.rbi +162 -0
  48. data/rbi/brand_dev/internal/type/enum.rbi +82 -0
  49. data/rbi/brand_dev/internal/type/file_input.rbi +59 -0
  50. data/rbi/brand_dev/internal/type/hash_of.rbi +104 -0
  51. data/rbi/brand_dev/internal/type/request_parameters.rbi +29 -0
  52. data/rbi/brand_dev/internal/type/union.rbi +116 -0
  53. data/rbi/brand_dev/internal/type/unknown.rbi +56 -0
  54. data/rbi/brand_dev/internal/util.rbi +485 -0
  55. data/rbi/brand_dev/internal.rbi +16 -0
  56. data/rbi/brand_dev/models/brand_identify_from_transaction_params.rbi +46 -0
  57. data/rbi/brand_dev/models/brand_identify_from_transaction_response.rbi +981 -0
  58. data/rbi/brand_dev/models/brand_retrieve_by_ticker_params.rbi +43 -0
  59. data/rbi/brand_dev/models/brand_retrieve_by_ticker_response.rbi +976 -0
  60. data/rbi/brand_dev/models/brand_retrieve_naics_params.rbi +44 -0
  61. data/rbi/brand_dev/models/brand_retrieve_naics_response.rbi +127 -0
  62. data/rbi/brand_dev/models/brand_retrieve_params.rbi +344 -0
  63. data/rbi/brand_dev/models/brand_retrieve_response.rbi +949 -0
  64. data/rbi/brand_dev/models/brand_search_params.rbi +40 -0
  65. data/rbi/brand_dev/models/brand_search_response.rbi +63 -0
  66. data/rbi/brand_dev/models.rbi +14 -0
  67. data/rbi/brand_dev/request_options.rbi +59 -0
  68. data/rbi/brand_dev/resources/brand.rbi +89 -0
  69. data/rbi/brand_dev/version.rbi +5 -0
  70. data/sig/brand_dev/client.rbs +26 -0
  71. data/sig/brand_dev/errors.rbs +101 -0
  72. data/sig/brand_dev/file_part.rbs +21 -0
  73. data/sig/brand_dev/internal/transport/base_client.rbs +131 -0
  74. data/sig/brand_dev/internal/transport/pooled_net_requester.rbs +45 -0
  75. data/sig/brand_dev/internal/type/array_of.rbs +48 -0
  76. data/sig/brand_dev/internal/type/base_model.rbs +102 -0
  77. data/sig/brand_dev/internal/type/base_page.rbs +24 -0
  78. data/sig/brand_dev/internal/type/boolean.rbs +26 -0
  79. data/sig/brand_dev/internal/type/converter.rbs +56 -0
  80. data/sig/brand_dev/internal/type/enum.rbs +32 -0
  81. data/sig/brand_dev/internal/type/file_input.rbs +25 -0
  82. data/sig/brand_dev/internal/type/hash_of.rbs +48 -0
  83. data/sig/brand_dev/internal/type/request_parameters.rbs +17 -0
  84. data/sig/brand_dev/internal/type/union.rbs +52 -0
  85. data/sig/brand_dev/internal/type/unknown.rbs +26 -0
  86. data/sig/brand_dev/internal/util.rbs +185 -0
  87. data/sig/brand_dev/internal.rbs +9 -0
  88. data/sig/brand_dev/models/brand_identify_from_transaction_params.rbs +24 -0
  89. data/sig/brand_dev/models/brand_identify_from_transaction_response.rbs +418 -0
  90. data/sig/brand_dev/models/brand_retrieve_by_ticker_params.rbs +23 -0
  91. data/sig/brand_dev/models/brand_retrieve_by_ticker_response.rbs +418 -0
  92. data/sig/brand_dev/models/brand_retrieve_naics_params.rbs +23 -0
  93. data/sig/brand_dev/models/brand_retrieve_naics_response.rbs +61 -0
  94. data/sig/brand_dev/models/brand_retrieve_params.rbs +148 -0
  95. data/sig/brand_dev/models/brand_retrieve_response.rbs +418 -0
  96. data/sig/brand_dev/models/brand_search_params.rbs +23 -0
  97. data/sig/brand_dev/models/brand_search_response.rbs +29 -0
  98. data/sig/brand_dev/models.rbs +11 -0
  99. data/sig/brand_dev/request_options.rbs +34 -0
  100. data/sig/brand_dev/resources/brand.rbs +33 -0
  101. data/sig/brand_dev/version.rbs +3 -0
  102. metadata +160 -0
@@ -0,0 +1,915 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrandDev
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 #{BrandDev::Internal::Type::BaseModel}, got #{data.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
+ class << self
241
+ # @api private
242
+ #
243
+ # @param uri [URI::Generic]
244
+ #
245
+ # @return [String]
246
+ def uri_origin(uri)
247
+ "#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? '' : ":#{uri.port}"}"
248
+ end
249
+
250
+ # @api private
251
+ #
252
+ # @param path [String, Array<String>]
253
+ #
254
+ # @return [String]
255
+ def interpolate_path(path)
256
+ case path
257
+ in String
258
+ path
259
+ in []
260
+ ""
261
+ in [String => p, *interpolations]
262
+ encoded = interpolations.map { ERB::Util.url_encode(_1) }
263
+ format(p, *encoded)
264
+ end
265
+ end
266
+ end
267
+
268
+ class << self
269
+ # @api private
270
+ #
271
+ # @param query [String, nil]
272
+ #
273
+ # @return [Hash{String=>Array<String>}]
274
+ def decode_query(query)
275
+ CGI.parse(query.to_s)
276
+ end
277
+
278
+ # @api private
279
+ #
280
+ # @param query [Hash{String=>Array<String>, String, nil}, nil]
281
+ #
282
+ # @return [String, nil]
283
+ def encode_query(query)
284
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
285
+ end
286
+ end
287
+
288
+ class << self
289
+ # @api private
290
+ #
291
+ # @param url [URI::Generic, String]
292
+ #
293
+ # @return [Hash{Symbol=>String, Integer, nil}]
294
+ def parse_uri(url)
295
+ parsed = URI::Generic.component.zip(URI.split(url)).to_h
296
+ {**parsed, query: decode_query(parsed.fetch(:query))}
297
+ end
298
+
299
+ # @api private
300
+ #
301
+ # @param parsed [Hash{Symbol=>String, Integer, nil}] .
302
+ #
303
+ # @option parsed [String, nil] :scheme
304
+ #
305
+ # @option parsed [String, nil] :host
306
+ #
307
+ # @option parsed [Integer, nil] :port
308
+ #
309
+ # @option parsed [String, nil] :path
310
+ #
311
+ # @option parsed [Hash{String=>Array<String>}] :query
312
+ #
313
+ # @return [URI::Generic]
314
+ def unparse_uri(parsed)
315
+ URI::Generic.build(**parsed, query: encode_query(parsed.fetch(:query)))
316
+ end
317
+
318
+ # @api private
319
+ #
320
+ # @param lhs [Hash{Symbol=>String, Integer, nil}] .
321
+ #
322
+ # @option lhs [String, nil] :scheme
323
+ #
324
+ # @option lhs [String, nil] :host
325
+ #
326
+ # @option lhs [Integer, nil] :port
327
+ #
328
+ # @option lhs [String, nil] :path
329
+ #
330
+ # @option lhs [Hash{String=>Array<String>}] :query
331
+ #
332
+ # @param rhs [Hash{Symbol=>String, Integer, nil}] .
333
+ #
334
+ # @option rhs [String, nil] :scheme
335
+ #
336
+ # @option rhs [String, nil] :host
337
+ #
338
+ # @option rhs [Integer, nil] :port
339
+ #
340
+ # @option rhs [String, nil] :path
341
+ #
342
+ # @option rhs [Hash{String=>Array<String>}] :query
343
+ #
344
+ # @return [URI::Generic]
345
+ def join_parsed_uri(lhs, rhs)
346
+ base_path, base_query = lhs.fetch_values(:path, :query)
347
+ slashed = base_path.end_with?("/") ? base_path : "#{base_path}/"
348
+
349
+ parsed_path, parsed_query = parse_uri(rhs.fetch(:path)).fetch_values(:path, :query)
350
+ override = URI::Generic.build(**rhs.slice(:scheme, :host, :port), path: parsed_path)
351
+
352
+ joined = URI.join(URI::Generic.build(lhs.except(:path, :query)), slashed, override)
353
+ query = deep_merge(
354
+ joined.path == base_path ? base_query : {},
355
+ parsed_query,
356
+ rhs[:query].to_h,
357
+ concat: true
358
+ )
359
+
360
+ joined.query = encode_query(query)
361
+ joined
362
+ end
363
+ end
364
+
365
+ class << self
366
+ # @api private
367
+ #
368
+ # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
369
+ #
370
+ # @return [Hash{String=>String}]
371
+ def normalized_headers(*headers)
372
+ {}.merge(*headers.compact).to_h do |key, val|
373
+ value =
374
+ case val
375
+ in Array
376
+ val.filter_map { _1&.to_s&.strip }.join(", ")
377
+ else
378
+ val&.to_s&.strip
379
+ end
380
+ [key.downcase, value]
381
+ end
382
+ end
383
+ end
384
+
385
+ # @api private
386
+ #
387
+ # An adapter that satisfies the IO interface required by `::IO.copy_stream`
388
+ class ReadIOAdapter
389
+ # @api private
390
+ #
391
+ # @return [Boolean, nil]
392
+ def close? = @closing
393
+
394
+ # @api private
395
+ def close
396
+ case @stream
397
+ in Enumerator
398
+ BrandDev::Internal::Util.close_fused!(@stream)
399
+ in IO if close?
400
+ @stream.close
401
+ else
402
+ end
403
+ end
404
+
405
+ # @api private
406
+ #
407
+ # @param max_len [Integer, nil]
408
+ #
409
+ # @return [String]
410
+ private def read_enum(max_len)
411
+ case max_len
412
+ in nil
413
+ @stream.to_a.join
414
+ in Integer
415
+ @buf << @stream.next while @buf.length < max_len
416
+ @buf.slice!(..max_len)
417
+ end
418
+ rescue StopIteration
419
+ @stream = nil
420
+ @buf.slice!(0..)
421
+ end
422
+
423
+ # @api private
424
+ #
425
+ # @param max_len [Integer, nil]
426
+ # @param out_string [String, nil]
427
+ #
428
+ # @return [String, nil]
429
+ def read(max_len = nil, out_string = nil)
430
+ case @stream
431
+ in nil
432
+ nil
433
+ in IO | StringIO
434
+ @stream.read(max_len, out_string)
435
+ in Enumerator
436
+ read = read_enum(max_len)
437
+ case out_string
438
+ in String
439
+ out_string.replace(read)
440
+ in nil
441
+ read
442
+ end
443
+ end
444
+ .tap(&@blk)
445
+ end
446
+
447
+ # @api private
448
+ #
449
+ # @param src [String, Pathname, StringIO, Enumerable<String>]
450
+ # @param blk [Proc]
451
+ #
452
+ # @yieldparam [String]
453
+ def initialize(src, &blk)
454
+ @stream =
455
+ case src
456
+ in String
457
+ StringIO.new(src)
458
+ in Pathname
459
+ @closing = true
460
+ src.open(binmode: true)
461
+ else
462
+ src
463
+ end
464
+ @buf = String.new
465
+ @blk = blk
466
+ end
467
+ end
468
+
469
+ class << self
470
+ # @param blk [Proc]
471
+ #
472
+ # @yieldparam [Enumerator::Yielder]
473
+ # @return [Enumerable<String>]
474
+ def writable_enum(&blk)
475
+ Enumerator.new do |y|
476
+ buf = String.new
477
+ y.define_singleton_method(:write) do
478
+ self << buf.replace(_1)
479
+ buf.bytesize
480
+ end
481
+
482
+ blk.call(y)
483
+ end
484
+ end
485
+ end
486
+
487
+ # @type [Regexp]
488
+ JSON_CONTENT = %r{^application/(?:vnd(?:\.[^.]+)*\+)?json(?!l)}
489
+ # @type [Regexp]
490
+ JSONL_CONTENT = %r{^application/(:?x-(?:n|l)djson)|(:?(?:x-)?jsonl)}
491
+
492
+ class << self
493
+ # @api private
494
+ #
495
+ # @param y [Enumerator::Yielder]
496
+ # @param val [Object]
497
+ # @param closing [Array<Proc>]
498
+ # @param content_type [String, nil]
499
+ private def write_multipart_content(y, val:, closing:, content_type: nil)
500
+ content_type ||= "application/octet-stream"
501
+
502
+ case val
503
+ in BrandDev::FilePart
504
+ return write_multipart_content(
505
+ y,
506
+ val: val.content,
507
+ closing: closing,
508
+ content_type: val.content_type
509
+ )
510
+ in Pathname
511
+ y << "Content-Type: #{content_type}\r\n\r\n"
512
+ io = val.open(binmode: true)
513
+ closing << io.method(:close)
514
+ IO.copy_stream(io, y)
515
+ in IO
516
+ y << "Content-Type: #{content_type}\r\n\r\n"
517
+ IO.copy_stream(val, y)
518
+ in StringIO
519
+ y << "Content-Type: #{content_type}\r\n\r\n"
520
+ y << val.string
521
+ in String
522
+ y << "Content-Type: #{content_type}\r\n\r\n"
523
+ y << val.to_s
524
+ in -> { primitive?(_1) }
525
+ y << "Content-Type: text/plain\r\n\r\n"
526
+ y << val.to_s
527
+ else
528
+ y << "Content-Type: application/json\r\n\r\n"
529
+ y << JSON.generate(val)
530
+ end
531
+ y << "\r\n"
532
+ end
533
+
534
+ # @api private
535
+ #
536
+ # @param y [Enumerator::Yielder]
537
+ # @param boundary [String]
538
+ # @param key [Symbol, String]
539
+ # @param val [Object]
540
+ # @param closing [Array<Proc>]
541
+ private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
542
+ y << "--#{boundary}\r\n"
543
+ y << "Content-Disposition: form-data"
544
+
545
+ unless key.nil?
546
+ name = ERB::Util.url_encode(key.to_s)
547
+ y << "; name=\"#{name}\""
548
+ end
549
+
550
+ case val
551
+ in BrandDev::FilePart unless val.filename.nil?
552
+ filename = ERB::Util.url_encode(val.filename)
553
+ y << "; filename=\"#{filename}\""
554
+ in Pathname | IO
555
+ filename = ERB::Util.url_encode(::File.basename(val.to_path))
556
+ y << "; filename=\"#{filename}\""
557
+ else
558
+ end
559
+ y << "\r\n"
560
+
561
+ write_multipart_content(y, val: val, closing: closing)
562
+ end
563
+
564
+ # @api private
565
+ #
566
+ # @param body [Object]
567
+ #
568
+ # @return [Array(String, Enumerable<String>)]
569
+ private def encode_multipart_streaming(body)
570
+ boundary = SecureRandom.urlsafe_base64(60)
571
+
572
+ closing = []
573
+ strio = writable_enum do |y|
574
+ case body
575
+ in Hash
576
+ body.each do |key, val|
577
+ case val
578
+ in Array if val.all? { primitive?(_1) }
579
+ val.each do |v|
580
+ write_multipart_chunk(y, boundary: boundary, key: key, val: v, closing: closing)
581
+ end
582
+ else
583
+ write_multipart_chunk(y, boundary: boundary, key: key, val: val, closing: closing)
584
+ end
585
+ end
586
+ else
587
+ write_multipart_chunk(y, boundary: boundary, key: nil, val: body, closing: closing)
588
+ end
589
+ y << "--#{boundary}--\r\n"
590
+ end
591
+
592
+ fused_io = fused_enum(strio) { closing.each(&:call) }
593
+ [boundary, fused_io]
594
+ end
595
+
596
+ # @api private
597
+ #
598
+ # @param headers [Hash{String=>String}]
599
+ # @param body [Object]
600
+ #
601
+ # @return [Object]
602
+ def encode_content(headers, body)
603
+ # rubocop:disable Style/CaseEquality
604
+ # rubocop:disable Layout/LineLength
605
+ content_type = headers["content-type"]
606
+ case [content_type, body]
607
+ in [BrandDev::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }]
608
+ [headers, JSON.generate(body)]
609
+ in [BrandDev::Internal::Util::JSONL_CONTENT, Enumerable] unless BrandDev::Internal::Type::FileInput === body
610
+ [headers, body.lazy.map { JSON.generate(_1) }]
611
+ in [%r{^multipart/form-data}, Hash | BrandDev::Internal::Type::FileInput]
612
+ boundary, strio = encode_multipart_streaming(body)
613
+ headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"}
614
+ [headers, strio]
615
+ in [_, Symbol | Numeric]
616
+ [headers, body.to_s]
617
+ in [_, StringIO]
618
+ [headers, body.string]
619
+ in [_, BrandDev::FilePart]
620
+ [headers, body.content]
621
+ else
622
+ [headers, body]
623
+ end
624
+ # rubocop:enable Layout/LineLength
625
+ # rubocop:enable Style/CaseEquality
626
+ end
627
+
628
+ # @api private
629
+ #
630
+ # https://www.iana.org/assignments/character-sets/character-sets.xhtml
631
+ #
632
+ # @param content_type [String]
633
+ # @param text [String]
634
+ def force_charset!(content_type, text:)
635
+ charset = /charset=([^;\s]+)/.match(content_type)&.captures&.first
636
+
637
+ return unless charset
638
+
639
+ begin
640
+ encoding = Encoding.find(charset)
641
+ text.force_encoding(encoding)
642
+ rescue ArgumentError
643
+ nil
644
+ end
645
+ end
646
+
647
+ # @api private
648
+ #
649
+ # Assumes each chunk in stream has `Encoding::BINARY`.
650
+ #
651
+ # @param headers [Hash{String=>String}, Net::HTTPHeader]
652
+ # @param stream [Enumerable<String>]
653
+ # @param suppress_error [Boolean]
654
+ #
655
+ # @raise [JSON::ParserError]
656
+ # @return [Object]
657
+ def decode_content(headers, stream:, suppress_error: false)
658
+ case (content_type = headers["content-type"])
659
+ in BrandDev::Internal::Util::JSON_CONTENT
660
+ json = stream.to_a.join
661
+ begin
662
+ JSON.parse(json, symbolize_names: true)
663
+ rescue JSON::ParserError => e
664
+ raise e unless suppress_error
665
+ json
666
+ end
667
+ in BrandDev::Internal::Util::JSONL_CONTENT
668
+ lines = decode_lines(stream)
669
+ chain_fused(lines) do |y|
670
+ lines.each { y << JSON.parse(_1, symbolize_names: true) }
671
+ end
672
+ in %r{^text/event-stream}
673
+ lines = decode_lines(stream)
674
+ decode_sse(lines)
675
+ else
676
+ text = stream.to_a.join
677
+ force_charset!(content_type, text: text)
678
+ StringIO.new(text)
679
+ end
680
+ end
681
+ end
682
+
683
+ class << self
684
+ # @api private
685
+ #
686
+ # https://doc.rust-lang.org/std/iter/trait.FusedIterator.html
687
+ #
688
+ # @param enum [Enumerable<Object>]
689
+ # @param external [Boolean]
690
+ # @param close [Proc]
691
+ #
692
+ # @return [Enumerable<Object>]
693
+ def fused_enum(enum, external: false, &close)
694
+ fused = false
695
+ iter = Enumerator.new do |y|
696
+ next if fused
697
+
698
+ fused = true
699
+ if external
700
+ loop { y << enum.next }
701
+ else
702
+ enum.each(&y)
703
+ end
704
+ ensure
705
+ close&.call
706
+ close = nil
707
+ end
708
+
709
+ iter.define_singleton_method(:rewind) do
710
+ fused = true
711
+ self
712
+ end
713
+ iter
714
+ end
715
+
716
+ # @api private
717
+ #
718
+ # @param enum [Enumerable<Object>, nil]
719
+ def close_fused!(enum)
720
+ return unless enum.is_a?(Enumerator)
721
+
722
+ # rubocop:disable Lint/UnreachableLoop
723
+ enum.rewind.each { break }
724
+ # rubocop:enable Lint/UnreachableLoop
725
+ end
726
+
727
+ # @api private
728
+ #
729
+ # @param enum [Enumerable<Object>, nil]
730
+ # @param blk [Proc]
731
+ #
732
+ # @yieldparam [Enumerator::Yielder]
733
+ # @return [Enumerable<Object>]
734
+ def chain_fused(enum, &blk)
735
+ iter = Enumerator.new { blk.call(_1) }
736
+ fused_enum(iter) { close_fused!(enum) }
737
+ end
738
+ end
739
+
740
+ class << self
741
+ # @api private
742
+ #
743
+ # Assumes Strings have been forced into having `Encoding::BINARY`.
744
+ #
745
+ # This decoder is responsible for reassembling lines split across multiple
746
+ # fragments.
747
+ #
748
+ # @param enum [Enumerable<String>]
749
+ #
750
+ # @return [Enumerable<String>]
751
+ def decode_lines(enum)
752
+ re = /(\r\n|\r|\n)/
753
+ buffer = String.new
754
+ cr_seen = nil
755
+
756
+ chain_fused(enum) do |y|
757
+ enum.each do |row|
758
+ offset = buffer.bytesize
759
+ buffer << row
760
+ while (match = re.match(buffer, cr_seen&.to_i || offset))
761
+ case [match.captures.first, cr_seen]
762
+ in ["\r", nil]
763
+ cr_seen = match.end(1)
764
+ next
765
+ in ["\r" | "\r\n", Integer]
766
+ y << buffer.slice!(..(cr_seen.pred))
767
+ else
768
+ y << buffer.slice!(..(match.end(1).pred))
769
+ end
770
+ offset = 0
771
+ cr_seen = nil
772
+ end
773
+ end
774
+
775
+ y << buffer.slice!(..(cr_seen.pred)) unless cr_seen.nil?
776
+ y << buffer unless buffer.empty?
777
+ end
778
+ end
779
+
780
+ # @api private
781
+ #
782
+ # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
783
+ #
784
+ # Assumes that `lines` has been decoded with `#decode_lines`.
785
+ #
786
+ # @param lines [Enumerable<String>]
787
+ #
788
+ # @return [Enumerable<Hash{Symbol=>Object}>]
789
+ def decode_sse(lines)
790
+ # rubocop:disable Metrics/BlockLength
791
+ chain_fused(lines) do |y|
792
+ blank = {event: nil, data: nil, id: nil, retry: nil}
793
+ current = {}
794
+
795
+ lines.each do |line|
796
+ case line.sub(/\R$/, "")
797
+ in ""
798
+ next if current.empty?
799
+ y << {**blank, **current}
800
+ current = {}
801
+ in /^:/
802
+ next
803
+ in /^([^:]+):\s?(.*)$/
804
+ field, value = Regexp.last_match.captures
805
+ case field
806
+ in "event"
807
+ current.merge!(event: value)
808
+ in "data"
809
+ (current[:data] ||= String.new) << (value << "\n")
810
+ in "id" unless value.include?("\0")
811
+ current.merge!(id: value)
812
+ in "retry" if /^\d+$/ =~ value
813
+ current.merge!(retry: Integer(value))
814
+ else
815
+ end
816
+ else
817
+ end
818
+ end
819
+ # rubocop:enable Metrics/BlockLength
820
+
821
+ y << {**blank, **current} unless current.empty?
822
+ end
823
+ end
824
+ end
825
+
826
+ # @api private
827
+ module SorbetRuntimeSupport
828
+ class MissingSorbetRuntimeError < ::RuntimeError
829
+ end
830
+
831
+ # @api private
832
+ #
833
+ # @return [Hash{Symbol=>Object}]
834
+ private def sorbet_runtime_constants = @sorbet_runtime_constants ||= {}
835
+
836
+ # @api private
837
+ #
838
+ # @param name [Symbol]
839
+ def const_missing(name)
840
+ super unless sorbet_runtime_constants.key?(name)
841
+
842
+ unless Object.const_defined?(:T)
843
+ message = "Trying to access a Sorbet constant #{name.inspect} without `sorbet-runtime`."
844
+ raise MissingSorbetRuntimeError.new(message)
845
+ end
846
+
847
+ sorbet_runtime_constants.fetch(name).call
848
+ end
849
+
850
+ # @api private
851
+ #
852
+ # @param name [Symbol]
853
+ #
854
+ # @return [Boolean]
855
+ def sorbet_constant_defined?(name) = sorbet_runtime_constants.key?(name)
856
+
857
+ # @api private
858
+ #
859
+ # @param name [Symbol]
860
+ # @param blk [Proc]
861
+ def define_sorbet_constant!(name, &blk) = sorbet_runtime_constants.store(name, blk)
862
+
863
+ # @api private
864
+ #
865
+ # @return [Object]
866
+ def to_sorbet_type = raise NotImplementedError
867
+
868
+ class << self
869
+ # @api private
870
+ #
871
+ # @param type [BrandDev::Internal::Util::SorbetRuntimeSupport, Object]
872
+ #
873
+ # @return [Object]
874
+ def to_sorbet_type(type)
875
+ case type
876
+ in BrandDev::Internal::Util::SorbetRuntimeSupport
877
+ type.to_sorbet_type
878
+ in Class | Module
879
+ type
880
+ in true | false
881
+ T::Boolean
882
+ else
883
+ type.class
884
+ end
885
+ end
886
+ end
887
+ end
888
+
889
+ extend BrandDev::Internal::Util::SorbetRuntimeSupport
890
+
891
+ define_sorbet_constant!(:ParsedUri) do
892
+ T.type_alias do
893
+ {
894
+ scheme: T.nilable(String),
895
+ host: T.nilable(String),
896
+ port: T.nilable(Integer),
897
+ path: T.nilable(String),
898
+ query: T::Hash[String, T::Array[String]]
899
+ }
900
+ end
901
+ end
902
+
903
+ define_sorbet_constant!(:ServerSentEvent) do
904
+ T.type_alias do
905
+ {
906
+ event: T.nilable(String),
907
+ data: T.nilable(String),
908
+ id: T.nilable(String),
909
+ retry: T.nilable(Integer)
910
+ }
911
+ end
912
+ end
913
+ end
914
+ end
915
+ end