forgex 0.9.0

CLI and runtime for the Forge full-stack framework
use anyhow::Result;
use std::fs;
use std::path::Path;
use std::process::Command;

pub struct BindingGeneratorInput<'a> {
    pub output_dir: &'a str,
    pub output_path: &'a Path,
    pub registry: &'a forge_core::schema::SchemaRegistry,
    pub has_schema: bool,
    pub force: bool,
}

pub type BindingGeneratorFn = for<'a> fn(&BindingGeneratorInput<'a>) -> Result<()>;

pub(crate) fn generate_svelte_bindings(input: &BindingGeneratorInput<'_>) -> Result<()> {
    if input.has_schema {
        let generate_auth = forge_core::config::ForgeConfig::from_file("forge.toml")
            .map(|c| c.auth.jwt_secret.is_some() || c.auth.jwks_url.is_some())
            .unwrap_or(false);

        let options = forge_codegen::GenerateOptions {
            generate_auth_store: generate_auth,
        };
        let generator = forge_codegen::TypeScriptGenerator::with_options(input.output_dir, options);
        generator.generate(input.registry)?;
        return Ok(());
    }

    if !input.output_path.exists() {
        fs::create_dir_all(input.output_path)?;
    }

    generate_types(input.output_path, input.force)?;
    generate_api(input.output_path, input.force)?;
    generate_stores(input.output_path, input.force)?;
    generate_runes(input.output_path)?;
    generate_index(input.output_path)?;
    Ok(())
}

pub(crate) fn generate_dioxus_bindings(input: &BindingGeneratorInput<'_>) -> Result<()> {
    let generator = forge_codegen::DioxusGenerator::new(input.output_dir);
    generator.generate(input.registry)?;
    format_generated_rust_bindings(input.output_path)?;
    Ok(())
}

fn format_generated_rust_bindings(output_dir: &Path) -> Result<()> {
    let mut rust_files = Vec::new();

    for entry in fs::read_dir(output_dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|ext| ext.to_str()) == Some("rs") {
            rust_files.push(path);
        }
    }

    if rust_files.is_empty() {
        return Ok(());
    }

    let mut rustfmt = Command::new("rustfmt");
    rustfmt.args(["--edition", "2024"]);
    for file in rust_files {
        rustfmt.arg(file);
    }

    let _ = rustfmt.status();
    Ok(())
}

fn generate_types(output_dir: &Path, force: bool) -> Result<()> {
    let file_path = output_dir.join("types.ts");
    if file_path.exists() && !force {
        return Ok(());
    }

    let content = r#"// Auto-generated by FORGE - DO NOT EDIT
// Run `forge generate` after adding or modifying models

// Model types will be generated here based on your Rust schema
// Example: export interface User { id: string; name: string; }

// Common types (re-exported from @forge-rs/svelte for convenience)
export type { ForgeError, QueryResult, SubscriptionResult } from "@forge-rs/svelte";
"#;

    fs::write(file_path, content)?;
    Ok(())
}

fn generate_api(output_dir: &Path, force: bool) -> Result<()> {
    let file_path = output_dir.join("api.ts");
    if file_path.exists() && !force {
        return Ok(());
    }

    let content = r#"// Auto-generated by FORGE - DO NOT EDIT
// Run `forge generate` after adding functions to src/functions/

// Function bindings will be generated here based on your Rust functions
// Example:
// import { createQuery, createMutation } from "@forge-rs/svelte";
// import type { User } from "./types";
// export const getUsers = createQuery<null, User[]>("get_users");
// export const createUser = createMutation<{ name: string }, User>("create_user");

export {};
"#;

    fs::write(file_path, content)?;
    Ok(())
}

fn generate_stores(output_dir: &Path, force: bool) -> Result<()> {
    let file_path = output_dir.join("stores.ts");
    if file_path.exists() && !force {
        return Ok(());
    }

    let content = r#"// Auto-generated by FORGE - DO NOT EDIT
export {
  getForgeClient,
  createConnectionStore,
  createQueryStore,
  createSubscriptionStore,
  createJobStore,
  createWorkflowStore,
  dt,
} from '@forge-rs/svelte';
export type {
  Readable,
  ConnectionStatusStore,
  QueryStore,
  SubscriptionStore,
  JobStore,
  WorkflowStore,
} from '@forge-rs/svelte';
"#;

    fs::write(file_path, content)?;
    Ok(())
}

fn generate_runes(output_dir: &Path) -> Result<()> {
    let file_path = output_dir.join("runes.svelte.ts");
    fs::write(file_path, forge_codegen::RUNES_SVELTE_TS)?;
    Ok(())
}

fn generate_index(output_dir: &Path) -> Result<()> {
    let file_path = output_dir.join("index.ts");

    let content = r#"// Auto-generated by FORGE - DO NOT EDIT

// Types
export * from './types';

// API bindings
export * from './api';

// Stores (re-exported from @forge-rs/svelte)
export * from './stores';

// Runes (Svelte 5 reactive helpers)
export * from './runes.svelte';

// Client and Provider (re-exported from @forge-rs/svelte)
export { ForgeClient, ForgeClientError, createForgeClient, ForgeProvider } from '@forge-rs/svelte';
"#;

    fs::write(file_path, content)?;

    Ok(())
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
mod tests {
    use super::*;
    use tempfile::tempdir;

    #[test]
    fn test_generate_types() {
        let dir = tempdir().unwrap();
        generate_types(dir.path(), false).unwrap();
        assert!(dir.path().join("types.ts").exists());
    }

    #[test]
    fn test_generate_api() {
        let dir = tempdir().unwrap();
        generate_api(dir.path(), false).unwrap();
        assert!(dir.path().join("api.ts").exists());
    }

    #[test]
    fn test_generate_stores() {
        let dir = tempdir().unwrap();
        generate_stores(dir.path(), false).unwrap();
        assert!(dir.path().join("stores.ts").exists());
    }
}