Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce simple HashSerializer #19

Merged
merged 4 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/typed/hash_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# typed: strict

module Typed
class HashSerializer < Serializer
Input = type_member { {fixed: T::Hash[T.any(Symbol, String), T.untyped]} }
Output = type_member { {fixed: T::Hash[Symbol, T.untyped]} }

sig { override.params(source: Input).returns(Result[T::Struct, DeserializeError]) }
def deserialize(source)
deserialize_from_creation_params(symbolize_keys(source))
end

sig { override.params(struct: T::Struct).returns(Output) }
def serialize(struct)
symbolize_keys(struct.serialize)
end

private

sig { params(hash: T::Hash[T.any(String, Symbol), T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
def symbolize_keys(hash)
hash.each_with_object({}) { |(k, v), h| h[k.intern] = v }
end
end
end
18 changes: 5 additions & 13 deletions lib/typed/json_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,23 @@

module Typed
class JSONSerializer < Serializer
extend T::Sig
Input = type_member { {fixed: String} }
Output = type_member { {fixed: String} }

sig { override.params(source: String).returns(Result[T::Struct, DeserializeError]) }
sig { override.params(source: Input).returns(Result[T::Struct, DeserializeError]) }
def deserialize(source)
parsed_json = JSON.parse(source)

creation_params = schema.fields.each_with_object(T.let({}, Params)) do |field, hsh|
hsh[field.name] = parsed_json[field.name.to_s]
end

results = creation_params.map do |name, value|
schema.field(name:)&.validate(value)
end.compact

Validations::ValidationResults
.new(results:)
.combine
.and_then do
Success.new(schema.target.new(**creation_params))
end
deserialize_from_creation_params(creation_params)
rescue JSON::ParserError
Failure.new(ParseError.new(format: :json))
end

sig { override.params(struct: T::Struct).returns(String) }
sig { override.params(struct: T::Struct).returns(Output) }
def serialize(struct)
JSON.generate(struct.serialize)
end
Expand Down
7 changes: 0 additions & 7 deletions lib/typed/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,9 @@

module Typed
class Schema < T::Struct
extend T::Sig

include T::Struct::ActsAsComparable

const :fields, T::Array[Field], default: []
const :target, T.class_of(T::Struct)

sig { params(name: Symbol).returns(T.nilable(Field)) }
def field(name:)
fields.find { |field| field.name == name }
end
end
end
24 changes: 22 additions & 2 deletions lib/typed/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@ module Typed
class Serializer
extend T::Sig
extend T::Helpers
extend T::Generic
abstract!

Input = type_member
Output = type_member
Params = T.type_alias { T::Hash[Symbol, T.untyped] }
DeserializeResult = T.type_alias { Typed::Result[T::Struct, DeserializeError] }

sig { returns(Schema) }
attr_reader :schema
Expand All @@ -16,12 +20,28 @@ def initialize(schema:)
@schema = schema
end

sig { abstract.params(source: String).returns(Typed::Result[T::Struct, DeserializeError]) }
sig { abstract.params(source: Output).returns(DeserializeResult) }
def deserialize(source)
end

sig { abstract.params(struct: T::Struct).returns(String) }
sig { abstract.params(struct: T::Struct).returns(Output) }
def serialize(struct)
end

private

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])
end

Validations::ValidationResults
.new(results:)
.combine
.and_then do
Success.new(schema.target.new(**creation_params))
end
end
end
end
2 changes: 0 additions & 2 deletions test/struct_ext_test.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
# typed: true

require "sorbet-schema/struct_ext"

class StructExtTest < Minitest::Test
def test_create_schema_is_available
assert_equal(PersonSchema, Person.create_schema)
Expand Down
1 change: 1 addition & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
require "debug"

require "sorbet-schema"
require "sorbet-schema/struct_ext"

require_relative "support/schemas/person_schema"
59 changes: 59 additions & 0 deletions test/typed/hash_serializer_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# typed: true

class HashSerializerTest < Minitest::Test
def setup
@serializer = Typed::HashSerializer.new(schema: PersonSchema)
end

# Serialize Tests

def test_it_can_simple_serialize
max = PersonSchema.target.new(name: "Max", age: 29)

assert_equal({name: "Max", age: 29}, @serializer.serialize(max))
end

# Deserialize Tests

def test_it_can_simple_deserialize
max_hash = {name: "Max", age: 29}

result = @serializer.deserialize(max_hash)

assert_success(result)
assert_payload(PersonSchema.target.new(name: "Max", age: 29), result)
end

def test_it_can_simple_deserialize_from_string_keys
max_hash = {"name" => "Max", "age" => 29}

result = @serializer.deserialize(max_hash)

assert_success(result)
assert_payload(PersonSchema.target.new(name: "Max", age: 29), result)
end

def test_it_reports_validation_errors_on_deserialize
max_hash = {name: "Max"}

result = @serializer.deserialize(max_hash)

assert_failure(result)
assert_error(Typed::Validations::RequiredFieldError.new(field_name: :age), result)
end

def test_it_reports_multiple_validation_errors_on_deserialize
result = @serializer.deserialize({})

assert_failure(result)
assert_error(
Typed::Validations::MultipleValidationError.new(
errors: [
Typed::Validations::RequiredFieldError.new(field_name: :name),
Typed::Validations::RequiredFieldError.new(field_name: :age)
]
),
result
)
end
end
11 changes: 0 additions & 11 deletions test/typed/schema_test.rb

This file was deleted.