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,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
|