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.
@@ -18,6 +18,16 @@ end
18
18
  class User < T::Struct
19
19
  const :name, String
20
20
  end
21
+
22
+ # Generate BAML
23
+ User.to_baml
24
+ ```
25
+
26
+ **Generated BAML:**
27
+ ```baml
28
+ class User {
29
+ name string
30
+ }
21
31
  ```
22
32
 
23
33
  ### Empty output
@@ -29,14 +39,44 @@ end
29
39
  ```ruby
30
40
  class User < T::Struct
31
41
  const :name, String # Add properties
42
+ const :age, Integer
32
43
  end
44
+
45
+ User.to_baml
46
+ ```
47
+
48
+ **Generated BAML:**
49
+ ```baml
50
+ class User {
51
+ name string
52
+ age int
53
+ }
33
54
  ```
34
55
 
35
- ### Circular dependency detected
56
+ ### Self-referential types work fine
57
+
58
+ **Problem**: You think self-referential types aren't supported.
59
+
60
+ **Solution**: They actually work perfectly! Self-referential types are fully supported:
61
+
62
+ ```ruby
63
+ class Category < T::Struct
64
+ const :name, String
65
+ const :parent, T.nilable(Category)
66
+ const :children, T::Array[Category]
67
+ end
36
68
 
37
- **Problem**: Two structs reference each other creating an infinite loop.
69
+ Category.to_baml
70
+ ```
38
71
 
39
- **Solution**: This is not yet supported. Consider flattening the structure or using a different approach.
72
+ **Generated BAML:**
73
+ ```baml
74
+ class Category {
75
+ name string
76
+ parent Category?
77
+ children Category[]
78
+ }
79
+ ```
40
80
 
41
81
  ## Type-Specific Issues
42
82
 
@@ -46,10 +86,23 @@ Ensure you're using the Sorbet array syntax:
46
86
 
47
87
  ```ruby
48
88
  # ❌ Wrong
49
- const :items, Array
89
+ class User < T::Struct
90
+ const :items, Array # Generic Array won't work
91
+ end
50
92
 
51
93
  # ✅ Correct
52
- const :items, T::Array[String]
94
+ class User < T::Struct
95
+ const :items, T::Array[String]
96
+ end
97
+
98
+ User.to_baml
99
+ ```
100
+
101
+ **Generated BAML:**
102
+ ```baml
103
+ class User {
104
+ items string[]
105
+ }
53
106
  ```
54
107
 
55
108
  ### Optional fields showing as required
@@ -58,24 +111,181 @@ Make sure to use `T.nilable`:
58
111
 
59
112
  ```ruby
60
113
  # ❌ Wrong - will be required
61
- const :email, String
114
+ class User < T::Struct
115
+ const :email, String
116
+ end
62
117
 
63
118
  # ✅ Correct - will be optional
