From 6a7988988de607cf94c65bc053524d6a6ab9d33d Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Tue, 5 Mar 2024 13:00:00 -0500 Subject: [PATCH] feat: Do type coercion before validation when deserializing --- lib/typed/serializer.rb | 20 ++++++++++++++++--- lib/typed/validations/validation_results.rb | 8 ++++++-- test/typed/hash_serializer_test.rb | 15 ++++++++++++++ .../validations/validation_results_test.rb | 11 +++++----- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/lib/typed/serializer.rb b/lib/typed/serializer.rb index f4db1c1..9f369cc 100644 --- a/lib/typed/serializer.rb +++ b/lib/typed/serializer.rb @@ -33,14 +33,28 @@ def serialize(struct) sig { params(creation_params: Params).returns(DeserializeResult) } def deserialize_from_creation_params(creation_params) results = schema.fields.map do |field| - field.validate(creation_params[field.name]) + value = creation_params[field.name] + + if value.nil? + field.validate(value) + elsif value.class != field.type + coercion_result = Coercion.coerce(field:, value:) + + if coercion_result.success? + field.validate(coercion_result.payload) + else + Failure.new(Validations::ValidationError.new(coercion_result.error.message)) + end + else + field.validate(value) + end end Validations::ValidationResults .new(results:) .combine - .and_then do - Success.new(schema.target.new(**creation_params)) + .and_then do |validated_params| + Success.new(schema.target.new(**validated_params)) end end end diff --git a/lib/typed/validations/validation_results.rb b/lib/typed/validations/validation_results.rb index ee5bed3..0148406 100644 --- a/lib/typed/validations/validation_results.rb +++ b/lib/typed/validations/validation_results.rb @@ -7,13 +7,17 @@ class ValidationResults < T::Struct const :results, T::Array[ValidationResult] - sig { returns(ValidationResult) } + sig { returns(Result[ValidatedParams, ValidationError]) } def combine failing_results = results.select(&:failure?) case failing_results.length when 0 - Success.blank + Success.new( + results.each_with_object({}) do |result, validated_params| + validated_params[result.payload.name] = result.payload.value + end + ) when 1 Failure.new(T.must(failing_results.first).error) else diff --git a/test/typed/hash_serializer_test.rb b/test/typed/hash_serializer_test.rb index 7c5729e..60e24a1 100644 --- a/test/typed/hash_serializer_test.rb +++ b/test/typed/hash_serializer_test.rb @@ -13,6 +13,12 @@ def test_it_can_simple_serialize assert_equal({name: "Max", age: 29}, @serializer.serialize(max)) end + def test_it_can_serialize_with_nested_struct + hank = Person.new(name: "Hank", age: 38, job: Job.new(title: "Software Developer", salary: 90_000_00)) + + assert_equal({name: "Hank", age: 38, job: {title: "Software Developer", salary: 90_000_00}}, @serializer.serialize(hank)) + end + # Deserialize Tests def test_it_can_simple_deserialize @@ -33,6 +39,15 @@ def test_it_can_simple_deserialize_from_string_keys assert_payload(Person.new(name: "Max", age: 29), result) end + def test_it_can_deserialize_with_nested_object + hank_hash = {name: "Hank", age: 38, job: {title: "Software Developer", salary: 90_000_00}} + + result = @serializer.deserialize(hank_hash) + + assert_success(result) + assert_payload(Person.new(name: "Hank", age: 38, job: Job.new(title: "Software Developer", salary: 90_000_00)), result) + end + def test_it_reports_validation_errors_on_deserialize max_hash = {name: "Max"} diff --git a/test/typed/validations/validation_results_test.rb b/test/typed/validations/validation_results_test.rb index 657b925..284dcb7 100644 --- a/test/typed/validations/validation_results_test.rb +++ b/test/typed/validations/validation_results_test.rb @@ -4,27 +4,28 @@ class ValidationResultsTest < Minitest::Test def setup - @success = Typed::Success.new("Testing") + @success1 = Typed::Success.new(Typed::Validations::ValidatedValue.new(name: :test, value: "testing")) + @success2 = Typed::Success.new(Typed::Validations::ValidatedValue.new(name: :test_again, value: 1)) @error = Typed::Validations::RequiredFieldError.new(field_name: :bad) @failure = Typed::Failure.new(@error) end def test_when_0_failures_combine_returns_success - result = Typed::Validations::ValidationResults.new(results: [@success]).combine + result = Typed::Validations::ValidationResults.new(results: [@success1, @success2]).combine assert_success(result) - assert_nil(result.payload) + assert_payload({test: "testing", test_again: 1}, result) end def test_when_1_failure_it_returns_failure_and_error - result = Typed::Validations::ValidationResults.new(results: [@success, @failure]).combine + result = Typed::Validations::ValidationResults.new(results: [@success1, @failure]).combine assert_failure(result) assert_error(@error, result) end def test_when_multiple_failures_it_returns_wrapped_failures - result = Typed::Validations::ValidationResults.new(results: [@failure, @success, @failure]).combine + result = Typed::Validations::ValidationResults.new(results: [@failure, @success1, @failure]).combine assert_failure(result) assert_error(