terminal-shop 0.1.0.pre.alpha.13 → 0.1.0.pre.alpha.14

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.
@@ -8,163 +8,153 @@ module TerminalShop
8
8
  module Util
9
9
  # @private
10
10
  #
11
- # @return [String]
11
+ # @return [Float]
12
12
  #
13
- def self.arch
14
- case (arch = RbConfig::CONFIG["arch"])&.downcase
15
- in nil
16
- "unknown"
17
- in /aarch64|arm64/
18
- "arm64"
19
- in /x86_64/
20
- "x64"
21
- in /arm/
22
- "arm"
23
- else
24
- "other:#{arch}"
25
- end
26
- end
13
+ def self.monotonic_secs = Process.clock_gettime(Process::CLOCK_MONOTONIC)
27
14
 
28
- # @private
29
- #
30
- # @return [String]
31
- #
32
- def self.os
33
- case (host = RbConfig::CONFIG["host_os"])&.downcase
34
- in nil
35
- "Unknown"
36
- in /linux/
37
- "Linux"
38
- in /darwin/
39
- "MacOS"
40
- in /freebsd/
41
- "FreeBSD"
42
- in /openbsd/
43
- "OpenBSD"
44
- in /mswin|mingw|cygwin|ucrt/
45
- "Windows"
46
- else
47
- "Other:#{host}"
15
+ class << self
16
+ # @private
17
+ #
18
+ # @return [String]
19
+ #
20
+ def arch
21
+ case (arch = RbConfig::CONFIG["arch"])&.downcase
22
+ in nil
23
+ "unknown"
24
+ in /aarch64|arm64/
25
+ "arm64"
26
+ in /x86_64/
27
+ "x64"
28
+ in /arm/
29
+ "arm"
30
+ else
31
+ "other:#{arch}"
32
+ end
48
33
  end
49
- end
50
34
 
51
- # @private
52
- #
53
- # @param input [Object]
54
- #
55
- # @return [Boolean, Object]
56
- #
57
- def self.primitive?(input)
58
- case input
59
- in true | false | Integer | Float | Symbol | String
60
- true
61
- else
62
- false
35
+ # @private
36
+ #
37
+ # @return [String]
38
+ #
39
+ def os
40
+ case (host = RbConfig::CONFIG["host_os"])&.downcase
41
+ in nil
42
+ "Unknown"
43
+ in /linux/
44
+ "Linux"
45
+ in /darwin/
46
+ "MacOS"
47
+ in /freebsd/
48
+ "FreeBSD"
49
+ in /openbsd/
50
+ "OpenBSD"
51
+ in /mswin|mingw|cygwin|ucrt/
52
+ "Windows"
53
+ else
54
+ "Other:#{host}"
55
+ end
63
56
  end
64
57
  end
65
58
 
66
- # @private
67
- #
68
- # @param input [Object]
69
- #
70
- # @return [Boolean, Object]
71
- #
72
- def self.coerce_boolean(input)
73
- case input.is_a?(String) ? input.downcase : input
74
- in Numeric
75
- !input.zero?
76
- in "true"
77
- true
78
- in "false"
79
- false
80
- else
81
- input
59
+ class << self
60
+ # @private
61
+ #
62
+ # @param input [Object]
63
+ #
64
+ # @return [Boolean, Object]
65
+ #
66
+ def primitive?(input)
67
+ case input
68
+ in true | false | Integer | Float | Symbol | String
69
+ true
70
+ else
71
+ false
72
+ end
82
73
  end
83
- end
84
74
 
85
- # @private
86
- #
87
- # @param input [Object]
88
- #
89
- # @raise [ArgumentError]
90
- # @return [Boolean, nil]
91
- #
92
- def self.coerce_boolean!(input)
93
- case coerce_boolean(input)
94
- in true | false | nil => coerced
95
- coerced
96
- else
97
- raise ArgumentError.new("Unable to coerce #{input.inspect} into boolean value")
75
+ # @private
76
+ #
77
+ # @param input [Object]
78
+ #
79
+ # @return [Boolean, Object]
80
+ #
81
+ def coerce_boolean(input)
82
+ case input.is_a?(String) ? input.downcase : input
83
+ in Numeric
84
+ !input.zero?
85
+ in "true"
86
+ true
87
+ in "false"
88
+ false
89
+ else
90
+ input
91
+ end
98
92
  end
