sorbet-baml 0.0.1 → 0.2.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 +4 -4
- data/.idea/.gitignore +8 -0
- data/.idea/inspectionProfiles/Project_Default.xml +5 -0
- data/CLAUDE.md +94 -0
- data/README.md +491 -65
- data/examples/description_parameters.rb +49 -0
- data/lib/sorbet_baml/comment_extractor.rb +170 -0
- data/lib/sorbet_baml/converter.rb +153 -3
- data/lib/sorbet_baml/dependency_resolver.rb +99 -0
- data/lib/sorbet_baml/description_extension.rb +34 -0
- data/lib/sorbet_baml/description_extractor.rb +36 -0
- data/lib/sorbet_baml/enum_extensions.rb +23 -0
- data/lib/sorbet_baml/struct_extensions.rb +23 -0
- data/lib/sorbet_baml/type_mapper.rb +49 -12
- data/lib/sorbet_baml/version.rb +1 -1
- data/lib/sorbet_baml.rb +10 -0
- metadata +11 -6
- data/docs/README.md +0 -67
- data/docs/advanced-usage.md +0 -85
- data/docs/getting-started.md +0 -54
- data/docs/troubleshooting.md +0 -81
- data/docs/type-mapping.md +0 -65
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
module SorbetBaml
|
|
5
|
+
# Extracts documentation comments from Sorbet struct and enum source files
|
|
6
|
+
class CommentExtractor
|
|
7
|
+
extend T::Sig
|
|
8
|
+
|
|
9
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T::Hash[String, T.nilable(String)]) }
|
|
10
|
+
def self.extract_field_comments(klass)
|
|
11
|
+
# First try to get descriptions from the description extractor (extra field)
|
|
12
|
+
descriptions = DescriptionExtractor.extract_prop_descriptions(klass)
|
|
13
|
+
|
|
14
|
+
# Then fall back to comment-based extraction for any missing descriptions
|
|
15
|
+
comments = {}
|
|
16
|
+
source_file = find_source_file(klass)
|
|
17
|
+
|
|
18
|
+
if source_file && File.exist?(source_file)
|
|
19
|
+
lines = File.readlines(source_file)
|
|
20
|
+
extract_comments_from_lines(lines, T.must(T.must(klass.name).split('::').last), comments)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Merge with priority: description parameters > comments
|
|
24
|
+
descriptions.merge(comments) { |key, desc_param, comment| desc_param }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
sig { params(klass: T.class_of(T::Enum)).returns(T::Hash[String, T.nilable(String)]) }
|
|
28
|
+
def self.extract_enum_comments(klass)
|
|
29
|
+
comments = {}
|
|
30
|
+
source_file = find_source_file(klass)
|
|
31
|
+
|
|
32
|
+
return comments unless source_file && File.exist?(source_file)
|
|
33
|
+
|
|
34
|
+
lines = File.readlines(source_file)
|
|
35
|
+
extract_enum_comments_from_lines(lines, T.must(T.must(klass.name).split('::').last), comments)
|
|
36
|
+
|
|
37
|
+
comments
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
sig { params(klass: T::Class[T.anything]).returns(T.nilable(String)) }
|
|
43
|
+
def self.find_source_file(klass)
|
|
44
|
+
# Try to find where the class was defined
|
|
45
|
+
# This is a heuristic approach since Ruby doesn't provide reliable source location for classes
|
|
46
|
+
|
|
47
|
+
# Method 1: Check if any methods have source location
|
|
48
|
+
begin
|
|
49
|
+
if klass.respond_to?(:new) && klass.method(:new).respond_to?(:source_location)
|
|
50
|
+
location = klass.method(:new).source_location
|
|
51
|
+
return location[0] if location
|
|
52
|
+
end
|
|
53
|
+
rescue
|
|
54
|
+
# Ignore errors
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Method 2: Look at the current call stack for files that might contain the class
|
|
58
|
+
caller_locations.each do |location|
|
|
59
|
+
file_path = location.absolute_path || location.path
|
|
60
|
+
next unless file_path && File.exist?(file_path)
|
|
61
|
+
|
|
62
|
+
# Read the file and check if it contains the class definition
|
|
63
|
+
begin
|
|
64
|
+
content = File.read(file_path)
|
|
65
|
+
class_name = T.must(klass.name).split('::').last
|
|
66
|
+
if content.match(/class\s+#{Regexp.escape(T.must(class_name))}\s*</)
|
|
67
|
+
return file_path
|
|
68
|
+
end
|
|
69
|
+
rescue
|
|
70
|
+
# Ignore file read errors
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
78
|
+
def self.extract_comments_from_lines(lines, class_name, comments)
|
|
79
|
+
in_target_class = T.let(false, T::Boolean)
|
|
80
|
+
current_comment = T.let(nil, T.nilable(String))
|
|
81
|
+
brace_depth = 0
|
|
82
|
+
|
|
83
|
+
lines.each do |line|
|
|
84
|
+
stripped = line.strip
|
|
85
|
+
|
|
86
|
+
# Check if we're entering the target class
|
|
87
|
+
if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Struct/)
|
|
88
|
+
in_target_class = true
|
|
89
|
+
brace_depth = 0
|
|
90
|
+
next
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
next unless in_target_class
|
|
94
|
+
|
|
95
|
+
# Track brace depth to handle nested classes
|
|
96
|
+
brace_depth += stripped.count('{')
|
|
97
|
+
brace_depth -= stripped.count('}')
|
|
98
|
+
|
|
99
|
+
# Exit when we reach the end of the class
|
|
100
|
+
if stripped == 'end' && brace_depth == 0
|
|
101
|
+
break
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Extract comment
|
|
105
|
+
if stripped.start_with?('#')
|
|
106
|
+
comment_text = T.must(stripped[1..-1]).strip
|
|
107
|
+
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
108
|
+
elsif stripped.match(/^const\s+:(\w+)/) && current_comment
|
|
109
|
+
field_name = T.must(stripped.match(/^const\s+:(\w+)/))[1]
|
|
110
|
+
comments[T.must(field_name)] = current_comment
|
|
111
|
+
current_comment = nil
|
|
112
|
+
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
113
|
+
# Reset comment if we hit non-comment, non-const line
|
|
114
|
+
current_comment = nil
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
120
|
+
def self.extract_enum_comments_from_lines(lines, class_name, comments)
|
|
121
|
+
in_target_class = T.let(false, T::Boolean)
|
|
122
|
+
in_enums_block = T.let(false, T::Boolean)
|
|
123
|
+
current_comment = T.let(nil, T.nilable(String))
|
|
124
|
+
|
|
125
|
+
lines.each do |line|
|
|
126
|
+
stripped = line.strip
|
|
127
|
+
|
|
128
|
+
# Check if we're entering the target enum class
|
|
129
|
+
if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Enum/)
|
|
130
|
+
in_target_class = true
|
|
131
|
+
next
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
next unless in_target_class
|
|
135
|
+
|
|
136
|
+
# Check if we're in the enums block
|
|
137
|
+
if stripped == 'enums do'
|
|
138
|
+
in_enums_block = true
|
|
139
|
+
next
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Exit enums block
|
|
143
|
+
if in_enums_block && stripped == 'end'
|
|
144
|
+
in_enums_block = false
|
|
145
|
+
next
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Exit class
|
|
149
|
+
if stripped == 'end' && !in_enums_block
|
|
150
|
+
break
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
next unless in_enums_block
|
|
154
|
+
|
|
155
|
+
# Extract comment
|
|
156
|
+
if stripped.start_with?('#')
|
|
157
|
+
comment_text = T.must(stripped[1..-1]).strip
|
|
158
|
+
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
159
|
+
elsif stripped.match(/^(\w+)\s*=\s*new/) && current_comment
|
|
160
|
+
enum_name = T.must(stripped.match(/^(\w+)\s*=\s*new/))[1]
|
|
161
|
+
comments[T.must(enum_name)] = current_comment
|
|
162
|
+
current_comment = nil
|
|
163
|
+
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
164
|
+
# Reset comment if we hit non-comment, non-enum line
|
|
165
|
+
current_comment = nil
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
4
|
require "sorbet-runtime"
|
|
5
|
+
require "set"
|
|
6
|
+
require_relative "comment_extractor"
|
|
5
7
|
|
|
6
8
|
module SorbetBaml
|
|
7
9
|
# Main converter class for transforming Sorbet types to BAML
|
|
@@ -16,31 +18,179 @@ module SorbetBaml
|
|
|
16
18
|
sig { params(klasses: T::Array[T.class_of(T::Struct)], options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
17
19
|
def self.from_structs(klasses, options = {})
|
|
18
20
|
converter = new(options)
|
|
19
|
-
|
|
21
|
+
|
|
22
|
+
if converter.instance_variable_get(:@include_dependencies)
|
|
23
|
+
# When dependencies are enabled, collect all unique dependencies and convert once
|
|
24
|
+
all_dependencies = Set.new
|
|
25
|
+
enum_dependencies = Set.new
|
|
26
|
+
|
|
27
|
+
klasses.each do |klass|
|
|
28
|
+
deps = DependencyResolver.resolve_dependencies(klass)
|
|
29
|
+
all_dependencies.merge(deps)
|
|
30
|
+
enum_deps = converter.send(:find_enum_dependencies, deps)
|
|
31
|
+
enum_dependencies.merge(enum_deps)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Convert all unique types
|
|
35
|
+
converted_types = []
|
|
36
|
+
enum_dependencies.each { |enum_klass| converted_types << converter.convert_enum(enum_klass) }
|
|
37
|
+
all_dependencies.each { |struct_klass| converted_types << converter.send(:convert_single_struct, struct_klass) }
|
|
38
|
+
|
|
39
|
+
converted_types.join("\n\n")
|
|
40
|
+
else
|
|
41
|
+
# When dependencies are disabled, convert each struct individually
|
|
42
|
+
klasses.map { |klass| converter.send(:convert_single_struct, klass) }.join("\n\n")
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
sig { params(klass: T.class_of(T::Enum), options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
47
|
+
def self.from_enum(klass, options = {})
|
|
48
|
+
new(options).convert_enum(klass)
|
|
20
49
|
end
|
|
21
50
|
|
|
22
51
|
sig { params(options: T::Hash[Symbol, T.untyped]).void }
|
|
23
52
|
def initialize(options = {})
|
|
24
53
|
@options = options
|
|
25
54
|
@indent_size = T.let(options.fetch(:indent_size, 2), Integer)
|
|
26
|
-
@include_descriptions = T.let(options.fetch(:include_descriptions,
|
|
55
|
+
@include_descriptions = T.let(options.fetch(:include_descriptions, true), T::Boolean)
|
|
56
|
+
@include_dependencies = T.let(options.fetch(:include_dependencies, true), T::Boolean)
|
|
27
57
|
end
|
|
28
58
|
|
|
29
59
|
sig { params(klass: T.class_of(T::Struct)).returns(String) }
|
|
30
60
|
def convert_struct(klass)
|
|
61
|
+
if @include_dependencies
|
|
62
|
+
# Get all dependencies in correct order and convert them all
|
|
63
|
+
dependencies = DependencyResolver.resolve_dependencies(klass)
|
|
64
|
+
|
|
65
|
+
# Also find all enum dependencies
|
|
66
|
+
enum_dependencies = find_enum_dependencies(dependencies)
|
|
67
|
+
|
|
68
|
+
# Convert enums first, then structs
|
|
69
|
+
converted_types = []
|
|
70
|
+
enum_dependencies.each { |enum_klass| converted_types << convert_enum(enum_klass) }
|
|
71
|
+
dependencies.each { |dep_klass| converted_types << convert_single_struct(dep_klass) }
|
|
72
|
+
|
|
73
|
+
converted_types.join("\n\n")
|
|
74
|
+
else
|
|
75
|
+
# Just convert the single struct
|
|
76
|
+
convert_single_struct(klass)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
sig { params(klass: T.class_of(T::Enum)).returns(String) }
|
|
81
|
+
def convert_enum(klass)
|
|
82
|
+
class_name = klass.name || klass.to_s
|
|
83
|
+
simple_name = class_name.split('::').last
|
|
84
|
+
lines = ["enum #{simple_name} {"]
|
|
85
|
+
|
|
86
|
+
# Extract comments if requested
|
|
87
|
+
comments = @include_descriptions ? CommentExtractor.extract_enum_comments(klass) : {}
|
|
88
|
+
|
|
89
|
+
# Get all enum values by calling values method
|
|
90
|
+
enum_values = klass.values
|
|
91
|
+
enum_values.each do |enum_instance|
|
|
92
|
+
value = enum_instance.serialize
|
|
93
|
+
|
|
94
|
+
# Find the constant name for this value
|
|
95
|
+
constant_name = T.let(nil, T.nilable(String))
|
|
96
|
+
klass.constants.each do |const_name|
|
|
97
|
+
const_value = klass.const_get(const_name)
|
|
98
|
+
if const_value.is_a?(klass) && const_value.serialize == value
|
|
99
|
+
constant_name = const_name.to_s
|
|
100
|
+
break
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
line = "#{' ' * @indent_size}\"#{value}\""
|
|
105
|
+
|
|
106
|
+
# Add description if available (BAML uses @description annotations, not comments)
|
|
107
|
+
if @include_descriptions && constant_name && comments[constant_name]
|
|
108
|
+
line += " @description(\"#{T.must(comments[constant_name]).gsub('"', '\\"')}\")"
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
lines << line
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
lines << "}"
|
|
115
|
+
lines.join("\n")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
sig { params(klass: T.class_of(T::Struct)).returns(String) }
|
|
121
|
+
def convert_single_struct(klass)
|
|
31
122
|
props = klass.props
|
|
32
123
|
|
|
33
124
|
class_name = klass.name || klass.to_s
|
|
34
125
|
simple_name = class_name.split('::').last
|
|
35
126
|
lines = ["class #{simple_name} {"]
|
|
36
127
|
|
|
128
|
+
# Extract comments if requested
|
|
129
|
+
comments = @include_descriptions ? CommentExtractor.extract_field_comments(klass) : {}
|
|
130
|
+
|
|
37
131
|
props.each do |name, prop_info|
|
|
38
132
|
baml_type = TypeMapper.map_type(prop_info[:type_object])
|
|
39
|
-
|
|
133
|
+
line = "#{' ' * @indent_size}#{name} #{baml_type}"
|
|
134
|
+
|
|
135
|
+
# Add description if available (BAML uses @description annotations)
|
|
136
|
+
if @include_descriptions && comments[name.to_s]
|
|
137
|
+
escaped_comment = T.must(comments[name.to_s]).gsub('"', '\\"')
|
|
138
|
+
line += " @description(\"#{escaped_comment}\")"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
lines << line
|
|
40
142
|
end
|
|
41
143
|
|
|
42
144
|
lines << "}"
|
|
43
145
|
lines.join("\n")
|
|
44
146
|
end
|
|
147
|
+
|
|
148
|
+
sig { params(struct_classes: T::Array[T.class_of(T::Struct)]).returns(T::Array[T.class_of(T::Enum)]) }
|
|
149
|
+
def find_enum_dependencies(struct_classes)
|
|
150
|
+
enum_deps = Set.new
|
|
151
|
+
|
|
152
|
+
struct_classes.each do |struct_klass|
|
|
153
|
+
struct_klass.props.each do |_name, prop_info|
|
|
154
|
+
type_object = prop_info[:type_object]
|
|
155
|
+
enum_deps.merge(extract_enum_types(type_object))
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
enum_deps.to_a
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
sig { params(type_object: T.untyped).returns(T::Array[T.class_of(T::Enum)]) }
|
|
163
|
+
def extract_enum_types(type_object)
|
|
164
|
+
return [] if type_object.nil?
|
|
165
|
+
|
|
166
|
+
case type_object
|
|
167
|
+
when T::Types::Simple
|
|
168
|
+
extract_enum_from_simple_type(type_object.raw_type)
|
|
169
|
+
when T::Types::TypedArray
|
|
170
|
+
extract_enum_types(type_object.type)
|
|
171
|
+
when T::Types::TypedHash
|
|
172
|
+
# Check both key and value types
|
|
173
|
+
key_types = extract_enum_types(type_object.keys)
|
|
174
|
+
value_types = extract_enum_types(type_object.values)
|
|
175
|
+
key_types + value_types
|
|
176
|
+
else
|
|
177
|
+
# Check if it's a union type
|
|
178
|
+
if type_object.respond_to?(:types)
|
|
179
|
+
type_object.types.flat_map { |t| extract_enum_types(t) }
|
|
180
|
+
else
|
|
181
|
+
[]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
sig { params(raw_type: T.untyped).returns(T::Array[T.class_of(T::Enum)]) }
|
|
187
|
+
def extract_enum_from_simple_type(raw_type)
|
|
188
|
+
# Check if this raw_type is a T::Enum subclass
|
|
189
|
+
if raw_type.is_a?(Class) && raw_type < T::Enum
|
|
190
|
+
[raw_type]
|
|
191
|
+
else
|
|
192
|
+
[]
|
|
193
|
+
end
|
|
194
|
+
end
|
|
45
195
|
end
|
|
46
196
|
end
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module SorbetBaml
|
|
7
|
+
# Resolves dependencies between T::Struct types and orders them topologically
|
|
8
|
+
class DependencyResolver
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T::Array[T.class_of(T::Struct)]) }
|
|
12
|
+
def self.resolve_dependencies(klass)
|
|
13
|
+
new.resolve_dependencies(klass)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
sig { void }
|
|
17
|
+
def initialize
|
|
18
|
+
@visited = T.let(Set.new, T::Set[T.class_of(T::Struct)])
|
|
19
|
+
@dependencies = T.let([], T::Array[T.class_of(T::Struct)])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T::Array[T.class_of(T::Struct)]) }
|
|
23
|
+
def resolve_dependencies(klass)
|
|
24
|
+
@visited.clear
|
|
25
|
+
@dependencies.clear
|
|
26
|
+
|
|
27
|
+
# Perform depth-first search to find all dependencies
|
|
28
|
+
visit(klass)
|
|
29
|
+
|
|
30
|
+
# Dependencies are already in correct topological order
|
|
31
|
+
# (dependencies first, then the types that depend on them)
|
|
32
|
+
@dependencies
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
sig { params(klass: T.class_of(T::Struct)).void }
|
|
38
|
+
def visit(klass)
|
|
39
|
+
return if @visited.include?(klass)
|
|
40
|
+
|
|
41
|
+
@visited.add(klass)
|
|
42
|
+
|
|
43
|
+
# Find all T::Struct dependencies in this class
|
|
44
|
+
struct_dependencies = find_struct_dependencies(klass)
|
|
45
|
+
|
|
46
|
+
# Visit dependencies first (depth-first)
|
|
47
|
+
struct_dependencies.each { |dep| visit(dep) }
|
|
48
|
+
|
|
49
|
+
# Add this class after its dependencies
|
|
50
|
+
@dependencies << klass
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
sig { params(klass: T.class_of(T::Struct)).returns(T::Array[T.class_of(T::Struct)]) }
|
|
54
|
+
def find_struct_dependencies(klass)
|
|
55
|
+
dependencies = []
|
|
56
|
+
|
|
57
|
+
klass.props.each do |_name, prop_info|
|
|
58
|
+
type_object = prop_info[:type_object]
|
|
59
|
+
dependencies.concat(extract_struct_types(type_object))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
dependencies.uniq
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
sig { params(type_object: T.untyped).returns(T::Array[T.class_of(T::Struct)]) }
|
|
66
|
+
def extract_struct_types(type_object)
|
|
67
|
+
return [] if type_object.nil?
|
|
68
|
+
|
|
69
|
+
case type_object
|
|
70
|
+
when T::Types::Simple
|
|
71
|
+
extract_from_simple_type(type_object.raw_type)
|
|
72
|
+
when T::Types::TypedArray
|
|
73
|
+
extract_struct_types(type_object.type)
|
|
74
|
+
when T::Types::TypedHash
|
|
75
|
+
# Check both key and value types
|
|
76
|
+
key_types = extract_struct_types(type_object.keys)
|
|
77
|
+
value_types = extract_struct_types(type_object.values)
|
|
78
|
+
key_types + value_types
|
|
79
|
+
else
|
|
80
|
+
# Check if it's a union type
|
|
81
|
+
if type_object.respond_to?(:types)
|
|
82
|
+
type_object.types.flat_map { |t| extract_struct_types(t) }
|
|
83
|
+
else
|
|
84
|
+
[]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
sig { params(raw_type: T.untyped).returns(T::Array[T.class_of(T::Struct)]) }
|
|
90
|
+
def extract_from_simple_type(raw_type)
|
|
91
|
+
# Check if this raw_type is a T::Struct subclass
|
|
92
|
+
if raw_type.is_a?(Class) && raw_type < T::Struct
|
|
93
|
+
[raw_type]
|
|
94
|
+
else
|
|
95
|
+
[]
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module SorbetBaml
|
|
7
|
+
# Extension module to add description: parameter support to T::Struct
|
|
8
|
+
module DescriptionExtension
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# Override const to support description parameter
|
|
12
|
+
sig { params(name: Symbol, type: T.untyped, description: T.nilable(String), kwargs: T.untyped).void }
|
|
13
|
+
def const(name, type, description: nil, **kwargs)
|
|
14
|
+
if description
|
|
15
|
+
super(name, type, extra: { description: description }, **kwargs)
|
|
16
|
+
else
|
|
17
|
+
super(name, type, **kwargs)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Override prop to support description parameter
|
|
22
|
+
sig { params(name: Symbol, type: T.untyped, description: T.nilable(String), kwargs: T.untyped).void }
|
|
23
|
+
def prop(name, type, description: nil, **kwargs)
|
|
24
|
+
if description
|
|
25
|
+
super(name, type, extra: { description: description }, **kwargs)
|
|
26
|
+
else
|
|
27
|
+
super(name, type, **kwargs)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Automatically extend T::Struct with description support
|
|
34
|
+
T::Struct.extend(SorbetBaml::DescriptionExtension)
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module SorbetBaml
|
|
7
|
+
# Extracts description parameters from T::Struct prop and const declarations
|
|
8
|
+
class DescriptionExtractor
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
sig { params(klass: T::Class[T.anything]).returns(T::Hash[String, T.nilable(String)]) }
|
|
12
|
+
def self.extract_prop_descriptions(klass)
|
|
13
|
+
descriptions = {}
|
|
14
|
+
|
|
15
|
+
# Check if this is a T::Struct with props
|
|
16
|
+
return descriptions unless klass.respond_to?(:props)
|
|
17
|
+
|
|
18
|
+
begin
|
|
19
|
+
T.unsafe(klass).props.each do |field_name, prop_info|
|
|
20
|
+
next unless prop_info.is_a?(Hash)
|
|
21
|
+
|
|
22
|
+
# Check if the prop has a description in the :extra field
|
|
23
|
+
extra = prop_info[:extra]
|
|
24
|
+
if extra.is_a?(Hash) && extra[:description].is_a?(String)
|
|
25
|
+
descriptions[field_name.to_s] = extra[:description]
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
rescue => e
|
|
29
|
+
# Handle any errors gracefully and return empty hash
|
|
30
|
+
return {}
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
descriptions
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module SorbetBaml
|
|
7
|
+
# Extensions to add BAML conversion methods to T::Enum
|
|
8
|
+
module EnumExtensions
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# Convert this enum to BAML type definition
|
|
12
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
13
|
+
def to_baml(options = {})
|
|
14
|
+
baml_type_definition(options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Convert this enum to BAML type definition with options
|
|
18
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
19
|
+
def baml_type_definition(options = {})
|
|
20
|
+
SorbetBaml::Converter.from_enum(T.cast(self, T.class_of(T::Enum)), options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# typed: strict
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "sorbet-runtime"
|
|
5
|
+
|
|
6
|
+
module SorbetBaml
|
|
7
|
+
# Extensions to add BAML conversion methods to T::Struct
|
|
8
|
+
module StructExtensions
|
|
9
|
+
extend T::Sig
|
|
10
|
+
|
|
11
|
+
# Convert this struct to BAML type definition
|
|
12
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
13
|
+
def to_baml(options = {})
|
|
14
|
+
baml_type_definition(options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Convert this struct to BAML type definition with options
|
|
18
|
+
sig { params(options: T::Hash[Symbol, T.untyped]).returns(String) }
|
|
19
|
+
def baml_type_definition(options = {})
|
|
20
|
+
SorbetBaml::Converter.from_struct(T.cast(self, T.class_of(T::Struct)), options)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -17,10 +17,12 @@ module SorbetBaml
|
|
|
17
17
|
map_simple_type(type_object.raw_type)
|
|
18
18
|
when T::Types::TypedArray
|
|
19
19
|
map_array_type(type_object)
|
|
20
|
+
when T::Types::TypedHash
|
|
21
|
+
map_hash_type(type_object)
|
|
20
22
|
else
|
|
21
|
-
# Check if it's a union type (T.nilable)
|
|
23
|
+
# Check if it's a union type (T.nilable or T.any)
|
|
22
24
|
if type_object.respond_to?(:types)
|
|
23
|
-
|
|
25
|
+
map_union_type(type_object)
|
|
24
26
|
else
|
|
25
27
|
# Fallback for unknown types
|
|
26
28
|
"unknown"
|
|
@@ -46,8 +48,8 @@ module SorbetBaml
|
|
|
46
48
|
when "Date", "DateTime", "Time"
|
|
47
49
|
"string"
|
|
48
50
|
else
|
|
49
|
-
# Check if it's a T::Struct
|
|
50
|
-
if raw_type < T::Struct
|
|
51
|
+
# Check if it's a T::Struct or T::Enum
|
|
52
|
+
if raw_type < T::Struct || raw_type < T::Enum
|
|
51
53
|
type_name = raw_type.name || raw_type.to_s
|
|
52
54
|
type_name.split('::').last || "unknown"
|
|
53
55
|
else
|
|
@@ -57,23 +59,58 @@ module SorbetBaml
|
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
sig { params(type_object: T.untyped).returns(String) }
|
|
60
|
-
def self.
|
|
61
|
-
# Extract the non-nil type from the union
|
|
62
|
+
def self.map_union_type(type_object)
|
|
62
63
|
types = type_object.types
|
|
63
|
-
|
|
64
|
+
nil_type = types.find { |t| t.raw_type == NilClass }
|
|
65
|
+
non_nil_types = types.reject { |t| t.raw_type == NilClass }
|
|
64
66
|
|
|
65
|
-
if
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if non_nil_types.empty?
|
|
68
|
+
return "null"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
if non_nil_types.size == 1
|
|
72
|
+
# This is T.nilable(T) - single type with nil
|
|
73
|
+
base_type = map_type(non_nil_types.first)
|
|
74
|
+
return nil_type ? "#{base_type}?" : base_type
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# This is T.any with multiple types
|
|
78
|
+
mapped_types = non_nil_types.map { |t| map_type(t) }
|
|
79
|
+
|
|
80
|
+
# Special case: TrueClass + FalseClass = bool
|
|
81
|
+
if mapped_types.sort == ["bool", "bool"]
|
|
82
|
+
union_string = "bool"
|
|
68
83
|
else
|
|
69
|
-
|
|
84
|
+
# Remove duplicates and join
|
|
85
|
+
union_string = mapped_types.uniq.join(" | ")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# If nil is present, wrap in parentheses and add ?
|
|
89
|
+
if nil_type
|
|
90
|
+
"(#{union_string})?"
|
|
91
|
+
else
|
|
92
|
+
union_string
|
|
70
93
|
end
|
|
71
94
|
end
|
|
72
95
|
|
|
73
96
|
sig { params(type_object: T.untyped).returns(String) }
|
|
74
97
|
def self.map_array_type(type_object)
|
|
75
98
|
element_type = map_type(type_object.type)
|
|
76
|
-
|
|
99
|
+
|
|
100
|
+
# If element type contains union (|), wrap in parentheses for correct precedence
|
|
101
|
+
if element_type.include?("|")
|
|
102
|
+
"(#{element_type})[]"
|
|
103
|
+
else
|
|
104
|
+
"#{element_type}[]"
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
sig { params(type_object: T.untyped).returns(String) }
|
|
109
|
+
def self.map_hash_type(type_object)
|
|
110
|
+
# T::Types::TypedHash has keys and values methods
|
|
111
|
+
key_type = map_type(type_object.keys)
|
|
112
|
+
value_type = map_type(type_object.values)
|
|
113
|
+
"map<#{key_type}, #{value_type}>"
|
|
77
114
|
end
|
|
78
115
|
end
|
|
79
116
|
end
|
data/lib/sorbet_baml/version.rb
CHANGED