forge-codegen 0.7.3

TypeScript code generator for the Forge framework
//! Svelte 5 runes-native reactive bindings generator.
//!
//! Generates `reactive.svelte.ts` — thin wrappers around the Store$
//! subscription factories from `api.ts`, converted to runes-native
//! reactive state via `toReactive()`.

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() {
        return Ok(String::new());
    }

    let mut output = String::from("// Auto-generated by FORGE - DO NOT EDIT\n\n");

    // Import Store$ factories from api.ts.
    let store_imports: Vec<String> = bindings
        .queries
        .iter()
        .map(|b| format!("{}Store$", to_camel_case(&b.name)))
        .collect();
    output.push_str(&format!(
        "import {{ {} }} from \"./api\";\n",
        store_imports.join(", ")
    ));
    output.push_str("import { toReactive, type ReactiveQuery } from \"./runes.svelte\";\n");

    // Type imports.
    let mut type_imports = Vec::new();
    for b in &bindings.queries {
        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(", ")
        ));
    }

    // Generate reactive wrappers.
    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
            ));
        }
    }

    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(&registry);
        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(&registry);
        let content = generate(&bindings).expect("reactive query bindings should generate");
        assert!(content.contains("listUsers$"));
        assert!(content.contains("ReactiveQuery<User[]>"));
        assert!(content.contains("toReactive(listUsersStore$())"));
    }
}