99
- end
100
93
 
101
- # @private
102
- #
103
- # @param input [Object]
104
- #
105
- # @return [Integer, Object]
106
- #
107
- def self.coerce_integer(input)
108
- case input
109
- in true
110
- 1
111
- in false
112
- 0
113
- else
114
- Integer(input, exception: false) || input
94
+ # @private
95
+ #
96
+ # @param input [Object]
97
+ #
98
+ # @raise [ArgumentError]
99
+ # @return [Boolean, nil]
100
+ #
101
+ def coerce_boolean!(input)
102
+ case coerce_boolean(input)
103
+ in true | false | nil => coerced
104
+ coerced
105
+ else
106
+ raise ArgumentError.new("Unable to coerce #{input.inspect} into boolean value")
107
+ end
115
108
  end
116
- end
117
109
 
118
- # @private
119
- #
120
- # @param input [Object]
121
- #
122
- # @return [Float, Object]
123
- #
124
- def self.coerce_float(input)
125
- case input
126
- in true
127
- 1.0
128
- in false
129
- 0.0
130
- else
131
- Float(input, exception: false) || input
110
+ # @private
111
+ #
112
+ # @param input [Object]
113
+ #
114
+ # @return [Integer, Object]
115
+ #
116
+ def coerce_integer(input)
117
+ case input
118
+ in true
119
+ 1
120
+ in false
121
+ 0
122
+ else
123
+ Integer(input, exception: false) || input
124
+ end
132
125
  end
133
- end
134
126
 
135
- # @private
136
- #
137
- # @param input [Object]
138
- #
139
- # @return [Hash{Object=>Object}, Object]
140
- #
141
- def self.coerce_hash(input)
142
- case input
143
- in NilClass | Array | Set | Enumerator
144
- input
145
- else
146
- input.respond_to?(:to_h) ? input.to_h : input
127
+ # @private
128
+ #
129
+ # @param input [Object]
130
+ #
131
+ # @return [Float, Object]
132
+ #
133
+ def coerce_float(input)
134
+ case input
135
+ in true
136
+ 1.0
137
+ in false
138
+ 0.0
139
+ else
140
+ Float(input, exception: false) || input
141
+ end
147
142
  end
148
- end
149
-
150
- # @private
151
- #
152
- # @return [Float]
153
- #
154
- def self.monotonic_secs = Process.clock_gettime(Process::CLOCK_MONOTONIC)
155
143
 
156
- # @private
157
- #
158
- # @param exceptions [Array<Exception>]
159
- # @param sentinel [Object, nil]
160
- # @param blk [Proc, nil]
161
- #
162
- # @return [Object, nil]
163
- #
164
- def self.suppress(*exceptions, sentinel: nil, &blk)
165
- blk.call
166
- rescue *exceptions
167
- sentinel
144
+ # @private
145
+ #
146
+ # @param input [Object]
147
+ #
148
+ # @return [Hash{Object=>Object}, Object]
149
+ #
150
+ def coerce_hash(input)
151
+ case input
152
+ in NilClass | Array | Set | Enumerator
153
+ input
154
+ else
155
+ input.respond_to?(:to_h) ? input.to_h : input
156
+ end
157
+ end
168
158
  end
169
159
 
170
160
  # Use this to indicate that a value should be explicitly removed from a data
@@ -174,335 +164,347 @@ module TerminalShop
174
164
  # `{a: 1}` and `{}` would produce `{a: 1}`.
175
165
  OMIT = Object.new.freeze
176
166
 