64
- const :email, T.nilable(String)
119
+ class User < T::Struct
120
+ const :email, T.nilable(String)
121
+ end
122
+
123
+ User.to_baml
124
+ ```
125
+
126
+ **Generated BAML:**
127
+ ```baml
128
+ class User {
129
+ email string?
130
+ }
131
+ ```
132
+
133
+ ### Union types not working
134
+
135
+ Ensure you're using `T.any` for union types:
136
+
137
+ ```ruby
138
+ # ❌ Wrong
139
+ class Config < T::Struct
140
+ const :value, String || Integer # Ruby OR, not Sorbet union
141
+ end
142
+
143
+ # ✅ Correct
144
+ class Config < T::Struct
145
+ const :value, T.any(String, Integer)
146
+ end
147
+
148
+ Config.to_baml
149
+ ```
150
+
151
+ **Generated BAML:**
152
+ ```baml
153
+ class Config {
154
+ value string | int
155
+ }
156
+ ```
157
+
158
+ ### Hash types not mapping correctly
159
+
160
+ Use the full `T::Hash[K, V]` syntax:
161
+
162
+ ```ruby
163
+ # ❌ Wrong
164
+ class User < T::Struct
165
+ const :metadata, Hash # Generic Hash won't work
166
+ end
167
+
168
+ # ✅ Correct
169
+ class User < T::Struct
170
+ const :metadata, T::Hash[String, T.any(String, Integer)]
171
+ end
172
+
173
+ User.to_baml
174
+ ```
175
+
176
+ **Generated BAML:**
177
+ ```baml
178
+ class User {
179
+ metadata map<string, string | int>
180
+ }
181
+ ```
182
+
183
+ ## Dependency Issues
184
+
185
+ ### Missing dependencies in output
186
+
187
+ Use `include_dependencies: true` to automatically include all referenced types:
188
+
189
+ ```ruby
190
+ class Address < T::Struct
191
+ const :street, String
192
+ const :city, String
193
+ end
194
+
195
+ class User < T::Struct
196
+ const :name, String
197
+ const :address, Address
198
+ end
199
+
200
+ # ❌ Only outputs User class
201
+ User.to_baml
202
+
203
+ # ✅ Outputs both Address and User in correct order
204
+ User.to_baml(include_dependencies: true)
205
+ ```
206
+
207
+ **Generated BAML (with dependencies):**
208
+ ```baml
209
+ class Address {
210
+ street string
211
+ city string
212
+ }
213
+
214
+ class User {
215
+ name string
216
+ address Address
217
+ }
218
+ ```
219
+
220
+ ### Wrong dependency order
221
+
222
+ The gem automatically handles dependency ordering using topological sorting. Dependencies always come before the types that reference them.
223
+
224
+ ## Enum Issues
225
+
226
+ ### Enums not converting
227
+
228
+ Ensure you're using the correct T::Enum syntax:
229
+
230
+ ```ruby
231
+ # ❌ Wrong
232
+ class Status
233
+ ACTIVE = 'active'
234
+ INACTIVE = 'inactive'
235
+ end
236
+
237
+ # ✅ Correct
238
+ class Status < T::Enum
239
+ enums do
240
+ Active = new('active')
241
+ Inactive = new('inactive')
242
+ end
243
+ end
244
+
245
+ Status.to_baml
246
+ ```
247
+
248
+ **Generated BAML:**
249
+ ```baml
250
+ enum Status {
251
+ "active"
252
+ "inactive"
253
+ }
65
254
  ```
66
255
 
67
256
  ## Getting Help
68
257
 
69
- 1. Check the [Type Mapping Reference](./type-mapping.md)
70
- 2. Review the examples in [Getting Started](./getting-started.md)
258
+ 1. Check the [Type Mapping Reference](./type-mapping.md) for complete type support
259
+ 2. Review examples in [Getting Started](./getting-started.md) and [Advanced Usage](./advanced-usage.md)
71
260
  3. File an issue at https://github.com/vicentereig/sorbet-baml/issues
72
261
 
73
- ## Debug Mode
262
+ ## Advanced Debugging
74
263
 
75
- To see detailed conversion information:
264
+ ### Inspect Sorbet type information
76
265
 
77
266
  ```ruby
