diff --git a/lib/sorbet-coerce/converter.rb b/lib/sorbet-coerce/converter.rb index 2fd90f9..cd2a8a2 100644 --- a/lib/sorbet-coerce/converter.rb +++ b/lib/sorbet-coerce/converter.rb @@ -59,28 +59,7 @@ def _convert(value, type, raise_coercion_error, coerce_empty_to_nil) elsif type.is_a?(T::Types::Simple) _convert(value, type.raw_type, raise_coercion_error, coerce_empty_to_nil) elsif type.is_a?(T::Types::Union) - true_idx = T.let(nil, T.nilable(Integer)) - false_idx = T.let(nil, T.nilable(Integer)) - nil_idx = T.let(nil, T.nilable(Integer)) - - type.types.each_with_index do |t, i| - nil_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == NilClass - true_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == TrueClass - false_idx = i if t.is_a?(T::Types::Simple) && t.raw_type == FalseClass - end - - return value unless ( - (!true_idx.nil? && !false_idx.nil? && !nil_idx.nil?) || # T.nilable(T::Boolean) - (type.types.length == 2 && ( - !nil_idx.nil? || (!true_idx.nil? && !false_idx.nil?) # T.nilable || T::Boolean - )) - ) - - if !true_idx.nil? && !false_idx.nil? - _convert_simple(value, T::Boolean, raise_coercion_error, coerce_empty_to_nil) - else - _convert(value, type.types[nil_idx == 0 ? 1 : 0], raise_coercion_error, coerce_empty_to_nil) - end + _convert_union(value, type, raise_coercion_error, coerce_empty_to_nil) elsif type.is_a?(T::Types::TypedHash) return {} if _nil_like?(value, type, coerce_empty_to_nil) @@ -117,6 +96,25 @@ def _convert(value, type, raise_coercion_error, coerce_empty_to_nil) end end + def _convert_union(value, type, raise_coercion_error, coerce_empty_to_nil) + type.types.each do |t| + # Always raise coercion error so that we can handle it here + if t.is_a?(T::Types::Simple) && (t.raw_type == NilClass || t.raw_type == TrueClass || t.raw_type == FalseClass) + return _convert_simple(value, T::Boolean, true, coerce_empty_to_nil) + end + + return _convert(value, t, true, coerce_empty_to_nil) + rescue TypeCoerce::CoercionError, TypeCoerce::ShapeError + # Keep trying to coerce + end + + if raise_coercion_error + raise TypeCoerce::CoercionError.new(value, type) + else + value + end + end + def _convert_enum(value, type, raise_coercion_error, coerce_empty_to_nil) if raise_coercion_error type.deserialize(value) diff --git a/spec/coerce_spec.rb b/spec/coerce_spec.rb index ed90d62..e6a124b 100644 --- a/spec/coerce_spec.rb +++ b/spec/coerce_spec.rb @@ -71,13 +71,13 @@ class UnsupportedCustomType # Does not respond to new end - class WithSupportedUnion < T::Struct + class WithNilableUnion < T::Struct const :nilable, T.nilable(String) const :nilable_boolean, T.nilable(T::Boolean) end - class WithUnsupportedUnion < T::Struct - const :union, T.any(String, Integer) + class WithComplexUnion < T::Struct + const :union, T.any(WithNilableUnion, Integer, String) end class WithFixedArray < T::Struct @@ -217,9 +217,9 @@ class Param2 < T::Struct end context 'when given union types' do - context 'supported union types' do + context 'nilable types' do it 'coerces correctly' do - coerced = TypeCoerce[WithSupportedUnion].new.from({ + coerced = TypeCoerce[WithNilableUnion].new.from({ nilable: 2, nilable_boolean: 'true' }) @@ -228,16 +228,20 @@ class Param2 < T::Struct end end - context 'unsupported union types' do - it 'keeps the values as-is' do - coerced = TypeCoerce[WithUnsupportedUnion].new.from({union: 'a'}) + context 'when give complex union types' do + it 'nested types' do + coerced = TypeCoerce[WithComplexUnion].new.from({union: 'a'}) expect(coerced.union).to eq('a') - coerced = TypeCoerce[WithUnsupportedUnion].new.from({union: 2}) + coerced = TypeCoerce[WithComplexUnion].new.from({union: 2}) expect(coerced.union).to eq(2) + coerced = TypeCoerce[WithComplexUnion].new.from({union: { nilable: "Test", nilable_boolean: nil } }) + expect(coerced.union.nilable).to eq("Test") + expect(coerced.union.nilable_boolean).to eq(nil) + expect do - TypeCoerce[WithUnsupportedUnion].new.from({union: nil}) + TypeCoerce[WithComplexUnion].new.from({union: nil}) end.to raise_error(TypeError) end end