177
- # @private
178
- #
179
- # Recursively merge one hash with another. If the values at a given key are not
180
- # both hashes, just take the new value.
181
- #
182
- # @param values [Array<Object>]
183
- #
184
- # @param sentinel [Object, nil] the value to return if no values are provided.
185
- #
186
- # @param concat [Boolean] whether to merge sequences by concatenation.
187
- #
188
- # @return [Object]
189
- #
190
- def self.deep_merge(*values, sentinel: nil, concat: false)
191
- case values
192
- in [value, *values]
193
- values.reduce(value) do |acc, val|
194
- deep_merge_lr(acc, val, concat: concat)
167
+ class << self
168
+ # @private
169
+ #
170
+ # @param lhs [Object]
171
+ # @param rhs [Object]
172
+ # @param concat [Boolean]
173
+ #
174
+ # @return [Object]
175
+ #
176
+ private def deep_merge_lr(lhs, rhs, concat: false)
177
+ case [lhs, rhs, concat]
178
+ in [Hash, Hash, _]
179
+ # rubocop:disable Style/YodaCondition
180
+ rhs_cleaned = rhs.reject { |_, val| OMIT == val }
181
+ lhs
182
+ .reject { |key, _| OMIT == rhs[key] }
183
+ .merge(rhs_cleaned) do |_, old_val, new_val|
184
+ deep_merge_lr(old_val, new_val, concat: concat)
185
+ end
186
+ # rubocop:enable Style/YodaCondition
187
+ in [Array, Array, true]
188
+ lhs.concat(rhs)
189
+ else
190
+ rhs
195
191
  end
196
- else
197
- sentinel
198
192
  end
199
- end
200
193
 
201
- # @private
202
- #
203
- # @param lhs [Object]
204
- # @param rhs [Object]
205
- # @param concat [Boolean]
206
- #
207
- # @return [Object]
208
- #
209
- private_class_method def self.deep_merge_lr(lhs, rhs, concat: false)
210
- case [lhs, rhs, concat]
211
- in [Hash, Hash, _]
212
- # rubocop:disable Style/YodaCondition
213
- rhs_cleaned = rhs.reject { |_, val| OMIT == val }
214
- lhs
215
- .reject { |key, _| OMIT == rhs[key] }
216
- .merge(rhs_cleaned) do |_, old_val, new_val|
217
- deep_merge_lr(old_val, new_val, concat: concat)
194
+ # @private
195
+ #
196
+ # Recursively merge one hash with another. If the values at a given key are not
197
+ # both hashes, just take the new value.
198
+ #
199
+ # @param values [Array<Object>]
200
+ #
201
+ # @param sentinel [Object, nil] the value to return if no values are provided.
202
+ #
203
+ # @param concat [Boolean] whether to merge sequences by concatenation.
204
+ #
205
+ # @return [Object]
206
+ #
207
+ def deep_merge(*values, sentinel: nil, concat: false)
208
+ case values
209
+ in [value, *values]
210
+ values.reduce(value) do |acc, val|
211
+ deep_merge_lr(acc, val, concat: concat)
218
212
  end
219
- # rubocop:enable Style/YodaCondition
220
- in [Array, Array, true]
221
- lhs.concat(rhs)
222
- else
223
- rhs
213
+ else
214
+ sentinel
215
+ end
224
216
  end
225
- end
226
217
 
227
- # @private
228
- #
229
- # @param data [Hash{Symbol=>Object}, Array<Object>, Object]
230
- # @param pick [Symbol, Integer, Array<Symbol, Integer>, nil]
231
- # @param sentinel [Object, nil]
232
- # @param blk [Proc, nil]
233
- #
234
- # @return [Object, nil]
235
- #
236
- def self.dig(data, pick, sentinel = nil, &blk)
237
- case [data, pick, blk]
238
- in [_, nil, nil]
239
- data
240
- in [Hash, Symbol, _] | [Array, Integer, _]
241
- blk.nil? ? data.fetch(pick, sentinel) : data.fetch(pick, &blk)
242
- in [Hash | Array, Array, _]
243
- pick.reduce(data) do |acc, key|
244
- case acc
245
- in Hash if acc.key?(key)
246
- acc.fetch(key)
247
- in Array if key.is_a?(Integer) && key < acc.length
248
- acc[key]
249
- else
250
- return blk.nil? ? sentinel : blk.call
218
+ # @private
219
+ #
220
+ # @param data [Hash{Symbol=>Object}, Array<Object>, Object]
221
+ # @param pick [Symbol, Integer, Array<Symbol, Integer>, nil]
222
+ # @param sentinel [Object, nil]
223
+ # @param blk [Proc, nil]
224
+ #
225
+ # @return [Object, nil]
226
+ #
227
+ def dig(data, pick, sentinel = nil, &blk)
228
+ case [data, pick, blk]
229
+ in [_, nil, nil]
230
+ data
231
+ in [Hash, Symbol, _] | [Array, Integer, _]
232
+ blk.nil? ? data.fetch(pick, sentinel) : data.fetch(pick, &blk)
233
+ in [Hash | Array, Array, _]
234
+ pick.reduce(data) do |acc, key|
235
+ case acc
236
+ in Hash if acc.key?(key)
237
+ acc.fetch(key)
238
+ in Array if key.is_a?(Integer) && key < acc.length
239
+ acc[key]
240
+ else
241
+ return blk.nil? ? sentinel : blk.call
242
+ end
251
243
  end
