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 tera::{Tera, Context};
use indexmap::IndexMap;

use crate::tree::Tree;
use crate::models::Statement;
use crate::translators::Rules;

pub struct FormRequestGenerator;

impl FormRequestGenerator {
    pub fn generate(tree: &Tree) -> IndexMap<String, String> {
        let mut files = IndexMap::new();
        let mut tera = Tera::default();
        tera.add_raw_template("request", REQUEST_TEMPLATE).unwrap();

        let mut mod_lines = vec!["pub mod prelude;".to_string()];
        let mut prelude_lines = vec![];

        for controller in tree.all_controllers() {
            let model_name = controller.name.trim_end_matches("Controller");
            for (method_name, statements) in &controller.methods {
                if !statements.iter().any(|s| matches!(s, Statement::Validate(_))) {
                    continue;
                }

                let struct_name = format!("{}{}", capitalize(model_name), capitalize(method_name));
                let mut ctx = Context::new();
                ctx.insert("struct_name", &struct_name);

                let mut fields = vec![];
                let mut validations = vec![];

                for stmt in statements {
                    if let Statement::Validate(ref v) = stmt {
                        for field in &v.fields {
                            let rust_type = "String";
                            fields.push(format!("    pub {}: {},", field, rust_type));
                            if let Some(model) = tree.model_for_context(model_name) {
                                if let Some(column) = model.columns.get(field) {
                                    let rules = Rules::from_column(column);
                                    let val_str = rules.iter()
                                        .map(|r| format!("\"{}\"", r))
                                        .collect::<Vec<_>>()
                                        .join(", ");
                                    validations.push(format!(
                                        "        let _ = validate_length!(&body.{}, 1, 255);",
                                        field
                                    ));
                                }
                            }
                        }
                    }
                }

                ctx.insert("fields", &fields);
                ctx.insert("validations", &validations);

                let rendered = tera.render("request", &ctx).unwrap();
                let path = format!("src/requests/{}.rs", to_snake_case(&struct_name));
                files.insert(path, rendered);

                mod_lines.push(format!("pub mod {};", to_snake_case(&struct_name)));
                prelude_lines.push(format!("pub use super::{}::{};", to_snake_case(&struct_name), struct_name));
            }
        }

        // Only write mod.rs if we generated requests
        if files.len() > 1 {
            files.insert("src/requests/mod.rs".to_string(), mod_lines.join("\n"));
            files.insert("src/requests/prelude.rs".to_string(), format!("//! Re-exports\n\n{}\n", prelude_lines.join("\n")));
        }

        files
    }
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, c) in s.chars().enumerate() {
        if c.is_uppercase() {
            if i > 0 { result.push('_'); }
            result.push(c.to_lowercase().next().unwrap());
        } else {
            result.push(c);
        }
    }
    result
}

fn capitalize(s: &str) -> String {
    let mut c = s.chars();
    match c.next() {
        None => String::new(),
        Some(f) => f.to_uppercase().to_string() + c.as_str(),
    }
}

const REQUEST_TEMPLATE: &str = r#"use serde::{Deserialize, Serialize};

/// Request body for {{ struct_name }}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct {{ struct_name }} {
{% for field in fields %}
{{ field }}
{% endfor %}
}

impl {{ struct_name }} {
    /// Validate the request fields.
    #[allow(dead_code)]
    pub fn validate(&self) -> Result<(), Vec<String>> {
        let mut errors = Vec::new();
{% for validation in validations %}
{{ validation }}
{% endfor %}
        if errors.is_empty() { Ok(()) } else { Err(errors) }
    }
}
"#;