ferriorm-codegen 0.2.2

Code generator for ferriorm ORM - generates type-safe Rust client code
Documentation
//! Top-level code generation orchestrator.
//!
//! [`generate`] is the main entry point. It takes a validated
//! [`ferriorm_core::schema::Schema`] and an output directory, then writes all
//! generated Rust source files: per-model modules, an enums module, the
//! `FerriormClient` module, and a `mod.rs` that ties them together. Each file is
//! prefixed with an auto-generated header so users know not to edit it.

use ferriorm_core::schema::Schema;
use ferriorm_core::utils::to_snake_case;
use std::fs;
use std::path::Path;

use crate::client::generate_client_module;
use crate::enums::generate_enums_module;
use crate::formatter::format_token_stream;
use crate::model::generate_model_module;

/// Generate all Rust source files from a validated schema.
///
/// # Errors
///
/// Returns a [`GenerateError`] if writing any output file fails.
pub fn generate(schema: &Schema, output_dir: &Path) -> Result<(), GenerateError> {
    // Ensure output directory exists
    fs::create_dir_all(output_dir)
        .map_err(|e| GenerateError::Io(format!("Failed to create output dir: {e}")))?;

    let mut mod_entries = Vec::new();

    // Generate enums.rs
    if !schema.enums.is_empty() {
        let tokens = generate_enums_module(&schema.enums);
        let code = format_token_stream(tokens);
        write_file(output_dir, "enums.rs", &code)?;
        mod_entries.push("pub mod enums;".to_string());
    }

    // Generate per-model modules
    for model in &schema.models {
        let model_tokens = generate_model_module(model);
        let relation_types = crate::relations::gen_relation_types(model, schema);
        let relation_include = crate::relations::gen_find_many_include(model, schema);
        let tokens = quote::quote! {
            #model_tokens
            #relation_types
            #relation_include
        };
        let code = format_token_stream(tokens);
        let filename = format!("{}.rs", to_snake_case(&model.name));
        write_file(output_dir, &filename, &code)?;
        mod_entries.push(format!("pub mod {};", to_snake_case(&model.name)));
    }

    // Generate client.rs
    let client_tokens = generate_client_module(schema);
    let client_code = format_token_stream(client_tokens);
    write_file(output_dir, "client.rs", &client_code)?;
    mod_entries.push("pub mod client;".to_string());

    // Generate mod.rs (write directly, not through write_file to avoid double header)
    let mut mod_content = String::from(
        "// AUTO-GENERATED by ferriorm. Do not edit.\n\
         //\n\
         // NOTE: The generated code uses `#[derive(sqlx::FromRow)]` which requires\n\
         // `sqlx` as a direct dependency in your Cargo.toml. The derive macro expands\n\
         // to code with absolute `::sqlx::` paths that cannot be resolved through\n\
         // re-exports alone. See the installation guide for required dependencies.\n\n",
    );
    for entry in &mod_entries {
        mod_content.push_str(entry);
        mod_content.push('\n');
    }
    mod_content.push('\n');
    mod_content.push_str("pub use client::FerriormClient;\n");
    if !schema.enums.is_empty() {
        mod_content.push_str("pub use enums::*;\n");
    }

    let mod_path = output_dir.join("mod.rs");
    fs::write(&mod_path, mod_content)
        .map_err(|e| GenerateError::Io(format!("Failed to write {}: {e}", mod_path.display())))?;

    Ok(())
}

fn write_file(dir: &Path, filename: &str, content: &str) -> Result<(), GenerateError> {
    let path = dir.join(filename);

    // Add auto-generated header
    let full_content = format!("// AUTO-GENERATED by ferriorm. Do not edit.\n\n{content}");

    fs::write(&path, full_content)
        .map_err(|e| GenerateError::Io(format!("Failed to write {}: {e}", path.display())))?;

    Ok(())
}

#[derive(Debug)]
pub enum GenerateError {
    Io(String),
    CodeGen(String),
}

impl std::fmt::Display for GenerateError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Io(msg) => write!(f, "IO error: {msg}"),
            Self::CodeGen(msg) => write!(f, "Code generation error: {msg}"),
        }
    }
}

impl std::error::Error for GenerateError {}