use forge_core::schema::{FieldDef, RustType, SchemaRegistry, TableDef};
use crate::Error;
use crate::emit::{self, contains_json, contains_upload};
pub fn generate(registry: &SchemaRegistry) -> Result<String, Error> {
let mut output =
String::from("// Auto-generated by FORGE - DO NOT EDIT\n\n#![allow(dead_code)]\n\n");
output.push_str("use serde::{Deserialize, Serialize};\n");
let tables = registry.all_tables();
let uses_upload = tables
.iter()
.any(|t| t.fields.iter().any(|f| contains_upload(&f.rust_type)));
let uses_json = tables
.iter()
.any(|t| t.fields.iter().any(|f| contains_json(&f.rust_type)));
if uses_upload {
output.push_str("use forge_dioxus::ForgeUpload;\n");
}
if uses_json {
output.push_str("use serde_json::Value as JsonValue;\n");
}
if uses_upload || uses_json {
output.push('\n');
}
let mut sorted_tables = tables;
sorted_tables.sort_by(|a, b| a.struct_name.cmp(&b.struct_name));
for table in sorted_tables {
output.push_str(&render_struct(&table));
output.push('\n');
}
let mut enums = registry.all_enums();
enums.sort_by(|a, b| a.name.cmp(&b.name));
for enum_def in enums {
output.push_str("#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]\n");
output.push_str("#[serde(rename_all = \"snake_case\")]\n");
output.push_str(&format!("pub enum {} {{\n", enum_def.name));
for variant in enum_def.variants {
output.push_str(&format!(" {},\n", variant.name));
}
output.push_str("}\n\n");
}
Ok(output)
}
fn render_struct(table: &TableDef) -> String {
let has_upload = table.fields.iter().any(|f| contains_upload(&f.rust_type));
let derives = if has_upload {
"#[derive(Debug, Clone)]\n"
} else {
"#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]\n"
};
let mut output = String::new();
output.push_str(derives);
output.push_str(&format!("pub struct {} {{\n", table.struct_name));
for field in &table.fields {
output.push_str(&format!(
" pub {}: {},\n",
field.name,
emit::dioxus_type(&field.rust_type)
));
}
output.push_str("}\n");
if table.is_dto {
output.push_str(&render_struct_impl(&table.struct_name, &table.fields));
}
output
}
fn render_struct_impl(struct_name: &str, fields: &[FieldDef]) -> String {
if fields.is_empty() {
return String::new();
}
let required_fields: Vec<_> = fields
.iter()
.filter(|field| !matches!(field.rust_type, RustType::Option(_)))
.collect();
let optional_fields: Vec<_> = fields
.iter()
.filter(|field| matches!(field.rust_type, RustType::Option(_)))
.collect();
let constructor_params = required_fields
.iter()
.map(|field| format!("{}: {}", field.name, builder_param_type(&field.rust_type)))
.collect::<Vec<_>>()
.join(", ");
let mut constructor_body = String::new();
for field in &required_fields {
constructor_body.push_str(&format!(
" {}: {},\n",
field.name,
builder_value_expr(&field.name, &field.rust_type)
));
}
for field in &optional_fields {
constructor_body.push_str(&format!(" {}: None,\n", field.name));
}
let constructor = if constructor_params.is_empty() {
format!(
" pub fn new() -> Self {{\n Self {{\n{constructor_body} }}\n }}\n"
)
} else {
format!(
" pub fn new({constructor_params}) -> Self {{\n Self {{\n{constructor_body} }}\n }}\n"
)
};
let mut setters = String::new();
for field in optional_fields {
let RustType::Option(inner) = &field.rust_type else {
continue;
};
setters.push_str(&format!(
"\n pub fn {field_name}(mut self, {field_name}: {param_type}) -> Self {{\n self.{field_name} = Some({value_expr});\n self\n }}\n",
field_name = field.name,
param_type = builder_param_type(inner),
value_expr = builder_value_expr(&field.name, inner),
));
}
format!("\nimpl {struct_name} {{\n{constructor}{setters}}}\n")
}
fn builder_param_type(rust_type: &RustType) -> String {
let ty = emit::dioxus_type(rust_type);
if ty == "String" {
"impl Into<String>".into()
} else {
ty
}
}
fn builder_value_expr(name: &str, rust_type: &RustType) -> String {
if emit::dioxus_type(rust_type) == "String" {
format!("{name}.into()")
} else {
name.to_string()
}
}
#[cfg(test)]
mod tests {
use super::*;
use forge_core::schema::{EnumDef, EnumVariant, FieldDef, RustType, TableDef};
#[test]
fn maps_timestamp_alias_to_string() {
assert_eq!(
emit::dioxus_type(&RustType::Custom("Timestamp".into())),
"String"
);
}
#[test]
fn generates_struct_with_fields() {
let registry = SchemaRegistry::new();
let mut table = TableDef::new("users", "User");
table.fields.push(FieldDef::new("id", RustType::Uuid));
table.fields.push(FieldDef::new("email", RustType::String));
table.fields.push(FieldDef::new(
"age",
RustType::Option(Box::new(RustType::I32)),
));
registry.register_table(table);
let output = generate(®istry).expect("struct generation should succeed");
assert!(output.contains("pub struct User {"));
assert!(output.contains("pub id: String,"));
assert!(output.contains("pub email: String,"));
assert!(output.contains("pub age: Option<i32>,"));
}
#[test]
fn upload_struct_skips_partial_eq() {
let registry = SchemaRegistry::new();
let mut table = TableDef::new("uploads", "FileUpload");
table.fields.push(FieldDef::new("file", RustType::Upload));
registry.register_table(table);
let output = generate(®istry).expect("upload struct generation should succeed");
assert!(output.contains("#[derive(Debug, Clone)]"));
assert!(!output.contains("PartialEq"));
assert!(output.contains("ForgeUpload"));
}
#[test]
fn generates_enums() {
let registry = SchemaRegistry::new();
let mut enum_def = EnumDef::new("Status");
enum_def.variants.push(EnumVariant::new("Active"));
enum_def.variants.push(EnumVariant::new("Inactive"));
registry.register_enum(enum_def);
let output = generate(®istry).expect("enum generation should succeed");
assert!(output.contains("pub enum Status {"));
assert!(output.contains(" Active,"));
assert!(output.contains(" Inactive,"));
assert!(output.contains("serde(rename_all = \"snake_case\")"));
}
#[test]
fn generates_dto_constructor_and_optional_builders() {
let registry = SchemaRegistry::new();
let mut dto = TableDef::new("update_user_input", "UpdateUserInput");
dto.is_dto = true;
dto.fields.push(FieldDef::new("id", RustType::Uuid));
dto.fields.push(FieldDef::new(
"email",
RustType::Option(Box::new(RustType::String)),
));
registry.register_table(dto);
let output = generate(®istry).expect("dto helpers should generate");
assert!(output.contains("impl UpdateUserInput {"));
assert!(output.contains("pub fn new(id: impl Into<String>) -> Self"));
assert!(output.contains("email: None"));
assert!(output.contains("pub fn email(mut self, email: impl Into<String>) -> Self"));
}
}