sorbet-baml 0.0.1 → 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.
@@ -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
- klasses.map { |klass| converter.convert_struct(klass) }.join("\n\n")
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, false), T::Boolean)
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 = nil
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(\"#{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
- lines << "#{' ' * @indent_size}#{name} #{baml_type}"
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 = 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,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
- map_nilable_type(type_object)
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.map_nilable_type(type_object)
61
- # Extract the non-nil type from the union
62
+ def self.map_union_type(type_object)
62
63
  types = type_object.types
63
- non_nil_type = types.find { |t| t.raw_type != NilClass }
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 non_nil_type
66
- base_type = map_type(non_nil_type)
67
- "#{base_type}?"
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
- "null"
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
- "#{element_type}[]"
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SorbetBaml
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/sorbet_baml.rb CHANGED
@@ -5,6 +5,9 @@ require "sorbet-runtime"
5
5
  require_relative "sorbet_baml/version"
6
6
  require_relative "sorbet_baml/converter"
7
7
  require_relative "sorbet_baml/type_mapper"
8
+ require_relative "sorbet_baml/dependency_resolver"
9
+ require_relative "sorbet_baml/struct_extensions"
10
+ require_relative "sorbet_baml/enum_extensions"
8
11
 
9
12
  module SorbetBaml
10
13
  class Error < StandardError; end
@@ -23,3 +26,7 @@ module SorbetBaml
23
26
  Converter.from_structs(klasses, options)
24
27
  end
25
28
  end
29
+
30
+ # Extend T::Struct and T::Enum with BAML conversion methods
31
+ T::Struct.extend(SorbetBaml::StructExtensions)
32
+ T::Enum.extend(SorbetBaml::EnumExtensions)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sorbet-baml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vicente Reig Rincon de Arellano
@@ -32,6 +32,8 @@ executables: []
32
32
  extensions: []
33
33
  extra_rdoc_files: []
34
34
  files:
35
+ - ".idea/.gitignore"
36
+ - ".idea/inspectionProfiles/Project_Default.xml"
35
37
  - ".rspec"
36
38
  - CHANGELOG.md
37
39
  - LICENSE.txt
@@ -43,7 +45,11 @@ files:
43
45
  - docs/troubleshooting.md
44
46
  - docs/type-mapping.md
45
47
  - lib/sorbet_baml.rb
48
+ - lib/sorbet_baml/comment_extractor.rb
46
49
  - lib/sorbet_baml/converter.rb
50
+ - lib/sorbet_baml/dependency_resolver.rb
51
+ - lib/sorbet_baml/enum_extensions.rb
52
+ - lib/sorbet_baml/struct_extensions.rb
47
53
  - lib/sorbet_baml/type_mapper.rb
48
54
  - lib/sorbet_baml/version.rb
49
55
  - sig/sorbet/baml.rbs