diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 2ce25fec..ff4f9a50 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.32" + ".": "0.1.0-alpha.33" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 22084c1b..665524aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 0.1.0-alpha.33 (2025-06-18) + +Full Changelog: [v0.1.0-alpha.32...v0.1.0-alpha.33](https://github.com/lithic-com/lithic-ruby/compare/v0.1.0-alpha.32...v0.1.0-alpha.33) + +### Bug Fixes + +* issue where we cannot mutate arrays on base model derivatives ([d9e95e7](https://github.com/lithic-com/lithic-ruby/commit/d9e95e745161befcf4908ea5fad28b5a9a30c131)) + ## 0.1.0-alpha.32 (2025-06-17) Full Changelog: [v0.1.0-alpha.31...v0.1.0-alpha.32](https://github.com/lithic-com/lithic-ruby/compare/v0.1.0-alpha.31...v0.1.0-alpha.32) diff --git a/Gemfile.lock b/Gemfile.lock index eab7a658..652316de 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - lithic (0.1.0.pre.alpha.32) + lithic (0.1.0.pre.alpha.33) connection_pool GEM diff --git a/README.md b/README.md index ac3aed0b..95481c53 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 "lithic", "~> 0.1.0.pre.alpha.32" +gem "lithic", "~> 0.1.0.pre.alpha.33" ``` diff --git a/lib/lithic/errors.rb b/lib/lithic/errors.rb index 51550dc2..b5ba7c04 100644 --- a/lib/lithic/errors.rb +++ b/lib/lithic/errors.rb @@ -9,6 +9,28 @@ class Error < StandardError end class ConversionError < Lithic::Errors::Error + # @return [StandardError, nil] + def cause = @cause.nil? ? super : @cause + + # @api private + # + # @param on [Class] + # @param method [Symbol] + # @param target [Object] + # @param value [Object] + # @param cause [StandardError, nil] + def initialize(on:, method:, target:, value:, cause: nil) + cls = on.name.split("::").last + + message = [ + "Failed to parse #{cls}.#{method} from #{value.class} to #{target.inspect}.", + "To get the unparsed API response, use #{cls}[#{method.inspect}].", + cause && "Cause: #{cause.message}" + ].filter(&:itself).join(" ") + + @cause = cause + super(message) + end end class APIError < Lithic::Errors::Error diff --git a/lib/lithic/internal/type/array_of.rb b/lib/lithic/internal/type/array_of.rb index 3ef8b672..c6168546 100644 --- a/lib/lithic/internal/type/array_of.rb +++ b/lib/lithic/internal/type/array_of.rb @@ -62,10 +62,14 @@ def hash = [self.class, item_type].hash # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Array, Object] @@ -74,6 +78,7 @@ def coerce(value, state:) unless value.is_a?(Array) exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Array}") return value end diff --git a/lib/lithic/internal/type/base_model.rb b/lib/lithic/internal/type/base_model.rb index 50fd47bf..39ef4121 100644 --- a/lib/lithic/internal/type/base_model.rb +++ b/lib/lithic/internal/type/base_model.rb @@ -60,7 +60,7 @@ def fields [Lithic::Internal::Type::Converter.type_info(type_info), type_info] end - setter = "#{name_sym}=" + setter = :"#{name_sym}=" api_name = info.fetch(:api_name, name_sym) nilable = info.fetch(:nil?, false) const = required && !nilable ? info.fetch(:const, Lithic::Internal::OMIT) : Lithic::Internal::OMIT @@ -77,30 +77,61 @@ def fields type_fn: type_fn } - define_method(setter) { @data.store(name_sym, _1) } + define_method(setter) do |value| + target = type_fn.call + state = Lithic::Internal::Type::Converter.new_coerce_state(translate_names: false) + coerced = Lithic::Internal::Type::Converter.coerce(target, value, state: state) + status = @coerced.store(name_sym, state.fetch(:error) || true) + stored = + case [target, status] + in [Lithic::Internal::Type::Converter | Symbol, true] + coerced + else + value + end + @data.store(name_sym, stored) + end + # rubocop:disable Style/CaseEquality + # rubocop:disable Metrics/BlockLength define_method(name_sym) do target = type_fn.call - value = @data.fetch(name_sym) { const == Lithic::Internal::OMIT ? nil : const } - state = {strictness: :strong, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} - if (nilable || !required) && value.nil? - nil - else - Lithic::Internal::Type::Converter.coerce( - target, - value, - state: state + + case @coerced[name_sym] + in true | false if Lithic::Internal::Type::Converter === target + @data.fetch(name_sym) + in ::StandardError => e + raise Lithic::Errors::ConversionError.new( + on: self.class, + method: __method__, + target: target, + value: @data.fetch(name_sym), + cause: e ) + else + Kernel.then do + value = @data.fetch(name_sym) { const == Lithic::Internal::OMIT ? nil : const } + state = Lithic::Internal::Type::Converter.new_coerce_state(translate_names: false) + if (nilable || !required) && value.nil? + nil + else + Lithic::Internal::Type::Converter.coerce( + target, value, state: state + ) + end + rescue StandardError => e + raise Lithic::Errors::ConversionError.new( + on: self.class, + method: __method__, + target: target, + value: value, + cause: e + ) + end end - rescue StandardError => e - cls = self.class.name.split("::").last - message = [ - "Failed to parse #{cls}.#{__method__} from #{value.class} to #{target.inspect}.", - "To get the unparsed API response, use #{cls}[#{__method__.inspect}].", - "Cause: #{e.message}" - ].join(" ") - raise Lithic::Errors::ConversionError.new(message) end + # rubocop:enable Metrics/BlockLength + # rubocop:enable Style/CaseEquality end # @api private @@ -200,23 +231,28 @@ class << self # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [self, Object] def coerce(value, state:) exactness = state.fetch(:exactness) - if value.is_a?(self.class) + if value.is_a?(self) exactness[:yes] += 1 return value end unless (val = Lithic::Internal::Util.coerce_hash(value)).is_a?(Hash) exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Hash}") return value end exactness[:yes] += 1 @@ -224,13 +260,15 @@ def coerce(value, state:) keys = val.keys.to_set instance = new data = instance.to_h + status = instance.instance_variable_get(:@coerced) # rubocop:disable Metrics/BlockLength fields.each do |name, field| mode, required, target = field.fetch_values(:mode, :required, :type) api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const) + src_name = state.fetch(:translate_names) ? api_name : name - unless val.key?(api_name) + unless val.key?(src_name) if required && mode != :dump && const == Lithic::Internal::OMIT exactness[nilable ? :maybe : :no] += 1 else @@ -239,9 +277,10 @@ def coerce(value, state:) next end - item = val.fetch(api_name) - keys.delete(api_name) + item = val.fetch(src_name) + keys.delete(src_name) + state[:error] = nil converted = if item.nil? && (nilable || !required) exactness[nilable ? :yes : :maybe] += 1 @@ -255,6 +294,8 @@ def coerce(value, state:) item end end + + status.store(name, state.fetch(:error) || true) data.store(name, converted) end # rubocop:enable Metrics/BlockLength @@ -430,7 +471,18 @@ def to_yaml(*a) = Lithic::Internal::Type::Converter.dump(self.class, self).to_ya # Create a new instance of a model. # # @param data [Hash{Symbol=>Object}, self] - def initialize(data = {}) = (@data = Lithic::Internal::Util.coerce_hash!(data).to_h) + def initialize(data = {}) + @data = {} + @coerced = {} + Lithic::Internal::Util.coerce_hash!(data).each do + if self.class.known_fields.key?(_1) + public_send(:"#{_1}=", _2) + else + @data.store(_1, _2) + @coerced.store(_1, false) + end + end + end class << self # @api private diff --git a/lib/lithic/internal/type/boolean.rb b/lib/lithic/internal/type/boolean.rb index 888c03dd..4c09b705 100644 --- a/lib/lithic/internal/type/boolean.rb +++ b/lib/lithic/internal/type/boolean.rb @@ -31,14 +31,20 @@ def self.==(other) = other.is_a?(Class) && other <= Lithic::Internal::Type::Bool class << self # @api private # + # Coerce value to Boolean if possible, otherwise return the original value. + # # @param value [Boolean, Object] # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Boolean, Object] diff --git a/lib/lithic/internal/type/converter.rb b/lib/lithic/internal/type/converter.rb index 9d3e303b..297e02a6 100644 --- a/lib/lithic/internal/type/converter.rb +++ b/lib/lithic/internal/type/converter.rb @@ -15,10 +15,14 @@ module Converter # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Object] @@ -94,6 +98,21 @@ def type_info(spec) end end + # @api private + # + # @param translate_names [Boolean] + # + # @return [Hash{Symbol=>Object}] + def new_coerce_state(translate_names: true) + { + translate_names: translate_names, + strictness: true, + exactness: {yes: 0, no: 0, maybe: 0}, + error: nil, + branched: 0 + } + end + # @api private # # Based on `target`, transform `value` into `target`, to the extent possible: @@ -110,14 +129,11 @@ def type_info(spec) # # @param value [Object] # - # @param state [Hash{Symbol=>Object}] The `strictness` is one of `true`, `false`, or `:strong`. This informs the - # coercion strategy when we have to decide between multiple possible conversion - # targets: + # @param state [Hash{Symbol=>Object}] The `strictness` is one of `true`, `false`. This informs the coercion strategy + # when we have to decide between multiple possible conversion targets: # # - `true`: the conversion must be exact, with minimum coercion. # - `false`: the conversion can be approximate, with some coercion. - # - `:strong`: the conversion must be exact, with no coercion, and raise an error - # if not possible. # # The `exactness` is `Hash` with keys being one of `yes`, `no`, or `maybe`. For # any given conversion attempt, the exactness will be updated based on how closely @@ -130,21 +146,20 @@ def type_info(spec) # # See implementation below for more details. # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Object] - def coerce( - target, - value, - state: {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} - ) - # rubocop:disable Lint/SuppressedException + def coerce(target, value, state: Lithic::Internal::Type::Converter.new_coerce_state) # rubocop:disable Metrics/BlockNesting - strictness, exactness = state.fetch_values(:strictness, :exactness) + exactness = state.fetch(:exactness) case target in Lithic::Internal::Type::Converter @@ -160,29 +175,26 @@ def coerce( exactness[value.nil? ? :yes : :maybe] += 1 return nil in -> { _1 <= Integer } - if value.is_a?(Integer) + case value + in Integer exactness[:yes] += 1 return value - elsif strictness == :strong && Integer(value, exception: false) != value - message = "no implicit conversion of #{value.class} into #{target.inspect}" - raise value.is_a?(Numeric) ? ArgumentError.new(message) : TypeError.new(message) else Kernel.then do return Integer(value).tap { exactness[:maybe] += 1 } - rescue ArgumentError, TypeError + rescue ArgumentError, TypeError => e + state[:error] = e end end in -> { _1 <= Float } if value.is_a?(Numeric) exactness[:yes] += 1 return Float(value) - elsif strictness == :strong - message = "no implicit conversion of #{value.class} into #{target.inspect}" - raise TypeError.new(message) else Kernel.then do return Float(value).tap { exactness[:maybe] += 1 } - rescue ArgumentError, TypeError + rescue ArgumentError, TypeError => e + state[:error] = e end end in -> { _1 <= String } @@ -194,16 +206,13 @@ def coerce( exactness[:yes] += 1 return value.string else - if strictness == :strong - message = "no implicit conversion of #{value.class} into #{target.inspect}" - raise TypeError.new(message) - end + state[:error] = TypeError.new("#{value.class} can't be coerced into #{String}") end in -> { _1 <= Date || _1 <= Time } Kernel.then do return target.parse(value).tap { exactness[:yes] += 1 } rescue ArgumentError, TypeError => e - raise e if strictness == :strong + state[:error] = e end in -> { _1 <= StringIO } if value.is_a?(String) exactness[:yes] += 1 @@ -221,10 +230,8 @@ def coerce( return value end else - if strictness == :strong - message = "cannot convert non-matching #{value.class} into #{target.inspect}" - raise ArgumentError.new(message) - end + message = "cannot convert non-matching #{value.class} into #{target.inspect}" + state[:error] = ArgumentError.new(message) end else end @@ -232,7 +239,6 @@ def coerce( exactness[:no] += 1 value # rubocop:enable Metrics/BlockNesting - # rubocop:enable Lint/SuppressedException end # @api private @@ -277,8 +283,10 @@ def inspect(target, depth:) define_sorbet_constant!(:CoerceState) do T.type_alias do { - strictness: T.any(T::Boolean, Symbol), + translate_names: T::Boolean, + strictness: T::Boolean, exactness: {yes: Integer, no: Integer, maybe: Integer}, + error: T::Class[StandardError], branched: Integer } end diff --git a/lib/lithic/internal/type/enum.rb b/lib/lithic/internal/type/enum.rb index 6f4500b2..902b14d2 100644 --- a/lib/lithic/internal/type/enum.rb +++ b/lib/lithic/internal/type/enum.rb @@ -77,10 +77,14 @@ def hash = values.to_set.hash # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Symbol, Object] @@ -91,8 +95,12 @@ def coerce(value, state:) if values.include?(val) exactness[:yes] += 1 val + elsif values.first&.class == val.class + exactness[:maybe] += 1 + value else - exactness[values.first&.class == val.class ? :maybe : :no] += 1 + exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{self}") value end end diff --git a/lib/lithic/internal/type/file_input.rb b/lib/lithic/internal/type/file_input.rb index acfb2f34..9d3feb93 100644 --- a/lib/lithic/internal/type/file_input.rb +++ b/lib/lithic/internal/type/file_input.rb @@ -45,10 +45,14 @@ class << self # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [StringIO, Object] @@ -62,6 +66,7 @@ def coerce(value, state:) exactness[:yes] += 1 value else + state[:error] = TypeError.new("#{value.class} can't be coerced into #{StringIO}") exactness[:no] += 1 value end diff --git a/lib/lithic/internal/type/hash_of.rb b/lib/lithic/internal/type/hash_of.rb index 30feaf39..a54c4efd 100644 --- a/lib/lithic/internal/type/hash_of.rb +++ b/lib/lithic/internal/type/hash_of.rb @@ -77,10 +77,14 @@ def hash = [self.class, item_type].hash # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Hash{Symbol=>Object}, Object] @@ -89,6 +93,7 @@ def coerce(value, state:) unless value.is_a?(Hash) exactness[:no] += 1 + state[:error] = TypeError.new("#{value.class} can't be coerced into #{Hash}") return value end diff --git a/lib/lithic/internal/type/union.rb b/lib/lithic/internal/type/union.rb index 409a74bd..29137960 100644 --- a/lib/lithic/internal/type/union.rb +++ b/lib/lithic/internal/type/union.rb @@ -126,14 +126,23 @@ def hash = variants.hash # @api private # + # Tries to efficiently coerce the given value to one of the known variants. + # + # If the value cannot match any of the known variants, the coercion is considered + # non-viable and returns the original value. + # # @param value [Object] # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Object] @@ -144,7 +153,6 @@ def coerce(value, state:) strictness = state.fetch(:strictness) exactness = state.fetch(:exactness) - state[:strictness] = strictness == :strong ? true : strictness alternatives = [] known_variants.each do |_, variant_fn| @@ -163,13 +171,10 @@ def coerce(value, state:) end end - case alternatives.sort_by(&:first) + case alternatives.sort_by!(&:first) in [] exactness[:no] += 1 - if strictness == :strong - message = "no possible conversion of #{value.class} into a variant of #{target.inspect}" - raise ArgumentError.new(message) - end + state[:error] = ArgumentError.new("no matching variant for #{value.inspect}") value in [[_, exact, coerced], *] exact.each { exactness[_1] += _2 } diff --git a/lib/lithic/internal/type/unknown.rb b/lib/lithic/internal/type/unknown.rb index 1b9c0f63..2c36f7a4 100644 --- a/lib/lithic/internal/type/unknown.rb +++ b/lib/lithic/internal/type/unknown.rb @@ -33,14 +33,20 @@ def self.==(other) = other.is_a?(Class) && other <= Lithic::Internal::Type::Unkn class << self # @api private # + # No coercion needed for Unknown type. + # # @param value [Object] # # @param state [Hash{Symbol=>Object}] . # - # @option state [Boolean, :strong] :strictness + # @option state [Boolean] :translate_names + # + # @option state [Boolean] :strictness # # @option state [Hash{Symbol=>Object}] :exactness # + # @option state [Class] :error + # # @option state [Integer] :branched # # @return [Object] diff --git a/lib/lithic/version.rb b/lib/lithic/version.rb index 977d7b1e..2a45c8a6 100644 --- a/lib/lithic/version.rb +++ b/lib/lithic/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module Lithic - VERSION = "0.1.0.pre.alpha.32" + VERSION = "0.1.0.pre.alpha.33" end diff --git a/rbi/lithic/errors.rbi b/rbi/lithic/errors.rbi index a26b2dda..4fb98e36 100644 --- a/rbi/lithic/errors.rbi +++ b/rbi/lithic/errors.rbi @@ -8,6 +8,22 @@ module Lithic end class ConversionError < Lithic::Errors::Error + sig { returns(T.nilable(StandardError)) } + def cause + end + + # @api private + sig do + params( + on: T::Class[StandardError], + method: Symbol, + target: T.anything, + value: T.anything, + cause: T.nilable(StandardError) + ).returns(T.attached_class) + end + def self.new(on:, method:, target:, value:, cause: nil) + end end class APIError < Lithic::Errors::Error diff --git a/rbi/lithic/internal/type/boolean.rbi b/rbi/lithic/internal/type/boolean.rbi index 493af362..59aa5083 100644 --- a/rbi/lithic/internal/type/boolean.rbi +++ b/rbi/lithic/internal/type/boolean.rbi @@ -22,6 +22,8 @@ module Lithic class << self # @api private + # + # Coerce value to Boolean if possible, otherwise return the original value. sig do override .params( diff --git a/rbi/lithic/internal/type/converter.rbi b/rbi/lithic/internal/type/converter.rbi index 70ad0897..165e6908 100644 --- a/rbi/lithic/internal/type/converter.rbi +++ b/rbi/lithic/internal/type/converter.rbi @@ -15,12 +15,14 @@ module Lithic CoerceState = T.type_alias do { - strictness: T.any(T::Boolean, Symbol), + translate_names: T::Boolean, + strictness: T::Boolean, exactness: { yes: Integer, no: Integer, maybe: Integer }, + error: T::Class[StandardError], branched: Integer } end @@ -84,6 +86,15 @@ module Lithic def self.type_info(spec) end + # @api private + sig do + params(translate_names: T::Boolean).returns( + Lithic::Internal::Type::Converter::CoerceState + ) + end + def self.new_coerce_state(translate_names: true) + end + # @api private # # Based on `target`, transform `value` into `target`, to the extent possible: @@ -105,14 +116,11 @@ module Lithic def self.coerce( target, value, - # The `strictness` is one of `true`, `false`, or `:strong`. This informs the - # coercion strategy when we have to decide between multiple possible conversion - # targets: + # The `strictness` is one of `true`, `false`. This informs the coercion strategy + # when we have to decide between multiple possible conversion targets: # # - `true`: the conversion must be exact, with minimum coercion. # - `false`: the conversion can be approximate, with some coercion. - # - `:strong`: the conversion must be exact, with no coercion, and raise an error - # if not possible. # # The `exactness` is `Hash` with keys being one of `yes`, `no`, or `maybe`. For # any given conversion attempt, the exactness will be updated based on how closely @@ -124,15 +132,7 @@ module Lithic # - `no`: the value cannot be converted to the target type. # # See implementation below for more details. - state: { - strictness: true, - exactness: { - yes: 0, - no: 0, - maybe: 0 - }, - branched: 0 - } + state: Lithic::Internal::Type::Converter.new_coerce_state ) end diff --git a/rbi/lithic/internal/type/union.rbi b/rbi/lithic/internal/type/union.rbi index 4c987fe4..0bce1328 100644 --- a/rbi/lithic/internal/type/union.rbi +++ b/rbi/lithic/internal/type/union.rbi @@ -78,6 +78,11 @@ module Lithic end # @api private + # + # Tries to efficiently coerce the given value to one of the known variants. + # + # If the value cannot match any of the known variants, the coercion is considered + # non-viable and returns the original value. sig do override .params( diff --git a/rbi/lithic/internal/type/unknown.rbi b/rbi/lithic/internal/type/unknown.rbi index 3e2b5851..0afb189b 100644 --- a/rbi/lithic/internal/type/unknown.rbi +++ b/rbi/lithic/internal/type/unknown.rbi @@ -22,6 +22,8 @@ module Lithic class << self # @api private + # + # No coercion needed for Unknown type. sig do override .params( diff --git a/sig/lithic/errors.rbs b/sig/lithic/errors.rbs index c96e5151..2d62a529 100644 --- a/sig/lithic/errors.rbs +++ b/sig/lithic/errors.rbs @@ -5,6 +5,15 @@ module Lithic end class ConversionError < Lithic::Errors::Error + def cause: -> StandardError? + + def initialize: ( + on: Class, + method: Symbol, + target: top, + value: top, + ?cause: StandardError? + ) -> void end class APIError < Lithic::Errors::Error diff --git a/sig/lithic/internal/type/converter.rbs b/sig/lithic/internal/type/converter.rbs index 21fdbf5f..b4a41907 100644 --- a/sig/lithic/internal/type/converter.rbs +++ b/sig/lithic/internal/type/converter.rbs @@ -8,8 +8,10 @@ module Lithic type coerce_state = { - strictness: bool | :strong, + translate_names: bool, + strictness: bool, exactness: { yes: Integer, no: Integer, maybe: Integer }, + error: Class, branched: Integer } @@ -37,6 +39,10 @@ module Lithic | Lithic::Internal::Type::Converter::input spec ) -> (^-> top) + def self.new_coerce_state: ( + ?translate_names: bool + ) -> Lithic::Internal::Type::Converter::coerce_state + def self.coerce: ( Lithic::Internal::Type::Converter::input target, top value, diff --git a/test/lithic/internal/type/base_model_test.rb b/test/lithic/internal/type/base_model_test.rb index 08d0e77e..32037c38 100644 --- a/test/lithic/internal/type/base_model_test.rb +++ b/test/lithic/internal/type/base_model_test.rb @@ -66,7 +66,7 @@ def test_coerce cases.each do |lhs, rhs| target, input = lhs exactness, expect = rhs - state = {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} + state = Lithic::Internal::Type::Converter.new_coerce_state assert_pattern do Lithic::Internal::Type::Converter.coerce(target, input, state: state) => ^expect state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness @@ -108,18 +108,19 @@ def test_dump def test_coerce_errors cases = { - [Integer, "one"] => TypeError, - [Float, "one"] => TypeError, + [Integer, "one"] => ArgumentError, + [Float, "one"] => ArgumentError, [String, Time] => TypeError, [Date, "one"] => ArgumentError, [Time, "one"] => ArgumentError } - cases.each do - target, input = _1 - state = {strictness: :strong, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} - assert_raises(_2) do - Lithic::Internal::Type::Converter.coerce(target, input, state: state) + cases.each do |testcase, expect| + target, input = testcase + state = Lithic::Internal::Type::Converter.new_coerce_state + Lithic::Internal::Type::Converter.coerce(target, input, state: state) + assert_pattern do + state => {error: ^expect} end end end @@ -217,7 +218,7 @@ def test_coerce cases.each do |lhs, rhs| target, input = lhs exactness, expect = rhs - state = {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} + state = Lithic::Internal::Type::Converter.new_coerce_state assert_pattern do Lithic::Internal::Type::Converter.coerce(target, input, state: state) => ^expect state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness @@ -291,7 +292,7 @@ def test_coerce cases.each do |lhs, rhs| target, input = lhs exactness, expect = rhs - state = {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} + state = Lithic::Internal::Type::Converter.new_coerce_state assert_pattern do Lithic::Internal::Type::Converter.coerce(target, input, state: state) => ^expect state.fetch(:exactness).filter { _2.nonzero? }.to_h => ^exactness @@ -340,6 +341,7 @@ class M5 < Lithic::Internal::Type::BaseModel class M6 < M1 required :a, Lithic::Internal::Type::ArrayOf[M6] + optional :b, M6 end def test_coerce @@ -365,13 +367,14 @@ def test_coerce [M5, {d: "d"}] => [{yes: 3}, {d: :d}], [M5, {d: nil}] => [{yes: 2, no: 1}, {d: nil}], - [M6, {a: [{a: []}]}] => [{yes: 4}, -> { _1 in {a: [M6]} }] + [M6, {a: [{a: []}]}] => [{yes: 6}, -> { _1 in {a: [M6]} }], + [M6, {b: {a: []}}] => [{yes: 4, no: 1}, -> { _1 in {b: M6} }] } cases.each do |lhs, rhs| target, input = lhs exactness, expect = rhs - state = {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} + state = Lithic::Internal::Type::Converter.new_coerce_state assert_pattern do coerced = Lithic::Internal::Type::Converter.coerce(target, input, state: state) assert_equal(coerced, coerced) @@ -410,20 +413,26 @@ def test_dump def test_accessors cases = { - M2.new({a: "1990-09-19", b: "1"}) => {a: Time.new(1990, 9, 19), b: TypeError}, - M2.new(a: "one", b: "one") => {a: ArgumentError, b: TypeError}, - M2.new(a: nil, b: 2.0) => {a: TypeError}, - M2.new(a: nil, b: 2.2) => {a: TypeError, b: ArgumentError}, + M2.new({a: "1990-09-19", b: "1"}) => [{a: "1990-09-19", b: "1"}, {a: Time.new(1990, 9, 19), b: 1}], + M2.new(a: "one", b: "one") => [{a: "one", b: "one"}, {a: ArgumentError, b: ArgumentError}], + M2.new(a: nil, b: 2.0) => [{a: nil, b: 2.0}, {a: TypeError}], + M2.new(a: nil, b: 2.2) => [{a: nil, b: 2.2}, {a: TypeError, b: 2}], - M3.new => {d: :d}, - M3.new(d: 1) => {d: ArgumentError}, + M3.new => [{}, {d: :d}], + M3.new(d: 1) => [{d: 1}, {d: ArgumentError}], - M5.new => {c: :c, d: :d} + M5.new => [{}, {c: :c, d: :d}] } cases.each do target = _1 - _2.each do |accessor, expect| + data, attributes = _2 + + assert_pattern do + target.to_h => ^data + end + + attributes.each do |accessor, expect| case expect in Class if expect <= StandardError tap do @@ -438,6 +447,24 @@ def test_accessors end end end + + def test_inplace_modification + m1 = M6.new(a: []) + m1.a << M6.new(a: []) + + m2 = M6.new(b: M6.new(a: [])) + m2.b.a << M6.new(a: []) + + m3 = M6.new(a: []) + m4 = M6.new(b: m3) + m3.a << M6.new(a: []) + + assert_pattern do + m1 => {a: [{a: []}]} + m2 => {b: {a: [{a: []}]}} + m4 => {b: {a: [{a: []}]}} + end + end end class Lithic::Test::UnionTest < Minitest::Test @@ -555,7 +582,7 @@ def test_coerce cases.each do |lhs, rhs| target, input = lhs exactness, branched, expect = rhs - state = {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0} + state = Lithic::Internal::Type::Converter.new_coerce_state assert_pattern do coerced = Lithic::Internal::Type::Converter.coerce(target, input, state: state) assert_equal(coerced, coerced)