checkout-intents 0.0.2

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 (137) hide show
  1. checksums.yaml +7 -0
  2. data/.ignore +2 -0
  3. data/CHANGELOG.md +16 -0
  4. data/README.md +358 -0
  5. data/SECURITY.md +27 -0
  6. data/lib/checkout_intents/client.rb +100 -0
  7. data/lib/checkout_intents/errors.rb +228 -0
  8. data/lib/checkout_intents/file_part.rb +58 -0
  9. data/lib/checkout_intents/internal/cursor_pagination.rb +125 -0
  10. data/lib/checkout_intents/internal/transport/base_client.rb +577 -0
  11. data/lib/checkout_intents/internal/transport/pooled_net_requester.rb +210 -0
  12. data/lib/checkout_intents/internal/type/array_of.rb +168 -0
  13. data/lib/checkout_intents/internal/type/base_model.rb +536 -0
  14. data/lib/checkout_intents/internal/type/base_page.rb +55 -0
  15. data/lib/checkout_intents/internal/type/boolean.rb +77 -0
  16. data/lib/checkout_intents/internal/type/converter.rb +327 -0
  17. data/lib/checkout_intents/internal/type/enum.rb +131 -0
  18. data/lib/checkout_intents/internal/type/file_input.rb +111 -0
  19. data/lib/checkout_intents/internal/type/hash_of.rb +188 -0
  20. data/lib/checkout_intents/internal/type/request_parameters.rb +42 -0
  21. data/lib/checkout_intents/internal/type/union.rb +258 -0
  22. data/lib/checkout_intents/internal/type/unknown.rb +81 -0
  23. data/lib/checkout_intents/internal/util.rb +920 -0
  24. data/lib/checkout_intents/internal.rb +20 -0
  25. data/lib/checkout_intents/models/base_checkout_intent.rb +76 -0
  26. data/lib/checkout_intents/models/betas/checkout_session_create_params.rb +148 -0
  27. data/lib/checkout_intents/models/brand_retrieve_params.rb +14 -0
  28. data/lib/checkout_intents/models/brand_retrieve_response.rb +52 -0
  29. data/lib/checkout_intents/models/buyer.rb +69 -0
  30. data/lib/checkout_intents/models/checkout_intent.rb +230 -0
  31. data/lib/checkout_intents/models/checkout_intent_add_payment_params.rb +20 -0
  32. data/lib/checkout_intents/models/checkout_intent_confirm_params.rb +20 -0
  33. data/lib/checkout_intents/models/checkout_intent_create_params.rb +68 -0
  34. data/lib/checkout_intents/models/checkout_intent_list_params.rb +58 -0
  35. data/lib/checkout_intents/models/checkout_intent_purchase_params.rb +74 -0
  36. data/lib/checkout_intents/models/checkout_intent_retrieve_params.rb +14 -0
  37. data/lib/checkout_intents/models/checkout_session.rb +23 -0
  38. data/lib/checkout_intents/models/money.rb +21 -0
  39. data/lib/checkout_intents/models/offer.rb +113 -0
  40. data/lib/checkout_intents/models/payment_method.rb +120 -0
  41. data/lib/checkout_intents/models/variant_selection.rb +33 -0
  42. data/lib/checkout_intents/models.rb +76 -0
  43. data/lib/checkout_intents/request_options.rb +78 -0
  44. data/lib/checkout_intents/resources/betas/checkout_sessions.rb +54 -0
  45. data/lib/checkout_intents/resources/betas.rb +18 -0
  46. data/lib/checkout_intents/resources/brands.rb +37 -0
  47. data/lib/checkout_intents/resources/checkout_intents.rb +173 -0
  48. data/lib/checkout_intents/version.rb +5 -0
  49. data/lib/checkout_intents.rb +77 -0
  50. data/manifest.yaml +17 -0
  51. data/rbi/checkout_intents/client.rbi +74 -0
  52. data/rbi/checkout_intents/errors.rbi +205 -0
  53. data/rbi/checkout_intents/file_part.rbi +37 -0
  54. data/rbi/checkout_intents/internal/cursor_pagination.rbi +74 -0
  55. data/rbi/checkout_intents/internal/transport/base_client.rbi +309 -0
  56. data/rbi/checkout_intents/internal/transport/pooled_net_requester.rbi +84 -0
  57. data/rbi/checkout_intents/internal/type/array_of.rbi +108 -0
  58. data/rbi/checkout_intents/internal/type/base_model.rbi +314 -0
  59. data/rbi/checkout_intents/internal/type/base_page.rbi +43 -0
  60. data/rbi/checkout_intents/internal/type/boolean.rbi +58 -0
  61. data/rbi/checkout_intents/internal/type/converter.rbi +225 -0
  62. data/rbi/checkout_intents/internal/type/enum.rbi +82 -0
  63. data/rbi/checkout_intents/internal/type/file_input.rbi +59 -0
  64. data/rbi/checkout_intents/internal/type/hash_of.rbi +108 -0
  65. data/rbi/checkout_intents/internal/type/request_parameters.rbi +31 -0
  66. data/rbi/checkout_intents/internal/type/union.rbi +134 -0
  67. data/rbi/checkout_intents/internal/type/unknown.rbi +58 -0
  68. data/rbi/checkout_intents/internal/util.rbi +487 -0
  69. data/rbi/checkout_intents/internal.rbi +18 -0
  70. data/rbi/checkout_intents/models/base_checkout_intent.rbi +142 -0
  71. data/rbi/checkout_intents/models/betas/checkout_session_create_params.rbi +281 -0
  72. data/rbi/checkout_intents/models/brand_retrieve_params.rbi +32 -0
  73. data/rbi/checkout_intents/models/brand_retrieve_response.rbi +109 -0
  74. data/rbi/checkout_intents/models/buyer.rbi +92 -0
  75. data/rbi/checkout_intents/models/checkout_intent.rbi +653 -0
  76. data/rbi/checkout_intents/models/checkout_intent_add_payment_params.rbi +59 -0
  77. data/rbi/checkout_intents/models/checkout_intent_confirm_params.rbi +59 -0
  78. data/rbi/checkout_intents/models/checkout_intent_create_params.rbi +141 -0
  79. data/rbi/checkout_intents/models/checkout_intent_list_params.rbi +146 -0
  80. data/rbi/checkout_intents/models/checkout_intent_purchase_params.rbi +165 -0
  81. data/rbi/checkout_intents/models/checkout_intent_retrieve_params.rbi +32 -0
  82. data/rbi/checkout_intents/models/checkout_session.rbi +36 -0
  83. data/rbi/checkout_intents/models/money.rbi +32 -0
  84. data/rbi/checkout_intents/models/offer.rbi +225 -0
  85. data/rbi/checkout_intents/models/payment_method.rbi +292 -0
  86. data/rbi/checkout_intents/models/variant_selection.rbi +55 -0
  87. data/rbi/checkout_intents/models.rbi +40 -0
  88. data/rbi/checkout_intents/request_options.rbi +64 -0
  89. data/rbi/checkout_intents/resources/betas/checkout_sessions.rbi +47 -0
  90. data/rbi/checkout_intents/resources/betas.rbi +15 -0
  91. data/rbi/checkout_intents/resources/brands.rbi +29 -0
  92. data/rbi/checkout_intents/resources/checkout_intents.rbi +165 -0
  93. data/rbi/checkout_intents/version.rbi +5 -0
  94. data/sig/checkout_intents/client.rbs +36 -0
  95. data/sig/checkout_intents/errors.rbs +117 -0
  96. data/sig/checkout_intents/file_part.rbs +21 -0
  97. data/sig/checkout_intents/internal/cursor_pagination.rbs +48 -0
  98. data/sig/checkout_intents/internal/transport/base_client.rbs +133 -0
  99. data/sig/checkout_intents/internal/transport/pooled_net_requester.rbs +48 -0
  100. data/sig/checkout_intents/internal/type/array_of.rbs +48 -0
  101. data/sig/checkout_intents/internal/type/base_model.rbs +104 -0
  102. data/sig/checkout_intents/internal/type/base_page.rbs +24 -0
  103. data/sig/checkout_intents/internal/type/boolean.rbs +26 -0
  104. data/sig/checkout_intents/internal/type/converter.rbs +79 -0
  105. data/sig/checkout_intents/internal/type/enum.rbs +32 -0
  106. data/sig/checkout_intents/internal/type/file_input.rbs +25 -0
  107. data/sig/checkout_intents/internal/type/hash_of.rbs +48 -0
  108. data/sig/checkout_intents/internal/type/request_parameters.rbs +20 -0
  109. data/sig/checkout_intents/internal/type/union.rbs +52 -0
  110. data/sig/checkout_intents/internal/type/unknown.rbs +26 -0
  111. data/sig/checkout_intents/internal/util.rbs +185 -0
  112. data/sig/checkout_intents/internal.rbs +10 -0
  113. data/sig/checkout_intents/models/base_checkout_intent.rbs +88 -0
  114. data/sig/checkout_intents/models/betas/checkout_session_create_params.rbs +172 -0
  115. data/sig/checkout_intents/models/brand_retrieve_params.rbs +15 -0
  116. data/sig/checkout_intents/models/brand_retrieve_response.rbs +43 -0
  117. data/sig/checkout_intents/models/buyer.rbs +67 -0
  118. data/sig/checkout_intents/models/checkout_intent.rbs +324 -0
  119. data/sig/checkout_intents/models/checkout_intent_add_payment_params.rbs +24 -0
  120. data/sig/checkout_intents/models/checkout_intent_confirm_params.rbs +24 -0
  121. data/sig/checkout_intents/models/checkout_intent_create_params.rbs +84 -0
  122. data/sig/checkout_intents/models/checkout_intent_list_params.rbs +77 -0
  123. data/sig/checkout_intents/models/checkout_intent_purchase_params.rbs +89 -0
  124. data/sig/checkout_intents/models/checkout_intent_retrieve_params.rbs +15 -0
  125. data/sig/checkout_intents/models/checkout_session.rbs +13 -0
  126. data/sig/checkout_intents/models/money.rbs +15 -0
  127. data/sig/checkout_intents/models/offer.rbs +135 -0
  128. data/sig/checkout_intents/models/payment_method.rbs +127 -0
  129. data/sig/checkout_intents/models/variant_selection.rbs +30 -0
  130. data/sig/checkout_intents/models.rbs +33 -0
  131. data/sig/checkout_intents/request_options.rbs +36 -0
  132. data/sig/checkout_intents/resources/betas/checkout_sessions.rbs +19 -0
  133. data/sig/checkout_intents/resources/betas.rbs +9 -0
  134. data/sig/checkout_intents/resources/brands.rbs +12 -0
  135. data/sig/checkout_intents/resources/checkout_intents.rbs +54 -0
  136. data/sig/checkout_intents/version.rbs +3 -0
  137. metadata +194 -0
