use forge_core::schema::RustType;
use forge_core::util::to_pascal_case;
use crate::Error;
use crate::binding::{BindingSet, FunctionBinding};
use crate::emit;
pub fn generate(bindings: &BindingSet) -> Result<String, Error> {
let mut output = String::from(
"// Auto-generated by FORGE - DO NOT EDIT\n\n#![allow(dead_code, unused_imports)]\n\n",
);
output.push_str("use forge_dioxus::{\n");
output.push_str(" ForgeClient, ForgeClientError, Mutation, QueryState,\n");
output
.push_str(" SubscriptionState, JobExecutionState, TokenPair, WorkflowExecutionState,\n");
output.push_str("};\n\n");
output.push_str("use super::types::*;\n");
output.push_str(
"use super::{\n use_forge_query, use_forge_subscription,\n use_forge_mutation, use_forge_job, use_forge_workflow,\n};\n\n",
);
for b in bindings.all() {
let params = render_params_struct(b);
if !params.is_empty() {
output.push_str(¶ms);
output.push('\n');
}
let rendered = render_binding(b);
if !rendered.is_empty() {
output.push_str(&rendered);
output.push('\n');
}
}
Ok(output)
}
fn render_binding(b: &FunctionBinding) -> String {
let fn_name = &b.name;
let return_type = emit::dioxus_type(&b.return_type);
let fn_params = dioxus_params(b, true).unwrap_or_default();
let hook_params_str = format_hook_params(dioxus_params(b, false));
let call_args = dioxus_call_args(b);
let arg_type = binding_arg_type(b);
match b.kind {
forge_core::schema::FunctionKind::Query => {
format!(
"pub async fn {fn_name}(client: &ForgeClient{fn_params}) -> Result<{return_type}, ForgeClientError> {{\n client.call(\"{fn_name}\", {call_args}).await\n}}\n\npub fn use_{fn_name}{hook_params_str} -> QueryState<{return_type}> {{\n use_forge_query(\"{fn_name}\", {call_args})\n}}\n\npub fn use_{fn_name}_live{hook_params_str} -> SubscriptionState<{return_type}> {{\n use_forge_subscription(\"{fn_name}\", {call_args})\n}}"
)
}
forge_core::schema::FunctionKind::Mutation => {
let mutation_arg = arg_type.unwrap_or_else(|| "()".to_string());
format!(
"pub async fn {fn_name}(client: &ForgeClient{fn_params}) -> Result<{return_type}, ForgeClientError> {{\n client.call(\"{fn_name}\", {call_args}).await\n}}\n\npub fn use_{fn_name}() -> Mutation<{mutation_arg}, {return_type}> {{\n use_forge_mutation(\"{fn_name}\")\n}}"
)
}
forge_core::schema::FunctionKind::Job => {
format!(
"pub fn use_{fn_name}{hook_params_str} -> JobExecutionState<{return_type}> {{\n use_forge_job(\"{fn_name}\", {call_args})\n}}"
)
}
forge_core::schema::FunctionKind::Workflow => {
format!(
"pub fn use_{fn_name}{hook_params_str} -> WorkflowExecutionState<{return_type}> {{\n use_forge_workflow(\"{fn_name}\", {call_args})\n}}"
)
}
forge_core::schema::FunctionKind::Cron => String::new(),
}
}
fn format_hook_params(params: Option<String>) -> String {
params
.map(|p| format!("({p})"))
.unwrap_or_else(|| "()".to_string())
}
fn render_params_struct(b: &FunctionBinding) -> String {
if !should_generate_params_struct(b) {
return String::new();
}
let struct_name = params_struct_name(b);
let fields = b
.args
.iter()
.map(|arg| {
format!(
" pub {}: {},\n",
arg.name,
emit::dioxus_type(&arg.rust_type)
)
})
.collect::<String>();
let impl_block = render_struct_impl(&struct_name, &b.args);
format!(
"#[derive(Debug, Clone, PartialEq, serde::Serialize)]\npub struct {struct_name} {{\n{fields}}}\n{impl_block}"
)
}
fn should_generate_params_struct(b: &FunctionBinding) -> bool {
b.has_args() && !b.is_custom_args
}
fn params_struct_name(b: &FunctionBinding) -> String {
format!("{}Params", to_pascal_case(&b.name))
}
fn binding_arg_type(b: &FunctionBinding) -> Option<String> {
if b.args.is_empty() {
return None;
}
if b.is_custom_args {
return b.args.first().map(|arg| emit::dioxus_type(&arg.rust_type));
}
Some(params_struct_name(b))
}
fn dioxus_params(b: &FunctionBinding, with_client: bool) -> Option<String> {
let arg_type = binding_arg_type(b)?;
let params = format!("args: {arg_type}");
if with_client {
Some(format!(", {params}"))
} else {
Some(params)
}
}
fn dioxus_call_args(b: &FunctionBinding) -> String {
if b.args.is_empty() {
return "()".into();
}
"args".into()
}
fn render_struct_impl(struct_name: &str, fields: &[forge_core::schema::FunctionArg]) -> 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!("impl {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 crate::binding::BindingSet;
use forge_core::schema::{
FunctionArg, FunctionDef, FunctionKind, RustType, SchemaRegistry, TableDef,
};
#[test]
fn generates_query_default_and_live_subscription() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::query("get_user", RustType::Custom("User".into()));
func.args.push(FunctionArg::new("id", RustType::Uuid));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("query bindings should generate");
assert!(content.contains("pub struct GetUserParams"));
assert!(
content.contains("pub async fn get_user(client: &ForgeClient, args: GetUserParams)")
);
assert!(content.contains("pub fn use_get_user(args: GetUserParams) -> QueryState<User>"));
assert!(content.contains("use_forge_query(\"get_user\", args)"));
assert!(
content.contains(
"pub fn use_get_user_live(args: GetUserParams) -> SubscriptionState<User>"
)
);
assert!(content.contains("use_forge_subscription(\"get_user\", args)"));
assert!(!content.contains("use_get_user_signal"));
assert!(!content.contains("use_get_user_query"));
assert!(content.contains("impl GetUserParams {"));
assert!(content.contains("pub fn new(id: impl Into<String>) -> Self"));
}
#[test]
fn generates_mutation_with_typed_mutation_struct() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::mutation("create_user", RustType::Custom("User".into()));
func.args.push(FunctionArg::new("name", RustType::String));
func.args.push(FunctionArg::new("email", RustType::String));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("mutation bindings should generate");
assert!(content.contains("pub struct CreateUserParams"));
assert!(
content
.contains("pub async fn create_user(client: &ForgeClient, args: CreateUserParams)")
);
assert!(content.contains("client.call(\"create_user\", args).await"));
assert!(content.contains("pub fn use_create_user() -> Mutation<CreateUserParams, User>"));
assert!(content.contains("use_forge_mutation(\"create_user\")"));
assert!(!content.contains("Pin<"));
assert!(!content.contains("Box<dyn"));
}
#[test]
fn generates_no_arg_query() {
let registry = SchemaRegistry::new();
let func = FunctionDef::query(
"list_users",
RustType::Vec(Box::new(RustType::Custom("User".into()))),
);
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("no-arg query should generate");
assert!(content.contains("pub async fn list_users(client: &ForgeClient)"));
assert!(content.contains("client.call(\"list_users\", ()).await"));
assert!(content.contains("pub fn use_list_users() -> QueryState<Vec<User>>"));
assert!(content.contains("pub fn use_list_users_live() -> SubscriptionState<Vec<User>>"));
}
#[test]
fn generates_job_hook() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::new(
"send_email",
FunctionKind::Job,
RustType::Custom("()".into()),
);
func.args.push(FunctionArg::new("to", RustType::String));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("job bindings should generate");
assert!(content.contains("pub struct SendEmailParams"));
assert!(content.contains("pub fn use_send_email(args: SendEmailParams)"));
assert!(content.contains("JobExecutionState"));
assert!(!content.contains("use_send_email_signal"));
}
#[test]
fn generates_workflow_hook() {
let registry = SchemaRegistry::new();
let func = FunctionDef::new(
"onboard_user",
FunctionKind::Workflow,
RustType::Custom("User".into()),
);
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("workflow bindings should generate");
assert!(content.contains("use_onboard_user"));
assert!(content.contains("WorkflowExecutionState<User>"));
}
#[test]
fn custom_args_struct_passed_directly() {
let registry = SchemaRegistry::new();
let mut dto = TableDef::new("CreateUserArgs", "CreateUserArgs");
dto.is_dto = true;
registry.register_table(dto);
let mut func = FunctionDef::mutation("create_user", RustType::Custom("User".into()));
func.args.push(FunctionArg::new(
"args",
RustType::Custom("CreateUserArgs".into()),
));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("custom args bindings should generate");
assert!(!content.contains("pub struct CreateUserParams"));
assert!(
content
.contains("pub async fn create_user(client: &ForgeClient, args: CreateUserArgs)")
);
assert!(content.contains("client.call(\"create_user\", args).await"));
assert!(content.contains("pub fn use_create_user() -> Mutation<CreateUserArgs, User>"));
}
#[test]
fn generates_optional_builder_methods_for_params() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::mutation("update_user", RustType::Custom("User".into()));
func.args.push(FunctionArg::new("id", RustType::Uuid));
func.args.push(FunctionArg::new(
"email",
RustType::Option(Box::new(RustType::String)),
));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("optional param builders should generate");
assert!(content.contains("pub struct UpdateUserParams"));
assert!(content.contains("pub fn new(id: impl Into<String>) -> Self"));
assert!(content.contains("email: None"));
assert!(content.contains("pub fn email(mut self, email: impl Into<String>) -> Self"));
}
#[test]
fn skips_cron_functions() {
let registry = SchemaRegistry::new();
registry.register_function(FunctionDef::new(
"daily_cleanup",
FunctionKind::Cron,
RustType::Custom("()".into()),
));
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("cron filtering should generate");
assert!(!content.contains("daily_cleanup"));
}
}