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,53 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class NilableType < 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
+ nilable_type?(type_spec)
13
+ end
14
+
15
+ sig { params(type_spec: T.untyped, registry: Registry).void }
16
+ def initialize(type_spec, registry:)
17
+ super
18
+
19
+ @inner_type = T.let(self.class.extract_nilable_inner_type(type_spec), T.untyped)
20
+ @inner_serializer = T.let(@registry.serializer_for(@inner_type), Base)
21
+ end
22
+
23
+ sig { override.params(value: T.untyped).returns(T.untyped) }
24
+ def serialize(value)
25
+ return nil if value.nil?
26
+
27
+ @inner_serializer.serialize(value)
28
+ end
29
+
30
+ sig { override.params(value: T.untyped).returns(T.untyped) }
31
+ def deserialize(value)
32
+ return nil if value.nil?
33
+
34
+ @inner_serializer.deserialize(value)
35
+ end
36
+
37
+ sig do
38
+ override.params(
39
+ value: T.untyped,
40
+ path: ::String,
41
+ blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: ::String).void
42
+ ).void
43
+ end
44
+ def traverse(value, path: "", &blk)
45
+ super
46
+
47
+ return if value.nil?
48
+
49
+ @inner_serializer.traverse(value, path: path, &blk)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,35 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class UnknownTypeError < StandardError; end
7
+
8
+ class Registry
9
+ extend T::Sig
10
+
11
+ sig { void }
12
+ def initialize
13
+ @serializers = T.let([], T::Array[T.class_of(Base)])
14
+ end
15
+
16
+ sig { params(serializer_class: T.class_of(Base)).void }
17
+ def register(serializer_class)
18
+ @serializers << serializer_class
19
+ end
20
+
21
+ sig { params(type_spec: T.untyped).returns(T.class_of(Base)) }
22
+ def find_serializer(type_spec)
23
+ serializer_class = @serializers.find { |s| s.handles?(type_spec) }
24
+ raise UnknownTypeError, "No serializer for #{type_spec}" unless serializer_class
25
+
26
+ serializer_class
27
+ end
28
+
29
+ sig { params(type_spec: T.untyped).returns(Base) }
30
+ def serializer_for(type_spec)
31
+ find_serializer(type_spec).new(type_spec, registry: self)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class SimpleType < Base
7
+ extend T::Sig
8
+ include TypeHelpers
9
+
10
+ sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
11
+ def self.handles?(type_spec)
12
+ type_spec.is_a?(T::Types::Simple)
13
+ end
14
+
15
+ sig { params(type_spec: T.untyped, registry: Registry).void }
16
+ def initialize(type_spec, registry:)
17
+ super
18
+ unless type_spec.is_a?(T::Types::Simple)
19
+ raise ArgumentError, "Expected T::Types::Simple, got #{type_spec.class}"
20
+ end
21
+
22
+ @raw_type = T.let(type_spec.raw_type, T.untyped)
23
+ end
24
+
25
+ sig { override.params(value: T.untyped).returns(T.untyped) }
26
+ def serialize(value)
27
+ serializer = @registry.serializer_for(@raw_type)
28
+ serializer.serialize(value)
29
+ end
30
+
31
+ sig { override.params(value: T.untyped).returns(T.untyped) }
32
+ def deserialize(value)
33
+ serializer = @registry.serializer_for(@raw_type)
34
+ serializer.deserialize(value)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class String < 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 == ::String
12
+ end
13
+
14
+ sig { override.params(value: T.untyped).returns(::String) }
15
+ def serialize(value)
16
+ unless value.is_a?(::String)
17
+ raise TypedAttrs::TypeMismatchError.new(
18
+ "Expected String, got #{value.class}",
19
+ value: value
20
+ )
21
+ end
22
+
23
+ value
24
+ end
25
+
26
+ sig { override.params(value: T.untyped).returns(::String) }
27
+ def deserialize(value)
28
+ unless value.is_a?(::String)
29
+ raise TypedAttrs::DeserializationError.new(
30
+ "Expected String, got #{value.class}",
31
+ value: value
32
+ )
33
+ end
34
+
35
+ value
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,70 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class Struct < 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
+ struct_class?(type_spec)
13
+ end
14
+
15
+ sig { params(type_spec: T.untyped, registry: Registry).void }
16
+ def initialize(type_spec, registry:)
17
+ super
18
+ unless type_spec.is_a?(Class) && type_spec < T::Struct
19
+ raise ArgumentError, "Expected T::Struct, got #{type_spec}"
20
+ end
21
+
22
+ @struct_class = T.let(type_spec, T.class_of(T::Struct))
23
+ end
24
+
25
+ sig { override.params(value: T.untyped).returns(T::Hash[::String, T.untyped]) }
26
+ def serialize(value)
27
+ unless value.is_a?(@struct_class)
28
+ raise TypedAttrs::TypeMismatchError.new(
29
+ "Expected #{@struct_class}, got #{value.class}",
30
+ value: value
31
+ )
32
+ end
33
+
34
+ @struct_class.props.each_with_object({}) do |(name, prop_def), hash|
35
+ prop_value = value.public_send(name)
36
+
37
+ prop_type = prop_def[:type_object]
38
+ serializer = @registry.serializer_for(prop_type)
39
+ hash[name.to_s] = serializer.serialize(prop_value)
40
+ end
41
+ end
42
+
43
+ sig { override.params(value: T.untyped).returns(T::Struct) }
44
+ def deserialize(value)
45
+ return value if value.is_a?(@struct_class)
46
+
47
+ unless value.is_a?(::Hash)
48
+ raise TypedAttrs::DeserializationError.new(
49
+ "Expected Hash, got #{value.class}",
50
+ value: value
51
+ )
52
+ end
53
+
54
+ symbolized = value.transform_keys(&:to_sym)
55
+
56
+ deserialized = @struct_class.props.each_with_object({}) do |(name, prop_def), hash|
57
+ next unless symbolized.key?(name)
58
+
59
+ prop_value = symbolized[name]
60
+
61
+ prop_type = prop_def[:type_object]
62
+ serializer = @registry.serializer_for(prop_type)
63
+ hash[name] = serializer.deserialize(prop_value)
64
+ end
65
+
66
+ @struct_class.new(**deserialized)
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,41 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class Symbol < 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 == ::Symbol
12
+ end
13
+
14
+ sig { override.params(value: T.untyped).returns(::String) }
15
+ def serialize(value)
16
+ unless value.is_a?(::Symbol)
17
+ raise TypedAttrs::TypeMismatchError.new(
18
+ "Expected Symbol, got #{value.class}",
19
+ value: value
20
+ )
21
+ end
22
+
23
+ value.to_s
24
+ end
25
+
26
+ sig { override.params(value: T.untyped).returns(::Symbol) }
27
+ def deserialize(value)
28
+ return value if value.is_a?(::Symbol)
29
+
30
+ unless value.is_a?(::String)
31
+ raise TypedAttrs::DeserializationError.new(
32
+ "Expected String or Symbol, got #{value.class}",
33
+ value: value
34
+ )
35
+ end
36
+
37
+ value.to_sym
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ class Time < 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 == ::Time
12
+ end
13
+
14
+ sig { override.params(value: T.untyped).returns(::String) }
15
+ def serialize(value)
16
+ unless value.is_a?(::Time)
17
+ raise TypedAttrs::TypeMismatchError.new(
18
+ "Expected Time, got #{value.class}",
19
+ value: value
20
+ )
21
+ end
22
+
23
+ value.iso8601
24
+ end
25
+
26
+ sig { override.params(value: T.untyped).returns(::Time) }
27
+ def deserialize(value)
28
+ case value
29
+ when ::Time
30
+ value
31
+ when ::String
32
+ ::Time.iso8601(value)
33
+ else
34
+ raise TypedAttrs::DeserializationError.new(
35
+ "Expected Time or String, got #{value.class}",
36
+ value: value
37
+ )
38
+ end
39
+ rescue ArgumentError => e
40
+ raise TypedAttrs::DeserializationError.new(
41
+ "Failed to deserialize Time: #{e.message}",
42
+ value: value
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module TypeProcessors
6
+ extend T::Sig
7
+
8
+ sig { returns(Registry) }
9
+ def self.default_registry
10
+ @default_registry ||= T.let(build_default_registry, T.nilable(Registry))
11
+ end
12
+
13
+ sig { returns(Registry) }
14
+ def self.build_default_registry
15
+ registry = Registry.new
16
+
17
+ # Register in search order (more specific types first)
18
+ registry.register(SimpleType) # Unwrap T::Types::Simple (first)
19
+ registry.register(NilableType) # Handle T.nilable(...)
20
+ registry.register(Array) # Handle T::Array[...]
21
+ registry.register(Hash) # Handle T::Hash[...]
22
+ registry.register(DiscriminatedUnion) # Handle DiscriminatedUnion
23
+ registry.register(Integer) # Handle Integer
24
+ registry.register(Float) # Handle Float
25
+ registry.register(Boolean) # Handle T::Boolean
26
+ registry.register(BooleanPrimitive) # Handle TrueClass/FalseClass
27
+ registry.register(String) # Handle String
28
+ registry.register(Symbol) # Handle Symbol
29
+ registry.register(Date) # Handle Date
30
+ registry.register(Time) # Handle Time
31
+ registry.register(Struct) # Handle T::Struct (last)
32
+
33
+ registry
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,51 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ module Types
6
+ class Type < ActiveModel::Type::Value
7
+ include TypeHelpers
8
+ extend T::Sig
9
+
10
+ sig { params(type_spec: T.untyped).void }
11
+ def initialize(type_spec)
12
+ super()
13
+ @type_spec = type_spec
14
+
15
+ registry = T.let(TypeProcessors.default_registry, TypeProcessors::Registry)
16
+ @processor = T.let(registry.serializer_for(type_spec), TypeProcessors::Base)
17
+ end
18
+
19
+ sig { returns(TypeProcessors::Base) }
20
+ attr_reader :processor
21
+
22
+ sig { params(value: T.untyped).returns(T.untyped) }
23
+ def cast(value)
24
+ return nil if value.nil?
25
+
26
+ @processor.deserialize(value)
27
+ end
28
+
29
+ sig { params(value: T.untyped).returns(T.untyped) }
30
+ def serialize(value)
31
+ return nil if value.nil?
32
+
33
+ serialized = @processor.serialize(value)
34
+ ActiveSupport::JSON.encode(serialized)
35
+ end
36
+
37
+ sig { params(value: T.untyped).returns(T.untyped) }
38
+ def deserialize(value)
39
+ return nil if value.nil?
40
+
41
+ data = if value.is_a?(String)
42
+ ActiveSupport::JSON.decode(value)
43
+ else
44
+ value
45
+ end
46
+
47
+ @processor.deserialize(data)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,6 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module TypedAttrs
5
+ VERSION = "0.1.0"
6
+ end
@@ -0,0 +1,103 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "active_model"
6
+
7
+ require_relative "typed_attrs/version"
8
+ require_relative "typed_attrs/type_helpers"
9
+ require_relative "typed_attrs/type_processors/registry"
10
+ require_relative "typed_attrs/type_processors"
11
+ require_relative "typed_attrs/type_processors/base"
12
+ require_relative "typed_attrs/type_processors/simple_type"
13
+ require_relative "typed_attrs/type_processors/nilable_type"
14
+ require_relative "typed_attrs/type_processors/integer"
15
+ require_relative "typed_attrs/type_processors/float"
16
+ require_relative "typed_attrs/type_processors/boolean"
17
+ require_relative "typed_attrs/type_processors/boolean_primitive"
18
+ require_relative "typed_attrs/type_processors/string"
19
+ require_relative "typed_attrs/type_processors/symbol"
20
+ require_relative "typed_attrs/type_processors/date"
21
+ require_relative "typed_attrs/type_processors/time"
22
+ require_relative "typed_attrs/type_processors/struct"
23
+ require_relative "typed_attrs/type_processors/array"
24
+ require_relative "typed_attrs/type_processors/hash"
25
+ require_relative "typed_attrs/type_processors/discriminated_union"
26
+ require_relative "typed_attrs/types/type"
27
+ require_relative "typed_attrs/discriminated_union"
28
+ require_relative "active_model/validations/typed_attrs_validator"
29
+
30
+ module TypedAttrs
31
+ extend T::Sig
32
+
33
+ # Exception class for type mismatch errors during serialization/deserialization/traversal
34
+ class TypeMismatchError < StandardError
35
+ extend T::Sig
36
+
37
+ sig { params(message: String, value: T.untyped).void }
38
+ def initialize(message, value: nil)
39
+ @value = value
40
+ super(message)
41
+ end
42
+
43
+ sig { returns(T.untyped) }
44
+ attr_reader :value
45
+ end
46
+
47
+ # Exception class for deserialization errors
48
+ class DeserializationError < StandardError
49
+ extend T::Sig
50
+
51
+ sig { params(message: String, value: T.untyped).void }
52
+ def initialize(message, value: nil)
53
+ @value = value
54
+ super(message)
55
+ end
56
+
57
+ sig { returns(T.untyped) }
58
+ attr_reader :value
59
+ end
60
+
61
+ # Exception class for missing union discriminator value
62
+ class MissingDiscriminatorValueError < StandardError
63
+ extend T::Sig
64
+
65
+ sig { params(struct_class: T.class_of(T::Struct)).void }
66
+ def initialize(struct_class)
67
+ super(
68
+ "Union discriminator value is not defined for #{struct_class}. " \
69
+ "Please add 'union_discriminator_value \"value\"' to the class definition."
70
+ )
71
+ end
72
+ end
73
+
74
+ class EmptyUnionModuleError < StandardError
75
+ extend T::Sig
76
+
77
+ sig { params(union_module: BasicObject).void }
78
+ def initialize(union_module)
79
+ super(
80
+ "Union module #{union_module} is empty. Please add 'sealed!', 'discriminator_key \"key\"', " \
81
+ "and at least one T::Struct subclass."
82
+ )
83
+ end
84
+ end
85
+
86
+ # Exception class for missing union discriminator key
87
+ class MissingDiscriminatorKeyError < StandardError
88
+ extend T::Sig
89
+
90
+ sig { params(union_module: T.untyped).void }
91
+ def initialize(union_module)
92
+ super(
93
+ "Union discriminator key is not defined for #{union_module}. " \
94
+ "Please add 'discriminator_key \"key\"' to the module definition."
95
+ )
96
+ end
97
+ end
98
+
99
+ sig { params(type_spec: T.untyped).returns(Types::Type) }
100
+ def self.to_type(type_spec)
101
+ Types::Type.new(type_spec)
102
+ end
103
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: typed_attrs
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Speria
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: activemodel
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '6.0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '6.0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: activerecord
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '6.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: sorbet-runtime
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.5'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '0.5'
54
+ description: TypedAttrs integrates Sorbet's T::Struct with ActiveRecord's JSON/JSONB
55
+ columns, providing type-safe structured data storage with runtime and static type
56
+ checking. Supports nested structs, arrays, hashes, union types, and automatic validation.
57
+ email:
58
+ - daichi.sakai@speria.jp
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - CHANGELOG.md
64
+ - LICENSE
65
+ - README.md
66
+ - lib/active_model/validations/typed_attrs_validator.rb
67
+ - lib/tapioca/dsl/compilers/typed_attribute.rb
68
+ - lib/typed_attrs.rb
69
+ - lib/typed_attrs/discriminated_union.rb
70
+ - lib/typed_attrs/type_helpers.rb
71
+ - lib/typed_attrs/type_processors.rb
72
+ - lib/typed_attrs/type_processors/array.rb
73
+ - lib/typed_attrs/type_processors/base.rb
74
+ - lib/typed_attrs/type_processors/boolean.rb
75
+ - lib/typed_attrs/type_processors/boolean_primitive.rb
76
+ - lib/typed_attrs/type_processors/date.rb
77
+ - lib/typed_attrs/type_processors/discriminated_union.rb
78
+ - lib/typed_attrs/type_processors/float.rb
79
+ - lib/typed_attrs/type_processors/hash.rb
80
+ - lib/typed_attrs/type_processors/integer.rb
81
+ - lib/typed_attrs/type_processors/nilable_type.rb
82
+ - lib/typed_attrs/type_processors/registry.rb
83
+ - lib/typed_attrs/type_processors/simple_type.rb
84
+ - lib/typed_attrs/type_processors/string.rb
85
+ - lib/typed_attrs/type_processors/struct.rb
86
+ - lib/typed_attrs/type_processors/symbol.rb
87
+ - lib/typed_attrs/type_processors/time.rb
88
+ - lib/typed_attrs/types/type.rb
89
+ - lib/typed_attrs/version.rb
90
+ homepage: https://github.com/speria-jp/typed_attrs
91
+ licenses:
92
+ - MIT
93
+ metadata:
94
+ homepage_uri: https://github.com/speria-jp/typed_attrs
95
+ source_code_uri: https://github.com/speria-jp/typed_attrs.git
96
+ changelog_uri: https://github.com/speria-jp/typed_attrs/blob/master/CHANGELOG.md
97
+ bug_tracker_uri: https://github.com/speria-jp/typed_attrs/issues
98
+ documentation_uri: https://github.com/speria-jp/typed_attrs/blob/master/README.md
99
+ rubygems_mfa_required: 'true'
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 3.2.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.6.7
115
+ specification_version: 4
116
+ summary: Type-safe ActiveRecord attributes using Sorbet T::Struct
117
+ test_files: []