From be4e17b14d2c6ca8c94dc679a8a11e1b1161c503 Mon Sep 17 00:00:00 2001 From: Max VelDink Date: Wed, 13 Mar 2024 14:23:00 -0400 Subject: [PATCH] refactor!: Changed serialize return value to a Result --- lib/sorbet-schema/hash_transformer.rb | 2 ++ lib/typed/hash_serializer.rb | 12 ++++--- lib/typed/json_serializer.rb | 6 ++-- lib/typed/serializer.rb | 4 +-- test/support/structs/person.rb | 7 ++++ test/typed/hash_serializer_test.rb | 51 ++++++++++++++++----------- test/typed/json_serializer_test.rb | 33 ++++++++--------- test/typed/schema_test.rb | 10 +++--- 8 files changed, 72 insertions(+), 53 deletions(-) diff --git a/lib/sorbet-schema/hash_transformer.rb b/lib/sorbet-schema/hash_transformer.rb index 3a847b9..e18d36a 100644 --- a/lib/sorbet-schema/hash_transformer.rb +++ b/lib/sorbet-schema/hash_transformer.rb @@ -36,6 +36,8 @@ def transform_value(value, hash_transformation_method:) send(hash_transformation_method, value) elsif value.is_a?(Array) value.map { |inner_val| transform_value(inner_val, hash_transformation_method: hash_transformation_method) } + elsif value.is_a?(T::Struct) && should_serialize_values + deep_symbolize_keys(value.serialize) elsif value.respond_to?(:serialize) && should_serialize_values value.serialize else diff --git a/lib/typed/hash_serializer.rb b/lib/typed/hash_serializer.rb index f642c38..861ff61 100644 --- a/lib/typed/hash_serializer.rb +++ b/lib/typed/hash_serializer.rb @@ -3,10 +3,8 @@ module Typed class HashSerializer < Serializer InputHash = T.type_alias { T::Hash[T.any(Symbol, String), T.untyped] } - OutputHash = T.type_alias { Params } - Input = type_member { {fixed: InputHash} } - Output = type_member { {fixed: OutputHash} } + Output = type_member { {fixed: Params} } sig { params(schema: Schema, should_serialize_values: T::Boolean).void } def initialize(schema:, should_serialize_values: false) @@ -20,9 +18,13 @@ def deserialize(source) deserialize_from_creation_params(HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(source)) end - sig { override.params(struct: T::Struct).returns(Output) } + sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) } def serialize(struct) - HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(struct.serialize) + return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target + + hsh = schema.fields.each_with_object({}) { |field, hsh| hsh[field.name] = struct.send(field.name) } + + Success.new(HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(hsh.compact)) end private diff --git a/lib/typed/json_serializer.rb b/lib/typed/json_serializer.rb index f893450..08a5dc2 100644 --- a/lib/typed/json_serializer.rb +++ b/lib/typed/json_serializer.rb @@ -20,9 +20,11 @@ def deserialize(source) Failure.new(ParseError.new(format: :json)) end - sig { override.params(struct: T::Struct).returns(Output) } + sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) } def serialize(struct) - JSON.generate(struct.serialize) + return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target + + Success.new(JSON.generate(struct.serialize)) end end end diff --git a/lib/typed/serializer.rb b/lib/typed/serializer.rb index a63bfcb..67fbb23 100644 --- a/lib/typed/serializer.rb +++ b/lib/typed/serializer.rb @@ -20,11 +20,11 @@ def initialize(schema:) @schema = schema end - sig { abstract.params(source: Output).returns(DeserializeResult) } + sig { abstract.params(source: Input).returns(DeserializeResult) } def deserialize(source) end - sig { abstract.params(struct: T::Struct).returns(Output) } + sig { abstract.params(struct: T::Struct).returns(Result[Output, SerializeError]) } def serialize(struct) end diff --git a/test/support/structs/person.rb b/test/support/structs/person.rb index 1722edf..377533f 100644 --- a/test/support/structs/person.rb +++ b/test/support/structs/person.rb @@ -1,9 +1,16 @@ # typed: true +require_relative "job" +require_relative "../enums/ruby_rank" + class Person < T::Struct include ActsAsComparable const :name, String const :age, Integer + const :ruby_rank, RubyRank const :job, T.nilable(Job) end + +MAX_PERSON = Person.new(name: "Max", age: 29, ruby_rank: RubyRank::Luminary) +ALEX_PERSON = Person.new(name: "Alex", age: 31, ruby_rank: RubyRank::Brilliant, job: Job.new(title: "Software Developer", salary: 1_000_000_00)) diff --git a/test/typed/hash_serializer_test.rb b/test/typed/hash_serializer_test.rb index d85367a..0df1cde 100644 --- a/test/typed/hash_serializer_test.rb +++ b/test/typed/hash_serializer_test.rb @@ -8,50 +8,60 @@ def setup # Serialize Tests def test_it_can_simple_serialize - max = Person.new(name: "Max", age: 29) + result = @serializer.serialize(MAX_PERSON) - assert_equal({name: "Max", age: 29}, @serializer.serialize(max)) + assert_success(result) + assert_payload({name: "Max", age: 29, ruby_rank: RubyRank::Luminary}, result) 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)) + result = @serializer.serialize(ALEX_PERSON) + + assert_success(result) + assert_payload({name: "Alex", age: 31, ruby_rank: RubyRank::Brilliant, job: Job.new(title: "Software Developer", salary: 1_000_000_00)}, result) + end - assert_equal({name: "Hank", age: 38, job: {title: "Software Developer", salary: 90_000_00}}, @serializer.serialize(hank)) + def test_it_can_deep_serialize + serializer = Typed::HashSerializer.new(schema: Typed::Schema.from_struct(Person), should_serialize_values: true) + + result = serializer.serialize(ALEX_PERSON) + + assert_success(result) + assert_payload({name: "Alex", age: 31, ruby_rank: "pretty", job: {title: "Software Developer", salary: 1_000_000_00}}, result) + end + + def test_when_struct_given_is_not_of_target_type_returns_failure + result = @serializer.serialize(Job.new(title: "Testing", salary: 90_00)) + + assert_failure(result) + assert_error(Typed::SerializeError.new("'Job' cannot be serialized to target type of 'Person'."), result) end # Deserialize Tests def test_it_can_simple_deserialize - max_hash = {name: "Max", age: 29} - - result = @serializer.deserialize(max_hash) + result = @serializer.deserialize({name: "Max", age: 29, ruby_rank: RubyRank::Luminary}) assert_success(result) - assert_payload(Person.new(name: "Max", age: 29), result) + assert_payload(MAX_PERSON, result) end def test_it_can_simple_deserialize_from_string_keys - max_hash = {"name" => "Max", "age" => 29} - - result = @serializer.deserialize(max_hash) + result = @serializer.deserialize({"name" => "Max", "age" => 29, "ruby_rank" => RubyRank::Luminary}) assert_success(result) - assert_payload(Person.new(name: "Max", age: 29), result) + assert_payload(MAX_PERSON, 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) + result = @serializer.deserialize({name: "Alex", age: 31, ruby_rank: RubyRank::Brilliant, job: {title: "Software Developer", salary: 1_000_000_00}}) assert_success(result) - assert_payload(Person.new(name: "Hank", age: 38, job: Job.new(title: "Software Developer", salary: 90_000_00)), result) + assert_payload(ALEX_PERSON, result) end def test_it_reports_validation_errors_on_deserialize - max_hash = {name: "Max"} - - result = @serializer.deserialize(max_hash) + result = @serializer.deserialize({name: "Max", ruby_rank: RubyRank::Luminary}) assert_failure(result) assert_error(Typed::Validations::RequiredFieldError.new(field_name: :age), result) @@ -65,7 +75,8 @@ def test_it_reports_multiple_validation_errors_on_deserialize Typed::Validations::MultipleValidationError.new( errors: [ Typed::Validations::RequiredFieldError.new(field_name: :name), - Typed::Validations::RequiredFieldError.new(field_name: :age) + Typed::Validations::RequiredFieldError.new(field_name: :age), + Typed::Validations::RequiredFieldError.new(field_name: :ruby_rank) ] ), result diff --git a/test/typed/json_serializer_test.rb b/test/typed/json_serializer_test.rb index 11a6714..1546abb 100644 --- a/test/typed/json_serializer_test.rb +++ b/test/typed/json_serializer_test.rb @@ -10,50 +10,44 @@ def setup # Serialize Tests def test_it_can_simple_serialize - max = Person.new(name: "Max", age: 29) + result = @serializer.serialize(MAX_PERSON) - assert_equal('{"name":"Max","age":29}', @serializer.serialize(max)) + assert_success(result) + assert_payload('{"name":"Max","age":29,"ruby_rank":"shiny"}', result) 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)) + result = @serializer.serialize(ALEX_PERSON) - assert_equal('{"name":"Hank","age":38,"job":{"title":"Software Developer","salary":9000000}}', @serializer.serialize(hank)) + assert_success(result) + assert_payload('{"name":"Alex","age":31,"ruby_rank":"pretty","job":{"title":"Software Developer","salary":100000000}}', result) end # Deserialize Tests def test_it_can_simple_deserialize - hank_json = '{"name":"Hank","age":38,"job":{"title":"Software Developer","salary":9000000}}' - - result = @serializer.deserialize(hank_json) + result = @serializer.deserialize('{"name":"Max","age":29,"ruby_rank":"shiny"}') assert_success(result) - assert_payload(Person.new(name: "Hank", age: 38, job: Job.new(title: "Software Developer", salary: 90_000_00)), result) + assert_payload(MAX_PERSON, result) end def test_it_can_deserialize_with_nested_object - hank_json = '{"name":"Hank","age":38,"job":{"title":"Software Developer","salary":9000000}}' - - result = @serializer.deserialize(hank_json) + result = @serializer.deserialize('{"name":"Alex","age":31,"ruby_rank":"pretty","job":{"title":"Software Developer","salary":100000000}}') assert_success(result) - assert_payload(Person.new(name: "Hank", age: 38, job: Job.new(title: "Software Developer", salary: 90_000_00)), result) + assert_payload(ALEX_PERSON, result) end def test_it_reports_on_parse_errors_on_deserialize - max_json = '{"name": "Max", age": 29}' # Missing quotation - - result = @serializer.deserialize(max_json) + result = @serializer.deserialize('{"name": "Max", age": 29, "ruby_rank": "shiny"}') # Missing quotation assert_failure(result) assert_error(Typed::ParseError.new(format: :json), result) end def test_it_reports_validation_errors_on_deserialize - max_json = '{"name": "Max"}' - - result = @serializer.deserialize(max_json) + result = @serializer.deserialize('{"name": "Max", "ruby_rank": "shiny"}') assert_failure(result) assert_error(Typed::Validations::RequiredFieldError.new(field_name: :age), result) @@ -69,7 +63,8 @@ def test_it_reports_multiple_validation_errors_on_deserialize Typed::Validations::MultipleValidationError.new( errors: [ Typed::Validations::RequiredFieldError.new(field_name: :name), - Typed::Validations::RequiredFieldError.new(field_name: :age) + Typed::Validations::RequiredFieldError.new(field_name: :age), + Typed::Validations::RequiredFieldError.new(field_name: :ruby_rank) ] ), result diff --git a/test/typed/schema_test.rb b/test/typed/schema_test.rb index af6ea89..e982ca0 100644 --- a/test/typed/schema_test.rb +++ b/test/typed/schema_test.rb @@ -6,11 +6,11 @@ def setup fields: [ Typed::Field.new(name: :name, type: String), Typed::Field.new(name: :age, type: Integer), + Typed::Field.new(name: :ruby_rank, type: RubyRank), Typed::Field.new(name: :job, type: Job, required: false) ], target: Person ) - @person = Person.new(name: "Max", age: 29) end def test_from_struct_returns_schema @@ -18,16 +18,16 @@ def test_from_struct_returns_schema end def test_from_hash_create_struct - result = @schema.from_hash({name: "Max", age: 29}) + result = @schema.from_hash({name: "Max", age: 29, ruby_rank: RubyRank::Luminary}) assert_success(result) - assert_payload(@person, result) + assert_payload(MAX_PERSON, result) end def test_from_json_creates_struct - result = @schema.from_json('{"name": "Max", "age": 29}') + result = @schema.from_json('{"name": "Max", "age": 29, "ruby_rank": "shiny"}') assert_success(result) - assert_payload(@person, result) + assert_payload(MAX_PERSON, result) end end