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 serde_yaml::Value;

use crate::tree::{Tree, TreeConfig};
use crate::lexers::*;
use crate::generators::*;

pub struct Blueprint {
    pub config: BlueprintConfig,
}

pub struct BlueprintConfig {
    pub generators: Vec<String>,
    pub only: Vec<String>,
    pub skip: Vec<String>,
}

impl Default for BlueprintConfig {
    fn default() -> Self {
        Self {
            generators: vec![
                "models".to_string(),
                "migrations".to_string(),
                "factories".to_string(),
                "controllers".to_string(),
                "routes".to_string(),
                "form_requests".to_string(),
                "tests".to_string(),
                "seeders".to_string(),
            ],
            only: vec![],
            skip: vec![],
        }
    }
}

impl Blueprint {
    pub fn new(config: BlueprintConfig) -> Self {
        Self { config }
    }

    /// Parse raw YAML content into a token tree
    pub fn parse(&self, content: &str) -> Result<IndexMap<String, Value>, anyhow::Error> {
        let value: Value = serde_yaml::from_str(content)?;
        match value {
            Value::Mapping(map) => {
                let mut result = IndexMap::new();
                for (k, v) in map {
                    if let Some(key_str) = k.as_str() {
                        result.insert(key_str.to_string(), v);
                    }
                }
                Ok(result)
            }
            _ => Err(anyhow::anyhow!("Expected a YAML mapping at the root")),
        }
    }

    /// Analyze parsed tokens into a Tree structure
    pub fn analyze(&self, tokens: &IndexMap<String, Value>) -> Tree {
        let mut tree = Tree::new();

        for (key, value) in tokens {
            match key.as_str() {
                "models" => {
                    if let Value::Mapping(models) = value {
                        let mut map = IndexMap::new();
                        for (k, v) in models {
                            if let Some(k_str) = k.as_str() {
                                map.insert(k_str.to_string(), v.clone());
                            }
                        }
                        tree.models = ModelLexer::analyze(&map);
                    }
                }
                "controllers" => {
                    if let Value::Mapping(controllers) = value {
                        let mut map = IndexMap::new();
                        for (k, v) in controllers {
                            if let Some(k_str) = k.as_str() {
                                map.insert(k_str.to_string(), v.clone());
                            }
                        }
                        tree.controllers = ControllerLexer::analyze(&map);
                    }
                }
                "seeders" => {
                    if let Value::Sequence(seeders) = value {
                        tree.seeders = SeederLexer::analyze(seeders);
                    }
                }
                "config" => {
                    if let Value::Mapping(config) = value {
                        Self::parse_config(&mut tree.config, config);
                    }
                }
                _ => {}
            }
        }

        tree
    }

    /// Generate PHP code from the analyzed tree
    pub fn generate(&self, tree: &Tree) -> IndexMap<String, String> {
        let mut files = IndexMap::new();

        if self.should_generate("models") {
            files.extend(ModelGenerator::generate(tree));
        }
        if self.should_generate("migrations") {
            files.extend(MigrationGenerator::generate(tree));
        }
        if self.should_generate("factories") {
            files.extend(FactoryGenerator::generate(tree));
        }
        if self.should_generate("controllers") {
            files.extend(ControllerGenerator::generate(tree));
        }
        if self.should_generate("routes") {
            files.extend(RouteGenerator::generate(tree));
        }
        if self.should_generate("form_requests") {
            files.extend(FormRequestGenerator::generate(tree));
        }
        if self.should_generate("tests") {
            files.extend(TestGenerator::generate(tree));
        }
        if self.should_generate("seeders") {
            files.extend(SeederGenerator::generate(tree));
        }

        files
    }

    /// Full pipeline: parse -> analyze -> generate
    pub fn build(&self, content: &str) -> Result<IndexMap<String, String>, anyhow::Error> {
        let tokens = self.parse(content)?;
        let tree = self.analyze(&tokens);
        Ok(self.generate(&tree))
    }

    fn should_generate(&self, generator: &str) -> bool {
        let in_generators = self.config.generators.contains(&generator.to_string());

        if !self.config.only.is_empty() {
            return in_generators && self.config.only.contains(&generator.to_string());
        }

        if !self.config.skip.is_empty() {
            return in_generators && !self.config.skip.contains(&generator.to_string());
        }

        in_generators
    }

    fn parse_config(config: &mut TreeConfig, mapping: &serde_yaml::Mapping) {
        for (key, value) in mapping {
            let key_str = key.as_str().unwrap_or("");
            match key_str {
                "phpunit" => config.generate_phpunit = value.as_bool().unwrap_or(true),
                "pest" => config.generate_pest = value.as_bool().unwrap_or(false),
                "forms" => config.forms = value.as_bool().unwrap_or(true),
                "pagination" => config.pagination = value.as_bool().unwrap_or(true),
                _ => {}
            }
        }
    }
}