typed_attrs 0.1.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 +7 -0
- data/CHANGELOG.md +22 -0
- data/LICENSE +21 -0
- data/README.md +355 -0
- data/lib/active_model/validations/typed_attrs_validator.rb +48 -0
- data/lib/tapioca/dsl/compilers/typed_attribute.rb +87 -0
- data/lib/typed_attrs/discriminated_union.rb +51 -0
- data/lib/typed_attrs/type_helpers.rb +45 -0
- data/lib/typed_attrs/type_processors/array.rb +88 -0
- data/lib/typed_attrs/type_processors/base.rb +45 -0
- data/lib/typed_attrs/type_processors/boolean.rb +39 -0
- data/lib/typed_attrs/type_processors/boolean_primitive.rb +39 -0
- data/lib/typed_attrs/type_processors/date.rb +47 -0
- data/lib/typed_attrs/type_processors/discriminated_union.rb +211 -0
- data/lib/typed_attrs/type_processors/float.rb +42 -0
- data/lib/typed_attrs/type_processors/hash.rb +116 -0
- data/lib/typed_attrs/type_processors/integer.rb +39 -0
- data/lib/typed_attrs/type_processors/nilable_type.rb +53 -0
- data/lib/typed_attrs/type_processors/registry.rb +35 -0
- data/lib/typed_attrs/type_processors/simple_type.rb +38 -0
- data/lib/typed_attrs/type_processors/string.rb +39 -0
- data/lib/typed_attrs/type_processors/struct.rb +70 -0
- data/lib/typed_attrs/type_processors/symbol.rb +41 -0
- data/lib/typed_attrs/type_processors/time.rb +47 -0
- data/lib/typed_attrs/type_processors.rb +36 -0
- data/lib/typed_attrs/types/type.rb +51 -0
- data/lib/typed_attrs/version.rb +6 -0
- data/lib/typed_attrs.rb +103 -0
- metadata +117 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Array < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec.is_a?(T::Types::TypedArray)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
15
|
+
def initialize(type_spec, registry:)
|
16
|
+
super
|
17
|
+
@element_type = T.let(extract_element_type(type_spec), T.untyped)
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { override.params(value: T.untyped).returns(T::Array[T.untyped]) }
|
21
|
+
def serialize(value)
|
22
|
+
unless value.is_a?(::Array)
|
23
|
+
raise TypedAttrs::TypeMismatchError.new("Expected Array, got #{value.class}", value: value)
|
24
|
+
end
|
25
|
+
|
26
|
+
value.map do |item|
|
27
|
+
serialize_item(item)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
sig { override.params(value: T.untyped).returns(T::Array[T.untyped]) }
|
32
|
+
def deserialize(value)
|
33
|
+
unless value.is_a?(::Array)
|
34
|
+
raise TypedAttrs::DeserializationError.new("Expected Array, got #{value.class}", value: value)
|
35
|
+
end
|
36
|
+
|
37
|
+
value.map do |item|
|
38
|
+
deserialize_item(item)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
sig do
|
43
|
+
override.params(
|
44
|
+
value: T.untyped,
|
45
|
+
path: ::String,
|
46
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: ::String).void
|
47
|
+
).void
|
48
|
+
end
|
49
|
+
def traverse(value, path: "", &blk)
|
50
|
+
super
|
51
|
+
|
52
|
+
unless value.is_a?(::Array)
|
53
|
+
raise TypedAttrs::TypeMismatchError.new("Expected Array, got #{value.class}", value: value)
|
54
|
+
end
|
55
|
+
|
56
|
+
value.each_with_index do |item, index|
|
57
|
+
item_processor = @registry.serializer_for(@element_type)
|
58
|
+
item_path = "#{path}[#{index}]"
|
59
|
+
item_processor.traverse(item, path: item_path, &blk)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
sig { params(item: T.untyped).returns(T.untyped) }
|
66
|
+
def serialize_item(item)
|
67
|
+
serializer = @registry.serializer_for(@element_type)
|
68
|
+
serializer.serialize(item)
|
69
|
+
end
|
70
|
+
|
71
|
+
sig { params(item: T.untyped).returns(T.untyped) }
|
72
|
+
def deserialize_item(item)
|
73
|
+
serializer = @registry.serializer_for(@element_type)
|
74
|
+
serializer.deserialize(item)
|
75
|
+
end
|
76
|
+
|
77
|
+
sig { params(type_spec: T.untyped).returns(T.untyped) }
|
78
|
+
def extract_element_type(type_spec)
|
79
|
+
case type_spec
|
80
|
+
when T::Types::TypedArray
|
81
|
+
type_spec.type
|
82
|
+
else
|
83
|
+
raise ArgumentError, "Expected T::Types::TypedArray, got #{type_spec.class}"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Base
|
7
|
+
extend T::Sig
|
8
|
+
extend T::Helpers
|
9
|
+
|
10
|
+
abstract!
|
11
|
+
|
12
|
+
sig { overridable.params(type_spec: T.untyped).returns(T::Boolean) }
|
13
|
+
def self.handles?(type_spec)
|
14
|
+
raise NotImplementedError, "#{self}.handles? must be implemented"
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
18
|
+
def initialize(type_spec, registry:)
|
19
|
+
@type_spec = type_spec
|
20
|
+
@registry = T.let(registry, Registry)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { overridable.params(value: T.untyped).returns(T.untyped) }
|
24
|
+
def serialize(value)
|
25
|
+
raise NotImplementedError, "#{self.class}#serialize must be implemented"
|
26
|
+
end
|
27
|
+
|
28
|
+
sig { overridable.params(value: T.untyped).returns(T.untyped) }
|
29
|
+
def deserialize(value)
|
30
|
+
raise NotImplementedError, "#{self.class}#deserialize must be implemented"
|
31
|
+
end
|
32
|
+
|
33
|
+
sig do
|
34
|
+
overridable.params(
|
35
|
+
value: T.untyped,
|
36
|
+
path: ::String,
|
37
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: ::String).void
|
38
|
+
).void
|
39
|
+
end
|
40
|
+
def traverse(value, path: "", &blk)
|
41
|
+
blk.call(@type_spec, value, path)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Boolean < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec == T::Boolean
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(value: T.untyped).returns(T::Boolean) }
|
15
|
+
def serialize(value)
|
16
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
17
|
+
raise TypedAttrs::TypeMismatchError.new(
|
18
|
+
"Expected TrueClass or FalseClass, got #{value.class}",
|
19
|
+
value: value
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { override.params(value: T.untyped).returns(T::Boolean) }
|
27
|
+
def deserialize(value)
|
28
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
29
|
+
raise TypedAttrs::DeserializationError.new(
|
30
|
+
"Expected TrueClass or FalseClass, got #{value.class}",
|
31
|
+
value: value
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class BooleanPrimitive < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
[TrueClass, FalseClass].include?(type_spec)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(value: T.untyped).returns(T::Boolean) }
|
15
|
+
def serialize(value)
|
16
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
17
|
+
raise TypedAttrs::TypeMismatchError.new(
|
18
|
+
"Expected TrueClass or FalseClass, got #{value.class}",
|
19
|
+
value: value
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { override.params(value: T.untyped).returns(T::Boolean) }
|
27
|
+
def deserialize(value)
|
28
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
29
|
+
raise TypedAttrs::DeserializationError.new(
|
30
|
+
"Expected TrueClass or FalseClass, got #{value.class}",
|
31
|
+
value: value
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Date < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec == ::Date
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(value: T.untyped).returns(::String) }
|
15
|
+
def serialize(value)
|
16
|
+
unless value.is_a?(::Date)
|
17
|
+
raise TypedAttrs::TypeMismatchError.new(
|
18
|
+
"Expected Date, got #{value.class}",
|
19
|
+
value: value
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
value.iso8601
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { override.params(value: T.untyped).returns(::Date) }
|
27
|
+
def deserialize(value)
|
28
|
+
case value
|
29
|
+
when ::Date
|
30
|
+
value
|
31
|
+
when ::String
|
32
|
+
::Date.iso8601(value)
|
33
|
+
else
|
34
|
+
raise TypedAttrs::DeserializationError.new(
|
35
|
+
"Expected Date or String, got #{value.class}",
|
36
|
+
value: value
|
37
|
+
)
|
38
|
+
end
|
39
|
+
rescue ArgumentError => e
|
40
|
+
raise TypedAttrs::DeserializationError.new(
|
41
|
+
"Failed to deserialize Date: #{e.message}",
|
42
|
+
value: value
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class DiscriminatedUnion < Base
|
7
|
+
extend T::Sig
|
8
|
+
extend TypeHelpers
|
9
|
+
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
11
|
+
def self.handles?(type_spec)
|
12
|
+
UnionModule.union_module?(type_spec)
|
13
|
+
end
|
14
|
+
|
15
|
+
# discriminated_union_module must extend TypedAttrs::DiscriminatedUnion
|
16
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
17
|
+
def initialize(type_spec, registry:)
|
18
|
+
super
|
19
|
+
|
20
|
+
@union_module = T.let(UnionModule.new(type_spec), UnionModule)
|
21
|
+
end
|
22
|
+
|
23
|
+
sig { override.params(value: T.untyped).returns(T::Hash[::String, T.untyped]) }
|
24
|
+
def serialize(value)
|
25
|
+
struct_class = @union_module.find_struct_class_by_instance(value)
|
26
|
+
|
27
|
+
unless struct_class
|
28
|
+
raise TypedAttrs::TypeMismatchError.new(
|
29
|
+
"Expected one of [#{@union_module.types_string}], got #{value.class}",
|
30
|
+
value: value
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
serializer = @registry.serializer_for(value.class)
|
35
|
+
hash = serializer.serialize(value)
|
36
|
+
|
37
|
+
discriminator_value = @union_module.discriminator_value_for(value.class)
|
38
|
+
raise StandardError, "No discriminator value found for #{value.class}" unless discriminator_value
|
39
|
+
|
40
|
+
hash[@union_module.discriminator_key] = discriminator_value
|
41
|
+
hash
|
42
|
+
end
|
43
|
+
|
44
|
+
sig { override.params(value: T.untyped).returns(T::Struct) }
|
45
|
+
def deserialize(value)
|
46
|
+
return value if @union_module.find_struct_class_by_instance(value)
|
47
|
+
|
48
|
+
unless value.is_a?(::Hash)
|
49
|
+
raise TypedAttrs::DeserializationError.new(
|
50
|
+
"Expected Hash, got #{value.class}",
|
51
|
+
value: value
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
discriminator_value = value[@union_module.discriminator_key] || value[@union_module.discriminator_key.to_sym]
|
56
|
+
unless discriminator_value
|
57
|
+
raise TypedAttrs::DeserializationError.new(
|
58
|
+
"Missing discriminator key '#{@union_module.discriminator_key}' in hash",
|
59
|
+
value: value
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
struct_class = @union_module.find_struct_class_by_discriminator_value(discriminator_value)
|
64
|
+
|
65
|
+
unless struct_class
|
66
|
+
raise TypedAttrs::DeserializationError.new(
|
67
|
+
"Unknown discriminator value '#{discriminator_value}' for key '#{@union_module.discriminator_key}'. ",
|
68
|
+
value: value
|
69
|
+
)
|
70
|
+
end
|
71
|
+
|
72
|
+
hash_without_discriminator = value.dup
|
73
|
+
hash_without_discriminator.delete(@union_module.discriminator_key)
|
74
|
+
hash_without_discriminator.delete(@union_module.discriminator_key.to_sym)
|
75
|
+
|
76
|
+
serializer = @registry.serializer_for(struct_class)
|
77
|
+
serializer.deserialize(hash_without_discriminator)
|
78
|
+
end
|
79
|
+
|
80
|
+
sig do
|
81
|
+
override.params(
|
82
|
+
value: T.untyped,
|
83
|
+
path: ::String,
|
84
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: ::String).void
|
85
|
+
).void
|
86
|
+
end
|
87
|
+
def traverse(value, path: "", &blk)
|
88
|
+
super
|
89
|
+
|
90
|
+
unless @union_module.find_struct_class_by_instance(value)
|
91
|
+
raise TypedAttrs::TypeMismatchError.new(
|
92
|
+
"Expected one of [#{@union_module.types_string}], got #{value.class}",
|
93
|
+
value: value
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
actual_processor = @registry.serializer_for(value.class)
|
98
|
+
actual_processor.traverse(value, path: path, &blk)
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
|
103
|
+
sig { params(struct_classes: T::Array[T.class_of(T::Struct)]).returns(T::Hash[T.class_of(T::Struct), ::String]) }
|
104
|
+
def build_discriminator_map(struct_classes)
|
105
|
+
struct_classes.to_h do |klass|
|
106
|
+
unless respond_to?(:discriminator)
|
107
|
+
raise TypedAttrs::MissingDiscriminatorValueError.new(klass) # rubocop:disable Style/RaiseArgs
|
108
|
+
end
|
109
|
+
|
110
|
+
value = T.unsafe(klass).discriminator
|
111
|
+
|
112
|
+
unless value
|
113
|
+
raise TypedAttrs::MissingDiscriminatorValueError.new(klass) # rubocop:disable Style/RaiseArgs
|
114
|
+
end
|
115
|
+
|
116
|
+
[klass, value]
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class UnionModule
|
122
|
+
extend T::Sig
|
123
|
+
|
124
|
+
sig { params(type_spec: T.untyped).returns(T::Boolean) }
|
125
|
+
def self.union_module?(type_spec)
|
126
|
+
return false unless type_spec.is_a?(Module)
|
127
|
+
return false unless type_spec.respond_to?(:sealed_subclasses)
|
128
|
+
return false unless type_spec.respond_to?(:_get_discriminator_key)
|
129
|
+
|
130
|
+
true
|
131
|
+
end
|
132
|
+
|
133
|
+
sig { params(type_spec: T.untyped).void }
|
134
|
+
def initialize(type_spec)
|
135
|
+
raise ArgumentError, "Not a union module" unless self.class.union_module?(type_spec)
|
136
|
+
|
137
|
+
@type_spec = type_spec
|
138
|
+
|
139
|
+
if type_spec._get_discriminator_key.nil?
|
140
|
+
raise TypedAttrs::MissingDiscriminatorKeyError.new(type_spec) # rubocop:disable Style/RaiseArgs
|
141
|
+
end
|
142
|
+
|
143
|
+
if type_spec.sealed_subclasses.empty?
|
144
|
+
raise TypedAttrs::EmptyUnionModuleError.new(type_spec) # rubocop:disable Style/RaiseArgs
|
145
|
+
end
|
146
|
+
|
147
|
+
@discriminator_key = T.let(type_spec._get_discriminator_key, ::String)
|
148
|
+
@discriminator_map = T.let(build_discriminator_map, T::Hash[T.class_of(T::Struct), ::String])
|
149
|
+
@reverse_map = T.let(@discriminator_map.invert, T::Hash[::String, T.class_of(T::Struct)])
|
150
|
+
end
|
151
|
+
|
152
|
+
sig { returns(::String) }
|
153
|
+
attr_reader :discriminator_key
|
154
|
+
|
155
|
+
sig { params(discriminator_value: T.any(::String, ::Symbol)).returns(T.nilable(T.class_of(T::Struct))) }
|
156
|
+
def find_struct_class_by_discriminator_value(discriminator_value)
|
157
|
+
@reverse_map[discriminator_value.to_s]
|
158
|
+
end
|
159
|
+
|
160
|
+
sig { params(struct_class: T.class_of(T::Struct)).returns(T.nilable(::String)) }
|
161
|
+
def discriminator_value_for(struct_class)
|
162
|
+
@discriminator_map[struct_class]
|
163
|
+
end
|
164
|
+
|
165
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T.nilable(T.class_of(T::Struct))) }
|
166
|
+
def find_struct_class(klass)
|
167
|
+
struct_classes.find { |k| k == klass }
|
168
|
+
end
|
169
|
+
|
170
|
+
sig { params(instance: T.untyped).returns(T.nilable(T.class_of(T::Struct))) }
|
171
|
+
def find_struct_class_by_instance(instance)
|
172
|
+
struct_classes.find { |k| k == instance.class }
|
173
|
+
end
|
174
|
+
|
175
|
+
sig { returns(::String) }
|
176
|
+
def types_string
|
177
|
+
struct_classes.map(&:name).join(", ")
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
sig { returns(T::Hash[T.class_of(T::Struct), ::String]) }
|
183
|
+
def build_discriminator_map
|
184
|
+
struct_classes.to_h do |klass|
|
185
|
+
unless klass.respond_to?(:discriminator)
|
186
|
+
raise TypedAttrs::MissingDiscriminatorValueError.new(klass) # rubocop:disable Style/RaiseArgs
|
187
|
+
end
|
188
|
+
|
189
|
+
value = T.unsafe(klass).discriminator
|
190
|
+
|
191
|
+
unless value
|
192
|
+
raise TypedAttrs::MissingDiscriminatorValueError.new(klass) # rubocop:disable Style/RaiseArgs
|
193
|
+
end
|
194
|
+
|
195
|
+
[klass, value]
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
sig { returns(T::Array[T.class_of(T::Struct)]) }
|
200
|
+
def struct_classes
|
201
|
+
T.let(
|
202
|
+
T.cast(
|
203
|
+
@type_spec.sealed_subclasses.select { |k| k < T::Struct },
|
204
|
+
T::Array[T.class_of(T::Struct)]
|
205
|
+
),
|
206
|
+
T::Array[T.class_of(T::Struct)]
|
207
|
+
)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Float < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec == ::Float
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(value: T.untyped).returns(::Float) }
|
15
|
+
def serialize(value)
|
16
|
+
unless value.is_a?(::Float)
|
17
|
+
raise TypedAttrs::TypeMismatchError.new(
|
18
|
+
"Expected Float, got #{value.class}",
|
19
|
+
value: value
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { override.params(value: T.untyped).returns(::Float) }
|
27
|
+
def deserialize(value)
|
28
|
+
case value
|
29
|
+
when ::Float
|
30
|
+
value
|
31
|
+
when ::Integer
|
32
|
+
value.to_f
|
33
|
+
else
|
34
|
+
raise TypedAttrs::DeserializationError.new(
|
35
|
+
"Expected Float or Integer, got #{value.class}",
|
36
|
+
value: value
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Hash < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec.is_a?(T::Types::TypedHash)
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
15
|
+
def initialize(type_spec, registry:)
|
16
|
+
super
|
17
|
+
@key_type = T.let(extract_key_type(type_spec), T.untyped)
|
18
|
+
@value_type = T.let(extract_value_type(type_spec), T.untyped)
|
19
|
+
end
|
20
|
+
|
21
|
+
sig { override.params(value: T.untyped).returns(T::Hash[::String, T.untyped]) }
|
22
|
+
def serialize(value)
|
23
|
+
unless value.is_a?(::Hash)
|
24
|
+
raise TypedAttrs::TypeMismatchError.new("Expected Hash, got #{value.class}", value: value)
|
25
|
+
end
|
26
|
+
|
27
|
+
result = {}
|
28
|
+
value.each do |key, item|
|
29
|
+
string_key = key.to_s
|
30
|
+
result[string_key] = serialize_value(item)
|
31
|
+
end
|
32
|
+
result
|
33
|
+
end
|
34
|
+
|
35
|
+
sig { override.params(value: T.untyped).returns(T::Hash[::String, T.untyped]) }
|
36
|
+
def deserialize(value)
|
37
|
+
unless value.is_a?(::Hash)
|
38
|
+
raise TypedAttrs::DeserializationError.new("Expected Hash, got #{value.class}", value: value)
|
39
|
+
end
|
40
|
+
|
41
|
+
result = {}
|
42
|
+
value.each do |key, item|
|
43
|
+
normalized_key = if @key_type == ::String
|
44
|
+
key.to_s
|
45
|
+
elsif @key_type == ::Symbol
|
46
|
+
key.to_sym
|
47
|
+
else
|
48
|
+
key
|
49
|
+
end
|
50
|
+
result[normalized_key] = deserialize_value(item)
|
51
|
+
end
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
sig do
|
56
|
+
override.params(
|
57
|
+
value: T.untyped,
|
58
|
+
path: ::String,
|
59
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: ::String).void
|
60
|
+
).void
|
61
|
+
end
|
62
|
+
def traverse(value, path: "", &blk)
|
63
|
+
super
|
64
|
+
|
65
|
+
unless value.is_a?(::Hash)
|
66
|
+
raise TypedAttrs::TypeMismatchError.new("Expected Hash, got #{value.class}", value: value)
|
67
|
+
end
|
68
|
+
|
69
|
+
value.each do |key, val|
|
70
|
+
value_processor = @registry.serializer_for(@value_type)
|
71
|
+
val_path = path.empty? ? key.to_s : "#{path}.#{key}"
|
72
|
+
value_processor.traverse(val, path: val_path, &blk)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
sig { params(item: T.untyped).returns(T.untyped) }
|
79
|
+
def serialize_value(item)
|
80
|
+
serializer = @registry.serializer_for(@value_type)
|
81
|
+
serializer.serialize(item)
|
82
|
+
end
|
83
|
+
|
84
|
+
sig { params(item: T.untyped).returns(T.untyped) }
|
85
|
+
def deserialize_value(item)
|
86
|
+
serializer = @registry.serializer_for(@value_type)
|
87
|
+
serializer.deserialize(item)
|
88
|
+
end
|
89
|
+
|
90
|
+
sig { params(type_spec: T.untyped).returns(T.untyped) }
|
91
|
+
def extract_key_type(type_spec)
|
92
|
+
case type_spec
|
93
|
+
when T::Types::TypedHash
|
94
|
+
key_type = type_spec.keys
|
95
|
+
if key_type.is_a?(T::Types::Simple)
|
96
|
+
T.unsafe(key_type).raw_type
|
97
|
+
else
|
98
|
+
key_type
|
99
|
+
end
|
100
|
+
else
|
101
|
+
raise ArgumentError, "Expected T::Types::TypedHash, got #{type_spec.class}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
sig { params(type_spec: T.untyped).returns(T.untyped) }
|
106
|
+
def extract_value_type(type_spec)
|
107
|
+
case type_spec
|
108
|
+
when T::Types::TypedHash
|
109
|
+
type_spec.values
|
110
|
+
else
|
111
|
+
raise ArgumentError, "Expected T::Types::TypedHash, got #{type_spec.class}"
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module TypedAttrs
|
5
|
+
module TypeProcessors
|
6
|
+
class Integer < Base
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
10
|
+
def self.handles?(type_spec)
|
11
|
+
type_spec == ::Integer
|
12
|
+
end
|
13
|
+
|
14
|
+
sig { override.params(value: T.untyped).returns(::Integer) }
|
15
|
+
def serialize(value)
|
16
|
+
unless value.is_a?(::Integer)
|
17
|
+
raise TypedAttrs::TypeMismatchError.new(
|
18
|
+
"Expected Integer, got #{value.class}",
|
19
|
+
value: value
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
value
|
24
|
+
end
|
25
|
+
|
26
|
+
sig { override.params(value: T.untyped).returns(::Integer) }
|
27
|
+
def deserialize(value)
|
28
|
+
unless value.is_a?(::Integer)
|
29
|
+
raise TypedAttrs::DeserializationError.new(
|
30
|
+
"Expected Integer, got #{value.class}",
|
31
|
+
value: value
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
value
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|