use forge_core::util::to_camel_case;
use crate::Error;
use crate::binding::BindingSet;
use crate::emit::{self, Position};
use super::api::ts_args_type;
pub fn generate(bindings: &BindingSet) -> Result<String, Error> {
if bindings.queries.is_empty() && bindings.mutations.is_empty() {
return Ok(String::new());
}
let mut output = String::from("// Auto-generated by FORGE - DO NOT EDIT\n\n");
let mut api_imports: Vec<String> = Vec::new();
for b in &bindings.queries {
api_imports.push(format!("{}Store$", to_camel_case(&b.name)));
}
for b in &bindings.mutations {
api_imports.push(to_camel_case(&b.name));
}
output.push_str(&format!(
"import {{ {} }} from \"./api\";\n",
api_imports.join(", ")
));
let mut runes_imports: Vec<&str> = Vec::new();
if !bindings.queries.is_empty() {
runes_imports.push("toReactive");
runes_imports.push("type ReactiveQuery");
}
if !bindings.mutations.is_empty() {
runes_imports.push("toReactiveMutation");
runes_imports.push("type ReactiveMutation");
}
output.push_str(&format!(
"import {{ {} }} from \"./runes.svelte\";\n",
runes_imports.join(", ")
));
let mut type_imports = Vec::new();
for b in bindings.queries.iter().chain(bindings.mutations.iter()) {
emit::collect_type_imports(&b.return_type, &mut type_imports);
for arg in &b.args {
emit::collect_type_imports(&arg.rust_type, &mut type_imports);
}
}
type_imports.sort();
type_imports.dedup();
if !type_imports.is_empty() {
output.push_str(&format!(
"import type {{ {} }} from \"./types\";\n",
type_imports.join(", ")
));
}
for b in &bindings.queries {
let ts_name = to_camel_case(&b.name);
let result_type = emit::ts_type(&b.return_type, Position::Return);
if b.has_args() {
let args_type = ts_args_type(b);
output.push_str(&format!(
"export const {}$ = (args: {}): ReactiveQuery<{}> =>\n toReactive({}Store$(args));\n",
ts_name, args_type, result_type, ts_name
));
} else {
output.push_str(&format!(
"export const {}$ = (): ReactiveQuery<{}> =>\n toReactive({}Store$());\n",
ts_name, result_type, ts_name
));
}
}
for b in &bindings.mutations {
let ts_name = to_camel_case(&b.name);
let result_type = emit::ts_type(&b.return_type, Position::Return);
let args_type = if b.has_args() {
ts_args_type(b)
} else {
"null".to_string()
};
output.push_str(&format!(
"export const {}$ = (): ReactiveMutation<{}, {}> =>\n toReactiveMutation({});\n",
ts_name, args_type, result_type, ts_name
));
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::binding::BindingSet;
use forge_core::schema::{FunctionDef, RustType, SchemaRegistry};
#[test]
fn test_generate_reactive_empty() {
let registry = SchemaRegistry::new();
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("empty reactive bindings should generate");
assert!(content.is_empty());
}
#[test]
fn test_generate_reactive_with_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("reactive query bindings should generate");
assert!(content.contains("listUsers$"));
assert!(content.contains("ReactiveQuery<User[]>"));
assert!(content.contains("toReactive(listUsersStore$())"));
}
#[test]
fn test_generate_reactive_mutation() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::mutation("create_user", RustType::Custom("User".into()));
func.args.push(forge_core::schema::FunctionArg::new(
"name",
RustType::String,
));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("reactive mutation bindings should generate");
assert!(content.contains("createUser$"));
assert!(content.contains("ReactiveMutation"));
assert!(content.contains("toReactiveMutation(createUser)"));
}
#[test]
fn test_generate_reactive_skips_when_only_crons() {
let registry = SchemaRegistry::new();
registry.register_function(FunctionDef::new(
"daily_cleanup",
forge_core::schema::FunctionKind::Cron,
RustType::Custom("()".into()),
));
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("no client-callable bindings should generate");
assert!(content.is_empty());
}
}