sorbet-schema 0.5.1 → 0.6.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 +4 -4
- data/CHANGELOG.md +19 -0
- data/Gemfile.lock +1 -1
- data/lib/sorbet-schema/version.rb +1 -1
- data/lib/typed/coercion/coercer_registry.rb +3 -1
- data/lib/typed/coercion/struct_coercer.rb +8 -34
- data/lib/typed/coercion/symbol_coercer.rb +27 -0
- data/lib/typed/coercion/typed_hash_coercer.rb +50 -0
- data/lib/typed/field.rb +29 -4
- data/lib/typed/schema.rb +2 -2
- data/lib/typed/serializer.rb +26 -4
- data/lib/typed/validations/field_type_validator.rb +6 -2
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1b4040ccaa87f90e0d32acee9868fffb0f4d645e599fe29726738fdb54c4f1e6
|
4
|
+
data.tar.gz: b83d96b38d8b7d786a3e174e576053f531bf03e237d7c1f049e98b9c813dc117
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 248121230fc7a86b616259a89db47c210360034aad0ec70a23e2b25d73b350e16eff09c09ea87efd0aa859ef86b6f5c7f7700043b2e0d3e1f570555e98baac31
|
7
|
+
data.tar.gz: 6f0c35a33ff1e84ebe704c30a6d0ba4b3520030c8ca59c897c33cc96f736c48c16e8672e7f4efef6f81f4a9816326db41c1cc625f49addd520a437066a04db1c
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,25 @@ 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.6.0](https://github.com/maxveldink/sorbet-schema/compare/v0.5.1...v0.6.0) (2024-07-07)
|
8
|
+
|
9
|
+
|
10
|
+
### ⚠ BREAKING CHANGES
|
11
|
+
|
12
|
+
* implement default handling for fields ([#105](https://github.com/maxveldink/sorbet-schema/issues/105))
|
13
|
+
|
14
|
+
### Features
|
15
|
+
|
16
|
+
* implement default handling for fields ([#105](https://github.com/maxveldink/sorbet-schema/issues/105)) ([054d59f](https://github.com/maxveldink/sorbet-schema/commit/054d59ff92c68b272d495a0816370b9a890f0f50))
|
17
|
+
* implement SymbolCoercer ([#109](https://github.com/maxveldink/sorbet-schema/issues/109)) ([422a995](https://github.com/maxveldink/sorbet-schema/commit/422a9957177039a3dde5c4daa41d597fd44f2b48))
|
18
|
+
* implement TypedHashCoercer ([#110](https://github.com/maxveldink/sorbet-schema/issues/110)) ([6d64db7](https://github.com/maxveldink/sorbet-schema/commit/6d64db7fcef8af56cb96f1ee6c42ba1e3ce076c3))
|
19
|
+
* support T.any for deserialization ([#107](https://github.com/maxveldink/sorbet-schema/issues/107)) ([c0c2ca3](https://github.com/maxveldink/sorbet-schema/commit/c0c2ca369abef136943e633b7987decad7291d98))
|
20
|
+
|
21
|
+
|
22
|
+
### Bug Fixes
|
23
|
+
|
24
|
+
* 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))
|
25
|
+
|
7
26
|
## [0.5.1](https://github.com/maxveldink/sorbet-schema/compare/v0.5.0...v0.5.1) (2024-06-26)
|
8
27
|
|
9
28
|
|
data/Gemfile.lock
CHANGED
@@ -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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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:,
|
33
|
+
def initialize(name:, type:, optional: false, default: nil, inline_serializer: nil)
|
30
34
|
@name = name
|
31
|
-
|
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
|
|
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
|
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,
|
37
|
+
Field.new(name: field.name, type: field.type, default: field.default, inline_serializer: serializer)
|
38
38
|
else
|
39
39
|
field
|
40
40
|
end
|
data/lib/typed/serializer.rb
CHANGED
@@ -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
|
36
|
+
value = creation_params.fetch(field.name, nil)
|
37
37
|
|
38
|
-
if value.nil?
|
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:
|
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:
|
74
|
+
.new(results:)
|
53
75
|
.combine
|
54
76
|
.and_then do |validated_params|
|
55
77
|
Success.new(schema.target.new(**validated_params))
|
@@ -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:
|
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
|
-
|
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.
|
4
|
+
version: 0.6.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-
|
11
|
+
date: 2024-07-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sorbet-result
|
@@ -99,7 +99,9 @@ files:
|
|
99
99
|
- lib/typed/coercion/integer_coercer.rb
|
100
100
|
- lib/typed/coercion/string_coercer.rb
|
101
101
|
- lib/typed/coercion/struct_coercer.rb
|
102
|
+
- lib/typed/coercion/symbol_coercer.rb
|
102
103
|
- lib/typed/coercion/typed_array_coercer.rb
|
104
|
+
- lib/typed/coercion/typed_hash_coercer.rb
|
103
105
|
- lib/typed/deserialize_error.rb
|
104
106
|
- lib/typed/field.rb
|
105
107
|
- lib/typed/hash_serializer.rb
|