sorbet-schema 0.3.0 → 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +35 -0
  3. data/Gemfile.lock +17 -17
  4. data/lib/sorbet-schema/hash_transformer.rb +27 -2
  5. data/lib/sorbet-schema/version.rb +1 -1
  6. data/lib/typed/coercion/boolean_coercer.rb +31 -0
  7. data/lib/typed/coercion/coercer.rb +3 -3
  8. data/lib/typed/coercion/coercer_registry.rb +14 -2
  9. data/lib/typed/coercion/date_coercer.rb +29 -0
  10. data/lib/typed/coercion/enum_coercer.rb +27 -0
  11. data/lib/typed/coercion/float_coercer.rb +6 -4
  12. data/lib/typed/coercion/integer_coercer.rb +6 -4
  13. data/lib/typed/coercion/string_coercer.rb +6 -4
  14. data/lib/typed/coercion/struct_coercer.rb +12 -10
  15. data/lib/typed/coercion/typed_array_coercer.rb +34 -0
  16. data/lib/typed/coercion.rb +4 -4
  17. data/lib/typed/deserialize_error.rb +1 -1
  18. data/lib/typed/field.rb +49 -5
  19. data/lib/typed/hash_serializer.rb +18 -6
  20. data/lib/typed/json_serializer.rb +4 -2
  21. data/lib/typed/schema.rb +10 -0
  22. data/lib/typed/serialization_error.rb +17 -0
  23. data/lib/typed/serialize_error.rb +6 -0
  24. data/lib/typed/serializer.rb +12 -7
  25. data/lib/typed/validations/field_type_validator.rb +1 -1
  26. data/lib/typed/validations/type_mismatch_error.rb +1 -1
  27. data/sorbet/rbi/gems/{minitest@5.22.2.rbi → minitest@5.22.3.rbi} +19 -18
  28. data/sorbet/rbi/gems/{rubocop-ast@1.31.1.rbi → rubocop-ast@1.31.2.rbi} +70 -70
  29. data/sorbet/rbi/gems/{rubocop@1.61.0.rbi → rubocop@1.62.1.rbi} +276 -233
  30. data/sorbet/rbi/gems/{standard@1.34.0.rbi → standard@1.35.1.rbi} +56 -56
  31. metadata +13 -7
  32. /data/sorbet/rbi/gems/{rubocop-sorbet@0.7.7.rbi → rubocop-sorbet@0.7.8.rbi} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 004ea246c450adc69f61c55875a4258a7353c9a5353084b20f3f927dd913a9fc
4
- data.tar.gz: d9375c481dda06ccb6af6955541993e44f5f1b1a249ab78c3c3624310d888796
3
+ metadata.gz: bfe8fbdabbb2b4f12be947525a1e6b36136a06ef0a3065f843e49aa990eb423a
4
+ data.tar.gz: eab4268bbdfcdd85ec57fe381b01e143d9c1cb21cf04d988d309e4e8f4282f36
5
5
  SHA512:
