sorbet-baml 0.1.0 → 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/CLAUDE.md +94 -0
- data/README.md +197 -125
- data/examples/description_parameters.rb +49 -0
- data/lib/sorbet_baml/comment_extractor.rb +23 -18
- data/lib/sorbet_baml/converter.rb +3 -3
- data/lib/sorbet_baml/description_extension.rb +34 -0
- data/lib/sorbet_baml/description_extractor.rb +36 -0
- data/lib/sorbet_baml/version.rb +1 -1
- data/lib/sorbet_baml.rb +3 -0
- metadata +5 -6
- data/docs/README.md +0 -117
- data/docs/advanced-usage.md +0 -427
- data/docs/getting-started.md +0 -91
- data/docs/troubleshooting.md +0 -291
- data/docs/type-mapping.md +0 -192
|
@@ -8,15 +8,20 @@ module SorbetBaml
|
|
|
8
8
|
|
|
9
9
|
sig { params(klass: T.class_of(T::Struct)).returns(T::Hash[String, T.nilable(String)]) }
|
|
10
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
|
|
11
15
|
comments = {}
|
|
12
16
|
source_file = find_source_file(klass)
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
18
22
|
|
|
19
|
-
comments
|
|
23
|
+
# Merge with priority: description parameters > comments
|
|
24
|
+
descriptions.merge(comments) { |key, desc_param, comment| desc_param }
|
|
20
25
|
end
|
|
21
26
|
|
|
22
27
|
sig { params(klass: T.class_of(T::Enum)).returns(T::Hash[String, T.nilable(String)]) }
|
|
@@ -27,14 +32,14 @@ module SorbetBaml
|
|
|
27
32
|
return comments unless source_file && File.exist?(source_file)
|
|
28
33
|
|
|
29
34
|
lines = File.readlines(source_file)
|
|
30
|
-
extract_enum_comments_from_lines(lines, klass.name.split('::').last, comments)
|
|
35
|
+
extract_enum_comments_from_lines(lines, T.must(T.must(klass.name).split('::').last), comments)
|
|
31
36
|
|
|
32
37
|
comments
|
|
33
38
|
end
|
|
34
39
|
|
|
35
40
|
private
|
|
36
41
|
|
|
37
|
-
sig { params(klass: Class).returns(T.nilable(String)) }
|
|
42
|
+
sig { params(klass: T::Class[T.anything]).returns(T.nilable(String)) }
|
|
38
43
|
def self.find_source_file(klass)
|
|
39
44
|
# Try to find where the class was defined
|
|
40
45
|
# This is a heuristic approach since Ruby doesn't provide reliable source location for classes
|
|
@@ -57,8 +62,8 @@ module SorbetBaml
|
|
|
57
62
|
# Read the file and check if it contains the class definition
|
|
58
63
|
begin
|
|
59
64
|
content = File.read(file_path)
|
|
60
|
-
class_name = klass.name.split('::').last
|
|
61
|
-
if content.match(/class\s+#{Regexp.escape(class_name)}\s*</)
|
|
65
|
+
class_name = T.must(klass.name).split('::').last
|
|
66
|
+
if content.match(/class\s+#{Regexp.escape(T.must(class_name))}\s*</)
|
|
62
67
|
return file_path
|
|
63
68
|
end
|
|
64
69
|
rescue
|
|
@@ -71,7 +76,7 @@ module SorbetBaml
|
|
|
71
76
|
|
|
72
77
|
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
73
78
|
def self.extract_comments_from_lines(lines, class_name, comments)
|
|
74
|
-
in_target_class = false
|
|
79
|
+
in_target_class = T.let(false, T::Boolean)
|
|
75
80
|
current_comment = T.let(nil, T.nilable(String))
|
|
76
81
|
brace_depth = 0
|
|
77
82
|
|
|
@@ -98,11 +103,11 @@ module SorbetBaml
|
|
|
98
103
|
|
|
99
104
|
# Extract comment
|
|
100
105
|
if stripped.start_with?('#')
|
|
101
|
-
comment_text = stripped[1..-1].strip
|
|
106
|
+
comment_text = T.must(stripped[1..-1]).strip
|
|
102
107
|
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
103
108
|
elsif stripped.match(/^const\s+:(\w+)/) && current_comment
|
|
104
|
-
field_name = stripped.match(/^const\s+:(\w+)/)[1]
|
|
105
|
-
comments[field_name] = current_comment
|
|
109
|
+
field_name = T.must(stripped.match(/^const\s+:(\w+)/))[1]
|
|
110
|
+
comments[T.must(field_name)] = current_comment
|
|
106
111
|
current_comment = nil
|
|
107
112
|
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
108
113
|
# Reset comment if we hit non-comment, non-const line
|
|
@@ -113,8 +118,8 @@ module SorbetBaml
|
|
|
113
118
|
|
|
114
119
|
sig { params(lines: T::Array[String], class_name: String, comments: T::Hash[String, T.nilable(String)]).void }
|
|
115
120
|
def self.extract_enum_comments_from_lines(lines, class_name, comments)
|
|
116
|
-
in_target_class = false
|
|
117
|
-
in_enums_block = false
|
|
121
|
+
in_target_class = T.let(false, T::Boolean)
|
|
122
|
+
in_enums_block = T.let(false, T::Boolean)
|
|
118
123
|
current_comment = T.let(nil, T.nilable(String))
|
|
119
124
|
|
|
120
125
|
lines.each do |line|
|
|
@@ -149,11 +154,11 @@ module SorbetBaml
|
|
|
149
154
|
|
|
150
155
|
# Extract comment
|
|
151
156
|
if stripped.start_with?('#')
|
|
152
|
-
comment_text = stripped[1..-1].strip
|
|
157
|
+
comment_text = T.must(stripped[1..-1]).strip
|
|
153
158
|
current_comment = current_comment ? "#{current_comment} #{comment_text}" : comment_text
|
|
154
159
|
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
|
|
160
|
+
enum_name = T.must(stripped.match(/^(\w+)\s*=\s*new/))[1]
|
|
161
|
+
comments[T.must(enum_name)] = current_comment
|
|
157
162
|
current_comment = nil
|
|
158
163
|
elsif !stripped.empty? && !stripped.start_with?('#')
|
|
159
164
|
# Reset comment if we hit non-comment, non-enum line
|
|
@@ -92,7 +92,7 @@ module SorbetBaml
|
|
|
92
92
|
value = enum_instance.serialize
|
|
93
93
|
|
|
94
94
|
# Find the constant name for this value
|
|
95
|
-
constant_name = nil
|
|
95
|
+
constant_name = T.let(nil, T.nilable(String))
|
|
96
96
|
klass.constants.each do |const_name|
|
|
97
97
|
const_value = klass.const_get(const_name)
|
|
98
98
|
if const_value.is_a?(klass) && const_value.serialize == value
|
|
@@ -105,7 +105,7 @@ module SorbetBaml
|
|
|
105
105
|
|
|
106
106
|
# Add description if available (BAML uses @description annotations, not comments)
|
|
107
107
|
if @include_descriptions && constant_name && comments[constant_name]
|
|
108
|
-
line += " @description(\"#{comments[constant_name].gsub('"', '\\"')}\")"
|
|
108
|
+
line += " @description(\"#{T.must(comments[constant_name]).gsub('"', '\\"')}\")"
|
|
109
109
|
end
|
|
110
110
|
|
|
111
111
|
lines << line
|
|
@@ -134,7 +134,7 @@ module SorbetBaml
|
|
|
134
134
|
|
|
135
135
|
# Add description if available (BAML uses @description annotations)
|
|
136
136
|
if @include_descriptions && comments[name.to_s]
|
|
137
|
-
escaped_comment = comments[name.to_s].gsub('"', '\\"')
|
|
137
|
+
escaped_comment = T.must(comments[name.to_s]).gsub('"', '\\"')
|
|
138
138
|
line += " @description(\"#{escaped_comment}\")"
|
|
139
139
|
end
|
|
140
140
|
|
|
@@ -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
|
data/lib/sorbet_baml/version.rb
CHANGED
data/lib/sorbet_baml.rb
CHANGED
|
@@ -6,6 +6,9 @@ require_relative "sorbet_baml/version"
|
|
|
6
6
|
require_relative "sorbet_baml/converter"
|
|
7
7
|
require_relative "sorbet_baml/type_mapper"
|
|
8
8
|
require_relative "sorbet_baml/dependency_resolver"
|
|
9
|
+
require_relative "sorbet_baml/comment_extractor"
|
|
10
|
+
require_relative "sorbet_baml/description_extractor"
|
|
11
|
+
require_relative "sorbet_baml/description_extension"
|
|
9
12
|
require_relative "sorbet_baml/struct_extensions"
|
|
10
13
|
require_relative "sorbet_baml/enum_extensions"
|
|
11
14
|
|
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vicente Reig Rincon de Arellano
|
|
@@ -36,18 +36,17 @@ files:
|
|
|
36
36
|
- ".idea/inspectionProfiles/Project_Default.xml"
|
|
37
37
|
- ".rspec"
|
|
38
38
|
- CHANGELOG.md
|
|
39
|
+
- CLAUDE.md
|
|
39
40
|
- LICENSE.txt
|
|
40
41
|
- README.md
|
|
41
42
|
- Rakefile
|
|
42
|
-
-
|
|
43
|
-
- docs/advanced-usage.md
|
|
44
|
-
- docs/getting-started.md
|
|
45
|
-
- docs/troubleshooting.md
|
|
46
|
-
- docs/type-mapping.md
|
|
43
|
+
- examples/description_parameters.rb
|
|
47
44
|
- lib/sorbet_baml.rb
|
|
48
45
|
- lib/sorbet_baml/comment_extractor.rb
|
|
49
46
|
- lib/sorbet_baml/converter.rb
|
|
50
47
|
- lib/sorbet_baml/dependency_resolver.rb
|
|
48
|
+
- lib/sorbet_baml/description_extension.rb
|
|
49
|
+
- lib/sorbet_baml/description_extractor.rb
|
|
51
50
|
- lib/sorbet_baml/enum_extensions.rb
|
|
52
51
|
- lib/sorbet_baml/struct_extensions.rb
|
|
53
52
|
- lib/sorbet_baml/type_mapper.rb
|
data/docs/README.md
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
# sorbet-baml Documentation
|
|
2
|
-
|
|
3
|
-
Developer documentation for the sorbet-baml gem.
|
|
4
|
-
|
|
5
|
-
## For Users
|
|
6
|
-
|
|
7
|
-
If you want to use this gem in your project:
|
|
8
|
-
|
|
9
|
-
1. **[Getting Started](./getting-started.md)** - Installation and basic usage
|
|
10
|
-
2. **[Type Mapping Reference](./type-mapping.md)** - Complete type conversion table
|
|
11
|
-
3. **[Advanced Usage](./advanced-usage.md)** - Complex scenarios and integrations
|
|
12
|
-
4. **[Troubleshooting](./troubleshooting.md)** - Common issues and solutions
|
|
13
|
-
|
|
14
|
-
## For Contributors
|
|
15
|
-
|
|
16
|
-
This gem has reached **feature completeness** for core BAML conversion needs. The implementation is production-ready with:
|
|
17
|
-
|
|
18
|
-
- ✅ **Complete type support** - All Sorbet types mapped to BAML
|
|
19
|
-
- ✅ **Ruby-idiomatic API** - `.to_baml` method on all T::Struct/T::Enum classes
|
|
20
|
-
- ✅ **Dependency management** - Automatic topological sorting
|
|
21
|
-
- ✅ **100% test coverage** - 34 comprehensive test cases
|
|
22
|
-
- ✅ **Full Sorbet type safety** - Zero type errors
|
|
23
|
-
|
|
24
|
-
Future enhancements are optional nice-to-haves rather than core requirements.
|
|
25
|
-
|
|
26
|
-
## Quick Example
|
|
27
|
-
|
|
28
|
-
```ruby
|
|
29
|
-
# Define a Sorbet struct
|
|
30
|
-
class User < T::Struct
|
|
31
|
-
const :name, String
|
|
32
|
-
const :age, Integer
|
|
33
|
-
const :email, T.nilable(String)
|
|
34
|
-
end
|
|
35
|
-
|
|
36
|
-
# Convert to BAML (Ruby-idiomatic API)
|
|
37
|
-
require 'sorbet-baml'
|
|
38
|
-
User.to_baml
|
|
39
|
-
|
|
40
|
-
# Legacy API also supported
|
|
41
|
-
# SorbetBaml.from_struct(User)
|
|
42
|
-
```
|
|
43
|
-
|
|
44
|
-
**Generated BAML:**
|
|
45
|
-
```baml
|
|
46
|
-
class User {
|
|
47
|
-
name string
|
|
48
|
-
age int
|
|
49
|
-
email string?
|
|
50
|
-
}
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
## Design Goals
|
|
54
|
-
|
|
55
|
-
1. **Ruby-Idiomatic** - Natural `.to_baml` API that feels native
|
|
56
|
-
2. **Production-Ready** - Complete type support, dependency management, full test coverage
|
|
57
|
-
3. **Token Efficiency** - 60% fewer tokens than JSON Schema for real workloads
|
|
58
|
-
4. **Zero-Config** - Works automatically with existing Sorbet codebases
|
|
59
|
-
5. **Type-Safe** - Full Sorbet type checking throughout the gem
|
|
60
|
-
|
|
61
|
-
## What This Is Not
|
|
62
|
-
|
|
63
|
-
- Not a BAML runtime or executor
|
|
64
|
-
- Not a JSON Schema generator (use [sorbet-schema](https://github.com/maxveldink/sorbet-schema) for that)
|
|
65
|
-
- Not a Sorbet type checker
|
|
66
|
-
- Not a serialization library
|
|
67
|
-
|
|
68
|
-
## Advanced Features
|
|
69
|
-
|
|
70
|
-
### Ruby-Idiomatic API
|
|
71
|
-
```ruby
|
|
72
|
-
User.to_baml # Single type
|
|
73
|
-
User.to_baml(indent_size: 4) # Custom formatting
|
|
74
|
-
User.to_baml(include_dependencies: true) # With dependencies
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Automatic Dependency Management
|
|
78
|
-
```ruby
|
|
79
|
-
class Address < T::Struct
|
|
80
|
-
const :street, String
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
class User < T::Struct
|
|
84
|
-
const :address, Address
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
User.to_baml(include_dependencies: true)
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
**Generated BAML (correct ordering):**
|
|
91
|
-
```baml
|
|
92
|
-
class Address {
|
|
93
|
-
street string
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
class User {
|
|
97
|
-
address Address
|
|
98
|
-
}
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
## Why BAML?
|
|
102
|
-
|
|
103
|
-
BAML (Boundary AI Markup Language) provides a concise way to define types for LLM consumption. **Real-world comparison** from production agentic workflows:
|
|
104
|
-
|
|
105
|
-
| Format | Tokens | Efficiency |
|
|
106
|
-
|--------|--------|-----------|
|
|
107
|
-
| JSON Schema | ~450 | baseline |
|
|
108
|
-
| **BAML** | **~180** | **🔥 60% fewer** |
|
|
109
|
-
|
|
110
|
-
### Benefits:
|
|
111
|
-
- **Cost Savings**: 60% reduction in prompt tokens = 60% lower LLM API costs
|
|
112
|
-
- **Performance**: Smaller prompts = faster LLM response times
|
|
113
|
-
- **Context Efficiency**: More room for actual content vs. type definitions
|
|
114
|
-
- **Readability**: Human-readable and maintainable
|
|
115
|
-
- **LLM-Friendly**: Designed specifically for AI consumption
|
|
116
|
-
|
|
117
|
-
Perfect for prompt engineering, structured output generation, and agentic workflows where token efficiency matters.
|