diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 0f02ea1c..3d2ac0bd 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.39" + ".": "0.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 937bb808..c9c6064a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 0.1.0 (2025-04-08) + +Full Changelog: [v0.1.0-alpha.39...v0.1.0](https://github.com/orbcorp/orb-ruby/compare/v0.1.0-alpha.39...v0.1.0) + +### Bug Fixes + +* raise connection error for errors that result from HTTP transports ([#244](https://github.com/orbcorp/orb-ruby/issues/244)) ([1213d2d](https://github.com/orbcorp/orb-ruby/commit/1213d2de5c7b0a80f1c96b8fed31a048ad57e2eb)) + + +### Chores + +* add README docs for using solargraph when installing gem from git ([#242](https://github.com/orbcorp/orb-ruby/issues/242)) ([48b892e](https://github.com/orbcorp/orb-ruby/commit/48b892e3b45412875d25c5683f0b7e1fd23d20ee)) +* easier to read examples in README.md ([#240](https://github.com/orbcorp/orb-ruby/issues/240)) ([eb00ee4](https://github.com/orbcorp/orb-ruby/commit/eb00ee4e8564a44dad9320bf89f3a656764a38d9)) +* **internal:** codegen related update ([#238](https://github.com/orbcorp/orb-ruby/issues/238)) ([fd54e32](https://github.com/orbcorp/orb-ruby/commit/fd54e32135d6ffbfbd3956b5505773169f5fca9f)) +* **internal:** codegen related update ([#243](https://github.com/orbcorp/orb-ruby/issues/243)) ([edfc0bf](https://github.com/orbcorp/orb-ruby/commit/edfc0bfb29f6989cb5d249f9dcfd118c115a35a9)) +* make client tests look prettier ([#241](https://github.com/orbcorp/orb-ruby/issues/241)) ([060c286](https://github.com/orbcorp/orb-ruby/commit/060c2864f0458bfc26030e97effa3340a0060dd0)) + ## 0.1.0-alpha.39 (2025-04-07) Full Changelog: [v0.1.0-alpha.38...v0.1.0-alpha.39](https://github.com/orbcorp/orb-ruby/compare/v0.1.0-alpha.38...v0.1.0-alpha.39) diff --git a/Gemfile.lock b/Gemfile.lock index 95bba502..54710652 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - orb-billing (0.1.0.pre.alpha.38) + orb-billing (0.1.0.pre.alpha.39) connection_pool GEM diff --git a/README.md b/README.md index 387d7a2e..2ae58f2f 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application ```ruby -gem "orb-billing", "~> 0.1.0.pre.alpha.38" +gem "orb-billing", "~> 0.1.0.pre.alpha.39" ``` @@ -148,6 +148,13 @@ After Solargraph is installed, **you must populate its index** either via the pr bundle exec solargraph gems ``` +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. + +```yaml +include: + - 'vendor/bundle/ruby/*/gems/orb-billing-*/lib/**/*.rb' +``` + Otherwise Solargraph will not be able to provide type information or auto-completion for any non-indexed libraries. ### Sorbet @@ -161,7 +168,8 @@ Due to limitations with the Sorbet type system, where a method otherwise can tak Please follow Sorbet's [setup guides](https://sorbet.org/docs/adopting) for best experience. ```ruby -params = Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer") +params = + Orb::Models::CustomerCreateParams.new(email: "example-customer@withorb.com", name: "My Customer") orb.customers.create(**params) ``` diff --git a/lib/orb.rb b/lib/orb.rb index 312f283f..3afbdb95 100644 --- a/lib/orb.rb +++ b/lib/orb.rb @@ -40,6 +40,7 @@ require_relative "orb/internal/type/converter" require_relative "orb/internal/type/unknown" require_relative "orb/internal/type/boolean" +require_relative "orb/internal/type/io_like" require_relative "orb/internal/type/enum" require_relative "orb/internal/type/union" require_relative "orb/internal/type/array_of" diff --git a/lib/orb/internal/transport/base_client.rb b/lib/orb/internal/transport/base_client.rb index c2959112..83d779fc 100644 --- a/lib/orb/internal/transport/base_client.rb +++ b/lib/orb/internal/transport/base_client.rb @@ -380,7 +380,7 @@ def initialize( in (400..) | Orb::Errors::APIConnectionError self.class.reap_connection!(status, stream: stream) - delay = retry_delay(response, retry_count: retry_count) + delay = retry_delay(response || {}, retry_count: retry_count) sleep(delay) send_request( diff --git a/lib/orb/internal/transport/pooled_net_requester.rb b/lib/orb/internal/transport/pooled_net_requester.rb index 3291cd5d..7db786fb 100644 --- a/lib/orb/internal/transport/pooled_net_requester.rb +++ b/lib/orb/internal/transport/pooled_net_requester.rb @@ -54,7 +54,7 @@ def calibrate_socket_timeout(conn, deadline) # @param blk [Proc] # # @yieldparam [String] - # @return [Net::HTTPGenericRequest] + # @return [Array(Net::HTTPGenericRequest, Proc)] def build_request(request, &blk) method, url, headers, body = request.fetch_values(:method, :url, :headers, :body) req = Net::HTTPGenericRequest.new( @@ -75,12 +75,12 @@ def build_request(request, &blk) in StringIO req["content-length"] ||= body.size.to_s unless req["transfer-encoding"] req.body_stream = Orb::Internal::Util::ReadIOAdapter.new(body, &blk) - in IO | Enumerator + in Pathname | IO | Enumerator req["transfer-encoding"] ||= "chunked" unless req["content-length"] req.body_stream = Orb::Internal::Util::ReadIOAdapter.new(body, &blk) end - req + [req, req.body_stream&.method(:close)] end end @@ -123,13 +123,17 @@ def build_request(request, &blk) def execute(request) url, deadline = request.fetch_values(:url, :deadline) + req = nil eof = false finished = false + closing = nil + + # rubocop:disable Metrics/BlockLength enum = Enumerator.new do |y| with_pool(url, deadline: deadline) do |conn| next if finished - req = self.class.build_request(request) do + req, closing = self.class.build_request(request) do self.class.calibrate_socket_timeout(conn, deadline) end @@ -154,8 +158,11 @@ def execute(request) end end rescue Timeout::Error - raise Orb::Errors::APITimeoutError + raise Orb::Errors::APITimeoutError.new(url: url, request: req) + rescue StandardError + raise Orb::Errors::APIConnectionError.new(url: url, request: req) end + # rubocop:enable Metrics/BlockLength conn, _, response = enum.next body = Orb::Internal::Util.fused_enum(enum, external: true) do @@ -165,7 +172,9 @@ def execute(request) rescue StopIteration nil end + ensure conn.finish if !eof && conn&.started? + closing&.call end [Integer(response.code), response, (response.body = body)] end diff --git a/lib/orb/internal/type/array_of.rb b/lib/orb/internal/type/array_of.rb index eb6b8f0f..bcbf948e 100644 --- a/lib/orb/internal/type/array_of.rb +++ b/lib/orb/internal/type/array_of.rb @@ -79,10 +79,20 @@ def coerce(value, state:) # # @param value [Array, Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Array, Object] - def dump(value) + def dump(value, state:) target = item_type - value.is_a?(Array) ? value.map { Orb::Internal::Type::Converter.dump(target, _1) } : super + if value.is_a?(Array) + value.map do + Orb::Internal::Type::Converter.dump(target, _1, state: state) + end + else + super + end end # @api private diff --git a/lib/orb/internal/type/base_model.rb b/lib/orb/internal/type/base_model.rb index 1250f71f..41cba58c 100644 --- a/lib/orb/internal/type/base_model.rb +++ b/lib/orb/internal/type/base_model.rb @@ -252,8 +252,12 @@ def coerce(value, state:) # # @param value [Orb::Internal::Type::BaseModel, Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Hash{Object=>Object}, Object] - def dump(value) + def dump(value, state:) unless (coerced = Orb::Internal::Util.coerce_hash(value)).is_a?(Hash) return super end @@ -264,7 +268,7 @@ def dump(value) name = key.is_a?(String) ? key.to_sym : key case (field = known_fields[name]) in nil - acc.store(name, super(val)) + acc.store(name, super(val, state: state)) else api_name, mode, type_fn = field.fetch_values(:api_name, :mode, :type_fn) case mode @@ -272,7 +276,7 @@ def dump(value) next else target = type_fn.call - acc.store(api_name, Orb::Internal::Type::Converter.dump(target, val)) + acc.store(api_name, Orb::Internal::Type::Converter.dump(target, val, state: state)) end end end @@ -337,12 +341,12 @@ def deconstruct_keys(keys) # @param a [Object] # # @return [String] - def to_json(*a) = self.class.dump(self).to_json(*a) + def to_json(*a) = Orb::Internal::Type::Converter.dump(self.class, self).to_json(*a) # @param a [Object] # # @return [String] - def to_yaml(*a) = self.class.dump(self).to_yaml(*a) + def to_yaml(*a) = Orb::Internal::Type::Converter.dump(self.class, self).to_yaml(*a) # Create a new instance of a model. # diff --git a/lib/orb/internal/type/boolean.rb b/lib/orb/internal/type/boolean.rb index 5dd36e40..d1a6b900 100644 --- a/lib/orb/internal/type/boolean.rb +++ b/lib/orb/internal/type/boolean.rb @@ -45,8 +45,12 @@ def coerce(value, state:) # # # # @param value [Boolean, Object] # # + # # @param state [Hash{Symbol=>Object}] . + # # + # # @option state [Boolean] :can_retry + # # # # @return [Boolean, Object] - # def dump(value) = super + # def dump(value, state:) = super end end end diff --git a/lib/orb/internal/type/converter.rb b/lib/orb/internal/type/converter.rb index f3ba827a..7a9bd683 100644 --- a/lib/orb/internal/type/converter.rb +++ b/lib/orb/internal/type/converter.rb @@ -26,15 +26,24 @@ def coerce(value, state:) = (raise NotImplementedError) # # @param value [Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Object] - def dump(value) + def dump(value, state:) case value in Array - value.map { Orb::Internal::Type::Unknown.dump(_1) } + value.map { Orb::Internal::Type::Unknown.dump(_1, state: state) } in Hash - value.transform_values { Orb::Internal::Type::Unknown.dump(_1) } + value.transform_values { Orb::Internal::Type::Unknown.dump(_1, state: state) } in Orb::Internal::Type::BaseModel - value.class.dump(value) + value.class.dump(value, state: state) + in StringIO + value.string + in Pathname | IO + state[:can_retry] = false if value.is_a?(IO) + Orb::Internal::Util::SerializationAdapter.new(value) else value end @@ -182,7 +191,7 @@ def coerce( rescue ArgumentError, TypeError => e raise e if strictness == :strong end - in -> { _1 <= IO } if value.is_a?(String) + in -> { _1 <= StringIO } if value.is_a?(String) exactness[:yes] += 1 return StringIO.new(value.b) else @@ -207,13 +216,21 @@ def coerce( # @api private # # @param target [Orb::Internal::Type::Converter, Class] + # # @param value [Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Object] - def dump(target, value) - # rubocop:disable Layout/LineLength - target.is_a?(Orb::Internal::Type::Converter) ? target.dump(value) : Orb::Internal::Type::Unknown.dump(value) - # rubocop:enable Layout/LineLength + def dump(target, value, state: {can_retry: true}) + case target + in Orb::Internal::Type::Converter + target.dump(value, state: state) + else + Orb::Internal::Type::Unknown.dump(value, state: state) + end end end end diff --git a/lib/orb/internal/type/enum.rb b/lib/orb/internal/type/enum.rb index 43eac2f8..bd9592d0 100644 --- a/lib/orb/internal/type/enum.rb +++ b/lib/orb/internal/type/enum.rb @@ -97,8 +97,12 @@ def coerce(value, state:) # # # # @param value [Symbol, Object] # # + # # @param state [Hash{Symbol=>Object}] . + # # + # # @option state [Boolean] :can_retry + # # # # @return [Symbol, Object] - # def dump(value) = super + # def dump(value, state:) = super end end end diff --git a/lib/orb/internal/type/hash_of.rb b/lib/orb/internal/type/hash_of.rb index 55b83b05..44173458 100644 --- a/lib/orb/internal/type/hash_of.rb +++ b/lib/orb/internal/type/hash_of.rb @@ -99,12 +99,16 @@ def coerce(value, state:) # # @param value [Hash{Object=>Object}, Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Hash{Symbol=>Object}, Object] - def dump(value) + def dump(value, state:) target = item_type if value.is_a?(Hash) value.transform_values do - Orb::Internal::Type::Converter.dump(target, _1) + Orb::Internal::Type::Converter.dump(target, _1, state: state) end else super diff --git a/lib/orb/internal/type/io_like.rb b/lib/orb/internal/type/io_like.rb new file mode 100644 index 00000000..fe821f55 --- /dev/null +++ b/lib/orb/internal/type/io_like.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module Orb + module Internal + module Type + # @api private + # + # @abstract + # + # Either `Pathname` or `StringIO`. + class IOLike + extend Orb::Internal::Type::Converter + + # @param other [Object] + # + # @return [Boolean] + def self.===(other) + case other + in StringIO | Pathname | IO + true + else + false + end + end + + # @param other [Object] + # + # @return [Boolean] + def self.==(other) = other.is_a?(Class) && other <= Orb::Internal::Type::IOLike + + class << self + # @api private + # + # @param value [StringIO, String, Object] + # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean, :strong] :strictness + # + # @option state [Hash{Symbol=>Object}] :exactness + # + # @option state [Integer] :branched + # + # @return [StringIO, Object] + def coerce(value, state:) + exactness = state.fetch(:exactness) + case value + in String + exactness[:yes] += 1 + StringIO.new(value) + in StringIO + exactness[:yes] += 1 + value + else + exactness[:no] += 1 + value + end + end + + # @!parse + # # @api private + # # + # # @param value [Pathname, StringIO, IO, String, Object] + # # + # # @param state [Hash{Symbol=>Object}] . + # # + # # @option state [Boolean] :can_retry + # # + # # @return [Pathname, StringIO, IO, String, Object] + # def dump(value, state:) = super + end + end + end + end +end diff --git a/lib/orb/internal/type/request_parameters.rb b/lib/orb/internal/type/request_parameters.rb index fd5a5b4b..65764d03 100644 --- a/lib/orb/internal/type/request_parameters.rb +++ b/lib/orb/internal/type/request_parameters.rb @@ -26,9 +26,18 @@ module Converter # # @return [Array(Object, Hash{Symbol=>Object})] def dump_request(params) - case (dumped = dump(params)) + state = {can_retry: true} + case (dumped = dump(params, state: state)) in Hash - [dumped.except(:request_options), dumped[:request_options]] + options = Orb::Internal::Util.coerce_hash(dumped[:request_options]) + request_options = + case [options, state.fetch(:can_retry)] + in [Hash | nil, false] + {**options.to_h, max_retries: 0} + else + options + end + [dumped.except(:request_options), request_options] else [dumped, nil] end diff --git a/lib/orb/internal/type/union.rb b/lib/orb/internal/type/union.rb index 24d166cb..36474e0c 100644 --- a/lib/orb/internal/type/union.rb +++ b/lib/orb/internal/type/union.rb @@ -205,15 +205,19 @@ def coerce(value, state:) # # @param value [Object] # + # @param state [Hash{Symbol=>Object}] . + # + # @option state [Boolean] :can_retry + # # @return [Object] - def dump(value) + def dump(value, state:) if (target = resolve_variant(value)) - return Orb::Internal::Type::Converter.dump(target, value) + return Orb::Internal::Type::Converter.dump(target, value, state: state) end known_variants.each do target = _2.call - return Orb::Internal::Type::Converter.dump(target, value) if target === value + return Orb::Internal::Type::Converter.dump(target, value, state: state) if target === value end super diff --git a/lib/orb/internal/type/unknown.rb b/lib/orb/internal/type/unknown.rb index d41d33b1..36966379 100644 --- a/lib/orb/internal/type/unknown.rb +++ b/lib/orb/internal/type/unknown.rb @@ -47,8 +47,12 @@ def coerce(value, state:) # # # # @param value [Object] # # + # # @param state [Hash{Symbol=>Object}] . + # # + # # @option state [Boolean] :can_retry + # # # # @return [Object] - # def dump(value) = super + # def dump(value, state:) = super end # rubocop:enable Lint/UnusedMethodArgument diff --git a/lib/orb/internal/util.rb b/lib/orb/internal/util.rb index 75239677..62195f31 100644 --- a/lib/orb/internal/util.rb +++ b/lib/orb/internal/util.rb @@ -122,7 +122,7 @@ def coerce_float(input) # @return [Hash{Object=>Object}, Object] def coerce_hash(input) case input - in NilClass | Array | Set | Enumerator + in NilClass | Array | Set | Enumerator | StringIO | IO input else input.respond_to?(:to_h) ? input.to_h : input @@ -348,10 +348,47 @@ def normalized_headers(*headers) end end + # @api private + class SerializationAdapter + # @return [Pathname, IO] + attr_reader :inner + + # @param a [Object] + # + # @return [String] + def to_json(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_json(*a) + + # @param a [Object] + # + # @return [String] + def to_yaml(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_yaml(*a) + + # @api private + # + # @param inner [Pathname, IO] + def initialize(inner) = @inner = inner + end + # @api private # # An adapter that satisfies the IO interface required by `::IO.copy_stream` class ReadIOAdapter + # @api private + # + # @return [Boolean, nil] + def close? = @closing + + # @api private + def close + case @stream + in Enumerator + Orb::Internal::Util.close_fused!(@stream) + in IO if close? + @stream.close + else + end + end + # @api private # # @param max_len [Integer, nil] @@ -396,12 +433,21 @@ def read(max_len = nil, out_string = nil) # @api private # - # @param stream [String, IO, StringIO, Enumerable] + # @param src [String, Pathname, StringIO, Enumerable] # @param blk [Proc] # # @yieldparam [String] - def initialize(stream, &blk) - @stream = stream.is_a?(String) ? StringIO.new(stream) : stream + def initialize(src, &blk) + @stream = + case src + in String + StringIO.new(src) + in Pathname + @closing = true + src.open(binmode: true) + else + src + end @buf = String.new.b @blk = blk end @@ -414,9 +460,10 @@ class << self # @return [Enumerable] def writable_enum(&blk) Enumerator.new do |y| + buf = String.new.b y.define_singleton_method(:write) do - self << _1.clone - _1.bytesize + self << buf.replace(_1) + buf.bytesize end blk.call(y) @@ -431,29 +478,39 @@ class << self # @param boundary [String] # @param key [Symbol, String] # @param val [Object] - private def write_multipart_chunk(y, boundary:, key:, val:) + # @param closing [Array] + private def write_multipart_chunk(y, boundary:, key:, val:, closing:) + val = val.inner if val.is_a?(Orb::Internal::Util::SerializationAdapter) + y << "--#{boundary}\r\n" y << "Content-Disposition: form-data" unless key.nil? name = ERB::Util.url_encode(key.to_s) y << "; name=\"#{name}\"" end - if val.is_a?(IO) + case val + in Pathname | IO filename = ERB::Util.url_encode(File.basename(val.to_path)) y << "; filename=\"#{filename}\"" + else end y << "\r\n" case val + in Pathname + y << "Content-Type: application/octet-stream\r\n\r\n" + io = val.open(binmode: true) + closing << io.method(:close) + IO.copy_stream(io, y) in IO y << "Content-Type: application/octet-stream\r\n\r\n" - IO.copy_stream(val.tap(&:rewind), y) + IO.copy_stream(val, y) in StringIO y << "Content-Type: application/octet-stream\r\n\r\n" y << val.string in String y << "Content-Type: application/octet-stream\r\n\r\n" y << val.to_s - in true | false | Integer | Float | Symbol + in _ if primitive?(val) y << "Content-Type: text/plain\r\n\r\n" y << val.to_s else @@ -471,6 +528,7 @@ class << self private def encode_multipart_streaming(body) boundary = SecureRandom.urlsafe_base64(60) + closing = [] strio = writable_enum do |y| case body in Hash @@ -478,19 +536,20 @@ class << self case val in Array if val.all? { primitive?(_1) } val.each do |v| - write_multipart_chunk(y, boundary: boundary, key: key, val: v) + write_multipart_chunk(y, boundary: boundary, key: key, val: v, closing: closing) end else - write_multipart_chunk(y, boundary: boundary, key: key, val: val) + write_multipart_chunk(y, boundary: boundary, key: key, val: val, closing: closing) end end else - write_multipart_chunk(y, boundary: boundary, key: nil, val: body) + write_multipart_chunk(y, boundary: boundary, key: nil, val: body, closing: closing) end y << "--#{boundary}--\r\n" end - [boundary, strio] + fused_io = fused_enum(strio) { closing.each(&:call) } + [boundary, fused_io] end # @api private @@ -501,21 +560,21 @@ class << self # @return [Object] def encode_content(headers, body) content_type = headers["content-type"] + body = body.inner if body.is_a?(Orb::Internal::Util::SerializationAdapter) + case [content_type, body] - in [%r{^application/(?:vnd\.api\+)?json}, _] unless body.nil? + in [%r{^application/(?:vnd\.api\+)?json}, Hash | Array | -> { primitive?(_1) }] [headers, JSON.fast_generate(body)] - in [%r{^application/(?:x-)?jsonl}, Enumerable] + in [%r{^application/(?:x-)?jsonl}, Enumerable] unless body.is_a?(StringIO) || body.is_a?(IO) [headers, body.lazy.map { JSON.fast_generate(_1) }] - in [%r{^multipart/form-data}, Hash | IO | StringIO] + in [%r{^multipart/form-data}, Hash | Pathname | StringIO | IO] boundary, strio = encode_multipart_streaming(body) headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"} [headers, strio] - in [_, IO] - [headers, body.tap(&:rewind)] - in [_, StringIO] - [headers, body.string] in [_, Symbol | Numeric] [headers, body.to_s] + in [_, StringIO] + [headers, body.string] else [headers, body] end diff --git a/lib/orb/version.rb b/lib/orb/version.rb index d473e523..773d6b33 100644 --- a/lib/orb/version.rb +++ b/lib/orb/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Orb - VERSION = "0.1.0.pre.alpha.39" + VERSION = "0.1.0" end diff --git a/rbi/lib/orb/internal/transport/pooled_net_requester.rbi b/rbi/lib/orb/internal/transport/pooled_net_requester.rbi index 0c229b63..6e2db75c 100644 --- a/rbi/lib/orb/internal/transport/pooled_net_requester.rbi +++ b/rbi/lib/orb/internal/transport/pooled_net_requester.rbi @@ -35,7 +35,7 @@ module Orb request: Orb::Internal::Transport::PooledNetRequester::RequestShape, blk: T.proc.params(arg0: String).void ) - .returns(Net::HTTPGenericRequest) + .returns([Net::HTTPGenericRequest, T.proc.void]) end def build_request(request, &blk); end end diff --git a/rbi/lib/orb/internal/type/array_of.rbi b/rbi/lib/orb/internal/type/array_of.rbi index 2ef09f6e..d9ef3137 100644 --- a/rbi/lib/orb/internal/type/array_of.rbi +++ b/rbi/lib/orb/internal/type/array_of.rbi @@ -40,7 +40,7 @@ module Orb T::Array[T.anything], T.anything ), - state: Orb::Internal::Type::Converter::State) + state: Orb::Internal::Type::Converter::CoerceState) .returns(T.any(T::Array[T.anything], T.anything)) end def coerce(value, state:); end @@ -48,10 +48,14 @@ module Orb # @api private sig(:final) do override - .params(value: T.any(T::Array[T.anything], T.anything)) + .params(value: T.any( + T::Array[T.anything], + T.anything + ), + state: Orb::Internal::Type::Converter::DumpState) .returns(T.any(T::Array[T.anything], T.anything)) end - def dump(value); end + def dump(value, state:); end # @api private sig(:final) { returns(Elem) } diff --git a/rbi/lib/orb/internal/type/base_model.rbi b/rbi/lib/orb/internal/type/base_model.rbi index 3260a99d..f525170c 100644 --- a/rbi/lib/orb/internal/type/base_model.rbi +++ b/rbi/lib/orb/internal/type/base_model.rbi @@ -122,7 +122,7 @@ module Orb override .params( value: T.any(Orb::Internal::Type::BaseModel, T::Hash[T.anything, T.anything], T.anything), - state: Orb::Internal::Type::Converter::State + state: Orb::Internal::Type::Converter::CoerceState ) .returns(T.any(T.attached_class, T.anything)) end @@ -131,10 +131,14 @@ module Orb # @api private sig do override - .params(value: T.any(T.attached_class, T.anything)) + .params(value: T.any( + T.attached_class, + T.anything + ), + state: Orb::Internal::Type::Converter::DumpState) .returns(T.any(T::Hash[T.anything, T.anything], T.anything)) end - def dump(value); end + def dump(value, state:); end end # Returns the raw value associated with the given key, if found. Otherwise, nil is diff --git a/rbi/lib/orb/internal/type/boolean.rbi b/rbi/lib/orb/internal/type/boolean.rbi index 7e89e364..51da0ac4 100644 --- a/rbi/lib/orb/internal/type/boolean.rbi +++ b/rbi/lib/orb/internal/type/boolean.rbi @@ -22,16 +22,22 @@ module Orb # @api private sig(:final) do override - .params(value: T.any(T::Boolean, T.anything), state: Orb::Internal::Type::Converter::State) + .params(value: T.any( + T::Boolean, + T.anything + ), + state: Orb::Internal::Type::Converter::CoerceState) .returns(T.any(T::Boolean, T.anything)) end def coerce(value, state:); end # @api private sig(:final) do - override.params(value: T.any(T::Boolean, T.anything)).returns(T.any(T::Boolean, T.anything)) + override + .params(value: T.any(T::Boolean, T.anything), state: Orb::Internal::Type::Converter::DumpState) + .returns(T.any(T::Boolean, T.anything)) end - def dump(value); end + def dump(value, state:); end end end end diff --git a/rbi/lib/orb/internal/type/converter.rbi b/rbi/lib/orb/internal/type/converter.rbi index 3f2b30a6..e134df41 100644 --- a/rbi/lib/orb/internal/type/converter.rbi +++ b/rbi/lib/orb/internal/type/converter.rbi @@ -7,7 +7,7 @@ module Orb module Converter Input = T.type_alias { T.any(Orb::Internal::Type::Converter, T::Class[T.anything]) } - State = + CoerceState = T.type_alias do { strictness: T.any(T::Boolean, Symbol), @@ -16,18 +16,24 @@ module Orb } end + DumpState = T.type_alias { {can_retry: T::Boolean} } + # @api private sig do - overridable.params( - value: T.anything, - state: Orb::Internal::Type::Converter::State - ).returns(T.anything) + overridable + .params(value: T.anything, state: Orb::Internal::Type::Converter::CoerceState) + .returns(T.anything) end def coerce(value, state:); end # @api private - sig { overridable.params(value: T.anything).returns(T.anything) } - def dump(value); end + sig do + overridable.params( + value: T.anything, + state: Orb::Internal::Type::Converter::DumpState + ).returns(T.anything) + end + def dump(value, state:); end class << self # @api private @@ -62,7 +68,7 @@ module Orb params( target: Orb::Internal::Type::Converter::Input, value: T.anything, - state: Orb::Internal::Type::Converter::State + state: Orb::Internal::Type::Converter::CoerceState ) .returns(T.anything) end @@ -91,8 +97,15 @@ module Orb state: {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} ); end # @api private - sig { params(target: Orb::Internal::Type::Converter::Input, value: T.anything).returns(T.anything) } - def self.dump(target, value); end + sig do + params( + target: Orb::Internal::Type::Converter::Input, + value: T.anything, + state: Orb::Internal::Type::Converter::DumpState + ) + .returns(T.anything) + end + def self.dump(target, value, state: {can_retry: true}); end end end end diff --git a/rbi/lib/orb/internal/type/enum.rbi b/rbi/lib/orb/internal/type/enum.rbi index 284212fd..b8e5c81d 100644 --- a/rbi/lib/orb/internal/type/enum.rbi +++ b/rbi/lib/orb/internal/type/enum.rbi @@ -40,14 +40,23 @@ module Orb # of the enum. sig do override - .params(value: T.any(String, Symbol, T.anything), state: Orb::Internal::Type::Converter::State) + .params(value: T.any( + String, + Symbol, + T.anything + ), + state: Orb::Internal::Type::Converter::CoerceState) .returns(T.any(Symbol, T.anything)) end def coerce(value, state:); end # @api private - sig { override.params(value: T.any(Symbol, T.anything)).returns(T.any(Symbol, T.anything)) } - def dump(value); end + sig do + override + .params(value: T.any(Symbol, T.anything), state: Orb::Internal::Type::Converter::DumpState) + .returns(T.any(Symbol, T.anything)) + end + def dump(value, state:); end end end end diff --git a/rbi/lib/orb/internal/type/hash_of.rbi b/rbi/lib/orb/internal/type/hash_of.rbi index 90803134..6c482004 100644 --- a/rbi/lib/orb/internal/type/hash_of.rbi +++ b/rbi/lib/orb/internal/type/hash_of.rbi @@ -38,7 +38,7 @@ module Orb override .params( value: T.any(T::Hash[T.anything, T.anything], T.anything), - state: Orb::Internal::Type::Converter::State + state: Orb::Internal::Type::Converter::CoerceState ) .returns(T.any(Orb::Internal::AnyHash, T.anything)) end @@ -47,10 +47,13 @@ module Orb # @api private sig(:final) do override - .params(value: T.any(T::Hash[T.anything, T.anything], T.anything)) + .params( + value: T.any(T::Hash[T.anything, T.anything], T.anything), + state: Orb::Internal::Type::Converter::DumpState + ) .returns(T.any(Orb::Internal::AnyHash, T.anything)) end - def dump(value); end + def dump(value, state:); end # @api private sig(:final) { returns(Elem) } diff --git a/rbi/lib/orb/internal/type/io_like.rbi b/rbi/lib/orb/internal/type/io_like.rbi new file mode 100644 index 00000000..06ff47b0 --- /dev/null +++ b/rbi/lib/orb/internal/type/io_like.rbi @@ -0,0 +1,49 @@ +# typed: strong + +module Orb + module Internal + module Type + # @api private + # + # Either `Pathname` or `StringIO`. + class IOLike + extend Orb::Internal::Type::Converter + + abstract! + final! + + sig(:final) { params(other: T.anything).returns(T::Boolean) } + def self.===(other); end + + sig(:final) { params(other: T.anything).returns(T::Boolean) } + def self.==(other); end + + class << self + # @api private + sig(:final) do + override + .params(value: T.any( + StringIO, + String, + T.anything + ), + state: Orb::Internal::Type::Converter::CoerceState) + .returns(T.any(StringIO, T.anything)) + end + def coerce(value, state:); end + + # @api private + sig(:final) do + override + .params( + value: T.any(Pathname, StringIO, IO, String, T.anything), + state: Orb::Internal::Type::Converter::DumpState + ) + .returns(T.any(Pathname, StringIO, IO, String, T.anything)) + end + def dump(value, state:); end + end + end + end + end +end diff --git a/rbi/lib/orb/internal/type/union.rbi b/rbi/lib/orb/internal/type/union.rbi index 5a0dd359..18ce6879 100644 --- a/rbi/lib/orb/internal/type/union.rbi +++ b/rbi/lib/orb/internal/type/union.rbi @@ -47,13 +47,21 @@ module Orb # @api private sig do - override.params(value: T.anything, state: Orb::Internal::Type::Converter::State).returns(T.anything) + override.params( + value: T.anything, + state: Orb::Internal::Type::Converter::CoerceState + ).returns(T.anything) end def coerce(value, state:); end # @api private - sig { override.params(value: T.anything).returns(T.anything) } - def dump(value); end + sig do + override.params( + value: T.anything, + state: Orb::Internal::Type::Converter::DumpState + ).returns(T.anything) + end + def dump(value, state:); end end end end diff --git a/rbi/lib/orb/internal/type/unknown.rbi b/rbi/lib/orb/internal/type/unknown.rbi index ba77afcc..2bd89240 100644 --- a/rbi/lib/orb/internal/type/unknown.rbi +++ b/rbi/lib/orb/internal/type/unknown.rbi @@ -23,14 +23,19 @@ module Orb sig(:final) do override.params( value: T.anything, - state: Orb::Internal::Type::Converter::State + state: Orb::Internal::Type::Converter::CoerceState ).returns(T.anything) end def coerce(value, state:); end # @api private - sig(:final) { override.params(value: T.anything).returns(T.anything) } - def dump(value); end + sig(:final) do + override.params( + value: T.anything, + state: Orb::Internal::Type::Converter::DumpState + ).returns(T.anything) + end + def dump(value, state:); end end end end diff --git a/rbi/lib/orb/internal/util.rbi b/rbi/lib/orb/internal/util.rbi index d3167fef..02bad0ea 100644 --- a/rbi/lib/orb/internal/util.rbi +++ b/rbi/lib/orb/internal/util.rbi @@ -140,10 +140,34 @@ module Orb def normalized_headers(*headers); end end + # @api private + class SerializationAdapter + sig { returns(T.any(Pathname, IO)) } + attr_reader :inner + + sig { params(a: T.anything).returns(String) } + def to_json(*a); end + + sig { params(a: T.anything).returns(String) } + def to_yaml(*a); end + + # @api private + sig { params(inner: T.any(Pathname, IO)).returns(T.attached_class) } + def self.new(inner); end + end + # @api private # # An adapter that satisfies the IO interface required by `::IO.copy_stream` class ReadIOAdapter + # @api private + sig { returns(T.nilable(T::Boolean)) } + def close?; end + + # @api private + sig { void } + def close; end + # @api private sig { params(max_len: T.nilable(Integer)).returns(String) } private def read_enum(max_len); end @@ -155,12 +179,12 @@ module Orb # @api private sig do params( - stream: T.any(String, IO, StringIO, T::Enumerable[String]), + src: T.any(String, Pathname, StringIO, T::Enumerable[String]), blk: T.proc.params(arg0: String).void ) .returns(T.attached_class) end - def self.new(stream, &blk); end + def self.new(src, &blk); end end class << self @@ -171,9 +195,16 @@ module Orb class << self # @api private sig do - params(y: Enumerator::Yielder, boundary: String, key: T.any(Symbol, String), val: T.anything).void + params( + y: Enumerator::Yielder, + boundary: String, + key: T.any(Symbol, String), + val: T.anything, + closing: T::Array[T.proc.void] + ) + .void end - private def write_multipart_chunk(y, boundary:, key:, val:); end + private def write_multipart_chunk(y, boundary:, key:, val:, closing:); end # @api private sig { params(body: T.anything).returns([String, T::Enumerable[String]]) } diff --git a/sig/orb/internal/transport/pooled_net_requester.rbs b/sig/orb/internal/transport/pooled_net_requester.rbs index 418aa202..67ec8296 100644 --- a/sig/orb/internal/transport/pooled_net_requester.rbs +++ b/sig/orb/internal/transport/pooled_net_requester.rbs @@ -21,7 +21,7 @@ module Orb Orb::Internal::Transport::PooledNetRequester::request request ) { (String arg0) -> void - } -> top + } -> [top, (^-> void)] private def with_pool: ( URI::Generic url, diff --git a/sig/orb/internal/type/array_of.rbs b/sig/orb/internal/type/array_of.rbs index 65c60fb5..f37088f6 100644 --- a/sig/orb/internal/type/array_of.rbs +++ b/sig/orb/internal/type/array_of.rbs @@ -17,10 +17,13 @@ module Orb def coerce: ( ::Array[top] | top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> (::Array[top] | top) - def dump: (::Array[top] | top value) -> (::Array[top] | top) + def dump: ( + ::Array[top] | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (::Array[top] | top) def item_type: -> Elem diff --git a/sig/orb/internal/type/base_model.rbs b/sig/orb/internal/type/base_model.rbs index 646d03d8..bae69564 100644 --- a/sig/orb/internal/type/base_model.rbs +++ b/sig/orb/internal/type/base_model.rbs @@ -53,10 +53,13 @@ module Orb def self.coerce: ( Orb::Internal::Type::BaseModel | ::Hash[top, top] | top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> (instance | top) - def self.dump: (instance | top value) -> (::Hash[top, top] | top) + def self.dump: ( + instance | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (::Hash[top, top] | top) def []: (Symbol key) -> top? diff --git a/sig/orb/internal/type/boolean.rbs b/sig/orb/internal/type/boolean.rbs index a2e9b3f8..e7542a2f 100644 --- a/sig/orb/internal/type/boolean.rbs +++ b/sig/orb/internal/type/boolean.rbs @@ -10,10 +10,13 @@ module Orb def self.coerce: ( bool | top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> (bool | top) - def self.dump: (bool | top value) -> (bool | top) + def self.dump: ( + bool | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (bool | top) end end end diff --git a/sig/orb/internal/type/converter.rbs b/sig/orb/internal/type/converter.rbs index c8b2890e..e3fb8663 100644 --- a/sig/orb/internal/type/converter.rbs +++ b/sig/orb/internal/type/converter.rbs @@ -4,19 +4,24 @@ module Orb module Converter type input = Orb::Internal::Type::Converter | Class - type state = + type coerce_state = { strictness: bool | :strong, exactness: { yes: Integer, no: Integer, maybe: Integer }, branched: Integer } + type dump_state = { can_retry: bool } + def coerce: ( top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> top - def dump: (top value) -> top + def dump: ( + top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> top def self.type_info: ( { @@ -31,12 +36,13 @@ module Orb def self.coerce: ( Orb::Internal::Type::Converter::input target, top value, - ?state: Orb::Internal::Type::Converter::state + ?state: Orb::Internal::Type::Converter::coerce_state ) -> top def self.dump: ( Orb::Internal::Type::Converter::input target, - top value + top value, + ?state: Orb::Internal::Type::Converter::dump_state ) -> top end end diff --git a/sig/orb/internal/type/enum.rbs b/sig/orb/internal/type/enum.rbs index e46839e1..61541787 100644 --- a/sig/orb/internal/type/enum.rbs +++ b/sig/orb/internal/type/enum.rbs @@ -14,10 +14,13 @@ module Orb def coerce: ( String | Symbol | top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> (Symbol | top) - def dump: (Symbol | top value) -> (Symbol | top) + def dump: ( + Symbol | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (Symbol | top) end end end diff --git a/sig/orb/internal/type/hash_of.rbs b/sig/orb/internal/type/hash_of.rbs index a45eed3f..54691d6e 100644 --- a/sig/orb/internal/type/hash_of.rbs +++ b/sig/orb/internal/type/hash_of.rbs @@ -17,10 +17,13 @@ module Orb def coerce: ( ::Hash[top, top] | top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> (::Hash[Symbol, top] | top) - def dump: (::Hash[top, top] | top value) -> (::Hash[Symbol, top] | top) + def dump: ( + ::Hash[top, top] | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (::Hash[Symbol, top] | top) def item_type: -> Elem diff --git a/sig/orb/internal/type/io_like.rbs b/sig/orb/internal/type/io_like.rbs new file mode 100644 index 00000000..11db22d6 --- /dev/null +++ b/sig/orb/internal/type/io_like.rbs @@ -0,0 +1,23 @@ +module Orb + module Internal + module Type + class IOLike + extend Orb::Internal::Type::Converter + + def self.===: (top other) -> bool + + def self.==: (top other) -> bool + + def self.coerce: ( + StringIO | String | top value, + state: Orb::Internal::Type::Converter::coerce_state + ) -> (StringIO | top) + + def self.dump: ( + Pathname | StringIO | IO | String | top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> (Pathname | StringIO | IO | String | top) + end + end + end +end diff --git a/sig/orb/internal/type/union.rbs b/sig/orb/internal/type/union.rbs index ab115829..e7c8de0c 100644 --- a/sig/orb/internal/type/union.rbs +++ b/sig/orb/internal/type/union.rbs @@ -32,10 +32,13 @@ module Orb def coerce: ( top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> top - def dump: (top value) -> top + def dump: ( + top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> top end end end diff --git a/sig/orb/internal/type/unknown.rbs b/sig/orb/internal/type/unknown.rbs index 0c44d119..c9d77b9c 100644 --- a/sig/orb/internal/type/unknown.rbs +++ b/sig/orb/internal/type/unknown.rbs @@ -10,10 +10,13 @@ module Orb def self.coerce: ( top value, - state: Orb::Internal::Type::Converter::state + state: Orb::Internal::Type::Converter::coerce_state ) -> top - def self.dump: (top value) -> top + def self.dump: ( + top value, + state: Orb::Internal::Type::Converter::dump_state + ) -> top end end end diff --git a/sig/orb/internal/util.rbs b/sig/orb/internal/util.rbs index f233573c..e0d7f2c4 100644 --- a/sig/orb/internal/util.rbs +++ b/sig/orb/internal/util.rbs @@ -73,13 +73,27 @@ module Orb | ::Array[(String | Integer)?])?] headers ) -> ::Hash[String, String] + class SerializationAdapter + attr_reader inner: Pathname | IO + + def to_json: (*top a) -> String + + def to_yaml: (*top a) -> String + + def initialize: (Pathname | IO inner) -> void + end + class ReadIOAdapter + def close?: -> bool? + + def close: -> void + private def read_enum: (Integer? max_len) -> String def read: (?Integer? max_len, ?String? out_string) -> String? def initialize: ( - String | IO | StringIO | Enumerable[String] stream + String | Pathname | StringIO | Enumerable[String] src ) { (String arg0) -> void } -> void @@ -93,7 +107,8 @@ module Orb Enumerator::Yielder y, boundary: String, key: Symbol | String, - val: top + val: top, + closing: ::Array[^-> void] ) -> void def self?.encode_multipart_streaming: ( diff --git a/test/orb/client_test.rb b/test/orb/client_test.rb index c0ad9065..c90a07d1 100644 --- a/test/orb/client_test.rb +++ b/test/orb/client_test.rb @@ -148,7 +148,10 @@ def test_retry_count_header orb.customers.create(email: "dev@stainless.com", name: "x") end - retry_count_headers = requester.attempts.map { _1[:headers]["x-stainless-retry-count"] } + retry_count_headers = requester.attempts.map do + _1.fetch(:headers).fetch("x-stainless-retry-count") + end + assert_equal(%w[0 1 2], retry_count_headers) end @@ -165,7 +168,10 @@ def test_omit_retry_count_header ) end - retry_count_headers = requester.attempts.map { _1[:headers]["x-stainless-retry-count"] } + retry_count_headers = requester.attempts.map do + _1.fetch(:headers).fetch("x-stainless-retry-count", nil) + end + assert_equal([nil, nil, nil], retry_count_headers) end @@ -182,7 +188,10 @@ def test_overwrite_retry_count_header ) end - retry_count_headers = requester.attempts.map { _1[:headers]["x-stainless-retry-count"] } + retry_count_headers = requester.attempts.map do + _1.fetch(:headers).fetch("x-stainless-retry-count") + end + assert_equal(%w[42 42 42], retry_count_headers) end @@ -195,12 +204,12 @@ def test_client_redirect_307 orb.customers.create(email: "dev@stainless.com", name: "x", request_options: {extra_headers: {}}) end - assert_equal("/redirected", requester.attempts.last[:url].path) - assert_equal(requester.attempts.first[:method], requester.attempts.last[:method]) - assert_equal(requester.attempts.first[:body], requester.attempts.last[:body]) + assert_equal("/redirected", requester.attempts.last.fetch(:url).path) + assert_equal(requester.attempts.first.fetch(:method), requester.attempts.last.fetch(:method)) + assert_equal(requester.attempts.first.fetch(:body), requester.attempts.last.fetch(:body)) assert_equal( - requester.attempts.first[:headers]["content-type"], - requester.attempts.last[:headers]["content-type"] + requester.attempts.first.fetch(:headers)["content-type"], + requester.attempts.last.fetch(:headers)["content-type"] ) end @@ -213,10 +222,10 @@ def test_client_redirect_303 orb.customers.create(email: "dev@stainless.com", name: "x", request_options: {extra_headers: {}}) end - assert_equal("/redirected", requester.attempts.last[:url].path) - assert_equal(:get, requester.attempts.last[:method]) - assert_nil(requester.attempts.last[:body]) - assert_nil(requester.attempts.last[:headers]["Content-Type"]) + assert_equal("/redirected", requester.attempts.last.fetch(:url).path) + assert_equal(:get, requester.attempts.last.fetch(:method)) + assert_nil(requester.attempts.last.fetch(:body)) + assert_nil(requester.attempts.last.fetch(:headers)["content-type"]) end def test_client_redirect_auth_keep_same_origin @@ -228,13 +237,13 @@ def test_client_redirect_auth_keep_same_origin orb.customers.create( email: "dev@stainless.com", name: "x", - request_options: {extra_headers: {"Authorization" => "Bearer xyz"}} + request_options: {extra_headers: {"authorization" => "Bearer xyz"}} ) end assert_equal( - requester.attempts.first[:headers]["authorization"], - requester.attempts.last[:headers]["authorization"] + requester.attempts.first.fetch(:headers)["authorization"], + requester.attempts.last.fetch(:headers)["authorization"] ) end @@ -247,11 +256,11 @@ def test_client_redirect_auth_strip_cross_origin orb.customers.create( email: "dev@stainless.com", name: "x", - request_options: {extra_headers: {"Authorization" => "Bearer xyz"}} + request_options: {extra_headers: {"authorization" => "Bearer xyz"}} ) end - assert_nil(requester.attempts.last[:headers]["Authorization"]) + assert_nil(requester.attempts.last.fetch(:headers)["authorization"]) end def test_client_default_idempotency_key_on_writes @@ -263,7 +272,9 @@ def test_client_default_idempotency_key_on_writes orb.customers.create(email: "dev@stainless.com", name: "x", request_options: {max_retries: 1}) end - idempotency_headers = requester.attempts.map { _1[:headers]["Idempotency-Key".downcase] } + idempotency_headers = requester.attempts.map do + _1.fetch(:headers).fetch("idempotency-key") + end assert_kind_of(String, idempotency_headers.first) refute_empty(idempotency_headers.first) @@ -283,7 +294,9 @@ def test_request_option_idempotency_key_on_writes ) end - requester.attempts.each { assert_equal("user-supplied-key", _1[:headers]["Idempotency-Key".downcase]) } + requester.attempts.each do + assert_equal("user-supplied-key", _1.fetch(:headers).fetch("idempotency-key")) + end end def test_default_headers @@ -291,7 +304,7 @@ def test_default_headers requester = MockRequester.new(200, {}, {}) orb.requester = requester orb.customers.create(email: "dev@stainless.com", name: "x") - headers = requester.attempts.first[:headers] + headers = requester.attempts.first.fetch(:headers) refute_empty(headers["accept"]) refute_empty(headers["content-type"]) diff --git a/test/orb/internal/type/base_model_test.rb b/test/orb/internal/type/base_model_test.rb index 3ae66ee0..3adca517 100644 --- a/test/orb/internal/type/base_model_test.rb +++ b/test/orb/internal/type/base_model_test.rb @@ -92,7 +92,9 @@ def test_dump [String, "one"] => "one", [String, :one] => :one, [:a, :b] => :b, - [:a, "a"] => "a" + [:a, "a"] => "a", + [String, StringIO.new("one")] => "one", + [String, Pathname(__FILE__)] => Orb::Internal::Util::SerializationAdapter } cases.each do @@ -122,6 +124,34 @@ def test_coerce_errors end end end + + def test_dump_retry + types = [ + Orb::Internal::Type::Unknown, + Orb::Internal::Type::Boolean, + A, + H, + E, + U, + B + ] + Pathname(__FILE__).open do |fd| + cases = [ + fd, + [fd], + {a: fd}, + {a: {b: fd}} + ] + types.product(cases).each do |target, input| + state = {can_retry: true} + Orb::Internal::Type::Converter.dump(target, input, state: state) + + assert_pattern do + state => {can_retry: false} + end + end + end + end end class Orb::Test::EnumModelTest < Minitest::Test diff --git a/test/orb/internal/util_test.rb b/test/orb/internal/util_test.rb index 1309f918..c5144373 100644 --- a/test/orb/internal/util_test.rb +++ b/test/orb/internal/util_test.rb @@ -160,11 +160,12 @@ def test_joining_queries class Orb::Test::UtilFormDataEncodingTest < Minitest::Test class FakeCGI < CGI def initialize(headers, io) + encoded = io.to_a @ctype = headers["content-type"] # rubocop:disable Lint/EmptyBlock - @io = Orb::Internal::Util::ReadIOAdapter.new(io) {} + @io = Orb::Internal::Util::ReadIOAdapter.new(encoded.to_enum) {} # rubocop:enable Lint/EmptyBlock - @c_len = io.to_a.join.bytesize.to_s + @c_len = encoded.join.bytesize.to_s super() end @@ -180,15 +181,17 @@ def env_table end def test_file_encode + file = Pathname(__FILE__) headers = {"content-type" => "multipart/form-data"} cases = { - StringIO.new("abc") => "abc" + StringIO.new("abc") => "abc", + file => /^class Orb/ } cases.each do |body, val| encoded = Orb::Internal::Util.encode_content(headers, body) cgi = FakeCGI.new(*encoded) assert_pattern do - cgi[""] => ^val + cgi[""].read => ^val end end end @@ -199,13 +202,16 @@ def test_hash_encode {a: 2, b: 3} => {"a" => "2", "b" => "3"}, {a: 2, b: nil} => {"a" => "2", "b" => "null"}, {a: 2, b: [1, 2, 3]} => {"a" => "2", "b" => "1"}, - {file: StringIO.new("a")} => {"file" => "a"} + {strio: StringIO.new("a")} => {"strio" => "a"}, + {pathname: Pathname(__FILE__)} => {"pathname" => -> { _1.read in /^class Orb/ }} } cases.each do |body, testcase| encoded = Orb::Internal::Util.encode_content(headers, body) cgi = FakeCGI.new(*encoded) testcase.each do |key, val| - assert_equal(val, cgi[key]) + assert_pattern do + cgi[key] => ^val + end end end end