rust-db-blueprint 0.1.0

A Rust code generator — reads YAML draft files and generates Axum + SQLx models, migrations, handlers, routes, requests, tests, and seeds
Documentation
use indexmap::IndexMap;

use crate::tree::Tree;

pub struct FactoryGenerator;

impl FactoryGenerator {
    pub fn generate(tree: &Tree) -> IndexMap<String, String> {
        let mut files = IndexMap::new();

        let mut factory_lines = vec![
            "//! Factory helpers for creating test data.".to_string(),
            "//! Each function creates a row with sensible defaults.".to_string(),
            String::new(),
            "use sqlx::PgPool;".to_string(),
            "use crate::models::prelude::*;".to_string(),
            String::new(),
        ];

        for model in tree.all_models() {
            if model.pivot {
                continue;
            }

            let name = &model.name;
            let lower = name.to_lowercase();
            let table = model.table_name();

            let mut params = vec![];
            let mut binds = vec![];
            let mut fields = vec![];

            for col in model.fillable_columns().iter().take(5) {
                let place = format!("${}", params.len() + 1);
                let (val, faker) = Self::column_to_faker(col, &place);
                params.push(val);
                binds.push(faker);
                fields.push(col.name.clone());
            }

            let field_list = fields.join(", ");
            let bind_lines: Vec<String> = binds.iter().map(|b| format!("        .{}", b)).collect();
            let bind_str = bind_lines.join("\n");

            let factory_fn = format!(
                "pub async fn create_{}(pool: &PgPool{}) -> {} {{\n    let row = sqlx::query_as::<_, {}>(\n        \"INSERT INTO {} ({}) VALUES ({}) RETURNING *\"\n    )\n{}\n    .fetch_one(pool)\n    .await\n    .expect(\"failed to create {}\");\n    row\n}}",
                lower,
                params.join(""),
                name,
                name,
                table,
                field_list,
                (1..=fields.len()).map(|i| format!("${}", i)).collect::<Vec<_>>().join(", "),
                bind_str,
                lower
            );

            factory_lines.push(factory_fn);
            factory_lines.push(String::new());

            // Default factory with no params — uses hardcoded defaults
            let default_fn = format!(
                "pub async fn create_{}_default(pool: &PgPool) -> {} {{\n    create_{}(pool).await\n}}",
                lower, name, lower
            );
            factory_lines.push(default_fn);
            factory_lines.push(String::new());
        }

        let content = factory_lines.join("\n");
        files.insert("src/db/factories.rs".to_string(), content);
        files
    }

    fn column_to_faker(column: &crate::models::Column, place: &str) -> (String, String) {
        let bind = match column.data_type.as_str() {
            "string" | "char" | "text" | "longtext" | "mediumtext" => {
                if column.name == "email" || column.name.ends_with("_email") {
                    format!("bind(\"test@example.com\")")
                } else if column.name == "name" || column.name.ends_with("_name") {
                    format!("bind(\"Test {}\")", column.name)
                } else {
                    format!("bind(\"test_{}\")", column.name)
                }
            }
            "integer" | "bigInteger" | "unsignedInteger" | "unsignedBigInteger" => {
                format!("bind(1)")
            }
            "smallInteger" | "tinyInteger" => format!("bind(1i16)"),
            "boolean" => format!("bind(true)"),
            "float" | "double" => format!("bind(1.0)"),
            "date" => format!("bind(chrono::Utc::now().date_naive())"),
            "datetime" | "timestamp" => format!("bind(chrono::Utc::now().naive_utc())"),
            "json" | "jsonb" => format!("bind(serde_json::Value::Null)"),
            "uuid" => format!("bind(uuid::Uuid::new_v4())"),
            _ => format!("bind(\"test\")"),
        };

        let param = match column.data_type.as_str() {
            "boolean" => String::new(),
            _ if column.has_modifier("nullable") => format!(", Option<{}>", crate::models::column::Column::new("x").php_type()),
            _ => String::new(),
        };

        (param, bind)
    }
}