Skip to main content

ferriorm_codegen/
generator.rs

1//! Top-level code generation orchestrator.
2//!
3//! [`generate`] is the main entry point. It takes a validated
4//! [`ferriorm_core::schema::Schema`] and an output directory, then writes all
5//! generated Rust source files: per-model modules, an enums module, the
6//! `FerriormClient` module, and a `mod.rs` that ties them together. Each file is
7//! prefixed with an auto-generated header so users know not to edit it.
8
9use ferriorm_core::schema::Schema;
10use ferriorm_core::utils::to_snake_case;
11use std::fs;
12use std::path::Path;
13
14use crate::client::generate_client_module;
15use crate::enums::generate_enums_module;
16use crate::formatter::format_token_stream;
17use crate::model::generate_model_module;
18
19/// Generate all Rust source files from a validated schema.
20///
21/// # Errors
22///
23/// Returns a [`GenerateError`] if writing any output file fails.
24pub fn generate(schema: &Schema, output_dir: &Path) -> Result<(), GenerateError> {
25    // Ensure output directory exists
26    fs::create_dir_all(output_dir)
27        .map_err(|e| GenerateError::Io(format!("Failed to create output dir: {e}")))?;
28
29    let mut mod_entries = Vec::new();
30
31    // Generate enums.rs
32    if !schema.enums.is_empty() {
33        let tokens = generate_enums_module(&schema.enums);
34        let code = format_token_stream(tokens);
35        write_file(output_dir, "enums.rs", &code)?;
36        mod_entries.push("pub mod enums;".to_string());
37    }
38
39    // Generate per-model modules
40    for model in &schema.models {
41        let model_tokens = generate_model_module(model);
42        let relation_types = crate::relations::gen_relation_types(model, schema);
43        let relation_include = crate::relations::gen_find_many_include(model, schema);
44        let tokens = quote::quote! {
45            #model_tokens
46            #relation_types
47            #relation_include
48        };
49        let code = format_token_stream(tokens);
50        let filename = format!("{}.rs", to_snake_case(&model.name));
51        write_file(output_dir, &filename, &code)?;
52        mod_entries.push(format!("pub mod {};", to_snake_case(&model.name)));
53    }
54
55    // Generate client.rs
56    let client_tokens = generate_client_module(schema);
57    let client_code = format_token_stream(client_tokens);
58    write_file(output_dir, "client.rs", &client_code)?;
59    mod_entries.push("pub mod client;".to_string());
60
61    // Generate mod.rs (write directly, not through write_file to avoid double header)
62    let mut mod_content = String::from(
63        "// AUTO-GENERATED by ferriorm. Do not edit.\n\
64         //\n\
65         // NOTE: The generated code uses `#[derive(sqlx::FromRow)]` which requires\n\
66         // `sqlx` as a direct dependency in your Cargo.toml. The derive macro expands\n\
67         // to code with absolute `::sqlx::` paths that cannot be resolved through\n\
68         // re-exports alone. See the installation guide for required dependencies.\n\n",
69    );
70    for entry in &mod_entries {
71        mod_content.push_str(entry);
72        mod_content.push('\n');
73    }
74    mod_content.push('\n');
75    mod_content.push_str("pub use client::FerriormClient;\n");
76    if !schema.enums.is_empty() {
77        mod_content.push_str("pub use enums::*;\n");
78    }
79
80    let mod_path = output_dir.join("mod.rs");
81    fs::write(&mod_path, mod_content)
82        .map_err(|e| GenerateError::Io(format!("Failed to write {}: {e}", mod_path.display())))?;
83
84    Ok(())
85}
86
87fn write_file(dir: &Path, filename: &str, content: &str) -> Result<(), GenerateError> {
88    let path = dir.join(filename);
89
90    // Add auto-generated header
91    let full_content = format!("// AUTO-GENERATED by ferriorm. Do not edit.\n\n{content}");
92
93    fs::write(&path, full_content)
94        .map_err(|e| GenerateError::Io(format!("Failed to write {}: {e}", path.display())))?;
95
96    Ok(())
97}
98
99#[derive(Debug)]
100pub enum GenerateError {
101    Io(String),
102    CodeGen(String),
103}
104
105impl std::fmt::Display for GenerateError {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        match self {
108            Self::Io(msg) => write!(f, "IO error: {msg}"),
109            Self::CodeGen(msg) => write!(f, "Code generation error: {msg}"),
110        }
111    }
112}
113
114impl std::error::Error for GenerateError {}