Skip to content

Commit

Permalink
feat: support nested structs
Browse files Browse the repository at this point in the history
  • Loading branch information
antoinesaliba committed Jun 25, 2024
1 parent 26bf059 commit 9f01686
Show file tree
Hide file tree
Showing 2 changed files with 78 additions and 2 deletions.
36 changes: 35 additions & 1 deletion lib/typed/coercion/struct_coercer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,41 @@ def coerce(type:, value:)

return Failure.new(CoercionError.new("Value of type '#{value.class}' cannot be coerced to #{type} Struct.")) unless value.is_a?(Hash)

Success.new(T.cast(type, T::Types::Simple).raw_type.from_hash!(HashTransformer.new.deep_stringify_keys(value)))
values = {}

type = T.cast(type, T::Types::Simple)

type.raw_type.props.each do |name, prop|
attribute_type = prop[:type_object]
value = HashTransformer.new.deep_symbolize_keys(value)

if value[name].nil?
# if the value is nil but the type is nilable, no need to coerce
next if attribute_type.try(:valid?, value[name])

return Typed::Failure.new(CoercionError.new("#{name} is required but nil given"))
end

# now that we've done the nil check, we can unwrap the nilable type to get the raw type
simple_attribute_type = attribute_type.unwrap_nilable if attribute_type.respond_to?(:unwrap_nilable)

# if the prop is a struct, we need to recursively coerce it
if simple_attribute_type.try(:raw_type).respond_to?(:props)
Typed::HashSerializer
.new(schema: Typed::Schema.from_struct(simple_attribute_type.raw_type))
.deserialize(value[name])
.and_then { |struct| Typed::Success.new(values[name] = struct) }
.on_error { |error| return Typed::Failure.new(CoercionError.new("Nested hash for #{type} could not be coerced to #{name}, error: #{error}")) }
else
value = HashTransformer.new.deep_symbolize_keys(value)

Coercion
.coerce(type: attribute_type, value: value[name])
.and_then { |coerced_value| Typed::Success.new(values[name] = coerced_value) }
end
end

Success.new(type.raw_type.new(values))
rescue ArgumentError, RuntimeError
Failure.new(CoercionError.new("Given hash could not be coerced to #{type}."))
end
Expand Down
44 changes: 43 additions & 1 deletion test/typed/coercion/struct_coercer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class StructCoercerTest < Minitest::Test
def setup
@coercer = Typed::Coercion::StructCoercer.new
@type = T::Utils.coerce(Job)
@type_with_nested_struct = T::Utils.coerce(Person)
end

def test_used_for_type_works
Expand Down Expand Up @@ -51,6 +52,47 @@ def test_when_struct_cannot_be_coerced_returns_failure
result = @coercer.coerce(type: @type, value: {"not" => "valid"})

assert_failure(result)
assert_error(Typed::Coercion::CoercionError.new("Given hash could not be coerced to Job."), result)
assert_error(Typed::Coercion::CoercionError.new("title is required but nil given"), result)
end

def test_when_struct_has_nested_struct_and_all_values_passed_for_nested_struct
name = "Alex"
age = 31
ruby_rank = "pretty"
salary = 90_000_00
title = "Software Developer"
start_date = Date.new(2024, 3, 1)

result = @coercer.coerce(type: @type_with_nested_struct, value: {name:, age:, ruby_rank:, job: {title:, salary:, start_date:}})

person = Person.new(
name:,
age:,
ruby_rank: RubyRank.deserialize(ruby_rank),
job: Job.new(title:, salary:, start_date:)
)

assert_success(result)
assert_payload(person, result)
end

def test_when_struct_has_nested_struct_and_optional_start_date_not_passed_for_nested_struct
name = "Alex"
age = 31
ruby_rank = "pretty"
salary = 90_000_00
title = "Software Developer"

result = @coercer.coerce(type: @type_with_nested_struct, value: {name:, age:, ruby_rank:, job: {title:, salary:}})

person = Person.new(
name:,
age:,
ruby_rank: RubyRank.deserialize(ruby_rank),
job: Job.new(title:, salary:)
)

assert_success(result)
assert_payload(person, result)
end
end

0 comments on commit 9f01686

Please sign in to comment.