Skip to content

Commit

Permalink
feat: Introduce simple HashSerializer (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
maxveldink authored Feb 27, 2024
1 parent 1f335b7 commit 80f20a9
Show file tree
Hide file tree
Showing 8 changed files with 112 additions and 35 deletions.
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.

0 comments on commit 80f20a9

Please sign in to comment.