Skip to content

Commit

Permalink
feat: Do type coercion before validation when deserializing
Browse files Browse the repository at this point in the history
  • Loading branch information
maxveldink committed Mar 5, 2024
1 parent 71228b4 commit 6a79889
Show file tree
Hide file tree
Showing 4 changed files with 44 additions and 10 deletions.
20 changes: 17 additions & 3 deletions lib/typed/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions lib/typed/validations/validation_results.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions test/typed/hash_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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"}

Expand Down
11 changes: 6 additions & 5 deletions test/typed/validations/validation_results_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down

0 comments on commit 6a79889

Please sign in to comment.