orb-billing 0.1.0.pre.alpha.39 → 0.1.1

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -2
  3. data/lib/orb/internal/transport/base_client.rb +1 -1
  4. data/lib/orb/internal/transport/pooled_net_requester.rb +14 -5
  5. data/lib/orb/internal/type/array_of.rb +12 -2
  6. data/lib/orb/internal/type/base_model.rb +9 -5
  7. data/lib/orb/internal/type/boolean.rb +5 -1
  8. data/lib/orb/internal/type/converter.rb +42 -17
  9. data/lib/orb/internal/type/enum.rb +5 -1
  10. data/lib/orb/internal/type/hash_of.rb +6 -2
  11. data/lib/orb/internal/type/io_like.rb +75 -0
  12. data/lib/orb/internal/type/request_parameters.rb +11 -2
  13. data/lib/orb/internal/type/union.rb +7 -3
  14. data/lib/orb/internal/type/unknown.rb +5 -1
  15. data/lib/orb/internal/util.rb +80 -21
  16. data/lib/orb/version.rb +1 -1
  17. data/lib/orb.rb +1 -0
  18. data/rbi/lib/orb/internal/transport/pooled_net_requester.rbi +1 -1
  19. data/rbi/lib/orb/internal/type/array_of.rbi +7 -3
  20. data/rbi/lib/orb/internal/type/base_model.rbi +7 -3
  21. data/rbi/lib/orb/internal/type/boolean.rbi +9 -3
  22. data/rbi/lib/orb/internal/type/converter.rbi +23 -10
  23. data/rbi/lib/orb/internal/type/enum.rbi +12 -3
  24. data/rbi/lib/orb/internal/type/hash_of.rbi +6 -3
  25. data/rbi/lib/orb/internal/type/io_like.rbi +49 -0
  26. data/rbi/lib/orb/internal/type/union.rbi +11 -3
  27. data/rbi/lib/orb/internal/type/unknown.rbi +8 -3
  28. data/rbi/lib/orb/internal/util.rbi +35 -4
  29. data/sig/orb/internal/transport/pooled_net_requester.rbs +1 -1
  30. data/sig/orb/internal/type/array_of.rbs +5 -2
  31. data/sig/orb/internal/type/base_model.rbs +5 -2
  32. data/sig/orb/internal/type/boolean.rbs +5 -2
  33. data/sig/orb/internal/type/converter.rbs +11 -5
  34. data/sig/orb/internal/type/enum.rbs +5 -2
  35. data/sig/orb/internal/type/hash_of.rbs +5 -2
  36. data/sig/orb/internal/type/io_like.rbs +23 -0
  37. data/sig/orb/internal/type/union.rbs +5 -2
  38. data/sig/orb/internal/type/unknown.rbs +5 -2
  39. data/sig/orb/internal/util.rbs +17 -2
  40. metadata +7 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c0a5b2b706493559363ab625bd5acbef451386a515a1c473447f7583006750a7
4
- data.tar.gz: 392dbe182794cc49819c6bc483e80d2dfc99fe3f96c5148fc644e43e9ffdc917
3
+ metadata.gz: 96cef32525e1e12a5b3d6e1eb8839eceb8aea9e3dddb1b8dc906b4b793bdec35
4
+ data.tar.gz: 148cb13c2428e83eb8e9dbf6720601ad3b03c90b2b2df73bad9b1d8022dab983
5
5
  SHA512:
6
- metadata.gz: d8cecd00cd1de61ce8349e487ec416372c135b5f90ef81d7acc42a0512d6a9b615d10bcdb37f2f334ea5c65cf01a5c31addb775ef116ba43eb880eabe0c0ebe9
7
- data.tar.gz: 61a0a748eece61489f3fb1e2c14c99d011505be14d7ed1aa0f1bee41469ae77ab19b8fbaf3f1c19c084b907cdff82afbad9e0bd0c0a0839aeb031a1ef120d7ca
6
+ metadata.gz: aeb7b0a8da2dc0c3b5134cb979eb60af78bc8ce3dc64fb204da38e40ef7cb5ca0f9eb476eb601dd40fd3b796f2b459a8182b83f1176dd37c68c35d4f7cb3be33
7
+ data.tar.gz: 87f4b582111e480f15e830a0b537ca205c89d5958b2b3ff4dc275e2c8966805b8e8b804205f5e945636cc7532940f25846dad64411c8110cd0dfc630f3b261fc
data/README.md CHANGED
@@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
15
15
  <!-- x-release-please-start-version -->
