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,69 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module JsonSchema
|
|
6
|
+
module Types
|
|
7
|
+
# Base class for JSON Schema const values.
|
|
8
|
+
#
|
|
9
|
+
# Subclasses should define a VALUE constant with the const value:
|
|
10
|
+
#
|
|
11
|
+
# class Dog < Skit::JsonSchema::Types::Const
|
|
12
|
+
# VALUE = "dog"
|
|
13
|
+
# end
|
|
14
|
+
#
|
|
15
|
+
# This enables type-safe discriminated unions:
|
|
16
|
+
#
|
|
17
|
+
# T.any(Dog, Cat) # "dog" deserializes to Dog, "cat" to Cat
|
|
18
|
+
#
|
|
19
|
+
class Const
|
|
20
|
+
extend T::Sig
|
|
21
|
+
|
|
22
|
+
# Returns the const value defined in the subclass.
|
|
23
|
+
sig { returns(T.untyped) }
|
|
24
|
+
def self.value
|
|
25
|
+
# rubocop:disable Sorbet/ConstantsFromStrings
|
|
26
|
+
const_get(:VALUE)
|
|
27
|
+
# rubocop:enable Sorbet/ConstantsFromStrings
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Returns the const value for this instance.
|
|
31
|
+
sig { returns(T.untyped) }
|
|
32
|
+
def value
|
|
33
|
+
self.class.value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Two Const instances are equal if they are of the same class.
|
|
37
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
38
|
+
def ==(other)
|
|
39
|
+
other.is_a?(self.class)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Alias for == to support Hash key comparison.
|
|
43
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
44
|
+
def eql?(other)
|
|
45
|
+
self == other
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Hash code based on the class, so all instances of the same class
|
|
49
|
+
# have the same hash code.
|
|
50
|
+
sig { returns(Integer) }
|
|
51
|
+
def hash
|
|
52
|
+
self.class.hash
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Returns a string representation for debugging.
|
|
56
|
+
sig { returns(String) }
|
|
57
|
+
def inspect
|
|
58
|
+
"#<#{self.class.name} value=#{value.inspect}>"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Returns the string representation (the const value as string).
|
|
62
|
+
sig { returns(String) }
|
|
63
|
+
def to_s
|
|
64
|
+
value.to_s
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative "json_schema/types/const"
|
|
5
|
+
require_relative "json_schema/naming_utils"
|
|
6
|
+
require_relative "json_schema/definitions/property_type"
|
|
7
|
+
require_relative "json_schema/definitions/const_type"
|
|
8
|
+
require_relative "json_schema/definitions/enum_type"
|
|
9
|
+
require_relative "json_schema/definitions/property_types"
|
|
10
|
+
require_relative "json_schema/definitions/array_property_type"
|
|
11
|
+
require_relative "json_schema/definitions/hash_property_type"
|
|
12
|
+
require_relative "json_schema/definitions/union_property_type"
|
|
13
|
+
require_relative "json_schema/definitions/struct_property"
|
|
14
|
+
require_relative "json_schema/definitions/struct"
|
|
15
|
+
require_relative "json_schema/definitions/module"
|
|
16
|
+
require_relative "json_schema/config"
|
|
17
|
+
require_relative "json_schema/class_name_path"
|
|
18
|
+
require_relative "json_schema/schema_analyzer"
|
|
19
|
+
require_relative "json_schema/code_generator"
|
|
20
|
+
require_relative "json_schema/cli"
|
|
21
|
+
|
|
22
|
+
module Skit
|
|
23
|
+
module JsonSchema
|
|
24
|
+
extend T::Sig
|
|
25
|
+
|
|
26
|
+
# Generate Sorbet T::Struct code from JSON Schema
|
|
27
|
+
#
|
|
28
|
+
# @param schema [Hash] The JSON Schema hash
|
|
29
|
+
# @param options [Hash] Configuration options
|
|
30
|
+
# @option options [String] :class_name Class name for the root struct (optional)
|
|
31
|
+
# @option options [String] :module_name Module name to wrap the generated struct (optional)
|
|
32
|
+
# @option options [String] :typed_strictness Sorbet typing level ('ignore', 'false', 'true', 'strict', 'strong')
|
|
33
|
+
#
|
|
34
|
+
# @return [String] Generated Ruby/Sorbet code
|
|
35
|
+
#
|
|
36
|
+
# @example Basic usage
|
|
37
|
+
# schema = { "type" => "object", "properties" => { "name" => { "type" => "string" } } }
|
|
38
|
+
# code = Skit::JsonSchema.generate(schema, class_name: "User")
|
|
39
|
+
# puts code
|
|
40
|
+
#
|
|
41
|
+
# @example With all options
|
|
42
|
+
# code = Skit::JsonSchema.generate(
|
|
43
|
+
# schema,
|
|
44
|
+
# class_name: "User",
|
|
45
|
+
# module_name: "MyModule",
|
|
46
|
+
# typed_strictness: "strict"
|
|
47
|
+
# )
|
|
48
|
+
sig do
|
|
49
|
+
params(
|
|
50
|
+
schema: T::Hash[String, T.untyped],
|
|
51
|
+
options: T::Hash[T.any(Symbol, String), T.untyped]
|
|
52
|
+
).returns(String)
|
|
53
|
+
end
|
|
54
|
+
def self.generate(schema, options = {})
|
|
55
|
+
# Extract and validate options (support both symbol and string keys)
|
|
56
|
+
class_name = T.cast(options[:class_name] || options["class_name"], T.nilable(String))
|
|
57
|
+
module_name = T.cast(options[:module_name] || options["module_name"], T.nilable(String))
|
|
58
|
+
typed_strictness = T.cast(options[:typed_strictness] || options["typed_strictness"],
|
|
59
|
+
T.nilable(String)) || "strict"
|
|
60
|
+
|
|
61
|
+
# Create config with smart defaults
|
|
62
|
+
config = Config.new(
|
|
63
|
+
class_name: class_name,
|
|
64
|
+
module_name: module_name,
|
|
65
|
+
typed_strictness: typed_strictness
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Analyze schema
|
|
69
|
+
analyzer = SchemaAnalyzer.new(schema, config)
|
|
70
|
+
module_definition = analyzer.analyze
|
|
71
|
+
|
|
72
|
+
# Generate code
|
|
73
|
+
generator = CodeGenerator.new(module_definition, config)
|
|
74
|
+
generator.generate
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
class Error < Skit::Error
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { returns(Path) }
|
|
10
|
+
attr_reader :path
|
|
11
|
+
|
|
12
|
+
sig { params(message: ::String, path: Path).void }
|
|
13
|
+
def initialize(message = "", path: Path.new)
|
|
14
|
+
@path = T.let(path, Path)
|
|
15
|
+
super(path.empty? ? message : "#{message} (at #{path})")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class UnknownTypeError < Error; end
|
|
20
|
+
class SerializeError < Error; end
|
|
21
|
+
class DeserializeError < Error; end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
class Path
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
Segment = T.type_alias { T.any(::String, ::Integer) }
|
|
10
|
+
|
|
11
|
+
sig { params(segments: T::Array[Segment]).void }
|
|
12
|
+
def initialize(segments = [])
|
|
13
|
+
@segments = T.let(segments.freeze, T::Array[Segment])
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { params(segment: Segment).returns(Path) }
|
|
17
|
+
def append(segment)
|
|
18
|
+
Path.new(@segments + [segment])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
sig { returns(T::Array[Segment]) }
|
|
22
|
+
attr_reader :segments
|
|
23
|
+
|
|
24
|
+
sig { returns(T::Boolean) }
|
|
25
|
+
def empty?
|
|
26
|
+
@segments.empty?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { returns(::String) }
|
|
30
|
+
def to_s
|
|
31
|
+
result = +""
|
|
32
|
+
@segments.each do |segment|
|
|
33
|
+
case segment
|
|
34
|
+
when ::Integer
|
|
35
|
+
result << "[#{segment}]"
|
|
36
|
+
when ::String
|
|
37
|
+
result << "." unless result.empty?
|
|
38
|
+
result << segment
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
result.freeze
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
sig { returns(::String) }
|
|
45
|
+
def to_json_pointer
|
|
46
|
+
return "" if @segments.empty?
|
|
47
|
+
|
|
48
|
+
"/#{@segments.map { |s| s.to_s.gsub("~", "~0").gsub("/", "~1") }.join("/")}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
52
|
+
def ==(other)
|
|
53
|
+
return false unless other.is_a?(Path)
|
|
54
|
+
|
|
55
|
+
@segments == other.segments
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
sig { returns(::Integer) }
|
|
59
|
+
def hash
|
|
60
|
+
@segments.hash
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sig { params(other: T.untyped).returns(T::Boolean) }
|
|
64
|
+
def eql?(other)
|
|
65
|
+
self == other
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Array < 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::TypedArray)
|
|
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::TypedArray)
|
|
19
|
+
raise ArgumentError, "Expected T::Types::TypedArray, got #{type_spec.class}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@element_type = T.let(type_spec.type, T.untyped)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Array[T.untyped]) }
|
|
26
|
+
def serialize(value, path: Path.new)
|
|
27
|
+
raise SerializeError.new("Expected Array, got #{value.class}", path: path) unless value.is_a?(::Array)
|
|
28
|
+
|
|
29
|
+
value.each_with_index.map do |item, index|
|
|
30
|
+
processor = @registry.processor_for(@element_type)
|
|
31
|
+
processor.serialize(item, path: path.append(index))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Array[T.untyped]) }
|
|
36
|
+
def deserialize(value, path: Path.new)
|
|
37
|
+
raise DeserializeError.new("Expected Array, got #{value.class}", path: path) unless value.is_a?(::Array)
|
|
38
|
+
|
|
39
|
+
value.each_with_index.map do |item, index|
|
|
40
|
+
processor = @registry.processor_for(@element_type)
|
|
41
|
+
processor.deserialize(item, path: path.append(index))
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
sig do
|
|
46
|
+
override.params(
|
|
47
|
+
value: T.untyped,
|
|
48
|
+
path: Path,
|
|
49
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
50
|
+
).void
|
|
51
|
+
end
|
|
52
|
+
def traverse(value, path: Path.new, &blk)
|
|
53
|
+
super
|
|
54
|
+
|
|
55
|
+
return unless value.is_a?(::Array)
|
|
56
|
+
|
|
57
|
+
value.each_with_index do |item, index|
|
|
58
|
+
processor = @registry.processor_for(@element_type)
|
|
59
|
+
processor.traverse(item, path: path.append(index), &blk)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Base
|
|
8
|
+
extend T::Sig
|
|
9
|
+
extend T::Helpers
|
|
10
|
+
|
|
11
|
+
abstract!
|
|
12
|
+
|
|
13
|
+
sig { overridable.params(type_spec: T.untyped).returns(T::Boolean) }
|
|
14
|
+
def self.handles?(type_spec)
|
|
15
|
+
raise NotImplementedError, "#{self}.handles? must be implemented"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
sig { params(type_spec: T.untyped, registry: Registry).void }
|
|
19
|
+
def initialize(type_spec, registry:)
|
|
20
|
+
@type_spec = type_spec
|
|
21
|
+
@registry = T.let(registry, Registry)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { overridable.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
25
|
+
def serialize(value, path: Path.new)
|
|
26
|
+
raise NotImplementedError, "#{self.class}#serialize must be implemented"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
sig { overridable.params(value: T.untyped, path: Path).returns(T.untyped) }
|
|
30
|
+
def deserialize(value, path: Path.new)
|
|
31
|
+
raise NotImplementedError, "#{self.class}#deserialize must be implemented"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
sig do
|
|
35
|
+
overridable.params(
|
|
36
|
+
value: T.untyped,
|
|
37
|
+
path: Path,
|
|
38
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
39
|
+
).void
|
|
40
|
+
end
|
|
41
|
+
def traverse(value, path: Path.new, &blk)
|
|
42
|
+
blk.call(@type_spec, value, path)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Boolean < 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 == T::Boolean
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Boolean) }
|
|
16
|
+
def serialize(value, path: Path.new)
|
|
17
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
18
|
+
raise SerializeError.new("Expected TrueClass or FalseClass, got #{value.class}", path: path)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
value
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Boolean) }
|
|
25
|
+
def deserialize(value, path: Path.new)
|
|
26
|
+
unless value.is_a?(TrueClass) || value.is_a?(FalseClass)
|
|
27
|
+
raise DeserializeError.new("Expected TrueClass or FalseClass, got #{value.class}", path: path)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
module Skit
|
|
7
|
+
module Serialization
|
|
8
|
+
module Processor
|
|
9
|
+
class Date < 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 == ::Date
|
|
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 Date, got #{value.class}", path: path) unless value.is_a?(::Date)
|
|
20
|
+
|
|
21
|
+
value.iso8601
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Date) }
|
|
25
|
+
def deserialize(value, path: Path.new)
|
|
26
|
+
case value
|
|
27
|
+
when ::Date
|
|
28
|
+
value
|
|
29
|
+
when ::String
|
|
30
|
+
::Date.iso8601(value)
|
|
31
|
+
else
|
|
32
|
+
raise DeserializeError.new("Expected Date or String, got #{value.class}", path: path)
|
|
33
|
+
end
|
|
34
|
+
rescue ArgumentError => e
|
|
35
|
+
raise DeserializeError.new("Failed to deserialize Date: #{e.message}", path: path)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Enum < 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::Enum)
|
|
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::Enum
|
|
21
|
+
raise ArgumentError, "Expected T::Enum subclass, got #{type_spec}"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
@enum_class = T.let(type_spec, T.class_of(T::Enum))
|
|
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?(@enum_class)
|
|
30
|
+
raise SerializeError.new("Expected #{@enum_class}, got #{value.class}",
|
|
31
|
+
path: path)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
value.serialize
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Enum) }
|
|
38
|
+
def deserialize(value, path: Path.new)
|
|
39
|
+
return value if value.is_a?(@enum_class)
|
|
40
|
+
|
|
41
|
+
begin
|
|
42
|
+
@enum_class.deserialize(value)
|
|
43
|
+
rescue KeyError
|
|
44
|
+
valid_values = @enum_class.values.map(&:serialize)
|
|
45
|
+
raise DeserializeError.new(
|
|
46
|
+
"Invalid value #{value.inspect} for #{@enum_class}. Valid values: #{valid_values.inspect}",
|
|
47
|
+
path: path
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
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 Float < 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 == ::Float
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Float) }
|
|
16
|
+
def serialize(value, path: Path.new)
|
|
17
|
+
raise SerializeError.new("Expected Float, got #{value.class}", path: path) unless value.is_a?(::Float)
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Float) }
|
|
23
|
+
def deserialize(value, path: Path.new)
|
|
24
|
+
case value
|
|
25
|
+
when ::Float
|
|
26
|
+
value
|
|
27
|
+
when ::Integer
|
|
28
|
+
value.to_f
|
|
29
|
+
else
|
|
30
|
+
raise DeserializeError.new("Expected Float or Integer, got #{value.class}", path: path)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module Skit
|
|
5
|
+
module Serialization
|
|
6
|
+
module Processor
|
|
7
|
+
class Hash < 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::TypedHash)
|
|
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::TypedHash)
|
|
19
|
+
raise ArgumentError, "Expected T::Types::TypedHash, got #{type_spec.class}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
@key_type = T.let(extract_raw_key_type(type_spec.keys), T.untyped)
|
|
23
|
+
@value_type = T.let(type_spec.values, T.untyped)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Hash[::String, T.untyped]) }
|
|
27
|
+
def serialize(value, path: Path.new)
|
|
28
|
+
raise SerializeError.new("Expected Hash, got #{value.class}", path: path) unless value.is_a?(::Hash)
|
|
29
|
+
|
|
30
|
+
result = T.let({}, T::Hash[::String, T.untyped])
|
|
31
|
+
value.each do |key, item|
|
|
32
|
+
processor = @registry.processor_for(@value_type)
|
|
33
|
+
result[key.to_s] = processor.serialize(item, path: path.append(key.to_s))
|
|
34
|
+
end
|
|
35
|
+
result
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
sig { override.params(value: T.untyped, path: Path).returns(T::Hash[T.untyped, T.untyped]) }
|
|
39
|
+
def deserialize(value, path: Path.new)
|
|
40
|
+
raise DeserializeError.new("Expected Hash, got #{value.class}", path: path) unless value.is_a?(::Hash)
|
|
41
|
+
|
|
42
|
+
result = T.let({}, T::Hash[T.untyped, T.untyped])
|
|
43
|
+
value.each do |key, item|
|
|
44
|
+
normalized_key = normalize_key(key)
|
|
45
|
+
processor = @registry.processor_for(@value_type)
|
|
46
|
+
result[normalized_key] = processor.deserialize(item, path: path.append(key.to_s))
|
|
47
|
+
end
|
|
48
|
+
result
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
sig do
|
|
52
|
+
override.params(
|
|
53
|
+
value: T.untyped,
|
|
54
|
+
path: Path,
|
|
55
|
+
blk: T.proc.params(type_spec: T.untyped, node: T.untyped, path: Path).void
|
|
56
|
+
).void
|
|
57
|
+
end
|
|
58
|
+
def traverse(value, path: Path.new, &blk)
|
|
59
|
+
super
|
|
60
|
+
|
|
61
|
+
return unless value.is_a?(::Hash)
|
|
62
|
+
|
|
63
|
+
value.each do |key, val|
|
|
64
|
+
processor = @registry.processor_for(@value_type)
|
|
65
|
+
processor.traverse(val, path: path.append(key.to_s), &blk)
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
sig { params(key_type: T.untyped).returns(T.untyped) }
|
|
72
|
+
def extract_raw_key_type(key_type)
|
|
73
|
+
if key_type.is_a?(T::Types::Simple)
|
|
74
|
+
key_type.raw_type
|
|
75
|
+
else
|
|
76
|
+
key_type
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(key: T.untyped).returns(T.untyped) }
|
|
81
|
+
def normalize_key(key)
|
|
82
|
+
if @key_type == ::String
|
|
83
|
+
key.to_s
|
|
84
|
+
elsif @key_type == ::Symbol
|
|
85
|
+
key.to_sym
|
|
86
|
+
else
|
|
87
|
+
key
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
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 Integer < 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 == ::Integer
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Integer) }
|
|
16
|
+
def serialize(value, path: Path.new)
|
|
17
|
+
raise SerializeError.new("Expected Integer, got #{value.class}", path: path) unless value.is_a?(::Integer)
|
|
18
|
+
|
|
19
|
+
value
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { override.params(value: T.untyped, path: Path).returns(::Integer) }
|
|
23
|
+
def deserialize(value, path: Path.new)
|
|
24
|
+
raise DeserializeError.new("Expected Integer, got #{value.class}", path: path) unless value.is_a?(::Integer)
|
|
25
|
+
|
|
26
|
+
value
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|