Skip to content

Commit

Permalink
feat: Add inline serializer to fields
Browse files Browse the repository at this point in the history
  • Loading branch information
maxveldink committed Mar 21, 2024
1 parent 172393c commit f33d4b7
Show file tree
Hide file tree
Showing 8 changed files with 67 additions and 6 deletions.
26 changes: 24 additions & 2 deletions lib/typed/field.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ module Typed
class Field
extend T::Sig

InlineSerializer = T.type_alias { T.proc.params(arg0: T.untyped).returns(T.untyped) }

sig { returns(Symbol) }
attr_reader :name

Expand All @@ -13,11 +15,22 @@ class Field
sig { returns(T::Boolean) }
attr_reader :required

sig { params(name: Symbol, type: T.any(T::Class[T.anything], T::Types::Base), required: T::Boolean).void }
def initialize(name:, type:, required: true)
sig { returns(T.nilable(InlineSerializer)) }
attr_reader :inline_serializer

sig do
params(
name: Symbol,
type: T.any(T::Class[T.anything], T::Types::Base),
required: T::Boolean,
inline_serializer: T.nilable(InlineSerializer)
).void
end
def initialize(name:, type:, required: true, inline_serializer: nil)
@name = name
@type = T.let(T::Utils.coerce(type), T::Types::Base)
@required = required
@inline_serializer = inline_serializer
end

sig { params(other: Field).returns(T.nilable(T::Boolean)) }
Expand All @@ -37,6 +50,15 @@ def optional?
!required
end

sig { params(value: Value).returns(Value) }
def serialize(value)
if inline_serializer && value
T.must(inline_serializer).call(value)
else
value
end
end

sig { params(value: Value).returns(Validations::ValidationResult) }
def validate(value)
Validations::FieldTypeValidator.new.validate(field: self, value: value)
Expand Down
4 changes: 1 addition & 3 deletions lib/typed/hash_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ def deserialize(source)
def serialize(struct)
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))
Success.new(serialize_from_struct(struct: struct, should_serialize_values: should_serialize_values))
end

private
Expand Down
2 changes: 1 addition & 1 deletion lib/typed/json_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def deserialize(source)
def serialize(struct)
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))
Success.new(JSON.generate(serialize_from_struct(struct: struct, should_serialize_values: true)))
end
end
end
7 changes: 7 additions & 0 deletions lib/typed/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,5 +55,12 @@ def deserialize_from_creation_params(creation_params)
Success.new(schema.target.new(**validated_params))
end
end

sig { params(struct: T::Struct, should_serialize_values: T::Boolean).returns(T::Hash[Symbol, T.untyped]) }
def serialize_from_struct(struct:, should_serialize_values: false)
hsh = schema.fields.each_with_object({}) { |field, hsh| hsh[field.name] = field.serialize(struct.send(field.name)) }.compact

HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(hsh)
end
end
end
8 changes: 8 additions & 0 deletions test/support/structs/job.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,13 @@ class Job < T::Struct
const :start_date, T.nilable(Date)
end

JOB_SCHEMA_WITH_INLINE_SERIALIZER = Typed::Schema.new(
target: Job,
fields: [
Typed::Field.new(name: :title, type: String),
Typed::Field.new(name: :salary, type: Integer),
Typed::Field.new(name: :start_date, type: Date, required: false, inline_serializer: ->(start_date) { start_date.strftime("%j %B") })
]
)
DEVELOPER_JOB = Job.new(title: "Software Developer", salary: 90_000_00)
DEVELOPER_JOB_WITH_START_DATE = Job.new(title: "Software Developer", salary: 90_000_00, start_date: Date.new(2024, 3, 1))
12 changes: 12 additions & 0 deletions test/typed/field_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ def test_required_and_optional_helpers_work_when_optional
refute_predicate(@optional_field, :required?)
end

def test_when_inline_serializer_serialize_uses_it
field = Typed::Field.new(name: :testing, type: String, inline_serializer: ->(_value) { "banana" })

assert_equal("banana", field.serialize("testing"))
assert_nil(field.serialize(nil))
end

def test_when_no_inline_serializer_serialize_returns_given_value
assert_equal("testing", @required_field.serialize("testing"))
assert_nil(@required_field.serialize(nil))
end

def test_when_standard_type_work_with_works
assert(@required_field.works_with?("Max"))
refute(@required_field.works_with?(1))
Expand Down
7 changes: 7 additions & 0 deletions test/typed/hash_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ def test_when_struct_given_is_not_of_target_type_returns_failure
assert_error(Typed::SerializeError.new("'Job' cannot be serialized to target type of 'Person'."), result)
end

def test_will_use_inline_serializers
result = Typed::HashSerializer.new(schema: JOB_SCHEMA_WITH_INLINE_SERIALIZER, should_serialize_values: true).serialize(DEVELOPER_JOB_WITH_START_DATE)

assert_success(result)
assert_payload({title: "Software Developer", salary: 90_000_00, start_date: "061 March"}, result)
end

# Deserialize Tests

def test_it_can_simple_deserialize
Expand Down
7 changes: 7 additions & 0 deletions test/typed/json_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@ def test_with_array_it_can_serialize
assert_payload('{"name":"US","cities":[{"name":"New York","capital":false},{"name":"DC","capital":true}]}', result)
end

def test_will_use_inline_serializers
result = Typed::JSONSerializer.new(schema: JOB_SCHEMA_WITH_INLINE_SERIALIZER).serialize(DEVELOPER_JOB_WITH_START_DATE)

assert_success(result)
assert_payload('{"title":"Software Developer","salary":9000000,"start_date":"061 March"}', result)
end

# Deserialize Tests

def test_it_can_simple_deserialize
Expand Down

0 comments on commit f33d4b7

Please sign in to comment.