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.
@@ -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
- 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
+ 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SorbetBaml
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
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.1.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
- - docs/README.md
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.