orb-billing 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/.ignore +2 -0
  3. data/CHANGELOG.md +972 -0
  4. data/README.md +7 -15
  5. data/SECURITY.md +27 -0
  6. data/lib/orb/client.rb +3 -2
  7. data/lib/orb/internal/page.rb +33 -31
  8. data/lib/orb/internal/transport/base_client.rb +14 -4
  9. data/lib/orb/internal/transport/pooled_net_requester.rb +1 -1
  10. data/lib/orb/internal/type/array_of.rb +17 -2
  11. data/lib/orb/internal/type/base_model.rb +52 -8
  12. data/lib/orb/internal/type/base_page.rb +1 -0
  13. data/lib/orb/internal/type/boolean.rb +2 -0
  14. data/lib/orb/internal/type/converter.rb +24 -0
  15. data/lib/orb/internal/type/enum.rb +19 -3
  16. data/lib/orb/internal/type/hash_of.rb +17 -2
  17. data/lib/orb/internal/type/io_like.rb +2 -0
  18. data/lib/orb/internal/type/union.rb +17 -3
  19. data/lib/orb/internal/type/unknown.rb +2 -0
  20. data/lib/orb/internal/util.rb +36 -9
  21. data/lib/orb/internal.rb +5 -1
  22. data/lib/orb/version.rb +1 -1
  23. data/rbi/lib/orb/client.rbi +3 -2
  24. data/rbi/lib/orb/internal/page.rbi +1 -0
  25. data/rbi/lib/orb/internal/transport/base_client.rbi +1 -0
  26. data/rbi/lib/orb/internal/type/array_of.rbi +12 -9
  27. data/rbi/lib/orb/internal/type/base_model.rbi +16 -0
  28. data/rbi/lib/orb/internal/type/boolean.rbi +4 -5
  29. data/rbi/lib/orb/internal/type/converter.rbi +8 -0
  30. data/rbi/lib/orb/internal/type/enum.rbi +4 -0
  31. data/rbi/lib/orb/internal/type/hash_of.rbi +12 -9
  32. data/rbi/lib/orb/internal/type/io_like.rbi +4 -5
  33. data/rbi/lib/orb/internal/type/union.rbi +4 -0
  34. data/rbi/lib/orb/internal/type/unknown.rbi +4 -5
  35. data/rbi/lib/orb/internal/util.rbi +15 -0
  36. data/rbi/lib/orb/internal.rbi +1 -1
  37. data/sig/orb/internal/type/array_of.rbs +2 -0
  38. data/sig/orb/internal/type/base_model.rbs +8 -0
  39. data/sig/orb/internal/type/converter.rbs +4 -0
  40. data/sig/orb/internal/type/enum.rbs +2 -0
  41. data/sig/orb/internal/type/hash_of.rbs +2 -0
  42. data/sig/orb/internal/type/union.rbs +2 -0
  43. data/sig/orb/internal/util.rbs +2 -0
  44. data/sig/orb/internal.rbs +1 -1
  45. metadata +5 -2
data/README.md CHANGED
@@ -4,9 +4,9 @@ The Orb Ruby library provides convenient access to the Orb REST API from any Rub
4
4
 
5
5
  ## Documentation
6
6
 
