skit 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/LICENSE +21 -0
- data/README.md +469 -0
- data/exe/skit +31 -0
- data/lib/active_model/validations/skit_validator.rb +54 -0
- data/lib/skit/attribute.rb +63 -0
- data/lib/skit/json_schema/class_name_path.rb +67 -0
- data/lib/skit/json_schema/cli.rb +166 -0
- data/lib/skit/json_schema/code_generator.rb +132 -0
- data/lib/skit/json_schema/config.rb +67 -0
- data/lib/skit/json_schema/definitions/array_property_type.rb +36 -0
- data/lib/skit/json_schema/definitions/const_type.rb +68 -0
- data/lib/skit/json_schema/definitions/enum_type.rb +71 -0
- data/lib/skit/json_schema/definitions/hash_property_type.rb +36 -0
- data/lib/skit/json_schema/definitions/module.rb +54 -0
- data/lib/skit/json_schema/definitions/property_type.rb +39 -0
- data/lib/skit/json_schema/definitions/property_types.rb +13 -0
- data/lib/skit/json_schema/definitions/struct.rb +99 -0
- data/lib/skit/json_schema/definitions/struct_property.rb +75 -0
- data/lib/skit/json_schema/definitions/union_property_type.rb +40 -0
- data/lib/skit/json_schema/naming_utils.rb +25 -0
- data/lib/skit/json_schema/schema_analyzer.rb +407 -0
- data/lib/skit/json_schema/types/const.rb +69 -0
- data/lib/skit/json_schema.rb +77 -0
- data/lib/skit/serialization/errors.rb +23 -0
- data/lib/skit/serialization/path.rb +69 -0
- data/lib/skit/serialization/processor/array.rb +65 -0
- data/lib/skit/serialization/processor/base.rb +47 -0
- data/lib/skit/serialization/processor/boolean.rb +35 -0
- data/lib/skit/serialization/processor/date.rb +40 -0
- data/lib/skit/serialization/processor/enum.rb +54 -0
- data/lib/skit/serialization/processor/float.rb +36 -0
- data/lib/skit/serialization/processor/hash.rb +93 -0
- data/lib/skit/serialization/processor/integer.rb +31 -0
- data/lib/skit/serialization/processor/json_schema_const.rb +55 -0
- data/lib/skit/serialization/processor/nilable.rb +87 -0
- data/lib/skit/serialization/processor/simple_type.rb +51 -0
- data/lib/skit/serialization/processor/string.rb +31 -0
- data/lib/skit/serialization/processor/struct.rb +84 -0
- data/lib/skit/serialization/processor/symbol.rb +36 -0
- data/lib/skit/serialization/processor/time.rb +40 -0
- data/lib/skit/serialization/processor/union.rb +120 -0
- data/lib/skit/serialization/registry.rb +33 -0
- data/lib/skit/serialization.rb +60 -0
- data/lib/skit/version.rb +6 -0
- data/lib/skit.rb +46 -0
- data/lib/tapioca/dsl/compilers/skit.rb +105 -0
- metadata +135 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class JsonSchemaConst < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
11
|
+
def self.handles?(type_spec)
|
|
12
|
+
return false unless type_spec.is_a?(Class)
|
|
13
|
+
|
|
14
|
+
!!(type_spec < Skit::JsonSchema::Types::Const)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
|
18
|
+
def initialize(type_spec, registry:)
|
|
19
|
+
super
|
|
20
|
+
unless type_spec.is_a?(Class) && type_spec < Skit::JsonSchema::Types::Const
|
|
21
|
+
raise ArgumentError, "Expected Skit::JsonSchema::Types::Const subclass, got #{type_spec}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@const_class = T.let(type_spec, T.class_of(Skit::JsonSchema::Types::Const))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { override.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
28
|
+
def serialize(value, path: Path.new)
|
|
29
|
+
unless value.is_a?(@const_class)
|
|
30
|
+
raise SerializeError.new("Expected #{@const_class}, got #{value.class}",
|
|
31
|
+
path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
value.value
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { override.params(value: T.untyped, path: Path).returns(Skit::JsonSchema::Types::Const) }
|
|
38
|
+
def deserialize(value, path: Path.new)
|
|
39
|
+
return value if value.is_a?(@const_class)
|
|
40
|
+
|
|
41
|
+
expected = @const_class.value
|
|
42
|
+
|
|
43
|
+
unless value == expected
|
|
44
|
+
raise DeserializeError.new(
|
|
45
|
+
"Expected #{expected.inspect}, got #{value.inspect} for #{@const_class}",
|
|
46
|
+
path: path
|
|
47
|
+
)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
@const_class.new
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Nilable < Base
|
|
8
|
+
extend T::Sig
|
|
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
|
+
unless self.class.nilable_type?(type_spec)
|
|
19
|
+
raise ArgumentError,
|
|
20
|
+
"Expected nilable type, got #{type_spec.class}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@inner_type = T.let(extract_inner_type(type_spec), T.untyped)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
27
|
+
def serialize(value, path: Path.new)
|
|
28
|
+
return nil if value.nil?
|
|
29
|
+
|
|
30
|
+
processor = @registry.processor_for(@inner_type)
|
|
31
|
+
processor.serialize(value, path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig { override.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
35
|
+
def deserialize(value, path: Path.new)
|
|
36
|
+
return nil if value.nil?
|
|
37
|
+
|
|
38
|
+
processor = @registry.processor_for(@inner_type)
|
|
39
|
+
processor.deserialize(value, path: path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig do
|
|
43
|
+
override.params(
|
|
44
|
+
value: T.untyped,
|
|
45
|
+
path: Path,
|
|
46
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
47
|
+
).void
|
|
48
|
+
end
|
|
49
|
+
def traverse(value, path: Path.new, &blk)
|
|
50
|
+
super
|
|
51
|
+
|
|
52
|
+
return if value.nil?
|
|
53
|
+
|
|
54
|
+
processor = @registry.processor_for(@inner_type)
|
|
55
|
+
processor.traverse(value, path: path, &blk)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
class << self
|
|
59
|
+
extend T::Sig
|
|
60
|
+
|
|
61
|
+
sig { params(type_object: T.untyped).returns(T::Boolean) }
|
|
62
|
+
def nilable_type?(type_object)
|
|
63
|
+
return false unless type_object.is_a?(T::Types::Union)
|
|
64
|
+
|
|
65
|
+
types = type_object.types
|
|
66
|
+
has_nil = types.any? { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
67
|
+
non_nil_count = types.count { |t| !(t.is_a?(T::Types::Simple) && t.raw_type == NilClass) }
|
|
68
|
+
|
|
69
|
+
has_nil && non_nil_count == 1
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
sig { params(nilable_type: T.untyped).returns(T.untyped) }
|
|
76
|
+
def extract_inner_type(nilable_type)
|
|
77
|
+
types = nilable_type.types
|
|
78
|
+
non_nil_types = types.reject { |t| t.is_a?(T::Types::Simple) && t.raw_type == NilClass }
|
|
79
|
+
|
|
80
|
+
raise ArgumentError, "T.nilable must have exactly one non-nil type" if non_nil_types.length != 1
|
|
81
|
+
|
|
82
|
+
non_nil_types.first
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class SimpleType < Base
|
|
8
|
+
extend T::Sig
|
|
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, path: Path).returns(T.untyped) }
|
|
26
|
+
def serialize(value, path: Path.new)
|
|
27
|
+
processor = @registry.processor_for(@raw_type)
|
|
28
|
+
processor.serialize(value, path: path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
sig { override.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
32
|
+
def deserialize(value, path: Path.new)
|
|
33
|
+
processor = @registry.processor_for(@raw_type)
|
|
34
|
+
processor.deserialize(value, path: path)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig do
|
|
38
|
+
override.params(
|
|
39
|
+
value: T.untyped,
|
|
40
|
+
path: Path,
|
|
41
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
42
|
+
).void
|
|
43
|
+
end
|
|
44
|
+
def traverse(value, path: Path.new, &blk)
|
|
45
|
+
processor = @registry.processor_for(@raw_type)
|
|
46
|
+
processor.traverse(value, path: path, &blk)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class String < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
11
|
+
def self.handles?(type_spec)
|
|
12
|
+
type_spec == ::String
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.params(value: T.untyped, path: Path).returns(::String) }
|
|
16
|
+
def serialize(value, path: Path.new)
|
|
17
|
+
raise SerializeError.new("Expected String, got #{value.class}", path: path) unless value.is_a?(::String)
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(value: T.untyped, path: Path).returns(::String) }
|
|
23
|
+
def deserialize(value, path: Path.new)
|
|
24
|
+
raise DeserializeError.new("Expected String, got #{value.class}", path: path) unless value.is_a?(::String)
|
|
25
|
+
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Struct < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
11
|
+
def self.handles?(type_spec)
|
|
12
|
+
return false unless type_spec.is_a?(Class)
|
|
13
|
+
|
|
14
|
+
!!(type_spec < T::Struct)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
|
18
|
+
def initialize(type_spec, registry:)
|
|
19
|
+
super
|
|
20
|
+
unless type_spec.is_a?(Class) && type_spec < T::Struct
|
|
21
|
+
raise ArgumentError, "Expected T::Struct, got #{type_spec}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@struct_class = T.let(type_spec, T.class_of(T::Struct))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Hash[::String, T.untyped]) }
|
|
28
|
+
def serialize(value, path: Path.new)
|
|
29
|
+
unless value.is_a?(@struct_class)
|
|
30
|
+
raise SerializeError.new("Expected #{@struct_class}, got #{value.class}",
|
|
31
|
+
path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
@struct_class.props.each_with_object({}) do |(name, prop_def), hash|
|
|
35
|
+
prop_value = value.public_send(name)
|
|
36
|
+
prop_type = prop_def[:type_object]
|
|
37
|
+
processor = @registry.processor_for(prop_type)
|
|
38
|
+
hash[name.to_s] = processor.serialize(prop_value, path: path.append(name.to_s))
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Struct) }
|
|
43
|
+
def deserialize(value, path: Path.new)
|
|
44
|
+
return value if value.is_a?(@struct_class)
|
|
45
|
+
|
|
46
|
+
raise DeserializeError.new("Expected Hash, got #{value.class}", path: path) unless value.is_a?(::Hash)
|
|
47
|
+
|
|
48
|
+
symbolized = value.transform_keys(&:to_sym)
|
|
49
|
+
|
|
50
|
+
deserialized = @struct_class.props.each_with_object({}) do |(name, prop_def), hash|
|
|
51
|
+
next unless symbolized.key?(name)
|
|
52
|
+
|
|
53
|
+
prop_value = symbolized[name]
|
|
54
|
+
prop_type = prop_def[:type_object]
|
|
55
|
+
processor = @registry.processor_for(prop_type)
|
|
56
|
+
hash[name] = processor.deserialize(prop_value, path: path.append(name.to_s))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
@struct_class.new(**deserialized)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
sig do
|
|
63
|
+
override.params(
|
|
64
|
+
value: T.untyped,
|
|
65
|
+
path: Path,
|
|
66
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
67
|
+
).void
|
|
68
|
+
end
|
|
69
|
+
def traverse(value, path: Path.new, &blk)
|
|
70
|
+
super
|
|
71
|
+
|
|
72
|
+
return unless value.is_a?(@struct_class)
|
|
73
|
+
|
|
74
|
+
@struct_class.props.each do |name, prop_def|
|
|
75
|
+
prop_value = value.public_send(name)
|
|
76
|
+
prop_type = prop_def[:type_object]
|
|
77
|
+
processor = @registry.processor_for(prop_type)
|
|
78
|
+
processor.traverse(prop_value, path: path.append(name.to_s), &blk)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Symbol < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
11
|
+
def self.handles?(type_spec)
|
|
12
|
+
type_spec == ::Symbol
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.params(value: T.untyped, path: Path).returns(::String) }
|
|
16
|
+
def serialize(value, path: Path.new)
|
|
17
|
+
raise SerializeError.new("Expected Symbol, got #{value.class}", path: path) unless value.is_a?(::Symbol)
|
|
18
|
+
|
|
19
|
+
value.to_s
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Symbol) }
|
|
23
|
+
def deserialize(value, path: Path.new)
|
|
24
|
+
return value if value.is_a?(::Symbol)
|
|
25
|
+
|
|
26
|
+
unless value.is_a?(::String)
|
|
27
|
+
raise DeserializeError.new("Expected String or Symbol, got #{value.class}",
|
|
28
|
+
path: path)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
value.to_sym
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
module Skit
|
|
7
|
+
module Serialization
|
|
8
|
+
module Processor
|
|
9
|
+
class Time < Base
|
|
10
|
+
extend T::Sig
|
|
11
|
+
|
|
12
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
13
|
+
def self.handles?(type_spec)
|
|
14
|
+
type_spec == ::Time
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
sig { override.params(value: T.untyped, path: Path).returns(::String) }
|
|
18
|
+
def serialize(value, path: Path.new)
|
|
19
|
+
raise SerializeError.new("Expected Time, got #{value.class}", path: path) unless value.is_a?(::Time)
|
|
20
|
+
|
|
21
|
+
value.iso8601
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Time) }
|
|
25
|
+
def deserialize(value, path: Path.new)
|
|
26
|
+
case value
|
|
27
|
+
when ::Time
|
|
28
|
+
value
|
|
29
|
+
when ::String
|
|
30
|
+
::Time.iso8601(value)
|
|
31
|
+
else
|
|
32
|
+
raise DeserializeError.new("Expected Time or String, got #{value.class}", path: path)
|
|
33
|
+
end
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
raise DeserializeError.new("Failed to deserialize Time: #{e.message}", path: path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Union < Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
|
|
10
|
+
sig { override.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
11
|
+
def self.handles?(type_spec)
|
|
12
|
+
return false unless type_spec.is_a?(T::Types::Union)
|
|
13
|
+
return false if contains_nil_class?(type_spec)
|
|
14
|
+
return false if boolean_union?(type_spec)
|
|
15
|
+
|
|
16
|
+
struct_types(type_spec).length == type_spec.types.length
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
|
20
|
+
def initialize(type_spec, registry:)
|
|
21
|
+
super
|
|
22
|
+
@struct_classes = T.let(
|
|
23
|
+
self.class.struct_types(type_spec),
|
|
24
|
+
T::Array[T.class_of(T::Struct)]
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Hash[::String, T.untyped]) }
|
|
29
|
+
def serialize(value, path: Path.new)
|
|
30
|
+
struct_class = find_struct_class_for_value(value)
|
|
31
|
+
|
|
32
|
+
unless struct_class
|
|
33
|
+
raise SerializeError.new(
|
|
34
|
+
"#{value.class} is not a member of this union: #{@struct_classes.map(&:name).join(", ")}",
|
|
35
|
+
path: path
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
processor = @registry.processor_for(struct_class)
|
|
40
|
+
processor.serialize(value, path: path)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
sig { override.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
44
|
+
def deserialize(value, path: Path.new)
|
|
45
|
+
return value if value_is_union_member?(value)
|
|
46
|
+
|
|
47
|
+
unless value.is_a?(::Hash)
|
|
48
|
+
raise DeserializeError.new("Expected Hash or union member struct, got #{value.class}",
|
|
49
|
+
path: path)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@struct_classes.each do |struct_class|
|
|
53
|
+
result = try_deserialize(value, struct_class, path: path)
|
|
54
|
+
return result if result
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
raise DeserializeError.new(
|
|
58
|
+
"No matching struct found for union: #{@struct_classes.map(&:name).join(", ")}",
|
|
59
|
+
path: path
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
class << self
|
|
64
|
+
extend T::Sig
|
|
65
|
+
|
|
66
|
+
sig { params(type_spec: T.untyped).returns(T::Boolean) }
|
|
67
|
+
def contains_nil_class?(type_spec)
|
|
68
|
+
type_spec.types.any? do |t|
|
|
69
|
+
t.is_a?(T::Types::Simple) && t.raw_type == NilClass
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
sig { params(type_spec: T.untyped).returns(T::Boolean) }
|
|
74
|
+
def boolean_union?(type_spec)
|
|
75
|
+
raw_types = type_spec.types.filter_map do |t|
|
|
76
|
+
t.is_a?(T::Types::Simple) ? t.raw_type : nil
|
|
77
|
+
end
|
|
78
|
+
raw_types.sort_by { |t| t.name.to_s } == [FalseClass, TrueClass]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
sig { params(type_spec: T.untyped).returns(T::Array[T.class_of(T::Struct)]) }
|
|
82
|
+
def struct_types(type_spec)
|
|
83
|
+
type_spec.types.filter_map do |t|
|
|
84
|
+
next unless t.is_a?(T::Types::Simple)
|
|
85
|
+
|
|
86
|
+
klass = t.raw_type
|
|
87
|
+
klass if klass.is_a?(Class) && klass < T::Struct
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
sig { params(value: T.untyped).returns(T.nilable(T.class_of(T::Struct))) }
|
|
95
|
+
def find_struct_class_for_value(value)
|
|
96
|
+
@struct_classes.find { |klass| value.is_a?(klass) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
sig { params(value: T.untyped).returns(T::Boolean) }
|
|
100
|
+
def value_is_union_member?(value)
|
|
101
|
+
@struct_classes.any? { |klass| value.is_a?(klass) }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
sig do
|
|
105
|
+
params(
|
|
106
|
+
value: T::Hash[T.untyped, T.untyped],
|
|
107
|
+
struct_class: T.class_of(T::Struct),
|
|
108
|
+
path: Path
|
|
109
|
+
).returns(T.nilable(T::Struct))
|
|
110
|
+
end
|
|
111
|
+
def try_deserialize(value, struct_class, path: Path.new)
|
|
112
|
+
processor = @registry.processor_for(struct_class)
|
|
113
|
+
processor.deserialize(value, path: path)
|
|
114
|
+
rescue SerializeError, DeserializeError, ArgumentError, TypeError
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
class Registry
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { void }
|
|
10
|
+
def initialize
|
|
11
|
+
@processors = T.let([], T::Array[T.class_of(Processor::Base)])
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
sig { params(processor_class: T.class_of(Processor::Base)).void }
|
|
15
|
+
def register(processor_class)
|
|
16
|
+
@processors << processor_class
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
sig { params(type_spec: T.untyped).returns(T.class_of(Processor::Base)) }
|
|
20
|
+
def find_processor(type_spec)
|
|
21
|
+
processor_class = @processors.find { |p| p.handles?(type_spec) }
|
|
22
|
+
raise UnknownTypeError, "No processor for #{type_spec}" unless processor_class
|
|
23
|
+
|
|
24
|
+
processor_class
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(type_spec: T.untyped).returns(Processor::Base) }
|
|
28
|
+
def processor_for(type_spec)
|
|
29
|
+
find_processor(type_spec).new(type_spec, registry: self)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "serialization/errors"
|
|
5
|
+
require_relative "serialization/path"
|
|
6
|
+
require_relative "serialization/processor/base"
|
|
7
|
+
require_relative "serialization/registry"
|
|
8
|
+
|
|
9
|
+
# Processors
|
|
10
|
+
require_relative "serialization/processor/string"
|
|
11
|
+
require_relative "serialization/processor/integer"
|
|
12
|
+
require_relative "serialization/processor/float"
|
|
13
|
+
require_relative "serialization/processor/boolean"
|
|
14
|
+
require_relative "serialization/processor/symbol"
|
|
15
|
+
require_relative "serialization/processor/date"
|
|
16
|
+
require_relative "serialization/processor/time"
|
|
17
|
+
require_relative "serialization/processor/struct"
|
|
18
|
+
require_relative "serialization/processor/simple_type"
|
|
19
|
+
require_relative "serialization/processor/array"
|
|
20
|
+
require_relative "serialization/processor/hash"
|
|
21
|
+
require_relative "serialization/processor/nilable"
|
|
22
|
+
require_relative "serialization/processor/union"
|
|
23
|
+
require_relative "serialization/processor/json_schema_const"
|
|
24
|
+
require_relative "serialization/processor/enum"
|
|
25
|
+
|
|
26
|
+
module Skit
|
|
27
|
+
module Serialization
|
|
28
|
+
extend T::Sig
|
|
29
|
+
|
|
30
|
+
sig { returns(Registry) }
|
|
31
|
+
def self.default_registry
|
|
32
|
+
@default_registry ||= T.let(build_default_registry, T.nilable(Registry))
|
|
33
|
+
@default_registry
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
sig { returns(Registry) }
|
|
37
|
+
def self.build_default_registry
|
|
38
|
+
registry = Registry.new
|
|
39
|
+
# Register processors in order of specificity (most specific first)
|
|
40
|
+
registry.register(Processor::Nilable)
|
|
41
|
+
registry.register(Processor::Union)
|
|
42
|
+
registry.register(Processor::Array)
|
|
43
|
+
registry.register(Processor::Hash)
|
|
44
|
+
registry.register(Processor::JsonSchemaConst)
|
|
45
|
+
registry.register(Processor::Enum)
|
|
46
|
+
registry.register(Processor::Struct)
|
|
47
|
+
registry.register(Processor::SimpleType)
|
|
48
|
+
registry.register(Processor::Date)
|
|
49
|
+
registry.register(Processor::Time)
|
|
50
|
+
registry.register(Processor::String)
|
|
51
|
+
registry.register(Processor::Integer)
|
|
52
|
+
registry.register(Processor::Float)
|
|
53
|
+
registry.register(Processor::Boolean)
|
|
54
|
+
registry.register(Processor::Symbol)
|
|
55
|
+
registry
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private_class_method :build_default_registry
|
|
59
|
+
end
|
|
60
|
+
end
|
data/lib/skit/version.rb
ADDED
data/lib/skit.rb
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module Skit
|
|
7
|
+
class Error < StandardError; end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
require_relative "skit/version"
|
|
11
|
+
# Load JsonSchema::Types::Const early so it can be used by serialization processors
|
|
12
|
+
require_relative "skit/json_schema/types/const"
|
|
13
|
+
require_relative "skit/serialization"
|
|
14
|
+
require_relative "skit/attribute"
|
|
15
|
+
require_relative "skit/json_schema"
|
|
16
|
+
require_relative "active_model/validations/skit_validator"
|
|
17
|
+
|
|
18
|
+
module Skit # rubocop:disable Style/OneClassPerFile
|
|
19
|
+
extend T::Sig
|
|
20
|
+
|
|
21
|
+
# Serialize a T::Struct instance to a Hash with string keys.
|
|
22
|
+
#
|
|
23
|
+
# @param struct [T::Struct] The struct instance to serialize
|
|
24
|
+
# @return [Hash] The serialized Hash
|
|
25
|
+
# @raise [Serialization::SerializeError] If the value is not a T::Struct
|
|
26
|
+
sig { params(struct: T::Struct).returns(T::Hash[::String, T.untyped]) }
|
|
27
|
+
def self.serialize(struct)
|
|
28
|
+
struct_class = struct.class
|
|
29
|
+
raise Serialization::SerializeError, "Expected T::Struct, got #{struct_class}" unless struct_class < T::Struct
|
|
30
|
+
|
|
31
|
+
processor = Serialization.default_registry.processor_for(struct_class)
|
|
32
|
+
processor.serialize(struct)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Deserialize a Hash to a T::Struct instance.
|
|
36
|
+
#
|
|
37
|
+
# @param hash [Hash] The hash to deserialize
|
|
38
|
+
# @param type [Class] The T::Struct class to deserialize to
|
|
39
|
+
# @return [T::Struct] The deserialized struct instance
|
|
40
|
+
# @raise [Serialization::DeserializeError] If deserialization fails
|
|
41
|
+
sig { params(hash: T.untyped, type: T.class_of(T::Struct)).returns(T::Struct) }
|
|
42
|
+
def self.deserialize(hash, type)
|
|
43
|
+
processor = Serialization.default_registry.processor_for(type)
|
|
44
|
+
processor.deserialize(hash)
|
|
45
|
+
end
|
|
46
|
+
end
|