6
- metadata.gz: c943356484fbc2f8d2bf9539d4432a1a9b3f9904b4e8663165bb152d22ac49548b9e50ff9af5031e74a9c734f1eb3dd3ca65b324d3b09af13c4835bf3a1e15db
7
- data.tar.gz: 73e261c571f2ad88ef04a7c552d4fa2d808a083e1306819323b13eddb0ec3506405c9690d59b4eab5efa37ac222cf15d2b4d08223ab88dcfc0cccfd18b9f7aff
6
+ metadata.gz: c45aa65c93bf22858a0034bd66320a060e8b4fa3e693623fbd948d37dc348e232003e6c5d459b0dfc7a3621d82b74a6a799087061b5b146b4895ae9585edd0d8
7
+ data.tar.gz: 540114ebcb2c1f1c5fd3061d25401d77b7c30a36d57481732bf92928e01733342027b63d94b42ba1e188ec2546c42946be49c97c521b1eaf21819cdd6afe6a5d
data/CHANGELOG.md CHANGED
@@ -4,6 +4,41 @@ 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.4.1](https://github.com/maxveldink/sorbet-schema/compare/v0.4.0...v0.4.1) (2024-03-21)
8
+
9
+
10
+ ### Features
11
+
12
+ * Add DateCoercer ([ef8c1db](https://github.com/maxveldink/sorbet-schema/commit/ef8c1dbdf3bd87ab2e64102fbd1434811aa353d8))
13
+ * Add inline serializer to fields ([dd8042d](https://github.com/maxveldink/sorbet-schema/commit/dd8042d8d88d67d530c7619ee3cbb108957990d1))
14
+
15
+ ## [0.4.0](https://github.com/maxveldink/sorbet-schema/compare/v0.3.0...v0.4.0) (2024-03-14)
16
+
17
+
18
+ ### ⚠ BREAKING CHANGES
19
+
20
+ * Have coercers take in a type instead of the full field
21
+ * Update Field's types to a support Sorbets T::Types::Base classes
22
+ * Changed serialize return value to a Result
23
+ * adds SerializationError ancestor
24
+
25
+ ### Features
26
+
27
+ * Add BooleanCoercer ([acd220f](https://github.com/maxveldink/sorbet-schema/commit/acd220f55fe0ebc823de6cc43776a1f510a4acd0))
28
+ * Add EnumCoercer ([5c0e2b5](https://github.com/maxveldink/sorbet-schema/commit/5c0e2b51990dfb72218129b0935f881622324194))
29
+ * Add from_hash and from_json helpers to Schemas ([#44](https://github.com/maxveldink/sorbet-schema/issues/44)) ([55c2da7](https://github.com/maxveldink/sorbet-schema/commit/55c2da77b7c2636339c684560bf15c51ff3ff4b1))
30
+ * Add idempotency to Struct coercer ([3a42957](https://github.com/maxveldink/sorbet-schema/commit/3a42957836afee55b074fc5c8a41eac6d31ada70))
31
+ * Add option to serialize values to HashSerializer ([710d365](https://github.com/maxveldink/sorbet-schema/commit/710d365d477bd6c6bff20929fcdcc1d1cc95bdb3))
32
+ * Adds TypedArray coercer ([795ddd9](https://github.com/maxveldink/sorbet-schema/commit/795ddd97f05502b154d4cb165878c1f78935cb3c))
33
+
34
+
35
+ ### Code Refactoring
36
+
37
+ * adds SerializationError ancestor ([f8ea753](https://github.com/maxveldink/sorbet-schema/commit/f8ea75304613cbab17f698006698a2524a44b538))
38
+ * Changed serialize return value to a Result ([948c678](https://github.com/maxveldink/sorbet-schema/commit/948c67815dab19793dd2f321a90797d123740e0e))
39
+ * Have coercers take in a type instead of the full field ([c06169e](https://github.com/maxveldink/sorbet-schema/commit/c06169e0fa1cf7c8f645ecabef19cc2f5facffe4))
40
+ * Update Field's types to a support Sorbets T::Types::Base classes ([9ef1cd5](https://github.com/maxveldink/sorbet-schema/commit/9ef1cd5caf09396d8dae6a4db1feeb4f88dd25e9))
41
+
7
42
  ## [0.3.0](https://github.com/maxveldink/sorbet-schema/compare/v0.2.2...v0.3.0) (2024-03-12)
8
43
 
9
44
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- sorbet-schema (0.3.0)
4
+ sorbet-schema (0.4.1)
5
5
  sorbet-result (~> 1.1)
6
6
  sorbet-runtime (~> 0.5)
7
7
  sorbet-struct-comparable (~> 1.3)
@@ -18,13 +18,13 @@ GEM
18
18
  reline (>= 0.3.8)
19
19
  erubi (1.12.0)
20
20
  io-console (0.7.2)
21
- irb (1.11.2)
21
+ irb (1.12.0)
22
22
  rdoc
23
23
  reline (>= 0.4.2)
24
24
  json (2.7.1)
25
25
  language_server-protocol (3.17.0.3)
26
26
  lint_roller (1.1.0)
27
- minitest (5.22.2)
27
+ minitest (5.22.3)
28
28
  minitest-focus (1.4.0)
29
29
  minitest (>= 4, < 6)
30
30
  minitest-reporters (1.6.1)
@@ -53,7 +53,7 @@ GEM
53
53
  reline (0.4.3)
54
54
  io-console (~> 0.5)
55
55
  rexml (3.2.6)
56
- rubocop (1.61.0)
56
+ rubocop (1.62.1)
57
57
  json (~> 2.3)
58
58
  language_server-protocol (>= 3.17.0)
59
59
  parallel (~> 1.10)
@@ -61,27 +61,27 @@ GEM
61
61
  rainbow (>= 2.2.2, < 4.0)
62
62
  regexp_parser (>= 1.8, < 3.0)
63
63
  rexml (>= 3.2.5, < 4.0)
64
- rubocop-ast (>= 1.30.0, < 2.0)
64
+ rubocop-ast (>= 1.31.1, < 2.0)
65
65
  ruby-progressbar (~> 1.7)
66
66
  unicode-display_width (>= 2.4.0, < 3.0)
67
- rubocop-ast (1.31.1)
67
+ rubocop-ast (1.31.2)
68
68
  parser (>= 3.3.0.4)
69
69
  rubocop-performance (1.20.2)
70
70
  rubocop (>= 1.48.1, < 2.0)
71
71
  rubocop-ast (>= 1.30.0, < 2.0)
72
- rubocop-sorbet (0.7.7)
72
+ rubocop-sorbet (0.7.8)
73
73
  rubocop (>= 0.90.0)
74
74
  ruby-progressbar (1.13.0)
75
- sorbet (0.5.11287)
76
- sorbet-static (= 0.5.11287)
75
+ sorbet (0.5.11295)
76
+ sorbet-static (= 0.5.11295)
77
77
  sorbet-result (1.1.0)
78
78
  sorbet-runtime (~> 0.5)
79
- sorbet-runtime (0.5.11287)
80
- sorbet-static (0.5.11287-universal-darwin)
81
- sorbet-static (0.5.11287-x86_64-linux)
82
- sorbet-static-and-runtime (0.5.11287)
83
- sorbet (= 0.5.11287)
84
- sorbet-runtime (= 0.5.11287)
79
+ sorbet-runtime (0.5.11295)
80
+ sorbet-static (0.5.11295-universal-darwin)
81
+ sorbet-static (0.5.11295-x86_64-linux)
82
+ sorbet-static-and-runtime (0.5.11295)
83
+ sorbet (= 0.5.11295)
84
+ sorbet-runtime (= 0.5.11295)
85
85
  sorbet-struct-comparable (1.3.0)
86
86
  sorbet-runtime (>= 0.5)
87
87
  spoom (1.2.4)
@@ -89,10 +89,10 @@ GEM
89
89
  sorbet-static-and-runtime (>= 0.5.10187)
90
90
  syntax_tree (>= 6.1.1)
91
91
  thor (>= 0.19.2)
92
- standard (1.34.0)
92
+ standard (1.35.1)
93
93
  language_server-protocol (~> 3.17.0.2)
94
94
  lint_roller (~> 1.0)
95
- rubocop (~> 1.60)
95
+ rubocop (~> 1.62.0)
96
96
  standard-custom (~> 1.0.0)
97
97
  standard-performance (~> 1.3)
98
98
  standard-custom (1.0.2)
@@ -6,17 +6,42 @@
6
6
  class HashTransformer
7
7
  extend T::Sig
8
8
 
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
+
9
14
  sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[Symbol, T.untyped]) }
10
15
  def deep_symbolize_keys(hash)
11
16
  hash.each_with_object({}) do |(key, value), result|
12
- result[key.to_sym] = value.is_a?(Hash) ? deep_symbolize_keys(value) : value
17
+ result[key.to_sym] = transform_value(value, hash_transformation_method: :deep_symbolize_keys)
13
18
  end
14
19
  end
15
20
 
16
21
  sig { params(hash: T::Hash[T.untyped, T.untyped]).returns(T::Hash[String, T.untyped]) }
17
22
  def deep_stringify_keys(hash)
18
23
  hash.each_with_object({}) do |(key, value), result|
19
- result[key.to_s] = value.is_a?(Hash) ? deep_stringify_keys(value) : value
24
+ result[key.to_s] = transform_value(value, hash_transformation_method: :deep_stringify_keys)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ sig { returns(T::Boolean) }
31
+ attr_reader :should_serialize_values
32
+
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
20
45
  end
21
46
  end
22
47
  end
@@ -1,5 +1,5 @@
1
1
  # typed: strict
2
2
 
3
3
  module SorbetSchema
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.1"
5
5
  end
@@ -0,0 +1,31 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ module Coercion
5
+ class BooleanCoercer < Coercer
6
+ extend T::Generic
7
+
8
+ Target = type_member { {fixed: T::Boolean} }
9
+
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
+ def used_for_type?(type)
12
+ type == T::Utils.coerce(T::Boolean)
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 T::Boolean.")) unless used_for_type?(type)
18
+
19
+ if type.recursively_valid?(value)
20
+ Success.new(value)
21
+ elsif value == "true"
22
+ Success.new(true)
23
+ elsif value == "false"
24
+ Success.new(false)
25
+ else
26
+ Failure.new(CoercionError.new)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -10,12 +10,12 @@ module Typed
10
10
 
11
11
  Target = type_member(:out)
12
12
 
13
- sig { abstract.params(type: T::Class[T.anything]).returns(T::Boolean) }
13
+ sig { abstract.params(type: T::Types::Base).returns(T::Boolean) }
14
14
  def used_for_type?(type)
15
15
  end
16
16
 
17
- sig { abstract.params(field: Field, value: Value).returns(Result[Target, CoercionError]) }
18
- def coerce(field:, value:)
17
+ sig { abstract.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
18
+ def coerce(type:, value:)
19
19
  end
20
20
  end
21
21
  end
@@ -11,7 +11,19 @@ module Typed
11
11
 
12
12
  Registry = T.type_alias { T::Array[T.class_of(Coercer)] }
13
13
 
14
- DEFAULT_COERCERS = T.let([StringCoercer, IntegerCoercer, FloatCoercer, StructCoercer], Registry)
14
+ DEFAULT_COERCERS = T.let(
15
+ [
16
+ StringCoercer,
17
+ BooleanCoercer,
18
+ IntegerCoercer,
19
+ FloatCoercer,
20
+ DateCoercer,
21
+ EnumCoercer,
22
+ StructCoercer,
23
+ TypedArrayCoercer
24
+ ],
25
+ Registry
26
+ )
15
27
 
16
28
  sig { void }
17
29
  def initialize
@@ -28,7 +40,7 @@ module Typed
28
40
  @available = DEFAULT_COERCERS.clone
29
41
  end
30
42
 
31
- sig { params(type: T::Class[T.anything]).returns(T.nilable(T.class_of(Coercer))) }
43
+ sig { params(type: T::Types::Base).returns(T.nilable(T.class_of(Coercer))) }
32
44
  def select_coercer_by(type:)
33
45
  @available.find { |coercer| coercer.new.used_for_type?(type) }
34
46
  end
@@ -0,0 +1,29 @@
1
+ # typed: strict
2
+
3
+ require "date"
4
+
5
+ module Typed
6
+ module Coercion
7
+ class DateCoercer < Coercer
8
+ extend T::Generic
9
+
10
+ Target = type_member { {fixed: Date} }
11
+
12
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
13
+ def used_for_type?(type)
14
+ T::Utils.coerce(type) == T::Utils.coerce(Date)
15
+ end
16
+
17
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
18
+ def coerce(type:, value:)
19
+ return Failure.new(CoercionError.new("Type must be a Date.")) unless used_for_type?(type)
20
+
21
+ return Success.new(value) if value.is_a?(Date)
22
+
23
+ Success.new(Date.parse(value))
24
+ rescue Date::Error, TypeError
25
+ Failure.new(CoercionError.new("'#{value}' cannot be coerced into Date."))
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ module Coercion
5
+ class EnumCoercer < Coercer
6
+ extend T::Generic
7
+
8
+ Target = type_member { {fixed: T::Enum} }
9
+
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
+ def used_for_type?(type)
12
+ return false unless type.respond_to?(:raw_type)
13
+
14
+ !!(T.cast(type, T::Types::Simple).raw_type < T::Enum)
15
+ end
16
+
17
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
18
+ def coerce(type:, value:)
19
+ return Failure.new(CoercionError.new("Field type must inherit from T::Enum for Enum coercion.")) unless used_for_type?(type)
20
+
21
+ Success.new(T.cast(type, T::Types::Simple).raw_type.from_serialized(value))
22
+ rescue KeyError => e
23
+ Failure.new(CoercionError.new(e.message))
24
+ end
25
+ end
26
+ end
27
+ end
@@ -7,13 +7,15 @@ module Typed
7
7
 
8
8
  Target = type_member { {fixed: Float} }
9
9
 
10
- sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) }
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
11
  def used_for_type?(type)
12
- type == Float
12
+ T::Utils.coerce(type) == T::Utils.coerce(Float)
13
13
  end
14
14
 
15
- sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) }
16
- def coerce(field:, value:)
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 Float.")) unless used_for_type?(type)
18
+
17
19
  Success.new(Float(value))
18
20
  rescue ArgumentError, TypeError
19
21
  Failure.new(CoercionError.new("'#{value}' cannot be coerced into Float."))
@@ -7,13 +7,15 @@ module Typed
7
7
 
8
8
  Target = type_member { {fixed: Integer} }
9
9
 
10
- sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) }
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
11
  def used_for_type?(type)
12
- type == Integer
12
+ type == T::Utils.coerce(Integer)
13
13
  end
14
14
 
15
- sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) }
16
- def coerce(field:, value:)
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 Integer.")) unless used_for_type?(type)
18
+
17
19
  Success.new(Integer(value))
18
20
  rescue ArgumentError, TypeError
19
21
  Failure.new(CoercionError.new("'#{value}' cannot be coerced into Integer."))
@@ -7,13 +7,15 @@ module Typed
7
7
 
8
8
  Target = type_member { {fixed: String} }
9
9
 
10
- sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) }
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
11
  def used_for_type?(type)
12
- type == String
12
+ type == T::Utils.coerce(String)
13
13
  end
14
14
 
15
- sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) }
16
- def coerce(field:, value:)
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 String.")) unless used_for_type?(type)
18
+
17
19
  Success.new(String(value))
18
20
  end
19
21
  end
@@ -7,21 +7,23 @@ module Typed
7
7
 
8
8
  Target = type_member { {fixed: T::Struct} }
9
9
 
10
- sig { override.params(type: T::Class[T.anything]).returns(T::Boolean) }
10
+ sig { override.params(type: T::Types::Base).returns(T::Boolean) }
11
11
  def used_for_type?(type)
12
- !!(type < T::Struct)
12
+ return false unless type.respond_to?(:raw_type)
13
+
14
+ !!(T.cast(type, T::Types::Simple).raw_type < T::Struct)
13
15
  end
14
16
 
15
- sig { override.params(field: Field, value: Value).returns(Result[Target, CoercionError]) }
16
- def coerce(field:, value:)
17
- type = field.type
17
+ sig { override.params(type: T::Types::Base, value: Value).returns(Result[Target, CoercionError]) }
18
+ def coerce(type:, value:)
19
+ return Failure.new(CoercionError.new("Field type must inherit from T::Struct for Struct coercion.")) unless used_for_type?(type)
20
+ return Success.new(value) if type.recursively_valid?(value)
18
21
 
19
- return Failure.new(CoercionError.new("Field type must inherit from T::Struct for Struct coercion.")) unless type < T::Struct
20
- return Failure.new(CoercionError.new("Value must be a Hash for Struct coercion.")) unless value.is_a?(Hash)
22
+ return Failure.new(CoercionError.new("Value of type '#{value.class}' cannot be coerced to #{type} Struct.")) unless value.is_a?(Hash)
21
23
 
22
- Success.new(type.from_hash!(HashTransformer.new.deep_stringify_keys(value)))
23
- rescue ArgumentError => e
24
- Failure.new(CoercionError.new(e.message))
24
+ Success.new(T.cast(type, T::Types::Simple).raw_type.from_hash!(HashTransformer.new.deep_stringify_keys(value)))
25
+ rescue ArgumentError, RuntimeError
26
+ Failure.new(CoercionError.new("Given hash could not be coerced to #{type}."))
25
27
  end
26
28
  end
27
29
  end
@@ -0,0 +1,34 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ module Coercion
5
+ class TypedArrayCoercer < Coercer
6
+ extend T::Generic
7
+
8
+ Target = type_member { {fixed: T::Array[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::TypedArray)
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::Array.")) unless used_for_type?(type)
18
+ return Failure.new(CoercionError.new("Value must be an Array.")) unless value.is_a?(Array)
19
+
20
+ return Success.new(value) if type.recursively_valid?(value)
21
+
22
+ coerced_results = value.map do |item|
23
+ Coercion.coerce(type: T.cast(type, T::Types::TypedArray).type, value: item)
24
+ end
25
+
26
+ if coerced_results.all?(&:success?)
27
+ Success.new(coerced_results.map(&:payload))
28
+ else
29
+ Failure.new(CoercionError.new(coerced_results.select(&:failure?).map(&:error).map(&:message).join(" | ")))
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -9,13 +9,13 @@ module Typed
9
9
  CoercerRegistry.instance.register(coercer)
10
10
  end
11
11
 
12
- sig { type_parameters(:U).params(field: Field, value: Value).returns(Result[Value, CoercionError]) }
13
- def self.coerce(field:, value:)
14
- coercer = CoercerRegistry.instance.select_coercer_by(type: field.type)
12
+ sig { type_parameters(:U).params(type: T::Types::Base, value: Value).returns(Result[Value, CoercionError]) }
13
+ def self.coerce(type:, value:)
14
+ coercer = CoercerRegistry.instance.select_coercer_by(type: type)
15
15
 
16
16
  return Failure.new(CoercionNotSupportedError.new) unless coercer
17
17
 
18
- coercer.new.coerce(field: field, value: value)
18
+ coercer.new.coerce(type: type, value: value)
19
19
  end
20
20
  end
21
21
  end
@@ -1,7 +1,7 @@
1
1
  # typed: strict
2
2
 
3
3
  module Typed
4
- class DeserializeError < StandardError
4
+ class DeserializeError < SerializationError
5
5
  extend T::Sig
6
6
 
7
7
  sig { returns({error: String}) }
data/lib/typed/field.rb CHANGED
@@ -1,14 +1,44 @@
1
1
  # typed: strict
2
2
 
3
3
  module Typed
4
- class Field < T::Struct
4
+ class Field
5
5
  extend T::Sig
6
6
 
7
- include ActsAsComparable
7
+ InlineSerializer = T.type_alias { T.proc.params(arg0: T.untyped).returns(T.untyped) }
8
8
 
9
- const :name, Symbol
10
- const :type, T::Class[T.anything]
11
- const :required, T::Boolean, default: true
9
+ sig { returns(Symbol) }
10
+ attr_reader :name
11
+
12
+ sig { returns(T::Types::Base) }
13
+ attr_reader :type
14
+
15
+ sig { returns(T::Boolean) }
16
+ attr_reader :required
17
+
18
+ sig { returns(T.nilable(InlineSerializer)) }
19
+ attr_reader :inline_serializer
20
+
21
+ sig do
22
+ params(
23
+ name: Symbol,
24
+ type: T.any(T::Class[T.anything], T::Types::Base),
25
+ required: T::Boolean,
26
+ inline_serializer: T.nilable(InlineSerializer)
27
+ ).void
28
+ end
29
+ def initialize(name:, type:, required: true, inline_serializer: nil)
30
+ @name = name
31
+ @type = T.let(T::Utils.coerce(type), T::Types::Base)
32
+ @required = required
33
+ @inline_serializer = inline_serializer
34
+ end
35
+
36
+ sig { params(other: Field).returns(T.nilable(T::Boolean)) }
37
+ def ==(other)
38
+ name == other.name &&
39
+ type == other.type &&
40
+ required == other.required
41
+ end
12
42
 
13
43
  sig { returns(T::Boolean) }
14
44
  def required?
@@ -20,9 +50,23 @@ module Typed
20
50
  !required
21
51
  end
22
52
 
53
+ sig { params(value: Value).returns(Value) }
54
+ def serialize(value)
55
+ if inline_serializer && value
56
+ T.must(inline_serializer).call(value)
57
+ else
58
+ value
59
+ end
60
+ end
61
+
23
62
  sig { params(value: Value).returns(Validations::ValidationResult) }
24
63
  def validate(value)
25
64
  Validations::FieldTypeValidator.new.validate(field: self, value: value)
26
65
  end
66
+
67
+ sig { params(value: Value).returns(T::Boolean) }
68
+ def works_with?(value)
69
+ type.recursively_valid?(value)
70
+ end
27
71
  end
28
72
  end
@@ -3,19 +3,31 @@
3
3
  module Typed
4
4
  class HashSerializer < Serializer
5
5
  InputHash = T.type_alias { T::Hash[T.any(Symbol, String), T.untyped] }
6
- OutputHash = T.type_alias { Params }
7
-
8
6
  Input = type_member { {fixed: InputHash} }
9
- Output = type_member { {fixed: OutputHash} }
7
+ Output = type_member { {fixed: Params} }
8
+
9
+ sig { params(schema: Schema, should_serialize_values: T::Boolean).void }
10
+ def initialize(schema:, should_serialize_values: false)
11
+ @should_serialize_values = should_serialize_values
12
+
13
+ super(schema: schema)
14
+ end
10
15
 
11
16
  sig { override.params(source: Input).returns(Result[T::Struct, DeserializeError]) }
12
17
  def deserialize(source)
13
- deserialize_from_creation_params(HashTransformer.new.deep_symbolize_keys(source))
18
+ deserialize_from_creation_params(HashTransformer.new(should_serialize_values: should_serialize_values).deep_symbolize_keys(source))
14
19
  end
15
20
 
16
- sig { override.params(struct: T::Struct).returns(Output) }
21
+ sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) }
17
22
  def serialize(struct)
18
- HashTransformer.new.deep_symbolize_keys(struct.serialize)
23
+ return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target
24
+
25
+ Success.new(serialize_from_struct(struct: struct, should_serialize_values: should_serialize_values))
19
26
  end
27
+
28
+ private
29
+
30
+ sig { returns(T::Boolean) }
31
+ attr_reader :should_serialize_values
20
32
  end
21
33
  end
@@ -20,9 +20,11 @@ module Typed
20
20
  Failure.new(ParseError.new(format: :json))
21
21
  end
22
22
 
23
- sig { override.params(struct: T::Struct).returns(Output) }
23
+ sig { override.params(struct: T::Struct).returns(Result[Output, SerializeError]) }
24
24
  def serialize(struct)
25
- JSON.generate(struct.serialize)
25
+ return Failure.new(SerializeError.new("'#{struct.class}' cannot be serialized to target type of '#{schema.target}'.")) if struct.class != schema.target
26
+
27
+ Success.new(JSON.generate(serialize_from_struct(struct: struct, should_serialize_values: true)))
26
28
  end
27
29
  end
28
30
  end
data/lib/typed/schema.rb CHANGED
@@ -17,5 +17,15 @@ module Typed
17
17
  end
18
18
  )
19
19
  end
20
+
21
+ sig { params(hash: Typed::HashSerializer::InputHash).returns(Typed::Serializer::DeserializeResult) }
22
+ def from_hash(hash)
23
+ Typed::HashSerializer.new(schema: self).deserialize(hash)
24
+ end
25
+
26
+ sig { params(json: String).returns(Typed::Serializer::DeserializeResult) }
27
+ def from_json(json)
28
+ Typed::JSONSerializer.new(schema: self).deserialize(json)
29
+ end
20
30
  end
21
31
  end
@@ -0,0 +1,17 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ class SerializationError < StandardError
5
+ extend T::Sig
6
+
7
+ sig { returns({error: String}) }
8
+ def to_h
9
+ {error: message}
10
+ end
11
+
12
+ sig { params(_options: T::Hash[T.untyped, T.untyped]).returns(String) }
13
+ def to_json(_options = {})
14
+ JSON.generate(to_h)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+
3
+ module Typed
4
+ class SerializeError < SerializationError
5
+ end
6
+ end