Skip to content

Commit

Permalink
fix!: Ensure that nested structs will deeply serialize (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
tbconroy authored Jul 19, 2024
1 parent 0e34091 commit 9216d02
Show file tree
Hide file tree
Showing 10 changed files with 48 additions and 20 deletions.
2 changes: 1 addition & 1 deletion lib/sorbet-schema/serialize_value.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ def self.serialize(value)
elsif value.is_a?(Array)
value.map { |item| serialize(item) }
elsif value.is_a?(T::Struct)
value.serialize_to(:hash).payload_or(value)
value.serialize_to(:deeply_nested_hash).payload_or(value)
elsif value.respond_to?(:serialize)
value.serialize
else
Expand Down
2 changes: 2 additions & 0 deletions lib/sorbet-schema/t/struct.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ def schema
sig { params(type: Symbol).returns(Typed::Serializer[T.untyped, T.untyped]) }
def serializer(type)
case type
when :deeply_nested_hash
Typed::HashSerializer.new(schema:, should_serialize_values: true)
when :hash
Typed::HashSerializer.new(schema:)
when :json
Expand Down
8 changes: 3 additions & 5 deletions lib/typed/hash_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ class HashSerializer < Serializer
Input = type_member { {fixed: InputHash} }
Output = type_member { {fixed: Params} }

sig { returns(T::Boolean) }
attr_reader :should_serialize_values

sig { params(schema: Schema, should_serialize_values: T::Boolean).void }
def initialize(schema:, should_serialize_values: false)
@should_serialize_values = should_serialize_values
Expand All @@ -24,10 +27,5 @@ def serialize(struct)

Success.new(serialize_from_struct(struct:, should_serialize_values:))
end

private

sig { returns(T::Boolean) }
attr_reader :should_serialize_values
end
end
7 changes: 7 additions & 0 deletions test/support/enums/currency.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# typed: true

class Currency < T::Enum
enums do
USD = new("USD")
end
end
7 changes: 4 additions & 3 deletions test/support/structs/job.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# typed: true

require "date"
require_relative "money"

class Job < T::Struct
include ActsAsComparable

const :title, String
const :salary, Integer
const :salary, Money
const :start_date, T.nilable(Date)
const :needs_credential, T::Boolean, default: false
end
Expand All @@ -19,5 +20,5 @@ class Job < T::Struct
Typed::Field.new(name: :start_date, type: T::Utils.coerce(T.nilable(Date)), 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))
DEVELOPER_JOB = Job.new(title: "Software Developer", salary: Money.new(cents: 90_000_00))
DEVELOPER_JOB_WITH_START_DATE = Job.new(title: "Software Developer", salary: Money.new(cents: 90_000_00), start_date: Date.new(2024, 3, 1))
10 changes: 10 additions & 0 deletions test/support/structs/money.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# typed: false

require_relative "../enums/currency"

class Money < T::Struct
include ActsAsComparable

