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 clap::{Parser, Subcommand};
use std::fs;
use std::path::Path;
use colored::*;

use rust_db_blueprint::blueprint::{Blueprint, BlueprintConfig};

#[derive(Parser)]
#[command(name = "blueprint")]
#[command(about = "Generate Laravel code from YAML draft files", long_about = None)]
struct Cli {
    #[command(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Build Laravel code from a draft.yaml file
    Build {
        /// Path to the draft YAML file
        #[arg(default_value = "draft.yaml")]
        draft: String,

        /// Only generate specific components (comma-separated): models, migrations, factories, controllers, routes, form_requests, tests, seeders
        #[arg(long, value_delimiter = ',')]
        only: Option<Vec<String>>,

        /// Skip specific components (comma-separated)
        #[arg(long, value_delimiter = ',')]
        skip: Option<Vec<String>>,

        /// Output directory (defaults to current directory)
        #[arg(long, default_value = ".")]
        output: String,
    },

    /// Create a new draft.yaml file
    Init {
        /// Force overwrite existing draft.yaml
        #[arg(long, short)]
        force: bool,
    },

    /// List available generators
    List,
}

fn main() -> anyhow::Result<()> {
    let cli = Cli::parse();

    match cli.command {
        Commands::Build { draft, only, skip, output } => {
            handle_build(&draft, only, skip, &output)?;
        }
        Commands::Init { force } => {
            handle_init(force)?;
        }
        Commands::List => {
            handle_list()?;
        }
    }

    Ok(())
}

fn handle_build(draft: &str, only: Option<Vec<String>>, skip: Option<Vec<String>>, output: &str) -> anyhow::Result<()> {
    let draft_path = Path::new(draft);
    if !draft_path.exists() {
        eprintln!("{}: Draft file '{}' not found.", "Error".red().bold(), draft);
        eprintln!("Run 'blueprint init' to create a starter draft.yaml file.");
        std::process::exit(1);
    }

    let content = fs::read_to_string(draft_path)?;
    if content.trim().is_empty() {
        eprintln!("{}: Draft file '{}' is empty.", "Error".red().bold(), draft);
        std::process::exit(1);
    }

    let mut config = BlueprintConfig::default();
    if let Some(ref only_list) = only {
        config.only = only_list.clone();
    }
    if let Some(ref skip_list) = skip {
        config.skip = skip_list.clone();
    }

    let blueprint = Blueprint::new(config);

    println!("{} blueprint from: {}", "Building".green().bold(), draft);
    println!();

    let files = blueprint.build(&content)?;

    if files.is_empty() {
        println!("{}: No files were generated.", "Warning".yellow().bold());
        println!("  Ensure your draft.yaml contains valid models and/or controllers.");
        return Ok(());
    }

    let mut created = 0;
    let mut updated = 0;

    for (path, code) in &files {
        let full_path = Path::new(output).join(path);
        if let Some(parent) = full_path.parent() {
            fs::create_dir_all(parent)?;
        }

        if full_path.exists() {
            let existing = fs::read_to_string(&full_path)?;
            if existing == *code {
                println!("  {} {}", "SKIP".yellow(), path);
                continue;
            }
            fs::write(&full_path, code)?;
            updated += 1;
            println!("  {} {}", "UPDATE".blue().bold(), path);
        } else {
            fs::write(&full_path, code)?;
            created += 1;
            println!("  {} {}", "CREATE".green().bold(), path);
        }
    }

    println!();
    println!("{}", "Done!".green().bold());
    println!("  Created: {} files", created);
    println!("  Updated: {} files", updated);

    Ok(())
}

fn handle_init(force: bool) -> anyhow::Result<()> {
    let draft_path = Path::new("draft.yaml");
    if draft_path.exists() && !force {
        eprintln!("{}: draft.yaml already exists. Use --force to overwrite.", "Error".red().bold());
        std::process::exit(1);
    }

    let content = r#"models:
  Post:
    title: string:400
    content: longtext
    published_at: nullable datetime
    author_id: id:user foreign

  User:
    name: string:100
    email: string:100 unique
    password: string:100

controllers:
  Post:
    resource: web

  User:
    resource: api

seeders:
  - Post
  - User
"#;

    fs::write(draft_path, content)?;
    println!("{} Created draft.yaml", "CREATE".green().bold());
    println!();
    println!("Next steps:");
    println!("  1. Edit draft.yaml to define your models and controllers");
    println!("  2. Run 'blueprint build' to generate Laravel code");

    Ok(())
}

fn handle_list() -> anyhow::Result<()> {
    let generators = vec![
        ("models", "Eloquent model classes with relationships, casts, and fillable attributes"),
        ("migrations", "Database migration files with columns, indexes, and foreign keys"),
        ("factories", "Model factories with Faker data definitions"),
        ("controllers", "Controller classes with resourceful action methods"),
        ("routes", "Web and API route definitions"),
        ("form_requests", "Form request validation classes"),
        ("tests", "Feature tests for controller actions"),
        ("seeders", "Database seeder classes"),
    ];

    println!("{}", "Available generators:".green().bold());
    println!();

    for (name, description) in generators {
        println!("  {:<15} {}", name, description);
    }

    println!();
    println!("Usage: blueprint build --only models,controllers");
    println!("       blueprint build --skip tests");

    Ok(())
}