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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 32a23b4d9a5e1eda6fcf49b64428f55fbf1d89f63754de35bf2e0e3a875e018c
4
- data.tar.gz: e76f273b8e566f78e6c18effa1699e3f6afa17311abee06a2183778e7a97f93a
3
+ metadata.gz: 1b4040ccaa87f90e0d32acee9868fffb0f4d645e599fe29726738fdb54c4f1e6
4
+ data.tar.gz: b83d96b38d8b7d786a3e174e576053f531bf03e237d7c1f049e98b9c813dc117
5
5
  SHA512:
6
- metadata.gz: 5e7dcb508366dcda8311bf089ae6877c8ff5ba8fc41e0bec9adcd18ca8f3b3723693cd5139a1ee34d46dfcf0643d511edc04d689a93715e72aa33a2f764454e2
7
- data.tar.gz: bf2c62a198b37caae70736bd1aa954476ef2697c44aa793381f91787b4ad6b78bad0e731a69c3dddb4c7398a3a07092cdebf779d95e27be1fbb68d43bf74958f
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-schema (0.5.1)
4
+ sorbet-schema (0.6.0)
5
5
  sorbet-result (~> 1.1)
6
6
  sorbet-runtime (~> 0.5)
7
7
  sorbet-struct-comparable (~> 1.3)
@@ -1,5 +1,5 @@
1
1
  # typed: strict
2
2
 
3
3
  module SorbetSchema
4
- VERSION = "0.5.1"
4
+ VERSION = "0.6.0"
5
5
  end
@@ -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
 
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))
@@ -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.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-06-26 00:00:00.000000000 Z
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