16
16
 
17
17
  ```ruby
18
- gem "orb-billing", "~> 0.1.0.pre.alpha.38"
18
+ gem "orb-billing", "~> 0.1.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -148,6 +148,13 @@ After Solargraph is installed, **you must populate its index** either via the pr
148
148
  bundle exec solargraph gems
149
149
  ```
150
150
 
151
+ Note: if you had installed the gem either using a `git:` or `github:` URL, or had vendored the gem using bundler, you will need to set up your [`.solargraph.yml`](https://solargraph.org/guides/configuration) to include the path to the gem's `lib` directory.
152
+
153
+ ```yaml
154
+ include:
155
+ - 'vendor/bundle/ruby/*/gems/orb-billing-*/lib/**/*.rb'
156
+ ```
157
+
151
158
  Otherwise Solargraph will not be able to provide type information or auto-completion for any non-indexed libraries.
152
159
 
153
160
  ### Sorbet
@@ -161,7 +168,8 @@ Due to limitations with the Sorbet type system, where a method otherwise can tak
161
168
  Please follow Sorbet's [setup guides](https://sorbet.org/docs/adopting) for best experience.
162
169
 
163
170
  ```ruby
164
- params = Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer")
171
+ params =
172
+ Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer")
165
173
 
166
174
  orb.customers.create(**params)
167
175
  ```
@@ -380,7 +380,7 @@ module Orb
380
380
  in (400..) | Orb::Errors::APIConnectionError
381
381
  self.class.reap_connection!(status, stream: stream)
382
382
 
383
- delay = retry_delay(response, retry_count: retry_count)
383
+ delay = retry_delay(response || {}, retry_count: retry_count)
384
384
  sleep(delay)
385
385
 
386
386
  send_request(
@@ -54,7 +54,7 @@ module Orb
54
54
  # @param blk [Proc]
55
55
  #
56
56
  # @yieldparam [String]
57
- # @return [Net::HTTPGenericRequest]
57
+ # @return [Array(Net::HTTPGenericRequest, Proc)]
58
58
  def build_request(request, &blk)
59
59
  method, url, headers, body = request.fetch_values(:method, :url, :headers, :body)
60
60
  req = Net::HTTPGenericRequest.new(
@@ -75,12 +75,12 @@ module Orb
75
75
  in StringIO
76
76
  req["content-length"] ||= body.size.to_s unless req["transfer-encoding"]
77
77
  req.body_stream = Orb::Internal::Util::ReadIOAdapter.new(body, &blk)
78
- in IO | Enumerator
78
+ in Pathname | IO | Enumerator
79
79
  req["transfer-encoding"] ||= "chunked" unless req["content-length"]
80
80
  req.body_stream = Orb::Internal::Util::ReadIOAdapter.new(body, &blk)
81
81
  end
82
82
 
83
- req
83
+ [req, req.body_stream&.method(:close)]
84
84
  end
85
85
  end
86
86
 
@@ -123,13 +123,17 @@ module Orb
123
123
  def execute(request)
124
124
  url, deadline = request.fetch_values(:url, :deadline)
125
125
 
126
+ req = nil
126
127
  eof = false
127
128
  finished = false
129
+ closing = nil
130
+
131
+ # rubocop:disable Metrics/BlockLength
128
132
  enum = Enumerator.new do |y|
129
133
  with_pool(url, deadline: deadline) do |conn|
130
134
  next if finished
131
135
 
132
- req = self.class.build_request(request) do
136
+ req, closing = self.class.build_request(request) do
133
137
  self.class.calibrate_socket_timeout(conn, deadline)
134
138
  end
135
139
 
@@ -154,8 +158,11 @@ module Orb
154
158
  end
155
159
  end
156
160
  rescue Timeout::Error
157
- raise Orb::Errors::APITimeoutError
161
+ raise Orb::Errors::APITimeoutError.new(url: url, request: req)
162
+ rescue StandardError
163
+ raise Orb::Errors::APIConnectionError.new(url: url, request: req)
158
164
  end
165
+ # rubocop:enable Metrics/BlockLength
159
166
 
160
167
  conn, _, response = enum.next
161
168
  body = Orb::Internal::Util.fused_enum(enum, external: true) do
@@ -165,7 +172,9 @@ module Orb
165
172
  rescue StopIteration
166
173
  nil
167
174
  end
175
+ ensure
168
176
  conn.finish if !eof && conn&.started?
177
+ closing&.call
169
178
  end
170
179
  [Integer(response.code), response, (response.body = body)]
171
180
  end
@@ -79,10 +79,20 @@ module Orb
79
79
  #
80
80
  # @param value [Array<Object>, Object]
81
81
  #
82
+ # @param state [Hash{Symbol=>Object}] .
83
+ #
84
+ # @option state [Boolean] :can_retry
85
+ #
82
86
  # @return [Array<Object>, Object]
83
- def dump(value)
87
+ def dump(value, state:)
84
88
  target = item_type
85
- value.is_a?(Array) ? value.map { Orb::Internal::Type::Converter.dump(target, _1) } : super
89
+ if value.is_a?(Array)
90
+ value.map do
91
+ Orb::Internal::Type::Converter.dump(target, _1, state: state)
92
+ end
93
+ else
94
+ super
95
+ end
86
96
  end
87
97
 
88
98
  # @api private
@@ -252,8 +252,12 @@ module Orb
252
252
  #
253
253
  # @param value [Orb::Internal::Type::BaseModel, Object]
254
254
  #
255
+ # @param state [Hash{Symbol=>Object}] .
256
+ #
257
+ # @option state [Boolean] :can_retry
258
+ #
255
259
  # @return [Hash{Object=>Object}, Object]
256
- def dump(value)
260
+ def dump(value, state:)
257
261
  unless (coerced = Orb::Internal::Util.coerce_hash(value)).is_a?(Hash)
258
262
  return super
259
263
  end
@@ -264,7 +268,7 @@ module Orb
264
268
  name = key.is_a?(String) ? key.to_sym : key
265
269
  case (field = known_fields[name])
266
270
  in nil
267
- acc.store(name, super(val))
271
+ acc.store(name, super(val, state: state))
268
272
  else
269
273
  api_name, mode, type_fn = field.fetch_values(:api_name, :mode, :type_fn)
270
274
  case mode
@@ -272,7 +276,7 @@ module Orb
272
276
  next
273
277
  else
274
278
  target = type_fn.call
275
- acc.store(api_name, Orb::Internal::Type::Converter.dump(target, val))
279
+ acc.store(api_name, Orb::Internal::Type::Converter.dump(target, val, state: state))
276
280
  end
277
281
  end
278
282
  end
@@ -337,12 +341,12 @@ module Orb
337
341
  # @param a [Object]
338
342
  #
339
343
  # @return [String]
340
- def to_json(*a) = self.class.dump(self).to_json(*a)
344
+ def to_json(*a) = Orb::Internal::Type::Converter.dump(self.class, self).to_json(*a)
341
345
 
342
346
  # @param a [Object]
343
347
  #
344
348
  # @return [String]
345
- def to_yaml(*a) = self.class.dump(self).to_yaml(*a)
349
+ def to_yaml(*a) = Orb::Internal::Type::Converter.dump(self.class, self).to_yaml(*a)
346
350
 
347
351
  # Create a new instance of a model.
348
352
  #
@@ -45,8 +45,12 @@ module Orb
45
45
  # #
46
46
  # # @param value [Boolean, Object]
47
47
  # #
48
+ # # @param state [Hash{Symbol=>Object}] .
49
+ # #
50
+ # # @option state [Boolean] :can_retry
51
+ # #
48
52
  # # @return [Boolean, Object]
49
- # def dump(value) = super
53
+ # def dump(value, state:) = super
50
54
  end
51
55
  end
52
56
  end
@@ -26,15 +26,24 @@ module Orb
26
26
  #
27
27
  # @param value [Object]
28
28
  #
29
+ # @param state [Hash{Symbol=>Object}] .
30
+ #
31
+ # @option state [Boolean] :can_retry
32
+ #
29
33
  # @return [Object]
30
- def dump(value)
34
+ def dump(value, state:)
31
35
  case value
32
36
  in Array
33
- value.map { Orb::Internal::Type::Unknown.dump(_1) }
37
+ value.map { Orb::Internal::Type::Unknown.dump(_1, state: state) }
34
38
  in Hash
35
- value.transform_values { Orb::Internal::Type::Unknown.dump(_1) }
39
+ value.transform_values { Orb::Internal::Type::Unknown.dump(_1, state: state) }
36
40
  in Orb::Internal::Type::BaseModel
37
- value.class.dump(value)
41
+ value.class.dump(value, state: state)
42
+ in StringIO
43
+ value.string
44
+ in Pathname | IO
45
+ state[:can_retry] = false if value.is_a?(IO)
46
+ Orb::Internal::Util::SerializationAdapter.new(value)
38
47
  else
39
48
  value
40
49
  end
@@ -140,9 +149,9 @@ module Orb
140
149
  if value.is_a?(Integer)
141
150
  exactness[:yes] += 1
142
151
  return value
143
- elsif strictness == :strong
152
+ elsif strictness == :strong && Integer(value, exception: false) != value
144
153
  message = "no implicit conversion of #{value.class} into #{target.inspect}"
145
- raise TypeError.new(message)
154
+ raise value.is_a?(Numeric) ? ArgumentError.new(message) : TypeError.new(message)
146
155
  else
147
156
  Kernel.then do
148
157
  return Integer(value).tap { exactness[:maybe] += 1 }
@@ -182,18 +191,26 @@ module Orb
182
191
  rescue ArgumentError, TypeError => e
183
192
  raise e if strictness == :strong
184
193
  end
185
- in -> { _1 <= IO } if value.is_a?(String)
194
+ in -> { _1 <= StringIO } if value.is_a?(String)
186
195
  exactness[:yes] += 1
187
196
  return StringIO.new(value.b)
188
197
  else
189
198
  end
190
199
  in Symbol
191
- if (value.is_a?(Symbol) || value.is_a?(String)) && value.to_sym == target
192
- exactness[:yes] += 1
193
- return target
194
- elsif strictness == :strong
195
- message = "cannot convert non-matching #{value.class} into #{target.inspect}"
196
- raise ArgumentError.new(message)
200
+ case value
201
+ in Symbol | String
202
+ if value.to_sym == target
203
+ exactness[:yes] += 1
204
+ return target
205
+ else
206
+ exactness[:maybe] += 1
207
+ return value
208
+ end
209
+ else
210
+ if strictness == :strong
211
+ message = "cannot convert non-matching #{value.class} into #{target.inspect}"
212
+ raise ArgumentError.new(message)
213
+ end
197
214
  end
198
215
  else
199
216
  end
@@ -207,13 +224,21 @@ module Orb
207
224
  # @api private
208
225
  #
209
226
  # @param target [Orb::Internal::Type::Converter, Class]
227
+ #
210
228
  # @param value [Object]
211
229
  #
230
+ # @param state [Hash{Symbol=>Object}] .
231
+ #
232
+ # @option state [Boolean] :can_retry
233
+ #
212
234
  # @return [Object]
213
- def dump(target, value)
214
- # rubocop:disable Layout/LineLength
215
- target.is_a?(Orb::Internal::Type::Converter) ? target.dump(value) : Orb::Internal::Type::Unknown.dump(value)
216
- # rubocop:enable Layout/LineLength
235
+ def dump(target, value, state: {can_retry: true})
236
+ case target
237
+ in Orb::Internal::Type::Converter
238
+ target.dump(value, state: state)
239
+ else
240
+ Orb::Internal::Type::Unknown.dump(value, state: state)
241
+ end
217
242
  end
218
243
  end
219
244
  end
@@ -97,8 +97,12 @@ module Orb
97
97
  # #
98
98
  # # @param value [Symbol, Object]
99
99
  # #
100
+ # # @param state [Hash{Symbol=>Object}] .
101
+ # #
102
+ # # @option state [Boolean] :can_retry
103
+ # #
100
104
  # # @return [Symbol, Object]
101
- # def dump(value) = super
105
+ # def dump(value, state:) = super
102
106
  end
103
107
  end
104
108
  end
@@ -99,12 +99,16 @@ module Orb
99
99
  #
100
100
  # @param value [Hash{Object=>Object}, Object]
101
101
  #
102
+ # @param state [Hash{Symbol=>Object}] .
103
+ #
104
+ # @option state [Boolean] :can_retry
105
+ #
102
106
  # @return [Hash{Symbol=>Object}, Object]
103
- def dump(value)
107
+ def dump(value, state:)
104
108
  target = item_type
105
109
  if value.is_a?(Hash)
106
110
  value.transform_values do
107
- Orb::Internal::Type::Converter.dump(target, _1)
111
+ Orb::Internal::Type::Converter.dump(target, _1, state: state)
108
112
  end
109
113
  else
110
114
  super
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Orb
4
+ module Internal
5
+ module Type
6
+ # @api private
7
+ #
8
+ # @abstract
9
+ #
10
+ # Either `Pathname` or `StringIO`.
11
+ class IOLike
12
+ extend Orb::Internal::Type::Converter
13
+
14
+ # @param other [Object]
15
+ #
16
+ # @return [Boolean]
17
+ def self.===(other)
18
+ case other
19
+ in StringIO | Pathname | IO
20
+ true
21
+ else
22
+ false
23
+ end
24
+ end
25
+
26
+ # @param other [Object]
27
+ #
28
+ # @return [Boolean]
29
+ def self.==(other) = other.is_a?(Class) && other <= Orb::Internal::Type::IOLike
30
+
31
+ class << self
32
+ # @api private
33
+ #
34
+ # @param value [StringIO, String, Object]
35
+ #
36
+ # @param state [Hash{Symbol=>Object}] .
37
+ #
38
+ # @option state [Boolean, :strong] :strictness
39
+ #
40
+ # @option state [Hash{Symbol=>Object}] :exactness
41
+ #
42
+ # @option state [Integer] :branched
43
+ #
44
+ # @return [StringIO, Object]
45
+ def coerce(value, state:)
46
+ exactness = state.fetch(:exactness)
47
+ case value
48
+ in String
49
+ exactness[:yes] += 1
50
+ StringIO.new(value)
51
+ in StringIO
52
+ exactness[:yes] += 1
53
+ value
54
+ else
55
+ exactness[:no] += 1
56
+ value
57
+ end
58
+ end
59
+
60
+ # @!parse
61
+ # # @api private
62
+ # #
63
+ # # @param value [Pathname, StringIO, IO, String, Object]
64
+ # #
65
+ # # @param state [Hash{Symbol=>Object}] .
66
+ # #
67
+ # # @option state [Boolean] :can_retry
68
+ # #
69
+ # # @return [Pathname, StringIO, IO, String, Object]
70
+ # def dump(value, state:) = super
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -26,9 +26,18 @@ module Orb
26
26
  #
27
27
  # @return [Array(Object, Hash{Symbol=>Object})]
28
28
  def dump_request(params)
29
- case (dumped = dump(params))
29
+ state = {can_retry: true}
30
+ case (dumped = dump(params, state: state))
30
31
  in Hash
31
- [dumped.except(:request_options), dumped[:request_options]]
32
+ options = Orb::Internal::Util.coerce_hash(dumped[:request_options])
33
+ request_options =
34
+ case [options, state.fetch(:can_retry)]
35
+ in [Hash | nil, false]
36
+ {**options.to_h, max_retries: 0}
37
+ else
38
+ options
39
+ end
40
+ [dumped.except(:request_options), request_options]
32
41
  else
33
42
  [dumped, nil]
34
43
  end
@@ -205,15 +205,19 @@ module Orb
205
205
  #
206
206
  # @param value [Object]
207
207
  #
208
+ # @param state [Hash{Symbol=>Object}] .
209
+ #
210
+ # @option state [Boolean] :can_retry
211
+ #
208
212
  # @return [Object]
209
- def dump(value)
213
+ def dump(value, state:)
210
214
  if (target = resolve_variant(value))
211
- return Orb::Internal::Type::Converter.dump(target, value)
215
+ return Orb::Internal::Type::Converter.dump(target, value, state: state)
212
216
  end
213
217
 
214
218
  known_variants.each do
215
219
  target = _2.call
216
- return Orb::Internal::Type::Converter.dump(target, value) if target === value
220
+ return Orb::Internal::Type::Converter.dump(target, value, state: state) if target === value
217
221
  end
218
222
 
219
223
  super
@@ -47,8 +47,12 @@ module Orb
47
47
  # #
48
48
  # # @param value [Object]
49
49
  # #
50
+ # # @param state [Hash{Symbol=>Object}] .
51
+ # #
52
+ # # @option state [Boolean] :can_retry
53
+ # #
50
54
  # # @return [Object]
51
- # def dump(value) = super
55
+ # def dump(value, state:) = super
52
56
  end
53
57
 
54
58
  # rubocop:enable Lint/UnusedMethodArgument
@@ -122,7 +122,7 @@ module Orb
122
122
  # @return [Hash{Object=>Object}, Object]
123
123
  def coerce_hash(input)
124
124
  case input
125
- in NilClass | Array | Set | Enumerator
125
+ in NilClass | Array | Set | Enumerator | StringIO | IO
126
126
  input
127
127
  else
128
128
  input.respond_to?(:to_h) ? input.to_h : input
@@ -348,10 +348,47 @@ module Orb
348
348
  end
349
349
  end
350
350
 
351
+ # @api private
352
+ class SerializationAdapter
353
+ # @return [Pathname, IO]
354
+ attr_reader :inner
355
+
356
+ # @param a [Object]
357
+ #
358
+ # @return [String]
359
+ def to_json(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_json(*a)
360
+
361
+ # @param a [Object]
362
+ #
363
+ # @return [String]
364
+ def to_yaml(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_yaml(*a)
365
+
366
+ # @api private
367
+ #
368
+ # @param inner [Pathname, IO]
369
+ def initialize(inner) = @inner = inner
370
+ end
371
+
351
372
  # @api private
352
373
  #
353
374
  # An adapter that satisfies the IO interface required by `::IO.copy_stream`
354
375
  class ReadIOAdapter
376
+ # @api private
377
+ #
378
+ # @return [Boolean, nil]
379
+ def close? = @closing
380
+
381
+ # @api private
382
+ def close
383
+ case @stream
384
+ in Enumerator
385
+ Orb::Internal::Util.close_fused!(@stream)
386
+ in IO if close?
387
+ @stream.close
388
+ else
389
+ end
390
+ end
391
+
355
392
  # @api private
356
393
  #
357
394
  # @param max_len [Integer, nil]
@@ -396,12 +433,21 @@ module Orb
396
433
 
397
434
  # @api private
398
435
  #
399
- # @param stream [String, IO, StringIO, Enumerable<String>]
436
+ # @param src [String, Pathname, StringIO, Enumerable<String>]
400
437
  # @param blk [Proc]
401
438
  #
402
439
  # @yieldparam [String]
403
- def initialize(stream, &blk)
404
- @stream = stream.is_a?(String) ? StringIO.new(stream) : stream
440
+ def initialize(src, &blk)
441
+ @stream =
442
+ case src
443
+ in String
444
+ StringIO.new(src)
445
+ in Pathname
446
+ @closing = true
447
+ src.open(binmode: true)
448
+ else
449
+ src
450
+ end
405
451
  @buf = String.new.b
406
452
  @blk = blk
407
453
  end
@@ -414,9 +460,10 @@ module Orb
414
460
  # @return [Enumerable<String>]
415
461
  def writable_enum(&blk)
416
462
  Enumerator.new do |y|
463
+ buf = String.new.b
417
464
  y.define_singleton_method(:write) do
418
- self << _1.clone
419
- _1.bytesize
465
+ self << buf.replace(_1)
466
+ buf.bytesize
420
467
  end
421
468
 
422
469
  blk.call(y)
@@ -431,29 +478,39 @@ module Orb
431
478
  # @param boundary [String]
432
479
  # @param key [Symbol, String]
433
480
  # @param val [Object]
434
- private def write_multipart_chunk(y, boundary:, key:, val:)
481
+ # @param closing [Array<Proc>]
482
+ private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
483
+ val = val.inner if val.is_a?(Orb::Internal::Util::SerializationAdapter)
484
+
435
485
  y << "--#{boundary}\r\n"
436
486
  y << "Content-Disposition: form-data"
437
487
  unless key.nil?
438
488
  name = ERB::Util.url_encode(key.to_s)
439
489
  y << "; name=\"#{name}\""
440
490
  end
441
- if val.is_a?(IO)
491
+ case val
492
+ in Pathname | IO
442
493
  filename = ERB::Util.url_encode(File.basename(val.to_path))
443
494
  y << "; filename=\"#{filename}\""
495
+ else
444
496
  end
445
497
  y << "\r\n"
446
498
  case val
499
+ in Pathname
500
+ y << "Content-Type: application/octet-stream\r\n\r\n"
501
+ io = val.open(binmode: true)
502
+ closing << io.method(:close)
503
+ IO.copy_stream(io, y)
447
504
  in IO
448
505
  y << "Content-Type: application/octet-stream\r\n\r\n"
449
- IO.copy_stream(val.tap(&:rewind), y)
506
+ IO.copy_stream(val, y)
450
507
  in StringIO
451
508
  y << "Content-Type: application/octet-stream\r\n\r\n"
452
509
  y << val.string
453
510
  in String
454
511
  y << "Content-Type: application/octet-stream\r\n\r\n"
455
512
  y << val.to_s
456
- in true | false | Integer | Float | Symbol
513
+ in _ if primitive?(val)
457
514
  y << "Content-Type: text/plain\r\n\r\n"
458
515
  y << val.to_s
459
516
  else
@@ -471,6 +528,7 @@ module Orb
471
528
  private def encode_multipart_streaming(body)
472
529
  boundary = SecureRandom.urlsafe_base64(60)
473
530
 
531
+ closing = []
474
532
  strio = writable_enum do |y|
475
533
  case body
476
534
  in Hash
@@ -478,19 +536,20 @@ module Orb
478
536
  case val
479
537
  in Array if val.all? { primitive?(_1) }
480
538
  val.each do |v|
481
- write_multipart_chunk(y, boundary: boundary, key: key, val: v)
539
+ write_multipart_chunk(y, boundary: boundary, key: key, val: v, closing: closing)
482
540
  end
483
541
  else
484
- write_multipart_chunk(y, boundary: boundary, key: key, val: val)
542
+ write_multipart_chunk(y, boundary: boundary, key: key, val: val, closing: closing)
485
543
  end
486
544
  end
487
545
  else
488
- write_multipart_chunk(y, boundary: boundary, key: nil, val: body)
546
+ write_multipart_chunk(y, boundary: boundary, key: nil, val: body, closing: closing)
489
547
  end
490
548
  y << "--#{boundary}--\r\n"
491
549
  end
492
550
 
493
- [boundary, strio]
551
+ fused_io = fused_enum(strio) { closing.each(&:call) }
552
+ [boundary, fused_io]
494
553
  end
495
554
 
496
555
  # @api private
@@ -501,21 +560,21 @@ module Orb
501
560
  # @return [Object]
502
561
  def encode_content(headers, body)
503
562
  content_type = headers["content-type"]
563
+ body = body.inner if body.is_a?(Orb::Internal::Util::SerializationAdapter)
564
+
504
565
  case [content_type, body]
505
- in [%r{^application/(?:vnd\.api\+)?json}, _] unless body.nil?
566
+ in [%r{^application/(?:vnd\.api\+)?json}, Hash | Array | -> { primitive?(_1) }]
506
567
  [headers, JSON.fast_generate(body)]
507
- in [%r{^application/(?:x-)?jsonl}, Enumerable]
568
+ in [%r{^application/(?:x-)?jsonl}, Enumerable] unless body.is_a?(StringIO) || body.is_a?(IO)
508
569
  [headers, body.lazy.map { JSON.fast_generate(_1) }]
509
- in [%r{^multipart/form-data}, Hash | IO | StringIO]
570
+ in [%r{^multipart/form-data}, Hash | Pathname | StringIO | IO]
510
571
  boundary, strio = encode_multipart_streaming(body)
511
572
  headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"}
512
573
  [headers, strio]
513
- in [_, IO]
514
- [headers, body.tap(&:rewind)]
515
- in [_, StringIO]
516
- [headers, body.string]
517
574
  in [_, Symbol | Numeric]
518
575
  [headers, body.to_s]
576
+ in [_, StringIO]
577
+ [headers, body.string]
519
578
  else
520
579
  [headers, body]
521
580
  end