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.
@@ -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