const :cents, Integer
const :currency, Currency, default: Currency::USD
end
14 changes: 12 additions & 2 deletions test/t/struct_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def test_schema_can_be_derived_from_struct_with_default
expected_schema = Typed::Schema.new(
fields: [
Typed::Field.new(name: :title, type: String),
Typed::Field.new(name: :salary, type: Integer),
Typed::Field.new(name: :salary, type: Money),
Typed::Field.new(name: :start_date, type: Date, optional: true),
Typed::Field.new(name: :needs_credential, type: T::Utils.coerce(T::Boolean), default: false, optional: true)
],
Expand All @@ -28,8 +28,18 @@ def test_schema_can_be_derived_from_struct_with_default
assert_equal(expected_schema, Job.schema)
end

def test_serializer_returns_deeply_nested_hash_serializer
serializer = City.serializer(:deeply_nested_hash)

assert_kind_of(Typed::HashSerializer, serializer)
assert(T.cast(serializer, Typed::HashSerializer).should_serialize_values)
end

def test_serializer_returns_hash_serializer
assert_kind_of(Typed::HashSerializer, City.serializer(:hash))
serializer = City.serializer(:hash)

assert_kind_of(Typed::HashSerializer, serializer)
refute(T.cast(serializer, Typed::HashSerializer).should_serialize_values)
end

def test_serializer_returns_json_serializer
Expand Down
6 changes: 3 additions & 3 deletions test/typed/coercion/struct_coercer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def test_when_struct_of_incorrect_type_given_returns_failure
end

def test_when_struct_can_be_coerced_returns_success
result = @coercer.coerce(type: @type, value: {"title" => "Software Developer", :salary => 90_000_00})
result = @coercer.coerce(type: @type, value: {"title" => "Software Developer", :salary => Money.new(cents: 90_000_00)})

assert_success(result)
assert_payload(DEVELOPER_JOB, result)
Expand All @@ -59,7 +59,7 @@ def test_when_struct_has_nested_struct_and_all_values_passed_for_nested_struct
name = "Alex"
age = 31
stone_rank = "pretty"
salary = 90_000_00
salary = Money.new(cents: 90_000_00)
title = "Software Developer"
start_date = Date.new(2024, 3, 1)

Expand All @@ -80,7 +80,7 @@ def test_when_struct_has_nested_struct_and_optional_start_date_not_passed_for_ne
name = "Alex"
age = 31
stone_rank = "pretty"
salary = 90_000_00
salary = Money.new(cents: 90_000_00)
title = "Software Developer"

result = @coercer.coerce(type: @type_with_nested_struct, value: {name:, age:, stone_rank:, job: {title:, salary:}})
Expand Down
6 changes: 3 additions & 3 deletions test/typed/hash_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def test_it_can_deep_serialize
result = serializer.serialize(ALEX_PERSON)

assert_success(result)
assert_payload({name: "Alex", age: 31, stone_rank: "pretty", job: {title: "Software Developer", salary: 90_000_00, needs_credential: false}}, result)
assert_payload({name: "Alex", age: 31, stone_rank: "pretty", job: {title: "Software Developer", salary: {cents: 9000000, currency: "USD"}, needs_credential: false}}, result)
end

def test_with_boolean_it_can_serialize
Expand Down Expand Up @@ -76,7 +76,7 @@ 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)
assert_payload({title: "Software Developer", salary: {cents: 90_000_00, currency: "USD"}, start_date: "061 March"}, result)
end

def test_with_hash_field_with_string_keys_serializes
Expand Down Expand Up @@ -124,7 +124,7 @@ def test_with_array_it_can_deep_deserialize
end

def test_it_can_deserialize_with_nested_object
result = @serializer.deserialize({name: "Alex", age: 31, stone_rank: "pretty", job: {title: "Software Developer", salary: 90_000_00}})
result = @serializer.deserialize({name: "Alex", age: 31, stone_rank: "pretty", job: {title: "Software Developer", salary: Money.new(cents: 90_000_00)}})

assert_success(result)
assert_payload(ALEX_PERSON, result)
Expand Down
6 changes: 3 additions & 3 deletions test/typed/json_serializer_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def test_it_can_serialize_with_nested_struct
result = @serializer.serialize(ALEX_PERSON)

assert_success(result)
assert_payload('{"name":"Alex","age":31,"stone_rank":"pretty","job":{"title":"Software Developer","salary":9000000,"needs_credential":false}}', result)
assert_payload('{"name":"Alex","age":31,"stone_rank":"pretty","job":{"title":"Software Developer","salary":{"cents":9000000,"currency":"USD"},"needs_credential":false}}', result)
end

def test_with_boolean_it_can_serialize
Expand All @@ -41,7 +41,7 @@ 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)
assert_payload('{"title":"Software Developer","salary":{"cents":9000000,"currency":"USD"},"start_date":"061 March"}', result)
end

# Deserialize Tests
Expand All @@ -68,7 +68,7 @@ def test_with_array_it_can_deep_deserialize
end

def test_it_can_deserialize_with_nested_object
result = @serializer.deserialize('{"name":"Alex","age":31,"stone_rank":"pretty","job":{"title":"Software Developer","salary":9000000}}')
result = @serializer.deserialize('{"name":"Alex","age":31,"stone_rank":"pretty","job":{"title":"Software Developer","salary":{"cents":9000000,"currency":"USD"}}}')

assert_success(result)
assert_payload(ALEX_PERSON, result)
Expand Down

0 comments on commit 9216d02

Please sign in to comment.