244
+ in _
245
+ blk.nil? ? sentinel : blk.call
252
246
  end
253
- in _
254
- blk.nil? ? sentinel : blk.call
255
247
  end
256
248
  end
257
249
 
258
- # @private
259
- #
260
- # @param uri [URI::Generic]
261
- #
262
- # @return [String]
263
- #
264
- def self.uri_origin(uri)
265
- "#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? '' : ":#{uri.port}"}"
266
- end
250
+ class << self
251
+ # @private
252
+ #
253
+ # @param uri [URI::Generic]
254
+ #
255
+ # @return [String]
256
+ #
257
+ def uri_origin(uri)
258
+ "#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? '' : ":#{uri.port}"}"
259
+ end
267
260
 
268
- # @private
269
- #
270
- # @param path [String, Array<String>]
271
- #
272
- # @return [String]
273
- #
274
- def self.interpolate_path(path)
275
- case path
276
- in String
277
- path
278
- in []
279
- ""
280
- in [String, *interpolations]
281
- encoded = interpolations.map { ERB::Util.url_encode(_1) }
282
- path.first % encoded
261
+ # @private
262
+ #
263
+ # @param path [String, Array<String>]
264
+ #
265
+ # @return [String]
266
+ #
267
+ def interpolate_path(path)
268
+ case path
269
+ in String
270
+ path
271
+ in []
272
+ ""
273
+ in [String, *interpolations]
274
+ encoded = interpolations.map { ERB::Util.url_encode(_1) }
275
+ path.first % encoded
276
+ end
283
277
  end
284
278
  end
285
279
 
286
- # @private
287
- #
288
- # @param url [URI::Generic, String]
289
- #
290
- # @return [Hash{Symbol=>String, Integer, nil}]
291
- #
292
- def self.parse_uri(url)
293
- parsed = URI::Generic.component.zip(URI.split(url)).to_h
294
- {**parsed, query: decode_query(parsed.fetch(:query))}
295
- end
280
+ class << self
281
+ # @private
282
+ #
283
+ # @param query [String, nil]
284
+ #
285
+ # @return [Hash{String=>Array<String>}]
286
+ #
287
+ def decode_query(query)
288
+ CGI.parse(query.to_s)
289
+ end
296
290
 
297
- # @private
298
- #
299
- # @param parsed [Hash{Symbol=>String, Integer, nil}] .
300
- #
301
- # @option parsed [String, nil] :scheme
302
- #
303
- # @option parsed [String, nil] :host
304
- #
305
- # @option parsed [Integer, nil] :port
306
- #
307
- # @option parsed [String, nil] :path
308
- #
309
- # @option parsed [Hash{String=>Array<String>}] :query
310
- #
311
- # @return [URI::Generic]
312
- #
313
- def self.unparse_uri(parsed)
314
- URI::Generic.build(**parsed, query: encode_query(parsed.fetch(:query)))
291
+ # @private
292
+ #
293
+ # @param query [Hash{String=>Array<String>, String, nil}, nil]
294
+ #
295
+ # @return [String, nil]
296
+ #
297
+ def encode_query(query)
298
+ query.to_h.empty? ? nil : URI.encode_www_form(query)
299
+ end
315
300
  end
316
301
 
