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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +469 -0
  4. data/exe/skit +31 -0
  5. data/lib/active_model/validations/skit_validator.rb +54 -0
  6. data/lib/skit/attribute.rb +63 -0
  7. data/lib/skit/json_schema/class_name_path.rb +67 -0
  8. data/lib/skit/json_schema/cli.rb +166 -0
  9. data/lib/skit/json_schema/code_generator.rb +132 -0
  10. data/lib/skit/json_schema/config.rb +67 -0
  11. data/lib/skit/json_schema/definitions/array_property_type.rb +36 -0
  12. data/lib/skit/json_schema/definitions/const_type.rb +68 -0
  13. data/lib/skit/json_schema/definitions/enum_type.rb +71 -0
  14. data/lib/skit/json_schema/definitions/hash_property_type.rb +36 -0
  15. data/lib/skit/json_schema/definitions/module.rb +54 -0
  16. data/lib/skit/json_schema/definitions/property_type.rb +39 -0
  17. data/lib/skit/json_schema/definitions/property_types.rb +13 -0
  18. data/lib/skit/json_schema/definitions/struct.rb +99 -0
  19. data/lib/skit/json_schema/definitions/struct_property.rb +75 -0
  20. data/lib/skit/json_schema/definitions/union_property_type.rb +40 -0
  21. data/lib/skit/json_schema/naming_utils.rb +25 -0
  22. data/lib/skit/json_schema/schema_analyzer.rb +407 -0
  23. data/lib/skit/json_schema/types/const.rb +69 -0
  24. data/lib/skit/json_schema.rb +77 -0
  25. data/lib/skit/serialization/errors.rb +23 -0
  26. data/lib/skit/serialization/path.rb +69 -0
  27. data/lib/skit/serialization/processor/array.rb +65 -0
  28. data/lib/skit/serialization/processor/base.rb +47 -0
  29. data/lib/skit/serialization/processor/boolean.rb +35 -0
  30. data/lib/skit/serialization/processor/date.rb +40 -0
  31. data/lib/skit/serialization/processor/enum.rb +54 -0
  32. data/lib/skit/serialization/processor/float.rb +36 -0
  33. data/lib/skit/serialization/processor/hash.rb +93 -0
  34. data/lib/skit/serialization/processor/integer.rb +31 -0
  35. data/lib/skit/serialization/processor/json_schema_const.rb +55 -0
  36. data/lib/skit/serialization/processor/nilable.rb +87 -0
  37. data/lib/skit/serialization/processor/simple_type.rb +51 -0
  38. data/lib/skit/serialization/processor/string.rb +31 -0
  39. data/lib/skit/serialization/processor/struct.rb +84 -0
  40. data/lib/skit/serialization/processor/symbol.rb +36 -0
  41. data/lib/skit/serialization/processor/time.rb +40 -0
  42. data/lib/skit/serialization/processor/union.rb +120 -0
  43. data/lib/skit/serialization/registry.rb +33 -0
  44. data/lib/skit/serialization.rb +60 -0
  45. data/lib/skit/version.rb +6 -0
  46. data/lib/skit.rb +46 -0
  47. data/lib/tapioca/dsl/compilers/skit.rb +105 -0
  48. metadata +135 -0