78
- # Future feature - not yet implemented
79
- SorbetBaml.debug = true
80
- baml = SorbetBaml.from_struct(MyStruct)
267
+ # See what Sorbet sees for your struct
268
+ MyStruct.props
269
+ # => {name: String, age: Integer, ...}
270
+
271
+ # Check if a class is a T::Struct
272
+ MyStruct < T::Struct
273
+ # => true
274
+
275
+ # See enum values
276
+ MyEnum.values
277
+ # => [#<MyEnum:0x... @serialize="value1">, ...]
278
+ ```
279
+
280
+ ### Testing your BAML output
281
+
282
+ ```ruby
283
+ # Verify the output looks correct
284
+ baml = User.to_baml(include_dependencies: true)
285
+ puts baml
286
+
287
+ # Check that all expected types are included
288
+ expected_types = ['class User', 'class Address', 'enum Status']
289
+ expected_types.all? { |type| baml.include?(type) }
290
+ # => true
81
291
  ```
data/docs/type-mapping.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Type Mapping Reference
2
2
 
3
- Complete mapping between Sorbet types and BAML output.
3
+ Complete mapping between Sorbet types and BAML output. All listed types are **fully supported**.
4
4
 
5
5
  ## Basic Types
6
6
 
@@ -12,54 +12,181 @@ Complete mapping between Sorbet types and BAML output.
12
12
  | `T::Boolean` | `bool` | `active bool` |
13
13
  | `NilClass` | `null` | `null` |
14
14
  | `Symbol` | `string` | `status string` |
15
+ | `Date/DateTime/Time` | `string` | `created_at string` |
15
16
 
16
- ## Optional Types
17
+ ## Optional Types (T.nilable)
17
18
 
18
19
  | Sorbet Type | BAML Output | Example |
19
20
  |-------------|-------------|---------|
20
21
  | `T.nilable(String)` | `string?` | `email string?` |
21
22
  | `T.nilable(Integer)` | `int?` | `age int?` |
23
+ | `T.nilable(MyStruct)` | `MyStruct?` | `address Address?` |
22
24
 
23
25
  ## Collection Types
24
26
 
25
27
  | Sorbet Type | BAML Output | Example |
26
28
  |-------------|-------------|---------|
27
29
  | `T::Array[String]` | `string[]` | `tags string[]` |
28
- | `T::Array[T::Struct]` | `StructName[]` | `addresses Address[]` |
30
+ | `T::Array[Integer]` | `int[]` | `scores int[]` |
31
+ | `T::Array[MyStruct]` | `MyStruct[]` | `addresses Address[]` |
29
32
 
30
- ## Nested Structures
33
+ ## Hash/Map Types
34
+
35
+ | Sorbet Type | BAML Output | Example |
36
+ |-------------|-------------|---------|
37
+ | `T::Hash[String, String]` | `map<string, string>` | `metadata map<string, string>` |
38
+ | `T::Hash[String, Integer]` | `map<string, int>` | `counts map<string, int>` |
39
+ | `T::Hash[Symbol, String]` | `map<string, string>` | `config map<string, string>` |
40
+
41
+ ## Union Types (T.any)
42
+
43
+ | Sorbet Type | BAML Output | Example |
44
+ |-------------|-------------|---------|
45
+ | `T.any(String, Integer)` | `string \| int` | `value string \| int` |
46
+ | `T.any(String, Integer, Float)` | `string \| int \| float` | `mixed string \| int \| float` |
47
+ | `T.nilable(T.any(String, Integer))` | `(string \| int)?` | `optional (string \| int)?` |
48
+
49
+ ## Complex Collection Types
50
+
51
+ | Sorbet Type | BAML Output | Example |
52
+ |-------------|-------------|---------|
53
+ | `T::Array[T.any(String, Integer)]` | `(string \| int)[]` | `mixed_array (string \| int)[]` |
54
+ | `T::Hash[String, T.any(String, Integer)]` | `map<string, string \| int>` | `settings map<string, string \| int>` |
55
+ | `T::Hash[String, T::Array[String]]` | `map<string, string[]>` | `labels map<string, string[]>` |
56
+
57
+ ## Structured Types
58
+
59
+ ### T::Struct to BAML Classes
31
60
 
32
61
  ```ruby
33
- # Input
34
62
  class Address < T::Struct
35
63
  const :street, String
36
64
  const :city, String
65
+ const :postal_code, T.nilable(String)
37
66
  end
38
67
 
39
68
  class User < T::Struct
40
69
  const :name, String
70
+ const :age, Integer
41
71
  const :address, Address
72
+ const :tags, T::Array[String]
42
73
  end