317
- # @private
318
- #
319
- # @param lhs [Hash{Symbol=>String, Integer, nil}] .
320
- #
321
- # @option lhs [String, nil] :scheme
322
- #
323
- # @option lhs [String, nil] :host
324
- #
325
- # @option lhs [Integer, nil] :port
326
- #
327
- # @option lhs [String, nil] :path
328
- #
329
- # @option lhs [Hash{String=>Array<String>}] :query
330
- #
331
- # @param rhs [Hash{Symbol=>String, Integer, nil}] .
332
- #
333
- # @option rhs [String, nil] :scheme
334
- #
335
- # @option rhs [String, nil] :host
336
- #
337
- # @option rhs [Integer, nil] :port
338
- #
339
- # @option rhs [String, nil] :path
340
- #
341
- # @option rhs [Hash{String=>Array<String>}] :query
342
- #
343
- # @return [URI::Generic]
344
- #
345
- def self.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}/"
302
+ class << self
303
+ # @private
304
+ #
305
+ # @param url [URI::Generic, String]
306
+ #
307
+ # @return [Hash{Symbol=>String, Integer, nil}]
308
+ #
309
+ def parse_uri(url)
310
+ parsed = URI::Generic.component.zip(URI.split(url)).to_h
311
+ {**parsed, query: decode_query(parsed.fetch(:query))}
312
+ end
348
313
 
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)
314
+ # @private
315
+ #
316
+ # @param parsed [Hash{Symbol=>String, Integer, nil}] .
317
+ #
318
+ # @option parsed [String, nil] :scheme
319
+ #
320
+ # @option parsed [String, nil] :host
321
+ #
322
+ # @option parsed [Integer, nil] :port
323
+ #
324
+ # @option parsed [String, nil] :path
325
+ #
326
+ # @option parsed [Hash{String=>Array<String>}] :query
327
+ #
328
+ # @return [URI::Generic]
329
+ #
330
+ def unparse_uri(parsed)
331
+ URI::Generic.build(**parsed, query: encode_query(parsed.fetch(:query)))
332
+ end
351
333
 
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
- )
334
+ # @private
335
+ #
336
+ # @param lhs [Hash{Symbol=>String, Integer, nil}] .
337
+ #
338
+ # @option lhs [String, nil] :scheme
339
+ #
340
+ # @option lhs [String, nil] :host
341
+ #
342
+ # @option lhs [Integer, nil] :port
343
+ #
344
+ # @option lhs [String, nil] :path
345
+ #
346
+ # @option lhs [Hash{String=>Array<String>}] :query
347
+ #
348
+ # @param rhs [Hash{Symbol=>String, Integer, nil}] .
349
+ #
350
+ # @option rhs [String, nil] :scheme
351
+ #
352
+ # @option rhs [String, nil] :host
353
+ #
354
+ # @option rhs [Integer, nil] :port
355
+ #
356
+ # @option rhs [String, nil] :path
357
+ #
358
+ # @option rhs [Hash{String=>Array<String>}] :query
359
+ #
360
+ # @return [URI::Generic]
361
+ #
362
+ def join_parsed_uri(lhs, rhs)
363
+ base_path, base_query = lhs.fetch_values(:path, :query)
364
+ slashed = base_path.end_with?("/") ? base_path : "#{base_path}/"
359
365
 
360
- joined.query = encode_query(query)
361
- joined
362
- end
366
+ parsed_path, parsed_query = parse_uri(rhs.fetch(:path)).fetch_values(:path, :query)
367
+ override = URI::Generic.build(**rhs.slice(:scheme, :host, :port), path: parsed_path)
363
368
 
364
- # @private
365
- #
366
- # @param query [String, nil]
367
- #
368
- # @return [Hash{String=>Array<String>}]
369
- #
370
- def self.decode_query(query)
371
- CGI.parse(query.to_s)
372
- end
369
+ joined = URI.join(URI::Generic.build(lhs.except(:path, :query)), slashed, override)
370
+ query = deep_merge(
371
+ joined.path == base_path ? base_query : {},
372
+ parsed_query,
373
+ rhs[:query].to_h,
374
+ concat: true
375
+ )
373
376
 
374
- # @private
375
- #
376
- # @param query [Hash{String=>Array<String>, String, nil}, nil]
377
- #
378
- # @return [String, nil]
379
- #
380
- def self.encode_query(query)
381
- query.to_h.empty? ? nil : URI.encode_www_form(query)
377
+ joined.query = encode_query(query)
378
+ joined
379
+ end
382
380
  end
383
381
 
