sorbet-schema 0.5.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32a23b4d9a5e1eda6fcf49b64428f55fbf1d89f63754de35bf2e0e3a875e018c
4
- data.tar.gz: e76f273b8e566f78e6c18effa1699e3f6afa17311abee06a2183778e7a97f93a
3
+ metadata.gz: d2adb1981aeb5218f9d137f472bd602a2b81bbe6e54166e2974f1c157f92d50a
4
+ data.tar.gz: 1f4ee3983069f2674aab270bb7d138ce3fc611154a4d5c37581b6698ac6f3189
5
5
  SHA512:
6
- metadata.gz: 5e7dcb508366dcda8311bf089ae6877c8ff5ba8fc41e0bec9adcd18ca8f3b3723693cd5139a1ee34d46dfcf0643d511edc04d689a93715e72aa33a2f764454e2
7
- data.tar.gz: bf2c62a198b37caae70736bd1aa954476ef2697c44aa793381f91787b4ad6b78bad0e731a69c3dddb4c7398a3a07092cdebf779d95e27be1fbb68d43bf74958f
6
+ metadata.gz: d8b876a03ee53cafde5a9bbd9f46823d517aa6d69a3417a4e3c04591f1471066bc2912c2c26e00ba9e9294642912e4a4d58dafada77e8206685b4f726c23ae52
7
+ data.tar.gz: 9bf510fe1ef9e4b56ef9206ed1c5ea43bcb3934b1749c427afe76291a9b09871383d443f226f15cac733c39540180c2eed810d653b1728f73e11a8572f5b80be
data/CHANGELOG.md CHANGED
@@ -4,6 +4,36 @@ All notable changes to this project will be documented in this file.
4
4
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.7.0](https://github.com/maxveldink/sorbet-schema/compare/v0.6.0...v0.7.0) (2024-07-08)
8
+
9
+
10
+ ### ⚠ BREAKING CHANGES
11
+
12
+ * Fix mis-serializing hash keys that were suppose to be strings ([#111](https://github.com/maxveldink/sorbet-schema/issues/111))
13
+
14
+ ### Bug Fixes
15
+
16
+ * Fix mis-serializing hash keys that were suppose to be strings ([#111](https://github.com/maxveldink/sorbet-schema/issues/111)) ([485a6c7](https://github.com/maxveldink/sorbet-schema/commit/485a6c7a83b9e70c731930d8406925304efa04a8))
17
+
18
+ ## [0.6.0](https://github.com/maxveldink/sorbet-schema/compare/v0.5.1...v0.6.0) (2024-07-07)
19
+
20
+
21
+ ### ⚠ BREAKING CHANGES
22
+
23
+ * implement default handling for fields ([#105](https://github.com/maxveldink/sorbet-schema/issues/105))
24
+
25
+ ### Features
26
+
27
+ * implement default handling for fields ([#105](https://github.com/maxveldink/sorbet-schema/issues/105)) ([054d59f](https://github.com/maxveldink/sorbet-schema/commit/054d59ff92c68b272d495a0816370b9a890f0f50))
28
+ * implement SymbolCoercer ([#109](https://github.com/maxveldink/sorbet-schema/issues/109)) ([422a995](https://github.com/maxveldink/sorbet-schema/commit/422a9957177039a3dde5c4daa41d597fd44f2b48))
29
+ * implement TypedHashCoercer ([#110](https://github.com/maxveldink/sorbet-schema/issues/110)) ([6d64db7](https://github.com/maxveldink/sorbet-schema/commit/6d64db7fcef8af56cb96f1ee6c42ba1e3ce076c3))
30
+ * support T.any for deserialization ([#107](https://github.com/maxveldink/sorbet-schema/issues/107)) ([c0c2ca3](https://github.com/maxveldink/sorbet-schema/commit/c0c2ca369abef136943e633b7987decad7291d98))
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * default value set to true causes undefined method [] error ([#108](https://github.com/maxveldink/sorbet-schema/issues/108)) ([6829bbf](https://github.com/maxveldink/sorbet-schema/commit/6829bbf8b6bf47db51209e9874608b7e10c38b8e))
36
+
7
37
  ## [0.5.1](https://github.com/maxveldink/sorbet-schema/compare/v0.5.0...v0.5.1) (2024-06-26)
8
38
 
9
39
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-schema (0.5.1)
4
+ sorbet-schema (0.7.0)
5
5
  sorbet-result (~> 1.1)
6
6
  sorbet-runtime (~> 0.5)
7
7
  sorbet-struct-comparable (~> 1.3)
@@ -4,44 +4,21 @@
4
4
  # This is a simplified version of ActiveSupport's Key Hash extension
5
5
  # https://github.com/rails/rails/blob/main/activesupport/lib/active_support/core_ext/hash/keys.rb
6
6
  class HashTransformer
7
- extend T::Sig
7
+ class << self
8
+ extend T::Sig
8
9
 
9
- sig { params(should_serialize_values: T::Boolean).void }
10
- def initialize(should_serialize_values: false)
11
- @should_serialize_values = should_serialize_values
12
- end
13
-
14
- sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
15
- def deep_symbolize_keys(hash)
16
- hash.each_with_object({}) do |(key, value), result|
17
- result[key.to_sym] = transform_value(value, hash_transformation_method: :deep_symbolize_keys)
18
- end
19
- end
20
-
21
- sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[String, T.untyped]) }
22
- def deep_stringify_keys(hash)
23
- hash.each_with_object({}) do |(key, value), result|
24
- result[key.to_s] = transform_value(value, hash_transformation_method: :deep_stringify_keys)
10
+ sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
11
+ def symbolize_keys(hash)
12
+ hash.each_with_object({}) do |(key, value), result|
13
+ result[key.to_sym] = value
14
+ end
25
15
  end
26
- end
27
-
28
- private
29
-
30
- sig { returns(T::Boolean) }
31
- attr_reader :should_serialize_values
32
16
 
33
- sig { params(value: T.untyped, hash_transformation_method: Symbol).returns(T.untyped) }
34
- def transform_value(value, hash_transformation_method:)
35
- if value.is_a?(Hash)
36
- send(hash_transformation_method, value)
37
- elsif value.is_a?(Array)
38
- value.map { |inner_val| transform_value(inner_val, hash_transformation_method: hash_transformation_method) }
39
- elsif value.is_a?(T::Struct) && should_serialize_values
40
- deep_symbolize_keys(value.serialize)
41
- elsif value.respond_to?(:serialize) && should_serialize_values
42
- value.serialize
43
- else
44
- value
17
+ sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[T.untyped, T.untyped]) }
18
+ def serialize_values(hash)
19
+ hash.each_with_object({}) do |(key, value), result|
20
+ result[key] = SerializeValue.serialize(value)
21
+ end
45
22
  end
46
23
  end
47
24
  end
@@ -0,0 +1,20 @@
1
+ # typed: strict
2
+
3
+ class SerializeValue
4
+ extend T::Sig
5
+
6
+ sig { params(value: T.untyped).returns(T.untyped) }
7
+ def self.serialize(value)
8
+ if value.is_a?(Hash)
9
+ HashTransformer.serialize_values(value)
10
+ elsif value.is_a?(Array)
11
+ value.map { |item| serialize(item) }
12
+ elsif value.is_a?(T::Struct)
13
+ value.serialize_to(:hash).payload_or(value)
14
+ elsif value.respond_to?(:serialize)
15
+ value.serialize
16
+ else
17
+ value
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  # typed: strict
2
2
 
3
3
  module SorbetSchema
4
- VERSION = "0.5.1"
4
+ VERSION = "0.7.0"
5
5
  end
data/lib/sorbet-schema.rb CHANGED
@@ -16,10 +16,11 @@ loader.inflector.inflect(
16
16
  )
17
17
  loader.setup
18
18
 
19
- # We don't want to place this in the `Typed` module.
19
+ # We don't want to place these in the `Typed` module.
20
20
  # `sorbet-schema` is a directory that is not autoloaded
21
21
  # but contains extensions, so we need to manually require it.
22
22
  require_relative "sorbet-schema/hash_transformer"
23
+ require_relative "sorbet-schema/serialize_value"
23
24
 
24
25
  # We want to add a default `schema` method to structs
25
26
  # that will guarentee a schema can be created for use
@@ -14,13 +14,15 @@ module Typed
14
14
  DEFAULT_COERCERS = T.let(
15
15
  [
16
16
  StringCoercer,
17
+ SymbolCoercer,
17
18
  BooleanCoercer,
18
19
  IntegerCoercer,
19
20
  FloatCoercer,
20
21
  DateCoercer,
21
22
  EnumCoercer,
22
23
  StructCoercer,
23
- TypedArrayCoercer
24
+ TypedArrayCoercer,
25
+ TypedHashCoercer
24
26
  ],
25
27
  Registry
26
28
  )
@@ -21,41 +21,15 @@ module Typed
21
21
 
22
22
  return Failure.new(CoercionError.new("Value of type '#{value.class}' cannot be coerced to #{type} Struct.")) unless value.is_a?(Hash)
23
23
 
24
- values = {}
25
-
26
- type = T.cast(type, T::Types::Simple)
27
-
28
- type.raw_type.props.each do |name, prop|
29
- attribute_type = prop[:type_object]
30
- value = HashTransformer.new.deep_symbolize_keys(value)
31
-
32
- if value[name].nil?
33
- # if the value is nil but the type is nilable, no need to coerce
34
- next if attribute_type.respond_to?(:valid?) && attribute_type.valid?(value[name])
35
-
36
- return Typed::Failure.new(CoercionError.new("#{name} is required but nil given"))
37
- end
38
-
39
- # now that we've done the nil check, we can unwrap the nilable type to get the raw type
40
- simple_attribute_type = attribute_type.respond_to?(:unwrap_nilable) ? attribute_type.unwrap_nilable : attribute_type
41
-
42
- # if the prop is a struct, we need to recursively coerce it
43
- if simple_attribute_type.respond_to?(:raw_type) && simple_attribute_type.raw_type <= T::Struct
44
- Typed::HashSerializer
45
- .new(schema: simple_attribute_type.raw_type.schema)
46
- .deserialize(value[name])
47
- .and_then { |struct| Typed::Success.new(values[name] = struct) }
48
- .on_error { |error| return Typed::Failure.new(CoercionError.new("Nested hash for #{type} could not be coerced to #{name}, error: #{error}")) }
49
- else
50
- value = HashTransformer.new.deep_symbolize_keys(value)
51
-
52
- Coercion
53
- .coerce(type: attribute_type, value: value[name])
54
- .and_then { |coerced_value| Typed::Success.new(values[name] = coerced_value) }
55
- end
24
+ deserialization_result = T.cast(type, T::Types::Simple)
25
+ .raw_type
26
+ .deserialize_from(:hash, value)
27
+
28
+ if deserialization_result.success?
29
+ deserialization_result
30
+ else
31
+ Failure.new(CoercionError.new(deserialization_result.error.message))
56
32
  end
57
-
58
- Success.new(type.raw_type.new(values))
59
33
  rescue ArgumentError, RuntimeError
60
34
  Failure.new(CoercionError.new("Given hash could not be coerced to #{type}."))
61
35
  end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ module Coercion
5
+ class SymbolCoercer < Coercer
6
+ extend T::Generic
7
+
8
+ Target = type_member { {fixed: Symbol} }
9
+
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
+ def used_for_type?(type)
12
+ type == T::Utils.coerce(Symbol)
13
+ end
14
+
15
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
16
+ def coerce(type:, value:)
17
+ return Failure.new(CoercionError.new("Type must be a Symbol.")) unless used_for_type?(type)
18
+
19
+ if value.respond_to?(:to_sym)
20
+ Success.new(value.to_sym)
21
+ else
22
+ Failure.new(CoercionError.new("Value cannot be coerced into Symbol. Consider adding a #to_sym implementation."))
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,50 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ module Coercion
5
+ class TypedHashCoercer < Coercer
6
+ extend T::Generic
7
+
8
+ Target = type_member { {fixed: T::Hash[T.untyped, T.untyped]} }
9
+
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
+ def used_for_type?(type)
12
+ type.is_a?(T::Types::TypedHash)
13
+ end
14
+
15
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
16
+ def coerce(type:, value:)
17
+ return Failure.new(CoercionError.new("Field type must be a T::Hash.")) unless used_for_type?(type)
18
+ return Failure.new(CoercionError.new("Value must be a Hash.")) unless value.is_a?(Hash)
19
+
20
+ return Success.new(value) if type.recursively_valid?(value)
21
+
22
+ coerced_hash = {}
23
+ errors = []
24
+
25
+ value.each do |k, v|
26
+ key_result = Coercion.coerce(type: T::Utils.coerce(T.cast(type, T::Types::TypedHash).type.types.first), value: k)
27
+ value_result = Coercion.coerce(type: T::Utils.coerce(T.cast(type, T::Types::TypedHash).type.types.last), value: v)
28
+
29
+ if key_result.success? && value_result.success?
30
+ coerced_hash[key_result.payload] = value_result.payload
31
+ else
32
+ if key_result.failure?
33
+ errors << key_result.error
34
+ end
35
+
36
+ if value_result.failure?
37
+ errors << value_result.error
38
+ end
39
+ end
40
+ end
41
+
42
+ if errors.empty?
43
+ Success.new(coerced_hash)
44
+ else
45
+ Failure.new(CoercionError.new(errors.map(&:message).join(" | ")))
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/typed/field.rb CHANGED
@@ -12,6 +12,9 @@ module Typed
12
12
  sig { returns(T::Types::Base) }
13
13
  attr_reader :type
14
14
 
15
+ sig { returns(T.untyped) }
16
+ attr_reader :default
17
+
15
18
  sig { returns(T::Boolean) }
16
19
  attr_reader :required
17
20
 
@@ -22,15 +25,36 @@ module Typed
22
25
  params(
23
26
  name: Symbol,
24
27
  type: T.any(T::Class[T.anything], T::Types::Base),
25
- required: T::Boolean,
28
+ optional: T::Boolean,
29
+ default: T.untyped,
26
30
  inline_serializer: T.nilable(InlineSerializer)
27
31
  ).void
28
32
  end
29
- def initialize(name:, type:, required: true, inline_serializer: nil)
33
+ def initialize(name:, type:, optional: false, default: nil, inline_serializer: nil)
30
34
  @name = name
31
- @type = T.let(T::Utils.coerce(type), T::Types::Base)
32
- @required = required
35
+ # TODO: Guarentee type signature of the serializer will be valid
33
36
  @inline_serializer = inline_serializer
37
+
38
+ coerced_type = T::Utils.coerce(type)
39
+
40
+ if coerced_type.valid?(nil)
41
+ @required = T.let(false, T::Boolean)
42
+ @type = T.let(T.unsafe(coerced_type).unwrap_nilable, T::Types::Base)
43
+ else
44
+ @required = true
45
+ @type = coerced_type
46
+ end
47
+
48
+ if optional
49
+ @required = false
50
+ end
51
+
52
+ if !default.nil? && @type.valid?(default)
53
+ @default = T.let(default, T.untyped)
54
+ @required = false
55
+ elsif !default.nil? && @required
56
+ raise ArgumentError, "Given #{default} with class of #{default.class} for default, invalid with type #{@type}"
57
+ end
34
58
  end
35
59
 
36
60
  sig { params(other: Field).returns(T.nilable(T::Boolean)) }
@@ -38,6 +62,7 @@ module Typed
38
62
  name == other.name &&
39
63
  type == other.type &&
40
64
  required == other.required &&
65
+ default == other.default &&
41
66
  inline_serializer == other.inline_serializer
42
67
  end
43
68
 
@@ -15,14 +15,14 @@ module Typed
15
15
 
16
16
  sig { override.params(source: Input).returns(Result[T::Struct, DeserializeError]) }
17
17
  def deserialize(source)
18
- deserialize_from_creation_params(HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(source))
18
+ deserialize_from_creation_params(HashTransformer.symbolize_keys(source))
19
19
  end
20
20
 
21
21
  sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) }
22
22
  def serialize(struct)
23
23
  return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target
24
24
 
25
- Success.new(serialize_from_struct(struct: struct, should_serialize_values: should_serialize_values))
25
+ Success.new(serialize_from_struct(struct:, should_serialize_values:))
26
26
  end
27
27
 
28
28
  private
@@ -24,7 +24,7 @@ module Typed
24
24
  def serialize(struct)
25
25
  return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target
26
26
 
27
- Success.new(JSON.generate(serialize_from_struct(struct: struct, should_serialize_values: true)))
27
+ Success.new(JSON.generate(serialize_from_struct(struct:, should_serialize_values: true)))
28
28
  end
29
29
  end
30
30
  end
data/lib/typed/schema.rb CHANGED
@@ -13,7 +13,7 @@ module Typed
13
13
  Typed::Schema.new(
14
14
  target: struct,
15
15
  fields: struct.props.map do |name, properties|
16
- Typed::Field.new(name: name, type: properties[:type], required: !properties[:fully_optional])
16
+ Typed::Field.new(name:, type: properties[:type_object], default: properties.fetch(:default, nil))
17
17
  end
18
18
  )
19
19
  end
@@ -34,7 +34,7 @@ module Typed
34
34
  target: target,
35
35
  fields: fields.map do |field|
36
36
  if field.name == field_name
37
- Field.new(name: field.name, type: field.type, required: field.required, inline_serializer: serializer)
37
+ Field.new(name: field.name, type: field.type, default: field.default, inline_serializer: serializer)
38
38
  else
39
39
  field
40
40
  end
@@ -33,12 +33,34 @@ module Typed
33
33
  sig { params(creation_params: Params).returns(DeserializeResult) }
34
34
  def deserialize_from_creation_params(creation_params)
35
35
  results = schema.fields.map do |field|
36
- value = creation_params[field.name]
36
+ value = creation_params.fetch(field.name, nil)
37
37
 
38
- if value.nil? || field.works_with?(value)
38
+ if value.nil? && !field.default.nil?
39
+ Success.new(Validations::ValidatedValue.new(name: field.name, value: field.default))
40
+ elsif value.nil? || field.works_with?(value)
39
41
  field.validate(value)
42
+ elsif field.type.class <= T::Types::Union
43
+ errors = []
44
+ validated_value = T.let(nil, T.nilable(Typed::Result[Typed::Validations::ValidatedValue, Typed::Validations::ValidationError]))
45
+
46
+ T.cast(field.type, T::Types::Union).types.each do |sub_type|
47
+ # the if clause took care of cases where value is nil so we can skip NilClass
48
+ next if sub_type.raw_type.equal?(NilClass)
49
+
50
+ coercion_result = Coercion.coerce(type: sub_type, value: value)
51
+
52
+ if coercion_result.success?
53
+ validated_value = field.validate(coercion_result.payload)
54
+
55
+ break
56
+ else
57
+ errors << Validations::ValidationError.new(coercion_result.error.message)
58
+ end
59
+ end
60
+
61
+ validated_value.nil? ? Failure.new(Validations::ValidationError.new(errors.map(&:message).join(", "))) : validated_value
40
62
  else
41
- coercion_result = Coercion.coerce(type: field.type, value: value)
63
+ coercion_result = Coercion.coerce(type: field.type, value:)
42
64
 
43
65
  if coercion_result.success?
44
66
  field.validate(coercion_result.payload)
@@ -49,7 +71,7 @@ module Typed
49
71
  end
50
72
 
51
73
  Validations::ValidationResults
52
- .new(results: results)
74
+ .new(results:)
53
75
  .combine
54
76
  .and_then do |validated_params|
55
77
  Success.new(schema.target.new(**validated_params))
@@ -60,7 +82,11 @@ module Typed
60
82
  def serialize_from_struct(struct:, should_serialize_values: false)
61
83
  hsh = schema.fields.each_with_object({}) { |field, hsh| hsh[field.name] = field.serialize(struct.send(field.name)) }.compact
62
84
 
63
- HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(hsh)
85
+ if should_serialize_values
86
+ hsh = HashTransformer.serialize_values(hsh)
87
+ end
88
+
89
+ hsh
64
90
  end
65
91
  end
66
92
  end
@@ -10,11 +10,15 @@ module Typed
10
10
  sig { override.params(field: Field, value: Value).returns(ValidationResult) }
11
11
  def validate(field:, value:)
12
12
  if field.works_with?(value)
13
- Success.new(ValidatedValue.new(name: field.name, value: value))
13
+ Success.new(ValidatedValue.new(name: field.name, value:))
14
14
  elsif field.required? && value.nil?
15
15
  Failure.new(RequiredFieldError.new(field_name: field.name))
16
16
  elsif field.optional? && value.nil?
17
- Success.new(ValidatedValue.new(name: field.name, value: value))
17
+ if field.default.nil?
18
+ Success.new(ValidatedValue.new(name: field.name, value:))
19
+ else
20
+ Success.new(ValidatedValue.new(name: field.name, value: field.default))
21
+ end
18
22
  else
19
23
  Failure.new(TypeMismatchError.new(field_name: field.name, field_type: field.type, given_type: value.class))
20
24
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.1
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Max VelDink
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-06-26 00:00:00.000000000 Z
11
+ date: 2024-07-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sorbet-result
@@ -85,6 +85,7 @@ files:
85
85
  - Rakefile
86
86
  - lib/sorbet-schema.rb
87
87
  - lib/sorbet-schema/hash_transformer.rb
88
+ - lib/sorbet-schema/serialize_value.rb
88
89
  - lib/sorbet-schema/t/struct.rb
89
90
  - lib/sorbet-schema/version.rb
90
91
  - lib/typed/coercion.rb
@@ -99,7 +100,9 @@ files:
99
100
  - lib/typed/coercion/integer_coercer.rb
100
101
  - lib/typed/coercion/string_coercer.rb
101
102
  - lib/typed/coercion/struct_coercer.rb
103
+ - lib/typed/coercion/symbol_coercer.rb
102
104
  - lib/typed/coercion/typed_array_coercer.rb
105
+ - lib/typed/coercion/typed_hash_coercer.rb
103
106
  - lib/typed/deserialize_error.rb
104
107
  - lib/typed/field.rb
105
108
  - lib/typed/hash_serializer.rb