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

fix!: Ensure that nested structs will deeply serialize #118

Merged
merged 8 commits into from
Jul 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 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
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
sorbet-schema (0.7.2)
sorbet-schema (0.8.0)
sorbet-result (~> 1.1)
sorbet-runtime (~> 0.5)
sorbet-struct-comparable (~> 1.3)
Expand Down
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
2 changes: 1 addition & 1 deletion lib/sorbet-schema/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# typed: strict

module SorbetSchema
VERSION = "0.7.2"
VERSION = "0.8.0"
maxveldink marked this conversation as resolved.
Show resolved Hide resolved
end
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
2 changes: 0 additions & 2 deletions lib/typed/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ def serialize(struct)
def deserialize_from_creation_params(creation_params)
results = schema.fields.map do |field|
value = creation_params.fetch(field.name, nil)

if value.nil? && !field.default.nil?
Success.new(Validations::ValidatedValue.new(name: field.name, value: field.default))
elsif value.nil? || field.works_with?(value)
Expand All @@ -48,7 +47,6 @@ def deserialize_from_creation_params(creation_params)
next if sub_type.raw_type.equal?(NilClass)

coercion_result = Coercion.coerce(type: sub_type, value: value)

if coercion_result.success?
validated_value = field.validate(coercion_result.payload)

Expand Down
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_equal(true, T.cast(serializer, Typed::HashSerializer).should_serialize_values)
tbconroy marked this conversation as resolved.
Show resolved Hide resolved
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)
assert_equal(false, T.cast(serializer, Typed::HashSerializer).should_serialize_values)
tbconroy marked this conversation as resolved.
Show resolved Hide resolved
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