diff --git a/engine/baml-lib/baml-types/src/lib.rs b/engine/baml-lib/baml-types/src/lib.rs index 2b56a7935..84b7d982e 100644 --- a/engine/baml-lib/baml-types/src/lib.rs +++ b/engine/baml-lib/baml-types/src/lib.rs @@ -16,3 +16,12 @@ pub use map::Map as BamlMap; pub use media::{BamlMedia, BamlMediaContent, BamlMediaType, MediaBase64, MediaUrl}; pub use minijinja::JinjaExpression; pub use value_expr::{EvaluationContext, GetEnvVar, ResolvedValue, StringOr, UnresolvedValue}; + +impl BamlValue { + pub fn as_string(&self) -> Option<&str> { + match self { + BamlValue::String(s) => Some(s), + _ => None, + } + } +} diff --git a/engine/baml-runtime/src/type_builder/mod.rs b/engine/baml-runtime/src/type_builder/mod.rs index 72fabece2..9ab933862 100644 --- a/engine/baml-runtime/src/type_builder/mod.rs +++ b/engine/baml-runtime/src/type_builder/mod.rs @@ -1,4 +1,5 @@ use std::sync::{Arc, Mutex}; +use std::fmt; use baml_types::{BamlValue, FieldType}; use indexmap::IndexMap; @@ -133,39 +134,221 @@ impl EnumBuilder { } } -impl std::fmt::Debug for TypeBuilder { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - // Start the debug printout with the struct name - writeln!(f, "TypeBuilder {{")?; - - // Safely attempt to acquire the lock and print classes - write!(f, " classes: ")?; - match self.classes.lock() { - Ok(classes) => { - // We iterate through the keys only to avoid deadlocks and because we might not be able to print the values - // safely without deep control over locking mechanisms - let keys: Vec<_> = classes.keys().collect(); - writeln!(f, "{:?},", keys)? +// displays a class property along with its current state and metadata +// the format shows three key pieces of information: +// 1. the property name as defined in the class +// 2. the type status: either 'set' (type defined) or 'unset' (type pending) +// 3. any metadata attached to the property in parentheses +// +// metadata is shown in key=value format, with values formatted according to their type +// multiple metadata entries are separated by commas for readability +// +// examples of the output format: +// name set (alias='username', description='full name') +// - shows a property with both alias and description metadata +// age unset +// - shows a property without a defined type or metadata +// email set (required=true, format='email') +// - shows a property with multiple metadata values of different types +impl fmt::Display for ClassPropertyBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let meta = self.meta.lock().unwrap(); + write!(f, "{}", self.r#type.lock().unwrap().as_ref().map_or("unset", |_| "set"))?; + + if !meta.is_empty() { + write!(f, " (")?; + for (i, (key, value)) in meta.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}={}", key, value)?; } - Err(_) => writeln!(f, "Cannot acquire lock,")?, + write!(f, ")")?; } + Ok(()) + } +} - // Safely attempt to acquire the lock and print enums - write!(f, " enums: ")?; - match self.enums.lock() { - Ok(enums) => { - // Similarly, print only the keys - let keys: Vec<_> = enums.keys().collect(); - writeln!(f, "{:?}", keys)? +// displays an enum value and its associated metadata +// the format focuses on clarity and completeness: +// 1. the enum value name in uppercase (following enum conventions) +// 2. any metadata in parentheses, showing all attached information +// +// metadata is displayed in a consistent key=value format: +// - each piece of metadata is separated by commas +// - values are formatted based on their type (quotes for strings, etc.) +// - all metadata is shown, not just common fields like alias +// +// examples of the output format: +// ACTIVE (alias='active', priority=1, enabled=true) +// - shows an enum value with multiple metadata types +// PENDING +// - shows a simple enum value with no metadata +// INACTIVE (description='not currently in use', status=null) +// - shows how null values and longer descriptions are formatted +impl fmt::Display for EnumValueBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let meta = self.meta.lock().unwrap(); + + if !meta.is_empty() { + write!(f, " (")?; + for (i, (key, value)) in meta.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}={}", key, value)?; } - Err(_) => writeln!(f, "Cannot acquire lock,")?, + write!(f, ")")?; } + Ok(()) + } +} - // Close the struct printout +// displays a complete class definition with all its properties +// the format provides a clear hierarchical structure: +// 1. class name followed by an opening brace +// 2. indented list of properties, each on its own line +// 3. closing brace aligned with the class name +// +// properties are displayed with consistent indentation and formatting: +// - each property starts on a new line with proper indentation +// - properties are separated by commas for valid syntax +// - the last property doesn't have a trailing comma +// +// example of the complete format: +// User { +// name set (alias='username', description='user's full name'), +// age set (type='integer', min=0), +// email set (format='email', required=true), +// status unset +// } +impl fmt::Display for ClassBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let properties = self.properties.lock().unwrap(); + write!(f, "{{")?; + if !properties.is_empty() { + for (i, (name, prop)) in properties.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "\n {} {}", name, prop.lock().unwrap())?; + } + write!(f, "\n ")?; + } write!(f, "}}") } } +// displays a complete enum definition with all its values +// the format creates a clear and readable structure: +// 1. enum name followed by an opening brace +// 2. indented list of enum values, each on its own line +// 3. closing brace aligned with the enum name +// +// values are displayed with consistent formatting: +// - each value starts on a new line with proper indentation +// - values are separated by commas for valid syntax +// - metadata is shown in parentheses when present +// - empty enums are shown with empty braces +// +// example of the complete format: +// Status { +// ACTIVE (alias='active', weight=1.0), +// PENDING (description='awaiting processing'), +// INACTIVE (enabled=false), +// ARCHIVED +// } +impl fmt::Display for EnumBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let values = self.values.lock().unwrap(); + write!(f, "{{")?; + if !values.is_empty() { + for (i, (name, value)) in values.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "\n {}{}", name, value.lock().unwrap())?; + } + write!(f, "\n ")?; + } + write!(f, "}}") + } +} + +// displays the complete type builder state in a clear, hierarchical format +// this is the top-level representation that shows all defined types +// +// +// 1. starts with "TypeBuilder(" to identify the structure +// 2. contains two main sections: Classes and Enums +// 3. each section is properly indented and bracketed +// 4. empty sections are omitted for conciseness +// +// the structure maintains consistent formatting: +// - each class and enum starts on a new line +// - proper indentation shows the hierarchy +// - commas separate multiple items +// - empty classes/enums are shown with empty braces +// +// example of the complete format: +// TypeBuilder( +// Classes: [ +// User { +// name set (alias='username'), +// email set (required=true) +// }, +// Address { } +// ], +// Enums: [ +// Status { +// ACTIVE (alias='active'), +// PENDING, +// INACTIVE (enabled=false) +// } +// ] +// ) +// +// this format makes it easy to: +// - understand the overall structure of defined types +// - see relationships between classes and their properties +// - identify enum values and their metadata +// - spot any missing or incomplete definitions +impl fmt::Display for TypeBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let classes = self.classes.lock().unwrap(); + let enums = self.enums.lock().unwrap(); + + writeln!(f, "TypeBuilder(")?; + + if !classes.is_empty() { + write!(f, " Classes: [")?; + for (i, (name, cls)) in classes.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "\n {} {}", name, cls.lock().unwrap())?; + } + write!(f, "\n ]")?; + } + + if !enums.is_empty() { + if !classes.is_empty() { + write!(f, ",")?; + } + write!(f, "\n Enums: [")?; + for (i, (name, e)) in enums.iter().enumerate() { + if i > 0 { + write!(f, ",")?; + } + write!(f, "\n {} {}", name, e.lock().unwrap())?; + } + write!(f, "\n ]")?; + } + + write!(f, "\n)") + } +} + #[derive(Clone)] pub struct TypeBuilder { classes: Arc>>>>, @@ -299,15 +482,165 @@ mod tests { #[test] fn test_type_builder() { let builder = TypeBuilder::new(); - let cls = builder.class("Person"); - let property = cls.lock().unwrap().property("name"); - property.lock().unwrap().r#type(FieldType::string()); - cls.lock() - .unwrap() - .property("age") - .lock() - .unwrap() - .r#type(FieldType::int()) - .with_meta("alias", BamlValue::String("years".to_string())); + + // Add a class with properties and metadata + let cls = builder.class("User"); + { + let cls = cls.lock().unwrap(); + // Add name property with alias and description + cls.property("name") + .lock() + .unwrap() + .r#type(FieldType::string()) + .with_meta("alias", BamlValue::String("username".to_string())) + .with_meta("description", BamlValue::String("The user's full name".to_string())); + + // Add age property with description only + cls.property("age") + .lock() + .unwrap() + .r#type(FieldType::int()) + .with_meta("description", BamlValue::String("User's age in years".to_string())); + + // Add email property with no metadata + cls.property("email") + .lock() + .unwrap() + .r#type(FieldType::string()); + } + + // Add an enum with values and metadata + let enm = builder.r#enum("Status"); + { + let enm = enm.lock().unwrap(); + // Add ACTIVE value with alias and description + enm.value("ACTIVE") + .lock() + .unwrap() + .with_meta("alias", BamlValue::String("active".to_string())) + .with_meta("description", BamlValue::String("User is active".to_string())); + + // Add INACTIVE value with alias only + enm.value("INACTIVE") + .lock() + .unwrap() + .with_meta("alias", BamlValue::String("inactive".to_string())); + + // Add PENDING value with no metadata + enm.value("PENDING"); + } + + // Convert to string and verify the format + let output = builder.to_string(); + assert_eq!( + output, + "TypeBuilder(\n Classes: [\n User {\n name set (alias='username', description='The user's full name'),\n age set (description='User's age in years'),\n email set\n }\n ],\n Enums: [\n Status {\n ACTIVE (alias='active', description='User is active'),\n INACTIVE (alias='inactive'),\n PENDING\n }\n ]\n)" + ); + } + +// my paranoia kicked in, so this test is to ensure that the string representation is correct +// and that the to_overrides method is working as expected + + #[test] + fn test_type_builder_advanced() { + let builder = TypeBuilder::new(); + + // 1. Complex class with nested types and all field types + let address = builder.class("Address"); + { + let address = address.lock().unwrap(); + // String with all metadata + address.property("street") + .lock() + .unwrap() + .r#type(FieldType::string()) + .with_meta("alias", BamlValue::String("streetAddress".to_string())) + .with_meta("description", BamlValue::String("Street address including number".to_string())); + + // Optional int with description + address.property("unit") + .lock() + .unwrap() + .r#type(FieldType::int().as_optional()) + .with_meta("description", BamlValue::String("Apartment/unit number if applicable".to_string())); + + // List of strings with alias + address.property("tags") + .lock() + .unwrap() + .r#type(FieldType::string().as_list()) + .with_meta("alias", BamlValue::String("labels".to_string())); + + // Boolean with no metadata + address.property("is_primary") + .lock() + .unwrap() + .r#type(FieldType::bool()); + + // Float with skip metadata + address.property("coordinates") + .lock() + .unwrap() + .r#type(FieldType::float()) + .with_meta("skip", BamlValue::Bool(true)); + } + + // 2. Empty class + builder.class("EmptyClass"); + + // 3. Complex enum with various metadata combinations + let priority = builder.r#enum("Priority"); + { + let priority = priority.lock().unwrap(); + // All metadata + priority.value("HIGH") + .lock() + .unwrap() + .with_meta("alias", BamlValue::String("urgent".to_string())) + .with_meta("description", BamlValue::String("Needs immediate attention".to_string())) + .with_meta("skip", BamlValue::Bool(false)); + + // Only description + priority.value("MEDIUM") + .lock() + .unwrap() + .with_meta("description", BamlValue::String("Standard priority".to_string())); + + // Only skip + priority.value("LOW") + .lock() + .unwrap() + .with_meta("skip", BamlValue::Bool(true)); + + // No metadata + priority.value("NONE"); + } + + // 4. Empty enum + builder.r#enum("EmptyEnum"); + + // Test string representation + let output = builder.to_string(); + assert_eq!( + output, + "TypeBuilder(\n Classes: [\n Address {\n street set (alias='streetAddress', description='Street address including number'),\n unit set (description='Apartment/unit number if applicable'),\n tags set (alias='labels'),\n is_primary set,\n coordinates set (skip=true)\n },\n EmptyClass {}\n ],\n Enums: [\n Priority {\n HIGH (alias='urgent', description='Needs immediate attention', skip=false),\n MEDIUM (description='Standard priority'),\n LOW (skip=true),\n NONE\n },\n EmptyEnum {}\n ]\n)" + ); + + // Test to_overrides() + let (classes, enums) = builder.to_overrides(); + + // Verify class overrides + assert_eq!(classes.len(), 2); + let address_override = classes.get("Address").unwrap(); + assert_eq!(address_override.new_fields.len(), 5); // All fields are new + assert!(address_override.new_fields.get("street").unwrap().1.alias.is_some()); + assert!(address_override.new_fields.get("coordinates").unwrap().1.skip.unwrap()); + + // Verify enum overrides + assert_eq!(enums.len(), 2); + let priority_override = enums.get("Priority").unwrap(); + assert_eq!(priority_override.values.len(), 4); + assert!(priority_override.values.get("HIGH").unwrap().alias.is_some()); + assert!(priority_override.values.get("LOW").unwrap().skip.unwrap()); } } diff --git a/engine/language_client_python/python_src/baml_py/type_builder.py b/engine/language_client_python/python_src/baml_py/type_builder.py index ff293a76e..d59476d38 100644 --- a/engine/language_client_python/python_src/baml_py/type_builder.py +++ b/engine/language_client_python/python_src/baml_py/type_builder.py @@ -15,19 +15,58 @@ def __init__(self, classes: typing.Set[str], enums: typing.Set[str]): self.__enums = enums self.__tb = _TypeBuilder() + def __str__(self) -> str: + """ + returns a comprehensive string representation of the typebuilder. + + this method provides a detailed view of the entire type hierarchy, + using the rust implementation to ensure compatibility. + + Format: + TypeBuilder( + Classes: [ + ClassName { + property_name type (alias='custom_name', desc='property description'), + another_property type (desc='another description'), + simple_property type + }, + EmptyClass { } + ], + Enums: [ + EnumName { + VALUE (alias='custom_value', desc='value description'), + ANOTHER_VALUE (alias='custom'), + SIMPLE_VALUE + }, + EmptyEnum { } + ] + ) + + the representation includes: + - complete class hierarchy with properties + - property types and their metadata (aliases, descriptions) + - full enum definitions with values + - enum value metadata (aliases, descriptions) + - empty classes and enums are shown explicitly + + returns: + str: the formatted string representation of the typebuilder + """ + return str(self._tb) + @property def _tb(self) -> _TypeBuilder: return self.__tb def string(self): return self._tb.string() - + def literal_string(self, value: str): return self._tb.literal_string(value) - + def literal_int(self, value: int): return self._tb.literal_int(value) - + def literal_bool(self, value: bool): return self._tb.literal_bool(value) diff --git a/engine/language_client_python/src/types/type_builder.rs b/engine/language_client_python/src/types/type_builder.rs index 4e9d698a3..60d6dc8b1 100644 --- a/engine/language_client_python/src/types/type_builder.rs +++ b/engine/language_client_python/src/types/type_builder.rs @@ -35,6 +35,23 @@ impl TypeBuilder { type_builder::TypeBuilder::new().into() } + /// provides a detailed string representation of the typebuilder for python users. + /// + /// this method exposes the rust-implemented string formatting to python, ensuring + /// consistent and professional output across both languages. the representation + /// includes a complete view of: + /// + /// * all defined classes with their properties + /// * all defined enums with their values + /// * metadata such as aliases and descriptions + /// * type information for properties + /// + /// the output format is carefully structured for readability, making it quite easy :D + /// to understand the complete type hierarchy at a glance. + pub fn __str__(&self) -> String { + self.inner.to_string() + } + pub fn r#enum(&self, name: &str) -> EnumBuilder { EnumBuilder { inner: self.inner.r#enum(name), diff --git a/engine/language_client_ruby/ext/ruby_ffi/src/types/type_builder.rs b/engine/language_client_ruby/ext/ruby_ffi/src/types/type_builder.rs index 1bbd8a429..2905fd1d6 100644 --- a/engine/language_client_ruby/ext/ruby_ffi/src/types/type_builder.rs +++ b/engine/language_client_ruby/ext/ruby_ffi/src/types/type_builder.rs @@ -113,10 +113,20 @@ impl TypeBuilder { .into()) } + // this implements ruby's friendly to_s method for converting objects to strings + // when someone calls .to_s on a typebuilder in ruby, this method gets called + // under the hood, it uses rust's display trait to format everything nicely + // by using the same display logic across languages, we keep things consistent + // this helps make debugging and logging work the same way everywhere :D + pub fn to_s(&self) -> String { + self.inner.to_string() + } + pub fn define_in_ruby(module: &RModule) -> Result<()> { let cls = module.define_class("TypeBuilder", class::object())?; cls.define_singleton_method("new", function!(TypeBuilder::new, 0))?; + cls.define_method("to_s", method!(TypeBuilder::to_s, 0))?; cls.define_method("enum", method!(TypeBuilder::r#enum, 1))?; // "class" is used by Kernel: https://ruby-doc.org/core-3.0.2/Kernel.html#method-i-class cls.define_method("class_", method!(TypeBuilder::class, 1))?; diff --git a/engine/language_client_ruby/test/type_builder_test.rb b/engine/language_client_ruby/test/type_builder_test.rb new file mode 100644 index 000000000..3c75dff04 --- /dev/null +++ b/engine/language_client_ruby/test/type_builder_test.rb @@ -0,0 +1,81 @@ +require 'test/unit' +require 'baml' + +# this test suite verifies that our type builder system correctly handles +# string representations of complex types like classes and enums. this is +# important for debugging and logging purposes, as it helps +# understand the structure of their type definitions at runtime. +class TypeBuilderTest < Test::Unit::TestCase + + # tests that class definitions are properly stringified with all their + # properties and metadata intact. this helps ensure our type system + # maintains semantic meaning when displayed to users. + def test_class_string_representation + # start with a fresh type builder - this is our main interface + # for constructing type definitions programmatically + builder = Baml::Ffi::TypeBuilder.new + + # create a new user class - this represents a person in our system + # with various attributes that describe them + user_class = builder.class_('User') + + # define the core properties that make up a user profile + # we use aliases and descriptions to make the api more human-friendly + user_class.property('name') + .alias('username') # allows 'username' as an alternative way to reference this + .description('The user\'s full name') # helps explain the purpose + + user_class.property('age') + .description('User\'s age in years') # clarifies the expected format + + user_class.property('email') # sometimes a property name is self-explanatory + + # convert our type definition to a human-readable string + # this is invaluable for debugging and documentation + output = builder.to_s + puts "\nClass output:\n#{output}\n" + + # verify that the string output matches our expectations + # we check for key structural elements and metadata + assert_match(/TypeBuilder\(Classes: \[User \{/, output) + assert_match(/name unset \(alias='username', desc='The user's full name'\)/, output) + assert_match(/age unset \(desc='User's age in years'\)/, output) + assert_match(/email unset/, output) + end + + # tests that enum definitions are correctly stringified with their + # values and associated metadata. enums help us model fixed sets + # of options in a type-safe way. + def test_enum_string_representation + # create a fresh builder for our enum definition + builder = Baml::Ffi::TypeBuilder.new + + # define a status enum to track user account states + # this gives us a type-safe way to handle different user situations + status_enum = builder.enum('Status') + + # add the possible status values with helpful metadata + # active users are currently using the system + status_enum.value('ACTIVE') + .alias('active') # lowercase alias for more natural usage + .description('User is active') # explains the meaning + + # inactive users have temporarily stopped using the system + status_enum.value('INACTIVE') + .alias('inactive') + + # pending users are in a transitional state + status_enum.value('PENDING') + + # generate a readable version of our enum definition + output = builder.to_s + puts "\nEnum output:\n#{output}\n" + + # verify the string representation includes all our carefully + # defined values and their metadata + assert_match(/TypeBuilder\(Enums: \[Status \{/, output) + assert_match(/ACTIVE \(alias='active', desc='User is active'\)/, output) + assert_match(/INACTIVE \(alias='inactive'\)/, output) + assert_match(/PENDING/, output) + end +end \ No newline at end of file diff --git a/engine/language_client_typescript/__test__/type_builder.test.ts b/engine/language_client_typescript/__test__/type_builder.test.ts new file mode 100644 index 000000000..b8fd34666 --- /dev/null +++ b/engine/language_client_typescript/__test__/type_builder.test.ts @@ -0,0 +1,69 @@ +// import the typescript wrapper for the type builder that provides a clean interface +// over the native rust implementation +import { TypeBuilder } from '../src/types/type_builder'; + +describe('TypeBuilder', () => { + // test that we can create classes with properties and add metadata like aliases and descriptions + it('should provide string representation for classes with properties and metadata', () => { + // create a fresh type builder instance to work with + const builder = new TypeBuilder(); + + // get a reference to a class named 'user', creating it if needed + const userClass = builder.getClass('User'); + + // add properties to the user class with helpful metadata + // the name property has both an alias and description + userClass.property('name') + .alias('username') // allows referencing the property as 'username' + .description('the user\'s full name'); // explains what this property represents + + // age property just has a description + userClass.property('age') + .description('user\'s age in years'); // clarifies the age units + + // email is a basic property with no extra metadata + userClass.property('email'); // simple email field + + // convert all the type definitions to a readable string + const output = builder.toString(); + + // make sure the output has the expected class structure + expect(output).toContain('TypeBuilder(Classes: [User {'); + // verify each property appears with its metadata + expect(output).toContain('name unset (alias=\'username\', desc=\'the user\'s full name\')'); + expect(output).toContain('age unset (desc=\'user\'s age in years\')'); + expect(output).toContain('email unset'); + }); + + // test that we can create enums with values and add metadata like aliases and descriptions + it('should provide string representation for enums with values and metadata', () => { + // create a fresh type builder instance to work with + const builder = new TypeBuilder(); + + // get a reference to an enum named 'status', creating it if needed + const statusEnum = builder.getEnum('Status'); + + // add possible values to the status enum with helpful metadata + // active state has both an alias and description + statusEnum.value('ACTIVE') + .alias('active') // allows using lowercase 'active' + .description('user is active'); // explains what active means + + // inactive state just has an alias + statusEnum.value('INACTIVE') + .alias('inactive'); // allows using lowercase 'inactive' + + // pending is a basic value with no extra metadata + statusEnum.value('PENDING'); // simple pending state + + // convert all the type definitions to a readable string + const output = builder.toString(); + + // make sure the output has the expected enum structure + expect(output).toContain('TypeBuilder(Enums: [Status {'); + // verify each value appears with its metadata + expect(output).toContain('ACTIVE (alias=\'active\', desc=\'user is active\')'); + expect(output).toContain('INACTIVE (alias=\'inactive\')'); + expect(output).toContain('PENDING'); + }); +}); \ No newline at end of file diff --git a/engine/language_client_typescript/jest.config.js b/engine/language_client_typescript/jest.config.js new file mode 100644 index 000000000..f637b5c99 --- /dev/null +++ b/engine/language_client_typescript/jest.config.js @@ -0,0 +1,23 @@ +/** + * this is our jest configuration for running typescript tests + * we use ts-jest to handle typescript compilation and testing + * @type {import('ts-jest').JestConfigWithTsJest} + */ +module.exports = { + // use the ts-jest preset which handles typescript files + preset: 'ts-jest', + + // run tests in a node environment rather than jsdom + testEnvironment: 'node', + + // look for both typescript and javascript files + moduleFileExtensions: ['ts', 'js'], + + // use ts-jest to transform typescript files before running tests + transform: { + '^.+\\.ts$': 'ts-jest', + }, + + // look for test files in __test__ directories that end in .test.ts + testMatch: ['**/__test__/**/*.test.ts'], +}; \ No newline at end of file diff --git a/engine/language_client_typescript/native.d.ts b/engine/language_client_typescript/native.d.ts index 5b34ee358..e937708ac 100644 --- a/engine/language_client_typescript/native.d.ts +++ b/engine/language_client_typescript/native.d.ts @@ -113,6 +113,7 @@ export declare class TypeBuilder { null(): FieldType map(key: FieldType, value: FieldType): FieldType union(types: Array): FieldType + toString(): string } export interface BamlLogEvent { diff --git a/engine/language_client_typescript/package.json b/engine/language_client_typescript/package.json index ce111573d..3baf21671 100644 --- a/engine/language_client_typescript/package.json +++ b/engine/language_client_typescript/package.json @@ -64,14 +64,17 @@ "format:biome": "biome --write .", "format:rs": "cargo fmt", "prepublishOnly": "napi prepublish --no-gh-release", - "test": "echo no tests implemented", + "test": "jest --config jest.config.js", "version": "napi version" }, "devDependencies": { "@biomejs/biome": "^1.7.3", "@napi-rs/cli": "3.0.0-alpha.62", + "@types/jest": "^29.5.14", "@types/node": "^20.12.11", + "jest": "^29.7.0", "npm-run-all2": "^6.1.2", + "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "typescript": "^5.4.5" }, @@ -83,5 +86,19 @@ "author": "", "dependencies": { "@scarf/scarf": "^1.3.0" + }, + "jest": { + "preset": "ts-jest", + "testEnvironment": "node", + "moduleFileExtensions": [ + "ts", + "js" + ], + "transform": { + "^.+\\.ts$": "ts-jest" + }, + "testMatch": [ + "**/__test__/**/*.test.ts" + ] } } diff --git a/engine/language_client_typescript/src/types/type_builder.rs b/engine/language_client_typescript/src/types/type_builder.rs index 2074f62c1..7dad78977 100644 --- a/engine/language_client_typescript/src/types/type_builder.rs +++ b/engine/language_client_typescript/src/types/type_builder.rs @@ -1,28 +1,65 @@ +// This file provides the native bindings between our Rust implementation and TypeScript +// We use NAPI-RS to expose Rust functionality to JavaScript/TypeScript use baml_runtime::type_builder::{self, WithMeta}; use baml_types::BamlValue; use napi_derive::napi; +// Create TypeScript-compatible wrappers for our Rust types +// These macros generate the necessary code for TypeScript interop crate::lang_wrapper!(TypeBuilder, type_builder::TypeBuilder); + +// Thread-safe wrapper for EnumBuilder with name tracking +// The sync_thread_safe attribute ensures safe concurrent access from TypeScript crate::lang_wrapper!(EnumBuilder, type_builder::EnumBuilder, sync_thread_safe, name: String); + +// Thread-safe wrapper for ClassBuilder with name tracking +// Enables safe TypeScript interop with class definitions crate::lang_wrapper!(ClassBuilder, type_builder::ClassBuilder, sync_thread_safe, name: String); + +// Thread-safe wrapper for EnumValueBuilder +// Ensures enum value definitions can be safely accessed across threads crate::lang_wrapper!( EnumValueBuilder, type_builder::EnumValueBuilder, sync_thread_safe ); + +// Thread-safe wrapper for ClassPropertyBuilder +// Enables concurrent access to class property definitions crate::lang_wrapper!( ClassPropertyBuilder, type_builder::ClassPropertyBuilder, sync_thread_safe ); + +// Thread-safe wrapper for FieldType +// Core type system representation with thread-safety guarantees crate::lang_wrapper!(FieldType, baml_types::FieldType, sync_thread_safe); +// Implement Default for TypeBuilder to allow easy instantiation +// This enables idiomatic Rust usage while maintaining TypeScript compatibility impl Default for TypeBuilder { fn default() -> Self { Self::new() } } + +// note: you may notice a rust-analyzer warning in vs code when working with this file. +// the warning "did not find struct napitypebuilder parsed before expand #[napi] for impl" +// is a known false positive that occurs due to how rust-analyzer processes macro state. +// +// don't worry - the code compiles and works correctly! the warning is yet to be addressed by napi maintainers. +// +// if you'd like to hide this warning in vs code, you can add this to your settings.json: +// "rust-analyzer.diagnostics.disabled": ["macro-error"] +// +// ref: +// https://github.com/napi-rs/napi-rs/issues/1630 + + + + #[napi] impl TypeBuilder { #[napi(constructor)] @@ -115,6 +152,11 @@ impl TypeBuilder { ) .into() } + + #[napi] + pub fn to_string(&self) -> String { + self.inner.to_string() + } } #[napi] diff --git a/engine/language_client_typescript/src/types/type_builder.ts b/engine/language_client_typescript/src/types/type_builder.ts new file mode 100644 index 000000000..eee6c70b1 --- /dev/null +++ b/engine/language_client_typescript/src/types/type_builder.ts @@ -0,0 +1,43 @@ +// import the native typebuilder that was compiled from rust code using napi-rs +// this provides the core functionality that our typescript wrapper will use +import { TypeBuilder as NativeTypeBuilder } from '../../native'; + +export class TypeBuilder { + // holds the instance of the native rust implementation + // the native instance handles all the actual type building logic + // we just provide a nice typescript interface on top + private native: NativeTypeBuilder; + + constructor() { + // instantiate a new native typebuilder when this wrapper is created + // this sets up the underlying rust state that we'll delegate to + this.native = new NativeTypeBuilder(); + } + + // creates a new class definition or returns an existing one with the given name + // we renamed this from addclass/class_ to getclass to better match what the native api expects + // this is used to define the structure and properties of classes in the type system + getClass(name: string) { + // pass the class creation request through to the native rust implementation + // the rust code handles all the details of managing the class definition + return this.native.getClass(name); + } + + // creates a new enum definition or returns an existing one with the given name + // we renamed this from addenum/enum to getenum to better match what the native api expects + // this is used to define enums with their possible values and metadata + getEnum(name: string) { + // delegate enum creation to the native rust implementation + // the rust code manages the enum definition and its allowed values + return this.native.getEnum(name); + } + + // converts the entire type definition to a human-readable string representation + // useful for debugging and seeing the full structure of defined types + toString(): string { + // let the native rust code generate the string representation + // it will include all classes and enums with their properties, values and metadata + // formatted in a consistent way for easy reading + return this.native.toString(); + } +} \ No newline at end of file diff --git a/integ-tests/python/test_type_builder_str.py b/integ-tests/python/test_type_builder_str.py new file mode 100644 index 000000000..fa93b8924 --- /dev/null +++ b/integ-tests/python/test_type_builder_str.py @@ -0,0 +1,78 @@ +import pytest +from baml_client.type_builder import TypeBuilder + +def test_type_builder_str(): + """ + test for typebuilder's string representation functionality. + + this test verifies that the typebuilder correctly represents its structure + in string format, ensuring proper exposure of the rust implementation to python. + + test coverage: + ------------- + 1. class representation: + - class names and structure + - property definitions with types + - property metadata: + * aliases for alternative naming + * descriptions for documentation + + 2. enum representation: + - enum names and structure + - value definitions + - value metadata: + * aliases for alternative naming + * descriptions for documentation + + 3. cross-language integration: + - verifies that the rust string representation is correctly + exposed through the python bindings + - ensures consistent formatting across language boundaries + """ + # Create a new TypeBuilder + tb = TypeBuilder() + + # Add a class with properties and metadata + user = tb.class_("User") + name_prop = user.property("name") + name_prop.type(tb.string()) + name_prop.with_meta("alias", "username") + name_prop.with_meta("description", "The user's full name") + + age_prop = user.property("age") + age_prop.type(tb.int()) + age_prop.with_meta("description", "User's age in years") + + email_prop = user.property("email") + email_prop.type(tb.string()) + + # Add an enum with values and metadata + status = tb.enum("Status") + active = status.value("ACTIVE") + active.with_meta("alias", "active") + active.with_meta("description", "User is active") + + inactive = status.value("INACTIVE") + inactive.with_meta("alias", "inactive") + + status.value("PENDING") + + # Convert to string and verify the format + output = str(tb) + print(f"TypeBuilder string representation:\n{output}") + + # Verify the expected format + assert "User" in output + assert "name" in output + assert "username" in output + assert "The user's full name" in output + assert "age" in output + assert "User's age in years" in output + assert "email" in output + assert "Status" in output + assert "ACTIVE" in output + assert "active" in output + assert "User is active" in output + assert "INACTIVE" in output + assert "inactive" in output + assert "PENDING" in output \ No newline at end of file