384
- # @private
385
- #
386
- # @param headers [Array<Hash{String=>String, Integer, nil}>]
387
- #
388
- # @return [Hash{String=>String}]
389
- #
390
- def self.normalized_headers(*headers)
391
- {}.merge(*headers.compact).to_h do |key, val|
392
- [key.downcase, val&.to_s&.strip]
382
+ class << self
383
+ # @private
384
+ #
385
+ # @param headers [Array<Hash{String=>String, Integer, nil}>]
386
+ #
387
+ # @return [Hash{String=>String}]
388
+ #
389
+ def normalized_headers(*headers)
390
+ {}.merge(*headers.compact).to_h do |key, val|
391
+ [key.downcase, val&.to_s&.strip]
392
+ end
393
393
  end
394
394
  end
395
395
 
396
- # @private
397
- #
398
- # @param headers [Hash{String=>String}]
399
- # @param body [Object]
400
- #
401
- # @return [Object]
402
- #
403
- def self.encode_content(headers, body)
404
- content_type = headers["content-type"]
405
- case [content_type, body]
406
- in ["application/json", Hash | Array]
407
- [headers, JSON.fast_generate(body)]
408
- in [%r{^multipart/form-data}, Hash | IO | StringIO]
409
- boundary = SecureRandom.urlsafe_base64(60)
410
- strio = StringIO.new.tap do |io|
411
- case body
412
- in Hash
413
- body.each do |key, val|
414
- case val
415
- in Array if val.all? { primitive?(_1) }
416
- val.each do |v|
417
- encode_multipart_formdata(io, boundary: boundary, key: key, val: v)
396
+ class << self
397
+ # @private
398
+ #
399
+ # @param io [StringIO]
400
+ # @param boundary [String]
401
+ # @param key [Symbol, String]
402
+ # @param val [Object]
403
+ #
404
+ private def encode_multipart_formdata(io, boundary:, key:, val:)
405
+ io << "--#{boundary}\r\n"
406
+ io << "Content-Disposition: form-data"
407
+ unless key.nil?
408
+ name = ERB::Util.url_encode(key.to_s)
409
+ io << "; name=\"#{name}\""
410
+ end
411
+ if val.is_a?(IO)
412
+ filename = ERB::Util.url_encode(File.basename(val.to_path))
413
+ io << "; filename=\"#{filename}\""
414
+ end
415
+ io << "\r\n"
416
+ case val
417
+ in IO | StringIO
418
+ io << "Content-Type: application/octet-stream\r\n\r\n"
419
+ IO.copy_stream(val, io)
420
+ in String
421
+ io << "Content-Type: application/octet-stream\r\n\r\n"
422
+ io << val.to_s
423
+ in true | false | Integer | Float | Symbol
424
+ io << "Content-Type: text/plain\r\n\r\n"
425
+ io << val.to_s
426
+ else
427
+ io << "Content-Type: application/json\r\n\r\n"
428
+ io << JSON.fast_generate(val)
429
+ end
430
+ io << "\r\n"
431
+ end
432
+
433
+ # @private
434
+ #
435
+ # @param headers [Hash{String=>String}]
436
+ # @param body [Object]
437
+ #
438
+ # @return [Object]
439
+ #
440
+ def encode_content(headers, body)
441
+ content_type = headers["content-type"]
442
+ case [content_type, body]
443
+ in ["application/json", Hash | Array]
444
+ [headers, JSON.fast_generate(body)]
445
+ in [%r{^multipart/form-data}, Hash | IO | StringIO]
446
+ boundary = SecureRandom.urlsafe_base64(60)
447
+ strio = StringIO.new.tap do |io|
448
+ case body
449
+ in Hash
450
+ body.each do |key, val|
451
+ case val
452
+ in Array if val.all? { primitive?(_1) }
453
+ val.each do |v|
454
+ encode_multipart_formdata(io, boundary: boundary, key: key, val: v)
455
+ end
456
+ else
457
+ encode_multipart_formdata(io, boundary: boundary, key: key, val: val)
418
458
  end
419
- else
420
- encode_multipart_formdata(io, boundary: boundary, key: key, val: val)
421
459
  end
460
+ else
461
+ encode_multipart_formdata(io, boundary: boundary, key: nil, val: body)
422
462
  end
