use audb::schema::{EmbeddingConfig, Field, Schema, Type};
use proc_macro2::TokenStream;
use quote::{ToTokens, format_ident, quote};
use tessera::model_registry::get_model;
use crate::naming::collection_name;
use crate::relationships::{ForeignKeyInfo, detect_foreign_keys, find_primary_key};
pub struct SchemaGenerator {
pub with_serde: bool,
pub custom_derives: Vec<String>,
pub with_crud: bool,
}
impl SchemaGenerator {
pub fn new() -> Self {
Self {
with_serde: true,
custom_derives: Vec::new(),
with_crud: true,
}
}
pub fn without_crud(&mut self) -> &mut Self {
self.with_crud = false;
self
}
pub fn add_derive(&mut self, derive: String) -> &mut Self {
self.custom_derives.push(derive);
self
}
pub fn generate(&self, schema: &Schema) -> TokenStream {
let struct_def = self.generate_struct(schema);
if self.with_crud && self.with_serde {
let crud_impl = self.generate_crud_impl(schema);
let embedding_impl = self.generate_embedding_impl(schema);
quote! {
#struct_def
#crud_impl
#embedding_impl
}
} else {
struct_def
}
}
fn generate_struct(&self, schema: &Schema) -> TokenStream {
let struct_name = format_ident!("{}", schema.name);
let doc_comment = if let Some(ref doc) = schema.doc_comment {
let doc_lines: Vec<_> = doc.lines().map(|line| quote! { #[doc = #line] }).collect();
quote! { #(#doc_lines)* }
} else {
quote! {}
};
let derives = self.generate_derives();
let fields: Vec<_> = schema
.fields
.iter()
.map(|field| self.generate_field(field))
.collect();
quote! {
#doc_comment
#derives
pub struct #struct_name {
#(#fields)*
}
}
}
fn generate_crud_impl(&self, schema: &Schema) -> TokenStream {
let pk_field = match find_primary_key(schema) {
Some(field) => field,
None => {
return quote! {};
}
};
let struct_name = format_ident!("{}", schema.name);
let create_method = self.generate_create_method(schema, pk_field);
let get_method = self.generate_get_method(schema, pk_field);
let update_method = self.generate_update_method(schema, pk_field);
let delete_method = self.generate_delete_method(schema, pk_field);
let list_all_method = self.generate_list_all_method(schema, pk_field);
let foreign_keys = detect_foreign_keys(schema);
let relationship_methods = self.generate_relationship_helpers(&foreign_keys);
quote! {
impl #struct_name {
#create_method
#get_method
#update_method
#delete_method
#list_all_method
#(#relationship_methods)*
}
}
}
fn generate_embedding_impl(&self, schema: &Schema) -> TokenStream {
let embedding_fields: Vec<_> = schema
.fields
.iter()
.filter(|f| f.embedding_config.is_some())
.collect();
if embedding_fields.is_empty() {
return quote! {};
}
let struct_name = format_ident!("{}", schema.name);
let mut methods = Vec::new();
for field in embedding_fields {
let config = field.embedding_config.as_ref().unwrap();
if let Some(create_method) =
self.generate_create_with_embedding_method(schema, field, config)
{
methods.push(create_method);
}
if let Some(find_method) = self.generate_find_similar_method(schema, field, config) {
methods.push(find_method);
}
}
if methods.is_empty() {
return quote! {};
}
quote! {
impl #struct_name {
#(#methods)*
}
}
}
fn generate_create_with_embedding_method(
&self,
schema: &Schema,
embedding_field: &Field,
config: &EmbeddingConfig,
) -> Option<TokenStream> {
let model_info = match get_model(&config.model) {
Some(info) => info,
None => {
eprintln!(
"Warning: Model '{}' not found in Tessera registry",
config.model
);
return None;
}
};
let dimension = config
.dimension
.unwrap_or_else(|| model_info.embedding_dim.default_dim());
let pk_field = find_primary_key(schema)?;
let collection = collection_name(&schema.name);
let source_field_name = format_ident!("{}", config.source_field);
let embedding_field_name = format_ident!("{}", embedding_field.name);
let pk_name = format_ident!("{}", pk_field.name);
let model_id = &config.model;
let mut params = Vec::new();
let mut json_fields = Vec::new();
let mut struct_fields = Vec::new();
for field in &schema.fields {
if field.name == pk_field.name || field.name == embedding_field.name {
continue;
}
let field_name = format_ident!("{}", field.name);
let field_name_str = &field.name;
let field_type = self.convert_type(&field.field_type, field.nullable);
params.push(quote! { #field_name: #field_type });
json_fields.push(quote! { #field_name_str: #field_name });
struct_fields.push(quote! { #field_name });
}
Some(quote! {
pub fn create_with_embedding(
db: &audb_runtime::Database,
#(#params),*
) -> audb_runtime::Result<Self> {
use tessera::TesseraDense;
let embedder = TesseraDense::new(#model_id)
.map_err(|e| audb_runtime::QueryError::execution(format!("Failed to load embedding model: {}", e)))?;
let dense_embedding = embedder.encode(&#source_field_name)
.map_err(|e| audb_runtime::QueryError::execution(format!("Failed to generate embedding: {}", e)))?;
let #embedding_field_name: Vec<f32> = dense_embedding.embedding.to_vec();
let collection = db.collection(#collection)?;
let #pk_name = uuid::Uuid::new_v4();
let data = serde_json::json!({
stringify!(#pk_name): #pk_name,
#(#json_fields,)*
stringify!(#embedding_field_name): #embedding_field_name
});
collection.create_entity(#pk_name, data)?;
let vectors = collection.vectors::<#dimension>(stringify!(#embedding_field_name))?;
vectors.insert(&#pk_name, &#embedding_field_name)?;
Ok(Self {
#pk_name,
#(#struct_fields,)*
#embedding_field_name
})
}
})
}
fn generate_find_similar_method(
&self,
schema: &Schema,
embedding_field: &Field,
config: &EmbeddingConfig,
) -> Option<TokenStream> {
let model_info = match get_model(&config.model) {
Some(info) => info,
None => {
return None;
}
};
let dimension = config
.dimension
.unwrap_or_else(|| model_info.embedding_dim.default_dim());
let pk_field = find_primary_key(schema)?;
let collection = collection_name(&schema.name);
let embedding_field_name = format_ident!("{}", embedding_field.name);
let pk_name = format_ident!("{}", pk_field.name);
let model_id = &config.model;
Some(quote! {
pub fn find_similar(
db: &audb_runtime::Database,
query: &str,
limit: usize,
) -> audb_runtime::Result<Vec<Self>> {
use tessera::TesseraDense;
let embedder = TesseraDense::new(#model_id)
.map_err(|e| audb_runtime::QueryError::execution(format!("Failed to load embedding model: {}", e)))?;
let dense_embedding = embedder.encode(query)
.map_err(|e| audb_runtime::QueryError::execution(format!("Failed to generate query embedding: {}", e)))?;
let query_embedding: Vec<f32> = dense_embedding.embedding.to_vec();
let collection = db.collection(#collection)?;
let vectors = collection.vectors::<#dimension>(stringify!(#embedding_field_name))?;
let similar_ids = vectors.search_similar(&query_embedding, limit)?;
let mut results = Vec::new();
for id in similar_ids {
if let Some(entity) = Self::get(db, id)? {
results.push(entity);
}
}
Ok(results)
}
})
}
fn generate_create_method(&self, schema: &Schema, pk_field: &Field) -> TokenStream {
let _struct_name = format_ident!("{}", schema.name);
let collection = collection_name(&schema.name);
let mut params = Vec::new();
let mut json_fields = Vec::new();
let mut struct_fields = Vec::new();
for field in &schema.fields {
if field.name == pk_field.name {
continue; }
let field_name = format_ident!("{}", field.name);
let field_name_str = &field.name;
let field_type = self.convert_type(&field.field_type, field.nullable);
params.push(quote! { #field_name: #field_type });
json_fields.push(quote! { #field_name_str: #field_name });
struct_fields.push(quote! { #field_name });
}
let pk_name = format_ident!("{}", pk_field.name);
quote! {
pub fn create(
db: &audb_runtime::Database,
#(#params),*
) -> audb_runtime::Result<Self> {
let collection = db.collection(#collection)?;
let #pk_name = uuid::Uuid::new_v4();
let data = serde_json::json!({ #(#json_fields),* });
collection.create_entity(#pk_name, data)?;
Ok(Self {
#pk_name,
#(#struct_fields),*
})
}
pub fn create_from_json(
db: &audb_runtime::Database,
json: serde_json::Value,
) -> audb_runtime::Result<Self> {
let collection = db.collection(#collection)?;
let #pk_name = uuid::Uuid::new_v4();
let mut data = json;
if let serde_json::Value::Object(ref mut map) = data {
map.insert(stringify!(#pk_name).to_string(), serde_json::json!(#pk_name));
}
let entity: Self = serde_json::from_value(data.clone())
.map_err(|e| audb_runtime::QueryError::serialization(
format!("Invalid JSON for create: {}. Make sure all required fields are provided.", e)
))?;
collection.create_entity(#pk_name, data)?;
Ok(entity)
}
}
}
fn generate_get_method(&self, schema: &Schema, pk_field: &Field) -> TokenStream {
let _struct_name = format_ident!("{}", schema.name);
let collection = collection_name(&schema.name);
let pk_name = format_ident!("{}", pk_field.name);
quote! {
pub fn get(
db: &audb_runtime::Database,
#pk_name: uuid::Uuid
) -> audb_runtime::Result<Option<Self>> {
let collection = db.collection(#collection)?;
let json_opt = collection.get_entity(#pk_name)?;
match json_opt {
Some(json) => {
let entity = serde_json::from_value(json)
.map_err(|e| audb_runtime::QueryError::serialization(e.to_string()))?;
Ok(Some(entity))
}
None => Ok(None)
}
}
}
}
fn generate_update_method(&self, schema: &Schema, pk_field: &Field) -> TokenStream {
let collection = collection_name(&schema.name);
let pk_name = format_ident!("{}", pk_field.name);
quote! {
pub fn update(&self, db: &audb_runtime::Database) -> audb_runtime::Result<()> {
let collection = db.collection(#collection)?;
let data = serde_json::to_value(self)
.map_err(|e| audb_runtime::QueryError::serialization(e.to_string()))?;
collection.update_entity(self.#pk_name, data)
}
}
}
fn generate_delete_method(&self, schema: &Schema, pk_field: &Field) -> TokenStream {
let collection = collection_name(&schema.name);
let pk_name = format_ident!("{}", pk_field.name);
quote! {
pub fn delete(
db: &audb_runtime::Database,
#pk_name: uuid::Uuid
) -> audb_runtime::Result<()> {
let collection = db.collection(#collection)?;
collection.delete_entity(#pk_name)
}
}
}
fn generate_list_all_method(&self, schema: &Schema, _pk_field: &Field) -> TokenStream {
let struct_name = format_ident!("{}", schema.name);
let collection = collection_name(&schema.name);
quote! {
pub fn list_all(
db: &audb_runtime::Database
) -> audb_runtime::Result<Vec<Self>> {
let collection = db.collection(#collection)?;
let entities = collection.list_all_entities()?;
let mut results = Vec::new();
for (_id, data) in entities {
match serde_json::from_value(data) {
Ok(entity) => results.push(entity),
Err(e) => {
eprintln!("Warning: Failed to deserialize entity: {}", e);
}
}
}
Ok(results)
}
}
}
fn generate_relationship_helpers(&self, foreign_keys: &[ForeignKeyInfo]) -> Vec<TokenStream> {
foreign_keys
.iter()
.map(|fk| {
let method_name = format_ident!("{}", fk.method_name());
let target_type = format_ident!("{}", fk.target_schema);
let field_name = format_ident!("{}", fk.field_name);
if fk.nullable {
quote! {
pub fn #method_name(&self, db: &audb_runtime::Database) -> audb_runtime::Result<Option<#target_type>> {
match self.#field_name {
Some(id) => #target_type::get(db, id),
None => Ok(None)
}
}
}
} else {
quote! {
pub fn #method_name(&self, db: &audb_runtime::Database) -> audb_runtime::Result<Option<#target_type>> {
#target_type::get(db, self.#field_name)
}
}
}
})
.collect()
}
fn generate_derives(&self) -> TokenStream {
let mut derives = vec!["Debug".to_string(), "Clone".to_string()];
if self.with_serde {
derives.push("serde::Serialize".to_string());
derives.push("serde::Deserialize".to_string());
}
derives.extend(self.custom_derives.clone());
let derive_tokens: Vec<TokenStream> = derives
.iter()
.map(|d| {
d.parse::<TokenStream>()
.unwrap_or_else(|_| format_ident!("{}", d).to_token_stream())
})
.collect();
quote! {
#[derive(#(#derive_tokens),*)]
}
}
fn generate_field(&self, field: &Field) -> TokenStream {
let field_name = format_ident!("{}", field.name);
let doc_comment = if let Some(ref doc) = field.doc_comment {
quote! { #[doc = #doc] }
} else {
quote! {}
};
let field_type = self.convert_type(&field.field_type, field.nullable);
quote! {
#doc_comment
pub #field_name: #field_type,
}
}
fn convert_type(&self, typ: &Type, nullable: bool) -> TokenStream {
let base_type = match typ {
Type::String => quote! { String },
Type::Integer => quote! { i64 },
Type::Float => quote! { f64 },
Type::Boolean => quote! { bool },
Type::Timestamp => quote! { chrono::DateTime<chrono::Utc> },
Type::EntityId => quote! { uuid::Uuid },
Type::Enum(variants) => {
let _ = variants; quote! { String }
}
Type::Option(inner) => {
let inner_type = self.convert_type(inner, false);
return quote! { Option<#inner_type> };
}
Type::Vec(inner) => {
let inner_type = self.convert_type(inner, false);
let base = quote! { Vec<#inner_type> };
if nullable {
return quote! { Option<#base> };
}
return base;
}
Type::Custom(name) | Type::Named(name) => {
let ident = format_ident!("{}", name);
quote! { #ident }
}
Type::JsonValue => quote! { serde_json::Value },
Type::Vector => quote! { Vec<f32> },
Type::Unit => quote! { () },
};
if nullable {
quote! { Option<#base_type> }
} else {
base_type
}
}
pub fn generate_all(&self, schemas: &[&Schema]) -> TokenStream {
let schema_code: Vec<_> = schemas.iter().map(|s| self.generate(s)).collect();
quote! {
#(#schema_code)*
}
}
}
impl Default for SchemaGenerator {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use audb::schema::{Schema, SchemaFormat};
#[test]
fn test_generate_simple_schema() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub struct User"));
assert!(code_str.contains("pub id"));
assert!(code_str.contains("pub name"));
}
#[test]
fn test_generate_with_derives() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("Debug"));
assert!(code_str.contains("Clone"));
assert!(code_str.contains("serde :: Serialize"));
assert!(code_str.contains("serde :: Deserialize"));
}
#[test]
fn test_generate_nullable_field() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
let mut field = Field::new("email".to_string(), Type::String);
field.nullable = true;
schema.add_field(field);
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("Option"));
}
#[test]
fn test_generate_vec_field() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new(
"tags".to_string(),
Type::Vec(Box::new(Type::String)),
));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("Vec"));
}
#[test]
fn test_generate_custom_type() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new(
"author".to_string(),
Type::Custom("User".to_string()),
));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("User"));
}
#[test]
fn test_generate_with_doc_comment() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.doc_comment = Some("A user in the system".to_string());
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("A user in the system"));
}
#[test]
fn test_custom_derives() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let mut generator = SchemaGenerator::new();
generator.add_derive("PartialEq".to_string());
generator.add_derive("Eq".to_string());
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("PartialEq"));
assert!(code_str.contains("Eq"));
}
#[test]
fn test_generate_all() {
let mut user = Schema::new("User".to_string(), SchemaFormat::Native);
user.add_field(Field::new("id".to_string(), Type::EntityId));
let mut post = Schema::new("Post".to_string(), SchemaFormat::Native);
post.add_field(Field::new("id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate_all(&[&user, &post]);
let code_str = code.to_string();
assert!(code_str.contains("pub struct User"));
assert!(code_str.contains("pub struct Post"));
}
#[test]
fn test_type_mapping() {
let generator = SchemaGenerator::new();
let string_type = generator.convert_type(&Type::String, false);
assert_eq!(string_type.to_string(), "String");
let int_type = generator.convert_type(&Type::Integer, false);
assert_eq!(int_type.to_string(), "i64");
let bool_type = generator.convert_type(&Type::Boolean, false);
assert_eq!(bool_type.to_string(), "bool");
let nullable_string = generator.convert_type(&Type::String, true);
assert!(nullable_string.to_string().contains("Option"));
}
#[test]
fn test_no_serde() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let mut generator = SchemaGenerator::new();
generator.with_serde = false;
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(!code_str.contains("serde"));
}
#[test]
fn test_crud_generation_basic() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
schema.add_field(Field::new("email".to_string(), Type::String));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub struct User"));
assert!(code_str.contains("pub fn create"));
assert!(code_str.contains("pub fn get"));
assert!(code_str.contains("pub fn update"));
assert!(code_str.contains("pub fn delete"));
assert!(code_str.contains("\"users\""));
assert!(code_str.contains("audb_runtime :: Database"));
assert!(code_str.contains("audb_runtime :: Result"));
}
#[test]
fn test_crud_generation_with_nullable_fields() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("title".to_string(), Type::String));
let mut optional_field = Field::new("subtitle".to_string(), Type::String);
optional_field.nullable = true;
schema.add_field(optional_field);
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("subtitle : Option < String >"));
}
#[test]
fn test_crud_generation_without_primary_key() {
let mut schema = Schema::new("Simple".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("value".to_string(), Type::String));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub struct Simple"));
assert!(!code_str.contains("pub fn create"));
assert!(!code_str.contains("pub fn get"));
}
#[test]
fn test_crud_can_be_disabled() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
let mut generator = SchemaGenerator::new();
generator.without_crud();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub struct User"));
assert!(!code_str.contains("pub fn create"));
assert!(!code_str.contains("pub fn get"));
}
#[test]
fn test_foreign_key_relationship_helper() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("title".to_string(), Type::String));
schema.add_field(Field::new("author_id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub fn author"));
assert!(code_str.contains("Author :: get"));
}
#[test]
fn test_nullable_foreign_key_relationship() {
let mut schema = Schema::new("Post".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("title".to_string(), Type::String));
let mut editor_field = Field::new("editor_id".to_string(), Type::EntityId);
editor_field.nullable = true;
schema.add_field(editor_field);
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub fn editor"));
assert!(code_str.contains("match self . editor_id"));
}
#[test]
fn test_multiple_foreign_keys() {
let mut schema = Schema::new("Comment".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("post_id".to_string(), Type::EntityId));
schema.add_field(Field::new("author_id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("pub fn post"));
assert!(code_str.contains("pub fn author"));
assert!(code_str.contains("Post :: get"));
assert!(code_str.contains("Author :: get"));
}
#[test]
fn test_create_method_parameter_order() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
schema.add_field(Field::new("email".to_string(), Type::String));
schema.add_field(Field::new("age".to_string(), Type::Integer));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
let name_pos = code_str.find("name : String").unwrap();
let email_pos = code_str.find("email : String").unwrap();
let age_pos = code_str.find("age : i64").unwrap();
assert!(name_pos < email_pos);
assert!(email_pos < age_pos);
}
#[test]
fn test_collection_name_pluralization() {
let test_cases = vec![
("User", "users"),
("Post", "posts"),
("Category", "categories"),
("Person", "people"),
];
for (schema_name, expected_collection) in test_cases {
let mut schema = Schema::new(schema_name.to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
let search_str = format!("\"{}\"", expected_collection);
assert!(
code_str.contains(&search_str),
"Expected collection name '{}' for schema '{}', but not found in generated code",
expected_collection,
schema_name
);
}
}
#[test]
fn test_doc_comments_in_crud_methods() {
let mut schema = Schema::new("User".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("name".to_string(), Type::String));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("Create a new entity"));
assert!(code_str.contains("Get an entity by ID"));
assert!(code_str.contains("Update this entity"));
assert!(code_str.contains("Delete an entity"));
assert!(code_str.contains("# Errors"));
}
#[test]
fn test_all_type_variants_in_create() {
let mut schema = Schema::new("Complex".to_string(), SchemaFormat::Native);
schema.add_field(Field::new("id".to_string(), Type::EntityId));
schema.add_field(Field::new("text".to_string(), Type::String));
schema.add_field(Field::new("count".to_string(), Type::Integer));
schema.add_field(Field::new("price".to_string(), Type::Float));
schema.add_field(Field::new("active".to_string(), Type::Boolean));
schema.add_field(Field::new(
"tags".to_string(),
Type::Vec(Box::new(Type::String)),
));
let generator = SchemaGenerator::new();
let code = generator.generate(&schema);
let code_str = code.to_string();
assert!(code_str.contains("text : String"));
assert!(code_str.contains("count : i64"));
assert!(code_str.contains("price : f64"));
assert!(code_str.contains("active : bool"));
assert!(code_str.contains("tags : Vec < String >"));
}
}