@@ -0,0 +1,920 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CheckoutIntents
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 #{CheckoutIntents::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}" unless uri.port == uri.default_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
+ merged = {**parse_uri(rhs.fetch(:path)), **rhs.except(:path, :query)}
350
+ parsed_path, parsed_query = merged.fetch_values(:path, :query)
351
+ override = URI::Generic.build(**merged.slice(:scheme, :host, :port), path: parsed_path)
352
+
353
+ joined = URI.join(URI::Generic.build(lhs.except(:path, :query)), slashed, override)
354
+ query = deep_merge(
355
+ joined.path == base_path ? base_query : {},
356
+ parsed_query,
357
+ rhs[:query].to_h,
358
+ concat: true
359
+ )
360
+
361
+ joined.query = encode_query(query)
362
+ joined
363
+ end
364
+ end
365
+
366
+ class << self
367
+ # @api private
368
+ #
369
+ # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
370
+ #
371
+ # @return [Hash{String=>String}]
372
+ def normalized_headers(*headers)
373
+ {}.merge(*headers.compact).to_h do |key, val|
374
+ value =
375
+ case val
376
+ in Array
377
+ val.filter_map { _1&.to_s&.strip }.join(", ")
378
+ else
379
+ val&.to_s&.strip
380
+ end
381
+ [key.downcase, value]
382
+ end
383
+ end
384
+ end
385
+
386
+ # @api private
387
+ #
388
+ # An adapter that satisfies the IO interface required by `::IO.copy_stream`
389
+ class ReadIOAdapter
390
+ # @api private
391
+ #
392
+ # @return [Boolean, nil]
393
+ def close? = @closing
394
+
395
+ # @api private
396
+ def close
397
+ case @stream
398
+ in Enumerator
399
+ CheckoutIntents::Internal::Util.close_fused!(@stream)
400
+ in IO if close?
401
+ @stream.close
402
+ else
403
+ end
404
+ end
405
+
406
+ # @api private
407
+ #
408
+ # @param max_len [Integer, nil]
409
+ #
410
+ # @return [String]
411
+ private def read_enum(max_len)
412
+ case max_len
413
+ in nil
414
+ @stream.to_a.join
415
+ in Integer
416
+ @buf << @stream.next while @buf.length < max_len
417
+ @buf.slice!(..max_len)
418
+ end
419
+ rescue StopIteration
420
+ @stream = nil
421
+ @buf.slice!(0..)
422
+ end
423
+
424
+ # @api private
425
+ #
426
+ # @param max_len [Integer, nil]
427
+ # @param out_string [String, nil]
428
+ #
429
+ # @return [String, nil]
430
+ def read(max_len = nil, out_string = nil)
431
+ case @stream
432
+ in nil
433
+ nil
434
+ in IO | StringIO
435
+ @stream.read(max_len, out_string)
436
+ in Enumerator
437
+ read = read_enum(max_len)
438
+ case out_string
439
+ in String
440
+ out_string.replace(read)
441
+ in nil
442
+ read
443
+ end
444
+ end
445
+ .tap(&@blk)
446
+ end
447
+
448
+ # @api private
449
+ #
450
+ # @param src [String, Pathname, StringIO, Enumerable<String>]
451
+ # @param blk [Proc]
452
+ #
453
+ # @yieldparam [String]
454
+ def initialize(src, &blk)
455
+ @stream =
456
+ case src
457
+ in String
458
+ StringIO.new(src)
459
+ in Pathname
460
+ @closing = true
461
+ src.open(binmode: true)
462
+ else
463
+ src
464
+ end
465
+ @buf = String.new
466
+ @blk = blk
467
+ end
468
+ end
469
+
470
+ class << self
471
+ # @param blk [Proc]
472
+ #
473
+ # @yieldparam [Enumerator::Yielder]
474
+ # @return [Enumerable<String>]
475
+ def writable_enum(&blk)
476
+ Enumerator.new do |y|
477
+ y.define_singleton_method(:write) do
478
+ self << _1.dup
479
+ _1.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_line = "Content-Type: %s\r\n\r\n"
501
+
502
+ case val
503
+ in CheckoutIntents::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 << format(content_line, content_type || "application/octet-stream")
512
+ io = val.open(binmode: true)
513
+ closing << io.method(:close)
514
+ IO.copy_stream(io, y)
515
+ in IO
516
+ y << format(content_line, content_type || "application/octet-stream")
517
+ IO.copy_stream(val, y)
518
+ in StringIO
519
+ y << format(content_line, content_type || "application/octet-stream")
520
+ y << val.string
521
+ in -> { primitive?(_1) }
522
+ y << format(content_line, content_type || "text/plain")
523
+ y << val.to_s
524
+ else
525
+ y << format(content_line, content_type || "application/json")
526
+ y << JSON.generate(val)
527
+ end
528
+ y << "\r\n"
529
+ end
530
+
531
+ # @api private
532
+ #
533
+ # @param y [Enumerator::Yielder]
534
+ # @param boundary [String]
535
+ # @param key [Symbol, String]
536
+ # @param val [Object]
537
+ # @param closing [Array<Proc>]
538
+ private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
539
+ y << "--#{boundary}\r\n"
540
+ y << "Content-Disposition: form-data"
541
+
542
+ unless key.nil?
543
+ name = ERB::Util.url_encode(key.to_s)
544
+ y << "; name=\"#{name}\""
545
+ end
546
+
547
+ case val
548
+ in CheckoutIntents::FilePart unless val.filename.nil?
549
+ filename = ERB::Util.url_encode(val.filename)
550
+ y << "; filename=\"#{filename}\""
551
+ in Pathname | IO
552
+ filename = ERB::Util.url_encode(::File.basename(val.to_path))
553
+ y << "; filename=\"#{filename}\""
554
+ else
555
+ end
556
+ y << "\r\n"
557
+
558
+ write_multipart_content(y, val: val, closing: closing)
559
+ end
560
+
561
+ # @api private
562
+ #
563
+ # https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.1.md#special-considerations-for-multipart-content
564
+ #
565
+ # @param body [Object]
566
+ #
567
+ # @return [Array(String, Enumerable<String>)]
568
+ private def encode_multipart_streaming(body)
569
+ # RFC 1521 Section 7.2.1 says we should have 70 char maximum for boundary length
570
+ boundary = SecureRandom.urlsafe_base64(46)
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 [CheckoutIntents::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }]
608
+ [headers, JSON.generate(body)]
609
+ in [CheckoutIntents::Internal::Util::JSONL_CONTENT, Enumerable] unless CheckoutIntents::Internal::Type::FileInput === body
610
+ [headers, body.lazy.map { JSON.generate(_1) }]
611
+ in [%r{^multipart/form-data}, Hash | CheckoutIntents::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 [_, CheckoutIntents::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}]
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 CheckoutIntents::Internal::Util::JSON_CONTENT
660
+ return nil if (json = stream.to_a.join).empty?
661
+
662
+ begin
663
+ JSON.parse(json, symbolize_names: true)
664
+ rescue JSON::ParserError => e
665
+ raise e unless suppress_error
666
+ json
667
+ end
668
+ in CheckoutIntents::Internal::Util::JSONL_CONTENT
669
+ lines = decode_lines(stream)
670
+ chain_fused(lines) do |y|
671
+ lines.each do
672
+ next if _1.empty?
673
+
674
+ y << JSON.parse(_1, symbolize_names: true)
675
+ end
676
+ end
677
+ in %r{^text/event-stream}
678
+ lines = decode_lines(stream)
679
+ decode_sse(lines)
680
+ else
681
+ text = stream.to_a.join
682
+ force_charset!(content_type, text: text)
683
+ StringIO.new(text)
684
+ end
685
+ end
686
+ end
687
+
688
+ class << self
689
+ # @api private
690
+ #
691
+ # https://doc.rust-lang.org/std/iter/trait.FusedIterator.html
692
+ #
693
+ # @param enum [Enumerable<Object>]
694
+ # @param external [Boolean]
695
+ # @param close [Proc]
696
+ #
697
+ # @return [Enumerable<Object>]
698
+ def fused_enum(enum, external: false, &close)
699
+ fused = false
700
+ iter = Enumerator.new do |y|
701
+ next if fused
702
+
703
+ fused = true
704
+ if external
705
+ loop { y << enum.next }
706
+ else
707
+ enum.each(&y)
708
+ end
709
+ ensure
710
+ close&.call
711
+ close = nil
712
+ end
713
+
714
+ iter.define_singleton_method(:rewind) do
715
+ fused = true
716
+ self
717
+ end
718
+ iter
719
+ end
720
+
721
+ # @api private
722
+ #
723
+ # @param enum [Enumerable<Object>, nil]
724
+ def close_fused!(enum)
725
+ return unless enum.is_a?(Enumerator)
726
+
727
+ # rubocop:disable Lint/UnreachableLoop
728
+ enum.rewind.each { break }
729
+ # rubocop:enable Lint/UnreachableLoop
730
+ end
731
+
732
+ # @api private
733
+ #
734
+ # @param enum [Enumerable<Object>, nil]
735
+ # @param blk [Proc]
736
+ #
737
+ # @yieldparam [Enumerator::Yielder]
738
+ # @return [Enumerable<Object>]
739
+ def chain_fused(enum, &blk)
740
+ iter = Enumerator.new { blk.call(_1) }
741
+ fused_enum(iter) { close_fused!(enum) }
742
+ end
743
+ end
744
+
745
+ class << self
746
+ # @api private
747
+ #
748
+ # Assumes Strings have been forced into having `Encoding::BINARY`.
749
+ #
750
+ # This decoder is responsible for reassembling lines split across multiple
751
+ # fragments.
752
+ #
753
+ # @param enum [Enumerable<String>]
754
+ #
755
+ # @return [Enumerable<String>]
756
+ def decode_lines(enum)
757
+ re = /(\r\n|\r|\n)/
758
+ buffer = String.new
759
+ cr_seen = nil
760
+
761
+ chain_fused(enum) do |y|
762
+ enum.each do |row|
763
+ offset = buffer.bytesize
764
+ buffer << row
765
+ while (match = re.match(buffer, cr_seen&.to_i || offset))
766
+ case [match.captures.first, cr_seen]
767
+ in ["\r", nil]
768
+ cr_seen = match.end(1)
769
+ next
770
+ in ["\r" | "\r\n", Integer]
771
+ y << buffer.slice!(..(cr_seen.pred))
772
+ else
773
+ y << buffer.slice!(..(match.end(1).pred))
774
+ end
775
+ offset = 0
776
+ cr_seen = nil
777
+ end
778
+ end
779
+
780
+ y << buffer.slice!(..(cr_seen.pred)) unless cr_seen.nil?
781
+ y << buffer unless buffer.empty?
782
+ end
783
+ end
784
+
785
+ # @api private
786
+ #
787
+ # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
788
+ #
789
+ # Assumes that `lines` has been decoded with `#decode_lines`.
790
+ #
791
+ # @param lines [Enumerable<String>]
792
+ #
793
+ # @return [Enumerable<Hash{Symbol=>Object}>]
794
+ def decode_sse(lines)
795
+ # rubocop:disable Metrics/BlockLength
796
+ chain_fused(lines) do |y|
797
+ blank = {event: nil, data: nil, id: nil, retry: nil}
798
+ current = {}
799
+
800
+ lines.each do |line|
801
+ case line.sub(/\R$/, "")
802
+ in ""
803
+ next if current.empty?
804
+ y << {**blank, **current}
805
+ current = {}
806
+ in /^:/
807
+ next
808
+ in /^([^:]+):\s?(.*)$/
809
+ field, value = Regexp.last_match.captures
810
+ case field
811
+ in "event"
812
+ current.merge!(event: value)
813
+ in "data"
814
+ (current[:data] ||= String.new) << (value << "\n")
815
+ in "id" unless value.include?("\0")
816
+ current.merge!(id: value)
817
+ in "retry" if /^\d+$/ =~ value
818
+ current.merge!(retry: Integer(value))
819
+ else
820
+ end
821
+ else
822
+ end
823
+ end
824
+ # rubocop:enable Metrics/BlockLength
825
+
826
+ y << {**blank, **current} unless current.empty?
827
+ end
828
+ end
829
+ end
830
+
831
+ # @api private
832
+ module SorbetRuntimeSupport
833
+ class MissingSorbetRuntimeError < ::RuntimeError
834
+ end
835
+
836
+ # @api private
837
+ #
838
+ # @return [Hash{Symbol=>Object}]
839
+ private def sorbet_runtime_constants = @sorbet_runtime_constants ||= {}
840
+
841
+ # @api private
842
+ #
843
+ # @param name [Symbol]
844
+ def const_missing(name)
845
+ super unless sorbet_runtime_constants.key?(name)
846
+
847
+ unless Object.const_defined?(:T)
848
+ message = "Trying to access a Sorbet constant #{name.inspect} without `sorbet-runtime`."
849
+ raise MissingSorbetRuntimeError.new(message)
850
+ end
851
+
852
+ sorbet_runtime_constants.fetch(name).call
853
+ end
854
+
855
+ # @api private
856
+ #
857
+ # @param name [Symbol]
858
+ #
859
+ # @return [Boolean]
860
+ def sorbet_constant_defined?(name) = sorbet_runtime_constants.key?(name)
861
+
862
+ # @api private
863
+ #
864
+ # @param name [Symbol]
865
+ # @param blk [Proc]
866
+ def define_sorbet_constant!(name, &blk) = sorbet_runtime_constants.store(name, blk)
867
+
868
+ # @api private
869
+ #
870
+ # @return [Object]
871
+ def to_sorbet_type = raise NotImplementedError
872
+
873
+ class << self
874
+ # @api private
875
+ #
876
+ # @param type [CheckoutIntents::Internal::Util::SorbetRuntimeSupport, Object]
877
+ #
878
+ # @return [Object]
879
+ def to_sorbet_type(type)
880
+ case type
881
+ in CheckoutIntents::Internal::Util::SorbetRuntimeSupport
882
+ type.to_sorbet_type
883
+ in Class | Module
884
+ type
885
+ in true | false
886
+ T::Boolean
887
+ else
888
+ type.class
889
+ end
890
+ end
891
+ end
892
+ end
893
+
894
+ extend CheckoutIntents::Internal::Util::SorbetRuntimeSupport
895
+
896
+ define_sorbet_constant!(:ParsedUri) do
897
+ T.type_alias do
898
+ {
899
+ scheme: T.nilable(String),
900
+ host: T.nilable(String),
901
+ port: T.nilable(Integer),
902
+ path: T.nilable(String),
903
+ query: T::Hash[String, T::Array[String]]
904
+ }
905
+ end
906
+ end
907
+
908
+ define_sorbet_constant!(:ServerSentEvent) do
909
+ T.type_alias do
910
+ {
911
+ event: T.nilable(String),
912
+ data: T.nilable(String),
913
+ id: T.nilable(String),
914
+ retry: T.nilable(Integer)
915
+ }
916
+ end
917
+ end
918
+ end
919
+ end
920
+ end