Skip to content

Commit

Permalink
refactor!: Changed serialize return value to a Result
Browse files Browse the repository at this point in the history
  • Loading branch information
maxveldink committed Mar 13, 2024
1 parent f8ea753 commit 948c678
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 53 deletions.
2 changes: 2 additions & 0 deletions lib/sorbet-schema/hash_transformer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 7 additions & 5 deletions lib/typed/hash_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
6 changes: 4 additions & 2 deletions lib/typed/json_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions lib/typed/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions test/support/structs/person.rb
Original file line number Diff line number Diff line change
@@ -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))
51 changes: 31 additions & 20 deletions test/typed/hash_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
33 changes: 14 additions & 19 deletions test/typed/json_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
10 changes: 5 additions & 5 deletions test/typed/schema_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,28 @@ 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
assert_equal(@schema, Typed::Schema.from_struct(Person))
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

0 comments on commit 948c678

Please sign in to comment.