74
+ ```
43
75
 
44
- # Output
45
- class Address {
46
- street string
47
- city string
48
- }
76
+ ```ruby
77
+ User.to_baml
78
+ ```
49
79
 
80
+ **Generated BAML:**
81
+ ```baml
50
82
  class User {
51
83
  name string
84
+ age int
52
85
  address Address
86
+ tags string[]
87
+ }
88
+ ```
89
+
90
+ ### T::Enum to BAML Enums
91
+
92
+ ```ruby
93
+ class Status < T::Enum
94
+ enums do
95
+ Active = new('active')
96
+ Inactive = new('inactive')
97
+ Pending = new('pending')
98
+ end
99
+ end
100
+
101
+ class User < T::Struct
102
+ const :name, String
103
+ const :status, Status
104
+ end
105
+ ```
106
+
107
+ ```ruby
108
+ [Status, User].map(&:to_baml).join("\n\n")
109
+ ```
110
+
111
+ **Generated BAML:**
112
+ ```baml
113
+ enum Status {
114
+ "active"
115
+ "inactive"
116
+ "pending"
117
+ }
118
+
119
+ class User {
120
+ name string
121
+ status Status
122
+ }
123
+ ```
124
+
125
+ ### Complex Real-World Example
126
+
127
+ ```ruby
128
+ class Priority < T::Enum
129
+ enums do
130
+ Low = new('low')
131
+ Medium = new('medium')
132
+ High = new('high')
133
+ end
134
+ end
135
+
136
+ class Task < T::Struct
137
+ const :title, String
138
+ const :description, T.nilable(String)
139
+ const :priority, Priority
140
+ const :tags, T::Array[String]
141
+ const :metadata, T::Hash[String, T.any(String, Integer)]
142
+ const :subtasks, T::Array[Task]
143
+ end
144
+ ```
145
+
146
+ ```ruby
147
+ [Priority, Task].map(&:to_baml).join("\n\n")
148
+ ```
149
+
150
+ **Generated BAML:**
151
+ ```baml
152
+ enum Priority {
153
+ "low"
154
+ "medium"
155
+ "high"
53
156
  }
