sorbet-schema 0.5.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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