forge-codegen 0.10.0

TypeScript code generator for the Forge framework
Documentation
//! TypeScript type definitions generator.
//!
//! Generates `types.ts` with interfaces for models/DTOs and
//! union types for enums.

use forge_core::schema::{EnumDef, FieldDef, RustType, SchemaRegistry};

use crate::Error;
use crate::emit::{Position, ts_type};

fn render_field(field: &FieldDef) -> String {
    let (rendered, optional) = if field.nullable {
        let inner = match &field.rust_type {
            RustType::Option(inner) => ts_type(inner, Position::Arg),
            other => ts_type(other, Position::Arg),
        };
        (inner, "?")
    } else {
        (ts_type(&field.rust_type, Position::Arg), "")
    };
    format!("  {}{}: {};", field.name, optional, rendered)
}

fn render_enum(enum_def: &EnumDef) -> String {
    let values: Vec<String> = enum_def
        .variants
        .iter()
        .map(|v| format!("'{}'", v.sql_value))
        .collect();
    format!("export type {} = {};", enum_def.name, values.join(" | "))
}

/// Well-known built-in types that may be referenced by generated API bindings
/// but aren't part of the user's schema registry.
const BUILTIN_TYPES: &[(&str, &str)] = &[(
    "TokenPair",
    "export interface TokenPair {\n  access_token: string;\n  refresh_token: string;\n}\n",
)];

pub fn generate(registry: &SchemaRegistry, referenced_types: &[String]) -> Result<String, Error> {
    let mut output = String::new();
    output.push_str("// @generated by FORGE - DO NOT EDIT\n\n");

    let mut tables = registry.all_tables();
    tables.sort_by(|a, b| a.struct_name.cmp(&b.struct_name));

    let defined_names: std::collections::HashSet<String> =
        tables.iter().map(|t| t.struct_name.clone()).collect();

    for table in tables {
        output.push_str(&format!("export interface {} {{\n", table.struct_name));
        for field in &table.fields {
            output.push_str(&render_field(field));
            output.push('\n');
        }
        output.push_str("}\n\n");
    }

    output.push_str(
        "export type Cursor = string;\n\nexport interface PageInfo {\n  has_next_page: boolean;\n  end_cursor?: Cursor;\n  total_count?: number;\n}\n\nexport interface Page<T> {\n  items: T[];\n  page_info: PageInfo;\n}\n\n",
    );

    for (name, definition) in BUILTIN_TYPES {
        if !defined_names.contains(*name) && referenced_types.iter().any(|t| t.as_str() == *name) {
            output.push_str(definition);
            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(&render_enum(&enum_def));
        output.push_str("\n\n");
    }

    output.push_str(
        "export type { ForgeError, QueryResult, SubscriptionResult } from \"@forge-rs/svelte\";\n",
    );

    Ok(output)
}

#[cfg(test)]
mod tests {
    use super::*;
    use forge_core::schema::{EnumDef, EnumVariant, FieldDef, RustType, TableDef};

    #[test]
    fn test_generate_with_table() {
        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(
            "avatar_url",
            RustType::Option(Box::new(RustType::String)),
        ));
        registry.register_table(table);

        let output = generate(&registry, &[]).expect("table types should generate");
        assert!(output.contains("export interface User {"));
        assert!(output.contains("id: string;"));
        assert!(output.contains("email: string;"));
        assert!(output.contains("avatar_url?: string;"));
    }

    #[test]
    fn test_generate_with_enum() {
        let registry = SchemaRegistry::new();

        let mut enum_def = EnumDef::new("ProjectStatus");
        enum_def.variants.push(EnumVariant::new("Draft"));
        enum_def.variants.push(EnumVariant::new("Active"));
        registry.register_enum(enum_def);

        let output = generate(&registry, &[]).expect("enum types should generate");
        assert!(output.contains("export type ProjectStatus"));
        assert!(output.contains("'draft'"));
        assert!(output.contains("'active'"));
    }

    #[test]
    fn test_generate_reexports_forge_types() {
        let registry = SchemaRegistry::new();
        let output = generate(&registry, &[]).expect("type reexports should generate");
        assert!(output.contains("ForgeError"));
        assert!(output.contains("@forge-rs/svelte"));
    }

    #[test]
    fn test_builtin_token_pair_emitted_when_referenced() {
        let registry = SchemaRegistry::new();
        let refs = vec!["TokenPair".to_string()];
        let output = generate(&registry, &refs).expect("builtin types should generate");
        assert!(output.contains("export interface TokenPair {"));
        assert!(output.contains("access_token: string;"));
        assert!(output.contains("refresh_token: string;"));
    }

    #[test]
    fn test_builtin_not_emitted_when_unreferenced() {
        let registry = SchemaRegistry::new();
        let output = generate(&registry, &[]).expect("no builtins when unreferenced");
        assert!(!output.contains("TokenPair"));
    }
}