157
+
158
+ class Task {
159
+ title string
160
+ description string?
161
+ priority Priority
162
+ tags string[]
163
+ metadata map<string, string | int>
164
+ subtasks Task[]
165
+ }
166
+ ```
167
+
168
+ ## Advanced Features
169
+
170
+ ### Dependency Management
171
+
172
+ ```ruby
173
+ # Dependencies automatically included with smart defaults
174
+ User.to_baml
175
+ # Outputs Address, then User in correct order
176
+ ```
177
+
178
+ ### Custom Formatting
179
+
180
+ ```ruby
181
+ # Smart defaults include dependencies automatically
182
+ User.to_baml(indent_size: 4)
54
183
  ```
55
184
 
56
- ## Not Yet Supported
185
+ ## Future Enhancements (Optional)
57
186
 
58
- These types will be added in future versions:
187
+ These are nice-to-have features for future versions:
59
188
 
60
- - `T::Hash[K, V]` → `map<K, V>`
61
- - `T.any(Type1, Type2)` → `Type1 | Type2`
62
- - `T::Enum` subclasses → `enum Name { ... }`
63
189
  - `T.type_alias` → `type Name = ...`
64
- - `T::Set[T]` `T[]` (with uniqueness note)
65
- - `T::Range[T]` Will need special handling
190
+ - Field descriptions from comments
191
+ - Custom naming strategies (snake_case camelCase)
192
+ - Self-referential type handling
@@ -0,0 +1,165 @@
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
+ comments = {}
12
+ source_file = find_source_file(klass)
13
+
14
+ return comments unless source_file && File.exist?(source_file)
15
+
16
+ lines = File.readlines(source_file)
17
+ extract_comments_from_lines(lines, klass.name.split('::').last, comments)
18
+
19
+ comments
20
+ end
21
+
22
+ sig { params(klass: T.class_of(T::Enum)).returns(T::Hash[String, T.nilable(String)]) }
23
+ def self.extract_enum_comments(klass)
24
+ comments = {}
25
+ source_file = find_source_file(klass)
26
+
27
+ return comments unless source_file && File.exist?(source_file)
28
+
29
+ lines = File.readlines(source_file)
30
+ extract_enum_comments_from_lines(lines, klass.name.split('::').last, comments)
31
+
32
+ comments
33
+ end
34
+
35
+ private
36
+
37
+ sig { params(klass: Class).returns(T.nilable(String)) }
38
+ def self.find_source_file(klass)
39
+ # Try to find where the class was defined
40
+ # This is a heuristic approach since Ruby doesn't provide reliable source location for classes
41
+
42
+ # Method 1: Check if any methods have source location
43
+ begin
44
+ if klass.respond_to?(:new) && klass.method(:new).respond_to?(:source_location)
45
+ location = klass.method(:new).source_location
46
+ return location[0] if location
47
+ end
48
+ rescue
49
+ # Ignore errors
50
+ end
51
+
52
+ # Method 2: Look at the current call stack for files that might contain the class
53
+ caller_locations.each do |location|
54
+ file_path = location.absolute_path || location.path
55
+ next unless file_path && File.exist?(file_path)
56
+
57
+ # Read the file and check if it contains the class definition
58
+ begin
59
+ content = File.read(file_path)
60
+ class_name = klass.name.split('::').last
61
+ if content.match(/class\s+#{Regexp.escape(class_name)}\s*</)
62
+ return file_path
63
+ end
64
+ rescue
65
+ # Ignore file read errors
66
+ end
67
+ end
68
+
69
+ nil
70
+ end
71
+
72
+ sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
73
+ def self.extract_comments_from_lines(lines, class_name, comments)
74
+ in_target_class = false
75
+ current_comment = T.let(nil, T.nilable(String))
76
+ brace_depth = 0
77
+
78
+ lines.each do |line|
79
+ stripped = line.strip
80
+
81
+ # Check if we're entering the target class
82
+ if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Struct/)
83
+ in_target_class = true
84
+ brace_depth = 0
85
+ next
86
+ end
87
+
88
+ next unless in_target_class
89
+
90
+ # Track brace depth to handle nested classes
91
+ brace_depth += stripped.count('{')
92
+ brace_depth -= stripped.count('}')
93
+
94
+ # Exit when we reach the end of the class
95
+ if stripped == 'end' && brace_depth == 0
96
+ break
97
+ end
98
+
99
+ # Extract comment
100
+ if stripped.start_with?('#')
101
+ comment_text = stripped[1..-1].strip
102
+ current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
103
+ elsif stripped.match(/^const\s+:(\w+)/) && current_comment
104
+ field_name = stripped.match(/^const\s+:(\w+)/)[1]
105
+ comments[field_name] = current_comment
106
+ current_comment = nil
107
+ elsif !stripped.empty? && !stripped.start_with?('#')
108
+ # Reset comment if we hit non-comment, non-const line
109
+ current_comment = nil
110
+ end
111
+ end
112
+ end
113
+
114
+ sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
115
+ def self.extract_enum_comments_from_lines(lines, class_name, comments)
116
+ in_target_class = false
117
+ in_enums_block = false
118
+ current_comment = T.let(nil, T.nilable(String))
119
+
120
+ lines.each do |line|
121
+ stripped = line.strip
122
+
123
+ # Check if we're entering the target enum class
124
+ if stripped.match(/^class\s+#{Regexp.escape(class_name)}\s*<\s*T::Enum/)
125
+ in_target_class = true
126
+ next
127
+ end
128
+
129
+ next unless in_target_class
130
+
131
+ # Check if we're in the enums block
132
+ if stripped == 'enums do'
133
+ in_enums_block = true
134
+ next
135
+ end
136
+
137
+ # Exit enums block
138
+ if in_enums_block && stripped == 'end'
139
+ in_enums_block = false
140
+ next
141
+ end
142
+
143
+ # Exit class
144
+ if stripped == 'end' && !in_enums_block
145
+ break
146
+ end
147
+
148
+ next unless in_enums_block
149
+
150
+ # Extract comment
151
+ if stripped.start_with?('#')
152
+ comment_text = stripped[1..-1].strip
153
+ current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
154
+ elsif stripped.match(/^(\w+)\s*=\s*new/) && current_comment
155
+ enum_name = stripped.match(/^(\w+)\s*=\s*new/)[1]
156
+ comments[enum_name] = current_comment
157
+ current_comment = nil
158
+ elsif !stripped.empty? && !stripped.start_with?('#')
159
+ # Reset comment if we hit non-comment, non-enum line
160
+ current_comment = nil
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end