423
- else
424
- encode_multipart_formdata(io, boundary: boundary, key: nil, val: body)
463
+ io << "--#{boundary}--\r\n"
464
+ io.rewind
425
465
  end
426
- io << "--#{boundary}--\r\n"
427
- io.rewind
466
+ headers = {
467
+ **headers,
468
+ "content-type" => "#{content_type}; boundary=#{boundary}",
469
+ "transfer-encoding" => "chunked"
470
+ }
471
+ [headers, strio]
472
+ in [_, StringIO]
473
+ [headers, body.string]
474
+ in [_, IO]
475
+ headers = {**headers, "transfer-encoding" => "chunked"}
476
+ [headers, body]
477
+ else
478
+ [headers, body]
428
479
  end
429
- headers = {
430
- **headers,
431
- "content-type" => "#{content_type}; boundary=#{boundary}",
432
- "transfer-encoding" => "chunked"
433
- }
434
- [headers, strio]
435
- in [_, StringIO]
436
- [headers, body.string]
437
- in [_, IO]
438
- headers = {**headers, "transfer-encoding" => "chunked"}
439
- [headers, body]
440
- else
441
- [headers, body]
442
480
  end
443
- end
444
481
 
445
- # @private
446
- #
447
- # @param headers [Hash{String=>String}, Net::HTTPHeader]
448
- # @param stream [Enumerable]
449
- # @param suppress_error [Boolean]
450
- #
451
- # @raise [JSON::ParserError]
452
- # @return [Object]
453
- #
454
- def self.decode_content(headers, stream:, suppress_error: false)
455
- case headers["content-type"]
456
- in %r{^application/json}
457
- json = stream.to_a.join
458
- begin
459
- JSON.parse(json, symbolize_names: true)
460
- rescue JSON::ParserError => e
461
- raise e unless suppress_error
462
- json
482
+ # @private
483
+ #
484
+ # @param headers [Hash{String=>String}, Net::HTTPHeader]
485
+ # @param stream [Enumerable]
486
+ # @param suppress_error [Boolean]
487
+ #
488
+ # @raise [JSON::ParserError]
489
+ # @return [Object]
490
+ #
491
+ def decode_content(headers, stream:, suppress_error: false)
492
+ case headers["content-type"]
493
+ in %r{^application/json}
494
+ json = stream.to_a.join
495
+ begin
496
+ JSON.parse(json, symbolize_names: true)
497
+ rescue JSON::ParserError => e
498
+ raise e unless suppress_error
499
+ json
500
+ end
501
+ in %r{^text/}
502
+ stream.to_a.join
503
+ else
504
+ # TODO: parsing other response types
505
+ StringIO.new(stream.to_a.join)
463
506
  end
464
- in %r{^text/}
465
- stream.to_a.join
466
- else
467
- # TODO: parsing other response types
468
- StringIO.new(stream.to_a.join)
469
- end
470
- end
471
-
472
- # @private
473
- #
474
- # @param io [StringIO]
475
- # @param boundary [String]
476
- # @param key [Symbol, String]
477
- # @param val [Object]
478
- #
479
- private_class_method def self.encode_multipart_formdata(io, boundary:, key:, val:)
480
- io << "--#{boundary}\r\n"
481
- io << "Content-Disposition: form-data"
482
- unless key.nil?
483
- name = ERB::Util.url_encode(key.to_s)
484
- io << "; name=\"#{name}\""
485
- end
486
- if val.is_a?(IO)
487
- filename = ERB::Util.url_encode(File.basename(val.to_path))
488
- io << "; filename=\"#{filename}\""
489
- end
490
- io << "\r\n"
491
- case val
492
- in IO | StringIO
493
- io << "Content-Type: application/octet-stream\r\n\r\n"
494
- IO.copy_stream(val, io)
495
- in String
496
- io << "Content-Type: application/octet-stream\r\n\r\n"
497
- io << val.to_s
498
- in true | false | Integer | Float | Symbol
499
- io << "Content-Type: text/plain\r\n\r\n"
500
- io << val.to_s
501
- else
502
- io << "Content-Type: application/json\r\n\r\n"
503
- io << JSON.fast_generate(val)
504
507
  end
505
- io << "\r\n"
506
508
  end
507
509
  end
508
510