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 rust_db_blueprint::blueprint::{Blueprint, BlueprintConfig};

#[test]
fn test_basic_draft_parsing_and_generation() {
    let draft = r#"
models:
  Post:
    title: string:400
    content: longtext
    published_at: datetime nullable
    author_id: unsignedBigInteger foreign:users.id

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

controllers:
  Post:
    resource: web

  User:
    resource: api

seeders:
  - Post
  - User
"#;

    let blueprint = Blueprint::new(BlueprintConfig::default());
    let result = blueprint.build(draft).unwrap();

    // Verify we got files
    assert!(!result.is_empty(), "Should generate files");

    let paths: Vec<&String> = result.keys().collect();

    // Check models
    assert!(paths.iter().any(|p| p.contains("src/models/post.rs")));
    assert!(paths.iter().any(|p| p.contains("src/models/user.rs")));
    assert!(paths.iter().any(|p| p.contains("src/models/mod.rs")));

    // Check migrations
    assert!(paths.iter().any(|p| p.contains("migrations/")));

    // Check factories
    assert!(paths.iter().any(|p| p.contains("src/db/factories.rs")));

    // Check handlers (controllers)
    assert!(paths.iter().any(|p| p.contains("src/handlers/post.rs")));
    assert!(paths.iter().any(|p| p.contains("src/handlers/mod.rs")));

    // Check routes
    assert!(paths.iter().any(|p| p.contains("src/routes.rs")));

    // Check tests
    assert!(paths.iter().any(|p| p.contains("tests/")));

    // Check seeds
    assert!(paths.iter().any(|p| p.contains("src/db/seeds.rs")));

    // Check form requests
    assert!(paths.iter().any(|p| p.contains("src/requests/")));

    // Verify content of a generated model
    let post_model = result.keys()
        .find(|p| p.contains("src/models/post.rs"))
        .and_then(|k| result.get(k.as_str()))
        .unwrap();
    assert!(post_model.contains("pub struct Post"));
    assert!(post_model.contains("sqlx::FromRow"));
    assert!(post_model.contains("title: String"));

    // Verify migration content
    let migration = result.keys().find(|p| p.contains("_posts.sql")).unwrap();
    let migration_content = result.get(migration.as_str()).unwrap();
    assert!(migration_content.contains("CREATE TABLE posts"));
    assert!(migration_content.contains("VARCHAR(400)"));
    assert!(migration_content.contains("REFERENCES users(id)"));

    // Verify handler content
    let handler = result.keys()
        .find(|p| p.contains("src/handlers/post.rs"))
        .and_then(|k| result.get(k.as_str()))
        .unwrap();
    assert!(handler.contains("pub struct PostHandlers"));

    // Verify route content
    let routes = result.get("src/routes.rs").unwrap();
    assert!(routes.contains("fn app_router"));
    assert!(routes.contains("axum::{Router"));

    // Verify seed content
    let seeds = result.get("src/db/seeds.rs").unwrap();
    assert!(seeds.contains("run_all"));
}

#[test]
fn test_only_and_skip_filters() {
    let draft = r#"
models:
  Product:
    name: string:200
    price: decimal:8,2
"#;

    // Test --only filter
    let config = BlueprintConfig {
        only: vec!["models".to_string()],
        ..BlueprintConfig::default()
    };
    let blueprint = Blueprint::new(config);
    let result = blueprint.build(draft).unwrap();
    assert!(result.keys().any(|p| p.contains("src/models/")));
    assert!(result.keys().all(|p| !p.contains("migrations/")));

    // Test --skip filter
    let config = BlueprintConfig {
        skip: vec!["migrations".to_string(), "factories".to_string()],
        ..BlueprintConfig::default()
    };
    let blueprint = Blueprint::new(config);
    let result = blueprint.build(draft).unwrap();
    assert!(result.keys().any(|p| p.contains("src/models/")));
    assert!(result.keys().all(|p| !p.contains("migrations/")));
    assert!(result.keys().all(|p| !p.contains("src/db/factories")));
}

#[test]
fn test_model_with_relationships() {
    let draft = r#"
models:
  Comment:
    content: text
    post_id: unsignedBigInteger

controllers:
  Comment:
    resource: api
"#;

    let blueprint = Blueprint::new(BlueprintConfig::default());
    let result = blueprint.build(draft).unwrap();

    let model_path = result.keys()
        .find(|p| p.contains("src/models/comment.rs"))
        .and_then(|k| result.get(k.as_str()))
        .unwrap();
    assert!(model_path.contains("pub struct Comment"));
    assert!(model_path.contains("post_id: i32"));
}

#[test]
fn test_with_soft_deletes() {
    let draft = r#"
models:
  Post:
    title: string
    softDeletes: true
"#;

    let blueprint = Blueprint::new(BlueprintConfig::default());
    let result = blueprint.build(draft).unwrap();

    let model = result.keys()
        .find(|p| p.contains("src/models/post.rs"))
        .and_then(|k| result.get(k.as_str()))
        .unwrap();
    assert!(model.contains("deleted_at: Option<chrono::NaiveDateTime>"));

    let migration = result.keys().find(|p| p.contains("_posts.sql")).unwrap();
    let migration_content = result.get(migration.as_str()).unwrap();
    assert!(migration_content.contains("deleted_at TIMESTAMP"));
}

#[test]
fn test_pivot_model() {
    let draft = r#"
models:
  PostUser:
    pivot: true
    post_id: unsignedBigInteger
    user_id: unsignedBigInteger
"#;

    let blueprint = Blueprint::new(BlueprintConfig::default());
    let result = blueprint.build(draft).unwrap();

    let model = result.keys()
        .find(|p| p.contains("src/models/post_user.rs"))
        .and_then(|k| result.get(k.as_str()))
        .unwrap();
    assert!(model.contains("pub struct PostUser"));
}