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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/terminal-shop/base_client.rb +8 -8
- data/lib/terminal-shop/base_model.rb +4 -3
- data/lib/terminal-shop/client.rb +12 -1
- data/lib/terminal-shop/pooled_net_requester.rb +3 -3
- data/lib/terminal-shop/util.rb +434 -432
- data/lib/terminal-shop/version.rb +1 -1
- data/rbi/lib/terminal-shop/client.rbi +6 -0
- data/rbi/lib/terminal-shop/pooled_net_requester.rbi +1 -3
- data/rbi/lib/terminal-shop/util.rbi +22 -29
- data/rbi/lib/terminal-shop/version.rbi +1 -1
- data/sig/terminal-shop/client.rbs +3 -0
- data/sig/terminal-shop/pooled_net_requester.rbs +1 -5
- data/sig/terminal-shop/util.rbs +17 -22
- data/sig/terminal-shop/version.rbs +1 -1
- metadata +2 -2
data/lib/terminal-shop/util.rb
CHANGED
@@ -8,163 +8,153 @@ module TerminalShop
|
|
8
8
|
module Util
|
9
9
|
# @private
|
10
10
|
#
|
11
|
-
# @return [
|
11
|
+
# @return [Float]
|
12
12
|
#
|
13
|
-
def self.
|
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
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
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
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
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
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
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
|
-
|
220
|
-
|
221
|
-
|
222
|
-
else
|
223
|
-
rhs
|
213
|
+
else
|
214
|
+
sentinel
|
215
|
+
end
|
224
216
|
end
|
225
|
-
end
|
226
217
|
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
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
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
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
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
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
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
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
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
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
|
-
|
350
|
-
|
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
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
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
|
-
|
361
|
-
|
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
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
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
|
-
|
375
|
-
|
376
|
-
|
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
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
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
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
417
|
-
|
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
|
-
|
424
|
-
|
463
|
+
io << "--#{boundary}--\r\n"
|
464
|
+
io.rewind
|
425
465
|
end
|
426
|
-
|
427
|
-
|
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
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
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
|
|