use forge_core::util::{to_camel_case, to_pascal_case};
use crate::Error;
use crate::binding::{BindingSet, FunctionBinding};
use crate::emit::{self, Position};
pub fn generate(bindings: &BindingSet) -> Result<String, Error> {
let mut output = String::from("// Auto-generated by FORGE - DO NOT EDIT\n\n");
let mut type_imports = Vec::new();
for binding in bindings.all() {
for arg in &binding.args {
emit::collect_type_imports(&arg.rust_type, &mut type_imports);
}
emit::collect_type_imports(&binding.return_type, &mut type_imports);
}
type_imports.sort();
type_imports.dedup();
let mut helpers: Vec<&str> = vec!["getForgeClient"];
if bindings.has_subscriptions() {
helpers.push("createSubscriptionStore");
}
if bindings.has_jobs() {
helpers.push("createJobStore");
}
if bindings.has_workflows() {
helpers.push("createWorkflowStore");
}
output.push_str(&format!(
"import {{ {} }} from \"@forge-rs/svelte\";\n",
helpers.join(", ")
));
if !type_imports.is_empty() {
output.push_str("import type {\n");
for t in &type_imports {
output.push_str(&format!(" {},\n", t));
}
output.push_str("} from \"./types\";\n");
}
if !bindings.queries.is_empty() {
output.push_str("\n// Queries\n");
for b in &bindings.queries {
output.push_str(&gen_rpc(b));
output.push('\n');
}
}
if bindings.has_subscriptions() {
output.push_str("\n// Subscriptions\n");
for b in &bindings.queries {
output.push_str(&gen_subscription(b));
output.push('\n');
}
}
if !bindings.mutations.is_empty() {
output.push_str("\n// Mutations\n");
for b in &bindings.mutations {
output.push_str(&gen_rpc(b));
output.push('\n');
}
}
if bindings.has_jobs() {
output.push_str("\n// Jobs\n");
for b in &bindings.jobs {
output.push_str(&gen_store_factory(b, "createJobStore"));
output.push('\n');
}
}
if bindings.has_workflows() {
output.push_str("\n// Workflows\n");
for b in &bindings.workflows {
output.push_str(&gen_store_factory(b, "createWorkflowStore"));
output.push('\n');
}
}
Ok(output)
}
fn gen_rpc(b: &FunctionBinding) -> String {
let ts_name = to_camel_case(&b.name);
let result_type = emit::ts_type(&b.return_type, Position::Return);
if !b.has_args() {
return format!(
"export const {} = (): Promise<{}> =>\n getForgeClient().call(\"{}\", null);",
ts_name, result_type, b.name
);
}
let args_type = ts_args_type(b);
format!(
"export const {} = (args: {}): Promise<{}> =>\n getForgeClient().call(\"{}\", args);",
ts_name, args_type, result_type, b.name
)
}
fn gen_subscription(b: &FunctionBinding) -> String {
let ts_name = to_camel_case(&b.name);
let result_type = emit::ts_type(&b.return_type, Position::Return);
if !b.has_args() {
return format!(
"export const {}Store$ = () =>\n createSubscriptionStore<null, {}>(\"{}\", null);",
ts_name, result_type, b.name
);
}
let args_type = ts_args_type(b);
format!(
"export const {}Store$ = (args: {}) =>\n createSubscriptionStore<{}, {}>(\"{}\", args);",
ts_name, args_type, args_type, result_type, b.name
)
}
fn gen_store_factory(b: &FunctionBinding, store_fn: &str) -> String {
let factory_name = format!("track{}", to_pascal_case(&b.name));
let output_type = emit::ts_type(&b.return_type, Position::Return);
if !b.has_args() {
return format!(
"export const {} = () =>\n {}<null, {}>(\"{}\", null);",
factory_name, store_fn, output_type, b.name
);
}
let args_type = ts_args_type(b);
format!(
"export const {} = (args: {}) =>\n {}<{}, {}>(\"{}\", args);",
factory_name, args_type, store_fn, args_type, output_type, b.name
)
}
pub(crate) fn ts_args_type(b: &FunctionBinding) -> String {
if b.args.is_empty() {
return "null".into();
}
if b.is_custom_args {
return b
.args
.first()
.map(|arg| emit::ts_type(&arg.rust_type, Position::Arg))
.unwrap_or_else(|| "null".into());
}
let fields: Vec<String> = b
.args
.iter()
.map(|arg| {
format!(
"{}: {}",
arg.name,
emit::ts_type(&arg.rust_type, Position::Arg)
)
})
.collect();
format!("{{ {} }}", fields.join(", "))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::binding::BindingSet;
use forge_core::schema::{FunctionArg, FunctionDef, RustType, SchemaRegistry};
#[test]
fn test_generate_empty_registry() {
let registry = SchemaRegistry::new();
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("empty registry should generate");
assert!(content.contains("Auto-generated by FORGE"));
assert!(content.contains("import { getForgeClient }"));
}
#[test]
fn test_generate_with_functions() {
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 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("bindings should generate");
assert!(content.contains("export const getUser = (args:"));
assert!(content.contains("getForgeClient().call(\"get_user\""));
assert!(content.contains("export const getUserStore$ = (args:"));
assert!(content.contains("createSubscriptionStore"));
assert!(content.contains("export const createUser = (args:"));
assert!(content.contains("getForgeClient().call(\"create_user\""));
}
#[test]
fn test_generate_no_arg_subscription() {
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 subscription should generate");
assert!(content.contains("export const listUsersStore$ = () =>"));
assert!(content.contains("createSubscriptionStore<null, User[]>(\"list_users\", null)"));
}
#[test]
fn test_rust_type_to_ts() {
assert_eq!(emit::ts_type(&RustType::String, Position::Arg), "string");
assert_eq!(emit::ts_type(&RustType::I32, Position::Arg), "number");
assert_eq!(emit::ts_type(&RustType::Bool, Position::Arg), "boolean");
assert_eq!(emit::ts_type(&RustType::Uuid, Position::Arg), "string");
assert_eq!(
emit::ts_type(&RustType::Option(Box::new(RustType::String)), Position::Arg),
"string | null"
);
assert_eq!(
emit::ts_type(&RustType::Vec(Box::new(RustType::I32)), Position::Arg),
"number[]"
);
assert_eq!(
emit::ts_type(&RustType::Custom("()".into()), Position::Arg),
"void"
);
}
#[test]
fn test_new_datetime_types() {
assert_eq!(emit::ts_type(&RustType::Instant, Position::Arg), "string");
assert_eq!(emit::ts_type(&RustType::LocalDate, Position::Arg), "string");
assert_eq!(emit::ts_type(&RustType::LocalTime, Position::Arg), "string");
}
#[test]
fn test_upload_type() {
assert_eq!(
emit::ts_type(&RustType::Upload, Position::Arg),
"File | Blob"
);
}
#[test]
fn test_mutation_with_upload_uses_multipart() {
let registry = SchemaRegistry::new();
let mut func = FunctionDef::mutation("upload_avatar", RustType::Custom("User".into()));
func.args.push(FunctionArg::new("user_id", RustType::Uuid));
func.args.push(FunctionArg::new("file", RustType::Upload));
registry.register_function(func);
let mut func = FunctionDef::mutation("update_name", RustType::Custom("User".into()));
func.args.push(FunctionArg::new("name", RustType::String));
registry.register_function(func);
let bindings = BindingSet::from_registry(®istry);
let content = generate(&bindings).expect("upload bindings should generate");
assert!(content.contains("getForgeClient().call(\"upload_avatar\""));
assert!(content.contains("getForgeClient().call(\"update_name\""));
}
#[test]
fn test_contains_upload() {
assert!(emit::contains_upload(&RustType::Upload));
assert!(emit::contains_upload(&RustType::Option(Box::new(
RustType::Upload
))));
assert!(emit::contains_upload(&RustType::Vec(Box::new(
RustType::Upload
))));
assert!(emit::contains_upload(&RustType::Custom("Upload".into())));
assert!(!emit::contains_upload(&RustType::String));
assert!(!emit::contains_upload(&RustType::Custom("User".into())));
}
#[test]
fn test_bytes_return_type() {
assert_eq!(emit::ts_type(&RustType::Bytes, Position::Return), "Blob");
assert_eq!(emit::ts_type(&RustType::Bytes, Position::Arg), "Uint8Array");
}
#[test]
fn test_to_camel_case() {
assert_eq!(to_camel_case("get_user"), "getUser");
assert_eq!(to_camel_case("create_project_task"), "createProjectTask");
assert_eq!(to_camel_case("getUser"), "getUser");
}
#[test]
fn test_to_pascal_case() {
assert_eq!(to_pascal_case("export_users"), "ExportUsers");
assert_eq!(
to_pascal_case("account_verification"),
"AccountVerification"
);
assert_eq!(to_pascal_case("ExportUsers"), "ExportUsers");
}
}