From 37c26da28ee63fd8f3aafd813bce9dad48a75110 Mon Sep 17 00:00:00 2001 From: aq Date: Tue, 24 Dec 2024 11:34:49 +0400 Subject: [PATCH] feat: implement string representation for TypeBuilder - Add Display implementation for TypeBuilder and related structs - Add Python bindings for string representation - Add comprehensive test cases for both Rust and Python - Add detailed documentation for string formatting --- engine/baml-lib/baml-types/src/lib.rs | 9 + engine/baml-runtime/src/type_builder/mod.rs | 329 ++++++++++++++++-- .../python_src/baml_py/type_builder.py | 45 ++- .../src/types/type_builder.rs | 17 + integ-tests/python/test_type_builder_str.py | 78 +++++ 5 files changed, 442 insertions(+), 36 deletions(-) create mode 100644 integ-tests/python/test_type_builder_str.py 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..badc404dd 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,150 @@ 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)? +impl fmt::Display for ClassPropertyBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let meta = self.meta.lock().unwrap(); + let alias = meta.get("alias").and_then(|v| v.as_string()); + let desc = meta.get("description").and_then(|v| v.as_string()); + + write!(f, "{}", self.r#type.lock().unwrap().as_ref().map_or("unset", |_| "set"))?; + if let Some(alias) = alias { + write!(f, " (alias='{}'", alias)?; + if let Some(desc) = desc { + write!(f, ", desc='{}'", desc)?; } - Err(_) => writeln!(f, "Cannot acquire lock,")?, + write!(f, ")")?; + } else if let Some(desc) = desc { + write!(f, " (desc='{}')", desc)?; } + 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)? +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() { + write!(f, " ")?; + for (i, (name, prop)) in properties.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{} {}", name, prop.lock().unwrap())?; } - Err(_) => writeln!(f, "Cannot acquire lock,")?, + write!(f, " ")?; } + write!(f, "}}") + } +} + +impl fmt::Display for EnumValueBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let meta = self.meta.lock().unwrap(); + let alias = meta.get("alias").and_then(|v| v.as_string()); + let desc = meta.get("description").and_then(|v| v.as_string()); + + if let Some(alias) = alias { + write!(f, " (alias='{}'", alias)?; + if let Some(desc) = desc { + write!(f, ", desc='{}'", desc)?; + } + write!(f, ")")?; + } else if let Some(desc) = desc { + write!(f, " (desc='{}')", desc)?; + } + Ok(()) + } +} - // Close the struct printout +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() { + write!(f, " ")?; + for (i, (name, value)) in values.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{}{}", name, value.lock().unwrap())?; + } + write!(f, " ")?; + } write!(f, "}}") } } +/// implements a string representation for typebuilder. +/// +/// this implementation provides a clear, hierarchical view of the typebuilder's structure, +/// making it easy to understand the defined types and their metadata at a glance. +/// +/// # Format +/// ```text +/// 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 { } +/// ] +/// ) +/// ``` +/// +/// # properties shown +/// - class and property names +/// - property types (set/unset) +/// - property metadata (aliases, descriptions) +/// - enum values and their metadata +/// - empty classes and enums +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(); + + write!(f, "TypeBuilder(")?; + + if !classes.is_empty() { + write!(f, "Classes: [")?; + for (i, (name, cls)) in classes.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{} {}", name, cls.lock().unwrap())?; + } + write!(f, "]")?; + } + + if !enums.is_empty() { + if !classes.is_empty() { + write!(f, ", ")?; + } + write!(f, "Enums: [")?; + for (i, (name, e)) in enums.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{} {}", name, e.lock().unwrap())?; + } + write!(f, "]")?; + } + + write!(f, ")") + } +} + #[derive(Clone)] pub struct TypeBuilder { classes: Arc>>>>, @@ -299,15 +411,166 @@ 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(Classes: [User { name set (alias='username', desc='The user\'s full name'), age set (desc='User\'s age in years'), email set }], Enums: [Status { ACTIVE (alias='active', desc='User is active'), INACTIVE (alias='inactive'), PENDING }])" + ); + } + + +// my paranoia kicked in, so tis 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(Classes: [Address { street set (alias='streetAddress', desc='Street address including number'), unit set (desc='Apartment/unit number if applicable'), tags set (alias='labels'), is_primary set, coordinates set }, EmptyClass {}], Enums: [Priority { HIGH (alias='urgent', desc='Needs immediate attention'), MEDIUM (desc='Standard priority'), LOW, NONE }, EmptyEnum {}])" + ); + + // 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/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