@@ -0,0 +1,166 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ require "json"
5
+ require "optparse"
6
+
7
+ module Skit
8
+ module JsonSchema
9
+ class CLI
10
+ extend T::Sig
11
+
12
+ sig { params(args: T::Array[String]).returns(Integer) }
13
+ def run(args)
14
+ options = parse_options(args)
15
+
16
+ # Exit normally when help or version is requested
17
+ return 0 if options[:help_requested] || options[:version_requested]
18
+
19
+ return 1 unless valid_args?(args)
20
+
21
+ schema_file = T.must(args.first)
22
+ return 1 unless valid_schema_file?(schema_file)
23
+
24
+ process_schema(schema_file, options)
25
+ end
26
+
27
+ private
28
+
29
+ sig { params(args: T::Array[String]).returns(T::Hash[Symbol, T.untyped]) }
30
+ def parse_options(args)
31
+ options = {
32
+ class_name: nil,
33
+ module_name: nil,
34
+ output_file: nil,
35
+ typed_strictness: "strict",
36
+ help_requested: T.let(false, T::Boolean),
37
+ version_requested: T.let(false, T::Boolean)
38
+ }
39
+
40
+ parser = OptionParser.new
41
+ setup_option_parser(parser, options)
42
+
43
+ begin
44
+ parser.parse!(args)
45
+ rescue OptionParser::ParseError => e
46
+ warn "Error: #{e.message}"
47
+ warn parser.help
48
+ options[:help_requested] = true
49
+ end
50
+
51
+ options
52
+ end
53
+
54
+ sig { params(args: T::Array[String]).returns(T::Boolean) }
55
+ def valid_args?(args)
56
+ return true unless args.empty?
57
+
58
+ warn "Error: Please provide a JSON Schema file"
59
+ warn "Use --help for usage information"
60
+ false
61
+ end
62
+
63
+ sig { params(schema_file: String).returns(T::Boolean) }
64
+ def valid_schema_file?(schema_file)
65
+ return true if File.exist?(schema_file)
66
+
67
+ warn "Error: File '#{schema_file}' not found"
68
+ false
69
+ end
70
+
71
+ sig { params(schema_file: String, options: T::Hash[Symbol, T.untyped]).returns(Integer) }
72
+ def process_schema(schema_file, options)
73
+ schema_content = File.read(schema_file)
74
+ schema = JSON.parse(schema_content)
75
+
76
+ config = Config.new(
77
+ class_name: T.cast(options[:class_name], T.nilable(String)),
78
+ module_name: T.cast(options[:module_name], T.nilable(String)),
79
+ typed_strictness: T.cast(options[:typed_strictness], String)
80
+ )
81
+ analyzer = SchemaAnalyzer.new(T.cast(schema, T::Hash[String, T.untyped]), config)
82
+ module_definition = analyzer.analyze
83
+
84
+ generator = CodeGenerator.new(module_definition, config)
85
+ ruby_code = generator.generate
86
+
87
+ output_result(ruby_code, options)
88
+ 0
89
+ rescue JSON::ParserError => e
90
+ warn "Error: Invalid JSON in '#{schema_file}': #{e.message}"
91
+ 1
92
+ rescue Skit::Error => e
93
+ warn "Error: #{e.message}"
94
+ 1
95
+ rescue StandardError => e
96
+ warn "Unexpected error: #{e.message}"
97
+ trace = e.backtrace&.join("\n")
98
+ warn trace if trace
99
+ 1
100
+ end
101
+
102
+ sig { params(ruby_code: String, options: T::Hash[Symbol, T.untyped]).void }
103
+ def output_result(ruby_code, options)
104
+ output_file = T.cast(options[:output_file], T.nilable(String))
105
+ if output_file
106
+ File.write(output_file, ruby_code)
107
+ warn "Generated T::Struct written to #{output_file}"
108
+ else
109
+ puts ruby_code
110
+ end
111
+ end
112
+
113
+ sig { params(parser: OptionParser, options: T::Hash[Symbol, T.untyped]).void }
114
+ def setup_option_parser(parser, options)
115
+ parser.banner = "Usage: skit generate [options] <schema_file>"
116
+ parser.separator ""
117
+ parser.separator "Generate Sorbet T::Struct definitions from JSON Schema"
118
+ parser.separator ""
119
+ parser.separator "Options:"
120
+
121
+ setup_options(parser, options)
122
+ setup_examples(parser)
123
+ end
124
+
125
+ sig { params(parser: OptionParser, options: T::Hash[Symbol, T.untyped]).void }
126
+ def setup_options(parser, options)
127
+ parser.on("-c", "--class-name NAME", "Specify the class name for the generated struct") do |name|
128
+ options[:class_name] = name
129
+ end
130
+
131
+ parser.on("-m", "--module-name NAME", "Specify the module name to wrap the generated struct") do |name|
132
+ options[:module_name] = name
133
+ end
134
+
135
+ parser.on("-o", "--output FILE", "Write output to specified file instead of stdout") do |file|
136
+ options[:output_file] = file
137
+ end
138
+
139
+ parser.on("-t", "--typed LEVEL", Config::VALID_TYPED_LEVELS, "Sorbet typed strictness level",
140
+ "(#{Config::VALID_TYPED_LEVELS.join(", ")})") do |level|
141
+ options[:typed_strictness] = level
142
+ end
143
+
144
+ parser.on("-h", "--help", "Show this help message") do
145
+ puts parser
146
+ options[:help_requested] = true
147
+ end
148
+
149
+ parser.on("-v", "--version", "Show version") do
150
+ puts "skit #{Skit::VERSION}"
151
+ options[:version_requested] = true
152
+ end
153
+ end
154
+
155
+ sig { params(opts: OptionParser).void }
156
+ def setup_examples(opts)
157
+ opts.separator ""
158
+ opts.separator "Examples:"
159
+ opts.separator " skit generate user.json"
160
+ opts.separator " skit generate -c User -o user.rb user_schema.json"
161
+ opts.separator " skit generate -m MyModule -c User user_schema.json"
162
+ opts.separator " skit generate -m Foo::Bar -c Baz user_schema.json"
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,132 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ class CodeGenerator
7
+ extend T::Sig
8
+
9
+ sig { params(module_definition: Definitions::Module, config: Config).void }
10
+ def initialize(module_definition, config)
11
+ @module_definition = module_definition
12
+ @config = config
13
+ end
14
+
15
+ sig { returns(String) }
16
+ def generate
17
+ parts = []
18
+
19
+ # Header
20
+ parts << "# typed: #{@config.typed_strictness}"
21
+ parts << "# frozen_string_literal: true"
22
+ parts << ""
23
+ parts << "# This file is autogenerated by skit"
24
+ parts << "# DO NOT EDIT MANUALLY"
25
+ parts << ""
26
+ parts << 'require "sorbet-runtime"'
27
+ parts << 'require "skit"' if const_types?
28
+
29
+ module_parts = @config.module_name&.split("::")
30
+ indent_level = module_parts&.length || 0
31
+
32
+ if module_parts
33
+ parts << ""
34
+ module_parts.each_with_index do |module_part, index|
35
+ parts << "#{" " * index}module #{module_part}"
36
+ end
37
+ end
38
+
39
+ # Enum types
40
+ @module_definition.enum_types.each do |enum_type|
41
+ parts << ""
42
+ parts << generate_enum_class(enum_type, indent_level)
43
+ end
44
+
45
+ # Const types
46
+ @module_definition.const_types.each do |const_type|
47
+ parts << ""
48
+ parts << generate_const_class(const_type, indent_level)
49
+ end
50
+
51
+ # Nested structs
52
+ @module_definition.nested_structs.each do |nested_struct|
53
+ parts << ""
54
+ parts << generate_single_struct(nested_struct, indent_level)
55
+ end
56
+
57
+ # Main struct
58
+ parts << ""
59
+ parts << generate_single_struct(@module_definition.root_struct, indent_level)
60
+
61
+ module_parts&.reverse&.each_with_index do |_, index|
62
+ parts << "#{" " * (T.must(module_parts).length - index - 1)}end"
63
+ end
64
+
65
+ parts.join("\n")
66
+ end
67
+
68
+ private
69
+
70
+ sig { returns(T::Boolean) }
71
+ def const_types?
72
+ !@module_definition.const_types.empty?
73
+ end
74
+
75
+ sig { params(enum_type: Definitions::EnumType, indent_level: Integer).returns(String) }
76
+ def generate_enum_class(enum_type, indent_level = 0)
77
+ lines = []
78
+ base_indent = " " * indent_level
79
+
80
+ lines << "#{base_indent}class #{enum_type.class_name} < T::Enum"
81
+ lines << "#{base_indent} enums do"
82
+
83
+ enum_type.values.each do |value| # rubocop:disable Style/HashEachMethods
84
+ member_name = Definitions::EnumType.value_to_member_name(value)
85
+ value_literal = Definitions::EnumType.value_literal(value)
86
+ lines << "#{base_indent} #{member_name} = new(#{value_literal})"
87
+ end
88
+
89
+ lines << "#{base_indent} end"
90
+ lines << "#{base_indent}end"
91
+
92
+ lines.join("\n")
93
+ end
94
+
95
+ sig { params(const_type: Definitions::ConstType, indent_level: Integer).returns(String) }
96
+ def generate_const_class(const_type, indent_level = 0)
97
+ lines = []
98
+ base_indent = " " * indent_level
99
+
100
+ lines << "#{base_indent}class #{const_type.class_name} < Skit::JsonSchema::Types::Const"
101
+ lines << "#{base_indent} VALUE = #{const_type.value_literal}"
102
+ lines << "#{base_indent}end"
103
+
104
+ lines.join("\n")
105
+ end
106
+
107
+ sig { params(struct_def: Definitions::Struct, indent_level: Integer).returns(String) }
108
+ def generate_single_struct(struct_def, indent_level = 0)
109
+ lines = []
110
+ base_indent = " " * indent_level
111
+
112
+ # Description comment
113
+ lines << "#{base_indent}# #{struct_def.description}" if struct_def.description
114
+
115
+ # Class definition
116
+ lines << "#{base_indent}class #{struct_def.class_name} < T::Struct"
117
+
118
+ # Properties
119
+ struct_def.properties.each do |property|
120
+ property.comment&.split("\n")&.each do |comment_line|
121
+ lines << "#{base_indent} # #{comment_line}"
122
+ end
123
+ lines << "#{base_indent} #{property.mutability} :#{property.name}, #{property.type.to_sorbet_type}"
124
+ end
125
+
126
+ lines << "#{base_indent}end"
127
+
128
+ lines.join("\n")
129
+ end
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,67 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ class Config
7
+ extend T::Sig
8
+
9
+ VALID_TYPED_LEVELS = T.let(%w[ignore false true strict strong].freeze, T::Array[String])
10
+
11
+ sig { returns(T.nilable(String)) }
12
+ attr_reader :class_name
13
+
14
+ sig { returns(T.nilable(String)) }
15
+ attr_reader :module_name
16
+
17
+ sig { returns(String) }
18
+ attr_reader :typed_strictness
19
+
20
+ sig { params(class_name: T.nilable(String), module_name: T.nilable(String), typed_strictness: String).void }
21
+ def initialize(class_name: nil, module_name: nil, typed_strictness: "strict")
22
+ @class_name = T.let(validate_class_name(class_name), T.nilable(String))
23
+ @module_name = T.let(validate_module_name(module_name), T.nilable(String))
24
+ @typed_strictness = T.let(validate_typed_strictness(typed_strictness), String)
25
+ end
26
+
27
+ private
28
+
29
+ sig { params(class_name: T.nilable(String)).returns(T.nilable(String)) }
30
+ def validate_class_name(class_name)
31
+ return nil if class_name.nil?
32
+
33
+ unless class_name.match?(/\A[A-Z][a-zA-Z0-9_]*\z/)
34
+ raise ArgumentError,
35
+ "Invalid class name: #{class_name.inspect}. Must start with uppercase letter " \
36
+ "and contain only alphanumeric characters and underscores."
37
+ end
38
+
39
+ class_name
40
+ end
41
+
42
+ sig { params(module_name: T.nilable(String)).returns(T.nilable(String)) }
43
+ def validate_module_name(module_name)
44
+ return nil if module_name.nil?
45
+
46
+ # Support Foo::Bar::Baz format
47
+ unless module_name.match?(/\A[A-Z][a-zA-Z0-9_]*(?:::[A-Z][a-zA-Z0-9_]*)*\z/)
48
+ raise ArgumentError,
49
+ "Invalid module name: #{module_name.inspect}. Must be valid Ruby module name " \
50
+ "(e.g., 'MyModule' or 'Foo::Bar::Baz')."
51
+ end
52
+
53
+ module_name
54
+ end
55
+
56
+ sig { params(level: String).returns(String) }
57
+ def validate_typed_strictness(level)
58
+ unless VALID_TYPED_LEVELS.include?(level)
59
+ raise ArgumentError,
60
+ "Invalid typed strictness level: #{level}. Valid options are: #{VALID_TYPED_LEVELS.join(", ")}"
61
+ end
62
+
63
+ level
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ class ArrayPropertyType
8
+ extend T::Sig
9
+
10
+ sig { returns(PropertyTypes) }
11
+ attr_reader :item_type
12
+
13
+ sig { returns(T::Boolean) }
14
+ attr_reader :nullable
15
+
16
+ sig { params(item_type: PropertyTypes, nullable: T::Boolean).void }
17
+ def initialize(item_type:, nullable: false)
18
+ @item_type = item_type
19
+ @nullable = nullable
20
+ end
21
+
22
+ sig { returns(String) }
23
+ def to_sorbet_type
24
+ item_type_str = @item_type.to_sorbet_type
25
+ array_type = "T::Array[#{item_type_str}]"
26
+ nullable ? "T.nilable(#{array_type})" : array_type
27
+ end
28
+
29
+ sig { returns(ArrayPropertyType) }
30
+ def with_nullable
31
+ ArrayPropertyType.new(item_type: @item_type, nullable: true)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,68 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ # Represents a JSON Schema const value type.
8
+ #
9
+ # This generates a Skit::JsonSchema::Types::Const subclass
10
+ # that matches a specific constant value.
11
+ class ConstType
12
+ extend T::Sig
13
+
14
+ sig { returns(String) }
15
+ attr_reader :class_name
16
+
17
+ sig { returns(T.untyped) }
18
+ attr_reader :value
19
+
20
+ sig { returns(T::Boolean) }
21
+ attr_reader :nullable
22
+
23
+ sig do
24
+ params(
25
+ class_name: String,
26
+ value: T.untyped,
27
+ nullable: T::Boolean
28
+ ).void
29
+ end
30
+ def initialize(class_name:, value:, nullable: false)
31
+ @class_name = class_name
32
+ @value = value
33
+ @nullable = nullable
34
+ end
35
+
36
+ sig { returns(String) }
37
+ def to_sorbet_type
38
+ nullable ? "T.nilable(#{@class_name})" : @class_name
39
+ end
40
+
41
+ sig { returns(ConstType) }
42
+ def with_nullable
43
+ ConstType.new(class_name: @class_name, value: @value, nullable: true)
44
+ end
45
+
46
+ # Returns the Ruby literal representation of the value
47
+ # rubocop:disable Lint/DuplicateBranch -- Sorbet requires exhaustive case for proper return type
48
+ sig { returns(String) }
49
+ def value_literal
50
+ case @value
51
+ when String
52
+ @value.inspect
53
+ when Integer, Float
54
+ @value.to_s
55
+ when TrueClass
56
+ "true"
57
+ when FalseClass
58
+ "false"
59
+ else
60
+ # This branch should never be reached due to validation in SchemaAnalyzer
61
+ @value.inspect
62
+ end
63
+ end
64
+ # rubocop:enable Lint/DuplicateBranch
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,71 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ # Represents a T::Enum type for code generation
8
+ class EnumType
9
+ extend T::Sig
10
+
11
+ sig { returns(String) }
12
+ attr_reader :class_name
13
+
14
+ sig { returns(T::Array[T.any(String, Integer, Float)]) }
15
+ attr_reader :values
16
+
17
+ sig { returns(T::Boolean) }
18
+ attr_reader :nullable
19
+
20
+ sig do
21
+ params(
22
+ class_name: String,
23
+ values: T::Array[T.any(String, Integer, Float)],
24
+ nullable: T::Boolean
25
+ ).void
26
+ end
27
+ def initialize(class_name:, values:, nullable: false)
28
+ @class_name = class_name
29
+ @values = values
30
+ @nullable = nullable
31
+ end
32
+
33
+ sig { returns(String) }
34
+ def to_sorbet_type
35
+ nullable ? "T.nilable(#{@class_name})" : @class_name
36
+ end
37
+
38
+ sig { returns(EnumType) }
39
+ def with_nullable
40
+ EnumType.new(class_name: @class_name, values: @values, nullable: true)
41
+ end
42
+
43
+ # Generate enum member name from value
44
+ sig { params(value: T.any(String, Integer, Float)).returns(String) }
45
+ def self.value_to_member_name(value)
46
+ case value
47
+ when String
48
+ result = NamingUtils.to_pascal_case(value)
49
+ return "Empty" if result.empty?
50
+
51
+ result = "Val#{result}" if result.match?(/\A\d/)
52
+ result
53
+ when Integer, Float
54
+ NamingUtils.number_to_name(value)
55
+ end
56
+ end
57
+
58
+ # Generate value literal for code generation
59
+ sig { params(value: T.any(String, Integer, Float)).returns(String) }
60
+ def self.value_literal(value)
61
+ case value
62
+ when String
63
+ value.inspect
64
+ when Integer, Float
65
+ value.to_s
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,36 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ class HashPropertyType
8
+ extend T::Sig
9
+
10
+ sig { returns(PropertyTypes) }
11
+ attr_reader :value_type
12
+
13
+ sig { returns(T::Boolean) }
14
+ attr_reader :nullable
15
+
16
+ sig { params(value_type: PropertyTypes, nullable: T::Boolean).void }
17
+ def initialize(value_type:, nullable: false)
18
+ @value_type = value_type
19
+ @nullable = nullable
20
+ end
21
+
22
+ sig { returns(String) }
23
+ def to_sorbet_type
24
+ value_type_str = @value_type.to_sorbet_type
25
+ hash_type = "T::Hash[String, #{value_type_str}]"
26
+ nullable ? "T.nilable(#{hash_type})" : hash_type
27
+ end
28
+
29
+ sig { returns(HashPropertyType) }
30
+ def with_nullable
31
+ HashPropertyType.new(value_type: @value_type, nullable: true)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,54 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ class Module
8
+ extend T::Sig
9
+
10
+ sig { returns(Struct) }
11
+ attr_reader :root_struct
12
+
13
+ sig { returns(T::Array[Struct]) }
14
+ attr_reader :nested_structs
15
+
16
+ sig { returns(T::Array[ConstType]) }
17
+ attr_reader :const_types
18
+
19
+ sig { returns(T::Array[EnumType]) }
20
+ attr_reader :enum_types
21
+
22
+ sig do
23
+ params(
24
+ root_struct: Struct,
25
+ nested_structs: T::Array[Struct],
26
+ const_types: T::Array[ConstType],
27
+ enum_types: T::Array[EnumType]
28
+ ).void
29
+ end
30
+ def initialize(root_struct:, nested_structs: [], const_types: [], enum_types: [])
31
+ @root_struct = root_struct
32
+ @nested_structs = nested_structs
33
+ @const_types = const_types
34
+ @enum_types = enum_types
35
+ end
36
+
37
+ sig { returns(T::Boolean) }
38
+ def const_types?
39
+ !@const_types.empty?
40
+ end
41
+
42
+ sig { returns(T::Boolean) }
43
+ def enum_types?
44
+ !@enum_types.empty?
45
+ end
46
+
47
+ sig { returns(T::Boolean) }
48
+ def nested_structs?
49
+ !@nested_structs.empty?
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,39 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ class PropertyType
8
+ extend T::Sig
9
+
10
+ sig { returns(String) }
11
+ attr_reader :base_type
12
+
13
+ sig { returns(T::Boolean) }
14
+ attr_reader :nullable
15
+
16
+ sig do
17
+ params(
18
+ base_type: String,
19
+ nullable: T::Boolean
20
+ ).void
21
+ end
22
+ def initialize(base_type:, nullable: false)
23
+ @base_type = base_type
24
+ @nullable = nullable
25
+ end
26
+
27
+ sig { returns(String) }
28
+ def to_sorbet_type
29
+ nullable ? "T.nilable(#{@base_type})" : @base_type
30
+ end
31
+
32
+ sig { returns(PropertyType) }
33
+ def with_nullable
34
+ PropertyType.new(base_type: @base_type, nullable: true)
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,13 @@
1
+ # typed: strict
2
+ # frozen_string_literal: true
3
+
4
+ module Skit
5
+ module JsonSchema
6
+ module Definitions
7
+ # Type alias: Union type of PropertyType-related classes
8
+ PropertyTypes = T.type_alias do
9
+ T.any(PropertyType, ArrayPropertyType, HashPropertyType, UnionPropertyType, ConstType, EnumType)
10
+ end
11
+ end
12
+ end
13
+ end