Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(python): show properties and values in TypeBuilder string representation #1260

Merged
merged 14 commits into from
Feb 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
521 changes: 513 additions & 8 deletions engine/baml-runtime/src/type_builder/mod.rs

Large diffs are not rendered by default.

38 changes: 35 additions & 3 deletions engine/language_client_python/python_src/baml_py/type_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,51 @@ 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 { }
]
)

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)

Expand Down
17 changes: 17 additions & 0 deletions engine/language_client_python/src/types/type_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
10 changes: 10 additions & 0 deletions engine/language_client_ruby/ext/ruby_ffi/src/types/type_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))?;
Expand Down
81 changes: 81 additions & 0 deletions engine/language_client_ruby/test/type_builder_test.rb
Original file line number Diff line number Diff line change
@@ -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\(\n Classes: \[\n User \{/, output)
assert_match(/name \(unknown-type\) \(alias=String\("username"\), description=String\("The user's full name"\)\)/, output)
assert_match(/age \(unknown-type\) \(description=String\("User's age in years"\)\)/, output)
assert_match(/email \(unknown-type\)/, 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\(\n Enums: \[\n Status \{/, output)
assert_match(/ACTIVE \(alias=String\("active"\), description=String\("User is active"\)\)/, output)
assert_match(/INACTIVE \(alias=String\("inactive"\)\)/, output)
assert_match(/PENDING/, output)
end
end
72 changes: 72 additions & 0 deletions engine/language_client_typescript/__test__/type_builder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// import the typescript wrapper for the type builder that provides a clean interface
// over the native rust implementation
import { TypeBuilder } from '../native';

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(\n Classes: [\n User {');
// verify each property appears with its metadata
expect(output).toContain("name (unknown-type) (alias=String(\"username\"), description=String(\"the user's full name\"))");
expect(output).toContain("age (unknown-type) (description=String(\"user's age in years\"))");
expect(output).toContain('email (unknown-type)');
});

// 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
console.log(output);
expect(output).toContain(`TypeBuilder(
Enums: [
Status {`);
// verify each value appears with its metadata
expect(output).toContain('ACTIVE (alias=String(\"active\"), description=String(\"user is active\"))');
expect(output).toContain('INACTIVE (alias=String(\"inactive\"))');
expect(output).toContain('PENDING');
});
});
23 changes: 23 additions & 0 deletions engine/language_client_typescript/jest.config.js
Original file line number Diff line number Diff line change
@@ -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'],
};
1 change: 1 addition & 0 deletions engine/language_client_typescript/native.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export declare class TypeBuilder {
null(): FieldType
map(key: FieldType, value: FieldType): FieldType
union(types: Array<FieldType>): FieldType
toString(): string
}

export interface BamlLogEvent {
Expand Down
19 changes: 18 additions & 1 deletion engine/language_client_typescript/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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"
]
}
}
Loading
Loading