7
- Documentation for released of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb-billing).
7
+ Documentation for releases of this gem can be found [on RubyDoc](https://gemdocs.org/gems/orb-billing).
8
8
 
9
- The underlying REST API documentation can be found on [docs.withorb.com](https://docs.withorb.com/reference/api-reference).
9
+ The REST API documentation can be found on [docs.withorb.com](https://docs.withorb.com/reference/api-reference).
10
10
 
11
11
  ## Installation
12
12
 
@@ -15,22 +15,16 @@ 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.2"
18
+ gem "orb-billing", "~> 0.2.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
22
22
 
23
- To fetch an initial copy of the gem:
24
-
25
- ```sh
26
- bundle install
27
- ```
28
-
29
23
  ## Usage
30
24
 
31
25
  ```ruby
32
26
  require "bundler/setup"
33
- require "orb-billing"
27
+ require "orb"
34
28
 
35
29
  orb = Orb::Client.new(
36
30
  api_key: "My API Key" # defaults to ENV["ORB_API_KEY"]
@@ -62,7 +56,7 @@ end
62
56
 
63
57
  ### Errors
64
58
 
65
- When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Orb::Error` will be thrown:
59
+ When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `Orb::Errors::APIError` will be thrown:
66
60
 
67
61
  ```ruby
68
62
  begin
@@ -168,8 +162,7 @@ Due to limitations with the Sorbet type system, where a method otherwise can tak
168
162
  Please follow Sorbet's [setup guides](https://sorbet.org/docs/adopting) for best experience.
169
163
 
170
164
  ```ruby
171
- params =
172
- Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer")
165
+ params = Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer")
173
166
 
174
167
  orb.customers.create(**params)
175
168
  ```
@@ -197,8 +190,7 @@ If you want to explicitly send an extra param, you can do so with the `extra_que
197
190
  To make requests to undocumented endpoints, you can make requests using `client.request`. Options on the client will be respected (such as retries) when making this request.
198
191
 
199
192
  ```ruby
200
- response =
201
- client.request(
193
+ response = client.request(
202
194
  method: :post,
203
195
  path: '/undocumented/endpoint',
204
196
  query: {"dog": "woof"},
data/SECURITY.md ADDED
@@ -0,0 +1,27 @@
1
+ # Security Policy
2
+
3
+ ## Reporting Security Issues
4
+
5
+ This SDK is generated by [Stainless Software Inc](http://stainless.com). Stainless takes security seriously, and encourages you to report any security vulnerability promptly so that appropriate action can be taken.
6
+
7
+ To report a security issue, please contact the Stainless team at security@stainless.com.
8
+
9
+ ## Responsible Disclosure
10
+
11
+ We appreciate the efforts of security researchers and individuals who help us maintain the security of
12
+ SDKs we generate. If you believe you have found a security vulnerability, please adhere to responsible
13
+ disclosure practices by allowing us a reasonable amount of time to investigate and address the issue
14
+ before making any information public.
15
+
16
+ ## Reporting Non-SDK Related Security Issues
17
+
18
+ If you encounter security issues that are not directly related to SDKs but pertain to the services
19
+ or products provided by Orb please follow the respective company's security reporting guidelines.
20
+
21
+ ### Orb Terms and Policies
22
+
23
+ Please contact team@withorb.com for any questions or concerns regarding security of our services.
24
+
25
+ ---
26
+
27
+ Thank you for helping us keep the SDKs and systems they interact with secure.
data/lib/orb/client.rb CHANGED
@@ -76,7 +76,8 @@ module Orb
76
76
  #
77
77
  # @param api_key [String, nil] Defaults to `ENV["ORB_API_KEY"]`
78
78
  #
79
- # @param base_url [String, nil] Override the default base URL for the API, e.g., `"https://api.example.com/v2/"`
79
+ # @param base_url [String, nil] Override the default base URL for the API, e.g.,
80
+ # `"https://api.example.com/v2/"`. Defaults to `ENV["ORB_BASE_URL"]`
80
81
  #
81
82
  # @param max_retries [Integer] Max number of retries to attempt after a failed retryable request.
82
83
  #
@@ -89,7 +90,7 @@ module Orb
89
90
  # @param idempotency_header [String]
90
91
  def initialize(
91
92
  api_key: ENV["ORB_API_KEY"],
92
- base_url: nil,
93
+ base_url: ENV["ORB_BASE_URL"],
93
94
  max_retries: DEFAULT_MAX_RETRIES,
94
95
  timeout: DEFAULT_TIMEOUT_IN_SECONDS,
95
96
  initial_retry_delay: DEFAULT_INITIAL_RETRY_DELAY,
@@ -22,33 +22,6 @@ module Orb
22
22
  # @return [PaginationMetadata]
23
23
  attr_accessor :pagination_metadata
24
24
 
25
- # @api private
26
- #
27
- # @param client [Orb::Internal::Transport::BaseClient]
28
- # @param req [Hash{Symbol=>Object}]
29
- # @param headers [Hash{String=>String}, Net::HTTPHeader]
30
- # @param page_data [Hash{Symbol=>Object}]
31
- def initialize(client:, req:, headers:, page_data:)
32
- super
33
- model = req.fetch(:model)
34
-
35
- case page_data
36
- in {data: Array | nil => data}
37
- @data = data&.map { Orb::Internal::Type::Converter.coerce(model, _1) }
38
- else
39
- end
40
-
41
- case page_data
42
- in {pagination_metadata: Hash | nil => pagination_metadata}
43
- @pagination_metadata =
44
- Orb::Internal::Type::Converter.coerce(
45
- Orb::Internal::Page::PaginationMetadata,
46
- pagination_metadata
47
- )
48
- else
49
- end
50
- end
51
-
52
25
  # @return [Boolean]
53
26
  def next_page?
54
27
  !pagination_metadata&.next_cursor.nil?
@@ -73,19 +46,48 @@ module Orb
73
46
  unless block_given?
74
47
  raise ArgumentError.new("A block must be given to ##{__method__}")
75
48
  end
49
+
76
50
  page = self
77
51
  loop do
78
- page.data&.each { blk.call(_1) }
52
+ page.data&.each(&blk)
53
+
79
54
  break unless page.next_page?
80
55
  page = page.next_page
81
56
  end
82
57
  end
83
58
 
59
+ # @api private
60
+ #
61
+ # @param client [Orb::Internal::Transport::BaseClient]
62
+ # @param req [Hash{Symbol=>Object}]
63
+ # @param headers [Hash{String=>String}, Net::HTTPHeader]
64
+ # @param page_data [Hash{Symbol=>Object}]
65
+ def initialize(client:, req:, headers:, page_data:)
66
+ super
67
+
68
+ case page_data
69
+ in {data: Array | nil => data}
70
+ @data = data&.map { Orb::Internal::Type::Converter.coerce(@model, _1) }
71
+ else
72
+ end
73
+ case page_data
74
+ in {pagination_metadata: Hash | nil => pagination_metadata}
75
+ @pagination_metadata =
76
+ Orb::Internal::Type::Converter.coerce(
77
+ Orb::Internal::Page::PaginationMetadata,
78
+ pagination_metadata
79
+ )
80
+ else
81
+ end
82
+ end
83
+
84
+ # @api private
85
+ #
84
86
  # @return [String]
85
87
  def inspect
86
- # rubocop:disable Layout/LineLength
87
- "#<#{self.class}:0x#{object_id.to_s(16)} data=#{data.inspect} pagination_metadata=#{pagination_metadata.inspect}>"
88
- # rubocop:enable Layout/LineLength
88
+ model = Orb::Internal::Type::Converter.inspect(@model, depth: 1)
89
+
90
+ "#<#{self.class}[#{model}]:0x#{object_id.to_s(16)}>"
89
91
  end
90
92
 
91
93
  class PaginationMetadata < Orb::Internal::Type::BaseModel
@@ -93,7 +93,11 @@ module Orb
93
93
  URI.join(url, response_headers["location"])
94
94
  rescue ArgumentError
95
95
  message = "Server responded with status #{status} but no valid location header."
96
- raise Orb::Errors::APIConnectionError.new(url: url, message: message)
96
+ raise Orb::Errors::APIConnectionError.new(
97
+ url: url,
98
+ response: response_headers,
99
+ message: message
100
+ )
97
101
  end
98
102
 
99
103
  request = {**request, url: location}
@@ -101,7 +105,11 @@ module Orb
101
105
  case [url.scheme, location.scheme]
102
106
  in ["https", "http"]
103
107
  message = "Tried to redirect to a insecure URL"
104
- raise Orb::Errors::APIConnectionError.new(url: url, message: message)
108
+ raise Orb::Errors::APIConnectionError.new(
109
+ url: url,
110
+ response: response_headers,
111
+ message: message
112
+ )
105
113
  else
106
114
  nil
107
115
  end
@@ -245,7 +253,7 @@ module Orb
245
253
 
246
254
  if @idempotency_header &&
247
255
  !headers.key?(@idempotency_header) &&
248
- !Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase)
256
+ (!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key))
249
257
  headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key }
250
258
  end
251
259
 
@@ -350,7 +358,7 @@ module Orb
350
358
  self.class.reap_connection!(status, stream: stream)
351
359
 
352
360
  message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
353
- raise Orb::Errors::APIConnectionError.new(url: url, message: message)
361
+ raise Orb::Errors::APIConnectionError.new(url: url, response: response, message: message)
354
362
  in 300..399
355
363
  self.class.reap_connection!(status, stream: stream)
356
364
 
@@ -460,6 +468,8 @@ module Orb
460
468
  end
461
469
  end
462
470
 
471
+ # @api private
472
+ #
463
473
  # @return [String]
464
474
  def inspect
465
475
  # rubocop:disable Layout/LineLength
@@ -149,7 +149,7 @@ module Orb
149
149
  break if finished
150
150
 
151
151
  rsp.read_body do |bytes|
152
- y << bytes
152
+ y << bytes.force_encoding(Encoding::BINARY)
153
153
  break if finished
154
154
 
155
155
  self.class.calibrate_socket_timeout(conn, deadline)
@@ -13,6 +13,10 @@ module Orb
13
13
  class ArrayOf
14
14
  include Orb::Internal::Type::Converter
15
15
 
16
+ private_class_method :new
17
+
18
+ # @overload [](type_info, spec = {})
19
+ #
16
20
  # @param type_info [Hash{Symbol=>Object}, Proc, Orb::Internal::Type::Converter, Class]
17
21
  #
18
22
  # @param spec [Hash{Symbol=>Object}] .
@@ -24,7 +28,7 @@ module Orb
24
28
  # @option spec [Proc] :union
25
29
  #
26
30
  # @option spec [Boolean] :"nil?"
27
- def self.[](type_info, spec = {}) = new(type_info, spec)
31
+ def self.[](...) = new(...)
28
32
 
29
33
  # @param other [Object]
30
34
  #
@@ -120,7 +124,18 @@ module Orb
120
124
  # @option spec [Boolean] :"nil?"
121
125
  def initialize(type_info, spec = {})
122
126
  @item_type_fn = Orb::Internal::Type::Converter.type_info(type_info || spec)
123
- @nilable = spec[:nil?]
127
+ @nilable = spec.fetch(:nil?, false)
128
+ end
129
+
130
+ # @api private
131
+ #
132
+ # @param depth [Integer]
133
+ #
134
+ # @return [String]
135
+ def inspect(depth: 0)
136
+ items = Orb::Internal::Type::Converter.inspect(item_type, depth: depth.succ)
137
+
138
+ "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]"
124
139
  end
125
140
  end
126
141
  end
@@ -63,7 +63,7 @@ module Orb
63
63
 
64
64
  setter = "#{name_sym}="
65
65
  api_name = info.fetch(:api_name, name_sym)
66
- nilable = info[:nil?]
66
+ nilable = info.fetch(:nil?, false)
67
67
  const = required && !nilable ? info.fetch(:const, Orb::Internal::OMIT) : Orb::Internal::OMIT
68
68
 
69
69
  [name_sym, setter].each { undef_method(_1) } if known_fields.key?(name_sym)
@@ -338,6 +338,27 @@ module Orb
338
338
  .to_h
339
339
  end
340
340
 
341
+ class << self
342
+ # @param model [Orb::Internal::Type::BaseModel]
343
+ #
344
+ # @return [Hash{Symbol=>Object}]
345
+ def walk(model)
346
+ walk = ->(x) do
347
+ case x
348
+ in Orb::Internal::Type::BaseModel
349
+ walk.call(x.to_h)
350
+ in Hash
351
+ x.transform_values(&walk)
352
+ in Array
353
+ x.map(&walk)
354
+ else
355
+ x
356
+ end
357
+ end
358
+ walk.call(model)
359
+ end
360
+ end
361
+
341
362
  # @param a [Object]
342
363
  #
343
364
  # @return [String]
@@ -361,15 +382,38 @@ module Orb
361
382
  end
362
383
  end
363
384
 
364
- # @return [String]
365
- def inspect
366
- rows = self.class.known_fields.keys.map do
367
- "#{_1}=#{@data.key?(_1) ? public_send(_1) : ''}"
368
- rescue Orb::Errors::ConversionError
369
- "#{_1}=#{@data.fetch(_1)}"
385
+ class << self
386
+ # @api private
387
+ #
388
+ # @param depth [Integer]
389
+ #
390
+ # @return [String]
391
+ def inspect(depth: 0)
392
+ return super() if depth.positive?
393
+
394
+ depth = depth.succ
395
+ deferred = fields.transform_values do |field|
396
+ type, required, nilable = field.fetch_values(:type, :required, :nilable)
397
+ inspected = [
398
+ Orb::Internal::Type::Converter.inspect(type, depth: depth),
399
+ !required || nilable ? "nil" : nil
400
+ ].compact.join(" | ")
401
+ -> { inspected }.tap { _1.define_singleton_method(:inspect) { call } }
402
+ end
403
+
404
+ "#{name}[#{deferred.inspect}]"
370
405
  end
371
- "#<#{self.class.name}:0x#{object_id.to_s(16)} #{rows.join(' ')}>"
372
406
  end
407
+
408
+ # @api private
409
+ #
410
+ # @return [String]
411
+ def to_s = self.class.walk(@data).to_s
412
+
413
+ # @api private
414
+ #
415
+ # @return [String]
416
+ def inspect = "#<#{self.class}:0x#{object_id.to_s(16)} #{self}>"
373
417
  end
374
418
  end
375
419
  end
@@ -36,6 +36,7 @@ module Orb
36
36
  def initialize(client:, req:, headers:, page_data:)
37
37
  @client = client
38
38
  @req = req
39
+ @model = req.fetch(:model)
39
40
  super()
40
41
  end
41
42
 
@@ -11,6 +11,8 @@ module Orb
11
11
  class Boolean
12
12
  extend Orb::Internal::Type::Converter
13
13
 
14
+ private_class_method :new
15
+
14
16
  # @param other [Object]
15
17
  #
16
18
  # @return [Boolean]
@@ -49,6 +49,15 @@ module Orb
49
49
  end
50
50
  end
51
51
 
52
+ # @api private
53
+ #
54
+ # @param depth [Integer]
55
+ #
56
+ # @return [String]
57
+ def inspect(depth: 0)
58
+ super()
59
+ end
60
+
52
61
  # rubocop:enable Lint/UnusedMethodArgument
53
62
 
54
63
  class << self
@@ -240,6 +249,21 @@ module Orb
240
249
  Orb::Internal::Type::Unknown.dump(value, state: state)
241
250
  end
242
251
  end
252
+
253
+ # @api private
254
+ #
255
+ # @param target [Object]
256
+ # @param depth [Integer]
257
+ #
258
+ # @return [String]
259
+ def inspect(target, depth:)
260
+ case target
261
+ in Orb::Internal::Type::Converter
262
+ target.inspect(depth: depth.succ)
263
+ else
264
+ target.inspect
265
+ end
266
+ end
243
267
  end
244
268
  end
245
269
  end
@@ -58,9 +58,9 @@ module Orb
58
58
  #
59
59
  # @return [Boolean]
60
60
  def ==(other)
61
- # rubocop:disable Layout/LineLength
62
- other.is_a?(Module) && other.singleton_class <= Orb::Internal::Type::Enum && other.values.to_set == values.to_set
63
- # rubocop:enable Layout/LineLength
61
+ # rubocop:disable Style/CaseEquality
62
+ Orb::Internal::Type::Enum === other && other.values.to_set == values.to_set
63
+ # rubocop:enable Style/CaseEquality
64
64
  end
65
65
 
66
66
  # @api private
@@ -103,6 +103,22 @@ module Orb
103
103
  # #
104
104
  # # @return [Symbol, Object]
105
105
  # def dump(value, state:) = super
106
+
107
+ # @api private
108
+ #
109
+ # @param depth [Integer]
110
+ #
111
+ # @return [String]
112
+ def inspect(depth: 0)
113
+ if depth.positive?
114
+ return is_a?(Module) ? super() : self.class.name
115
+ end
116
+
117
+ members = values.map { Orb::Internal::Type::Converter.inspect(_1, depth: depth.succ) }
118
+ prefix = is_a?(Module) ? name : self.class.name
119
+
120
+ "#{prefix}[#{members.join(' | ')}]"
121
+ end
106
122
  end
107
123
  end
108
124
  end
@@ -13,6 +13,10 @@ module Orb
13
13
  class HashOf
14
14
  include Orb::Internal::Type::Converter
15
15
 
16
+ private_class_method :new
17
+
18
+ # @overload [](type_info, spec = {})
19
+ #
16
20
  # @param type_info [Hash{Symbol=>Object}, Proc, Orb::Internal::Type::Converter, Class]
17
21
  #
18
22
  # @param spec [Hash{Symbol=>Object}] .
@@ -24,7 +28,7 @@ module Orb
24
28
  # @option spec [Proc] :union
25
29
  #
26
30
  # @option spec [Boolean] :"nil?"
27
- def self.[](type_info, spec = {}) = new(type_info, spec)
31
+ def self.[](...) = new(...)
28
32
 
29
33
  # @param other [Object]
30
34
  #
@@ -140,7 +144,18 @@ module Orb
140
144
  # @option spec [Boolean] :"nil?"
141
145
  def initialize(type_info, spec = {})
142
146
  @item_type_fn = Orb::Internal::Type::Converter.type_info(type_info || spec)
143
- @nilable = spec[:nil?]
147
+ @nilable = spec.fetch(:nil?, false)
148
+ end
149
+
150
+ # @api private
151
+ #
152
+ # @param depth [Integer]
153
+ #
154
+ # @return [String]
155
+ def inspect(depth: 0)
156
+ items = Orb::Internal::Type::Converter.inspect(item_type, depth: depth.succ)
157
+
158
+ "#{self.class}[#{[items, nilable? ? 'nil' : nil].compact.join(' | ')}]"
144
159
  end
145
160
  end
146
161
  end
@@ -11,6 +11,8 @@ module Orb
11
11
  class IOLike
12
12
  extend Orb::Internal::Type::Converter
13
13
 
14
+ private_class_method :new
15
+
14
16
  # @param other [Object]
15
17
  #
16
18
  # @return [Boolean]
@@ -140,9 +140,7 @@ module Orb
140
140
  #
141
141
  # @return [Boolean]
142
142
  def ==(other)
143
- # rubocop:disable Layout/LineLength
144
- other.is_a?(Module) && other.singleton_class <= Orb::Internal::Type::Union && other.derefed_variants == derefed_variants
145
- # rubocop:enable Layout/LineLength
143
+ Orb::Internal::Type::Union === other && other.derefed_variants == derefed_variants
146
144
  end
147
145
 
148
146
  # @api private
@@ -225,6 +223,22 @@ module Orb
225
223
 
226
224
  # rubocop:enable Style/CaseEquality
227
225
  # rubocop:enable Style/HashEachMethods
226
+
227
+ # @api private
228
+ #
229
+ # @param depth [Integer]
230
+ #
231
+ # @return [String]
232
+ def inspect(depth: 0)
233
+ if depth.positive?
234
+ return is_a?(Module) ? super() : self.class.name
235
+ end
236
+
237
+ members = variants.map { Orb::Internal::Type::Converter.inspect(_1, depth: depth.succ) }
238
+ prefix = is_a?(Module) ? name : self.class.name
239
+
240
+ "#{prefix}[#{members.join(' | ')}]"
241
+ end
228
242
  end
229
243
  end
230
244
  end
@@ -13,6 +13,8 @@ module Orb
13
13
 
14
14
  # rubocop:disable Lint/UnusedMethodArgument
15
15
 
16
+ private_class_method :new
17
+
16
18
  # @param other [Object]
17
19
  #
18
20
  # @return [Boolean]
@@ -448,7 +448,7 @@ module Orb
448
448
  else
449
449
  src
450
450
  end
451
- @buf = String.new.b
451
+ @buf = String.new
452
452
  @blk = blk
453
453
  end
454
454
  end
@@ -460,7 +460,7 @@ module Orb
460
460
  # @return [Enumerable<String>]
461
461
  def writable_enum(&blk)
462
462
  Enumerator.new do |y|
463
- buf = String.new.b
463
+ buf = String.new
464
464
  y.define_singleton_method(:write) do
465
465
  self << buf.replace(_1)
466
466
  buf.bytesize
@@ -582,6 +582,27 @@ module Orb
582
582
 
583
583
  # @api private
584
584
  #
585
+ # https://www.iana.org/assignments/character-sets/character-sets.xhtml
586
+ #
587
+ # @param content_type [String]
588
+ # @param text [String]
589
+ def force_charset!(content_type, text:)
590
+ charset = /charset=([^;\s]+)/.match(content_type)&.captures&.first
591
+
592
+ return unless charset
593
+
594
+ begin
595
+ encoding = Encoding.find(charset)
596
+ text.force_encoding(encoding)
597
+ rescue ArgumentError
598
+ nil
599
+ end
600
+ end
601
+
602
+ # @api private
603
+ #
604
+ # Assumes each chunk in stream has `Encoding::BINARY`.
605
+ #
585
606
  # @param headers [Hash{String=>String}, Net::HTTPHeader]
586
607
  # @param stream [Enumerable<String>]
587
608
  # @param suppress_error [Boolean]
@@ -589,7 +610,7 @@ module Orb
589
610
  # @raise [JSON::ParserError]
590
611
  # @return [Object]
591
612
  def decode_content(headers, stream:, suppress_error: false)
592
- case headers["content-type"]
613
+ case (content_type = headers["content-type"])
593
614
  in %r{^application/(?:vnd\.api\+)?json}
594
615
  json = stream.to_a.join
595
616
  begin
@@ -606,11 +627,10 @@ module Orb
606
627
  in %r{^text/event-stream}
607
628
  lines = decode_lines(stream)
608
629
  decode_sse(lines)
609
- in %r{^text/}
610
- stream.to_a.join
611
630
  else
612
- # TODO: parsing other response types
613
- StringIO.new(stream.to_a.join)
631
+ text = stream.to_a.join
632
+ force_charset!(content_type, text: text)
633
+ StringIO.new(text)
614
634
  end
615
635
  end
616
636
  end
@@ -675,12 +695,17 @@ module Orb
675
695
  class << self
676
696
  # @api private
677
697
  #
698
+ # Assumes Strings have been forced into having `Encoding::BINARY`.
699
+ #
700
+ # This decoder is responsible for reassembling lines split across multiple
701
+ # fragments.
702
+ #
678
703
  # @param enum [Enumerable<String>]
679
704
  #
680
705
  # @return [Enumerable<String>]
681
706
  def decode_lines(enum)
682
707
  re = /(\r\n|\r|\n)/
683
- buffer = String.new.b
708
+ buffer = String.new
684
709
  cr_seen = nil
685
710
 
686
711
  chain_fused(enum) do |y|
@@ -711,6 +736,8 @@ module Orb
711
736
  #
712
737
  # https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream
713
738
  #
739
+ # Assumes that `lines` has been decoded with `#decode_lines`.
740
+ #
714
741
  # @param lines [Enumerable<String>]
715
742
  #
716
743
  # @return [Enumerable<Hash{Symbol=>Object}>]
@@ -734,7 +761,7 @@ module Orb
734
761
  in "event"
735
762
  current.merge!(event: value)
736
763
  in "data"
737
- (current[:data] ||= String.new.b) << (value << "\n")
764
+ (current[:data] ||= String.new) << (value << "\n")
738
765
  in "id" unless value.include?("\0")
739
766
  current.merge!(id: value)
740
767
  in "retry" if /^\d+$/ =~ value