keelson-gen 0.1.0

keelson's code generator: introspect a live schema, emit readable model .rs files against keelson-models.
Documentation
//! SQLite generation: determinism (generate twice, byte-identical) and
//! freshness (the output equals the checked-in `tests/generated/sqlite`
//! fixture, which the behaviour tests compile and execute).
//!
//! To regenerate the fixture after changing the emitter:
//!
//! ```text
//! KEELSON_GEN_BLESS=1 cargo test -p keelson-gen --test generate_sqlite
//! ```

use std::path::{Path, PathBuf};

use keelson_gen::Config;

fn manifest_path(rel: &str) -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR")).join(rel)
}

/// Build a temp database from the fixture DDL and generate from it.
fn generate() -> Vec<(String, String)> {
    static NEXT: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
    let db_path = std::env::temp_dir().join(format!(
        "keelson-gen-{}-{}.db",
        std::process::id(),
        NEXT.fetch_add(1, std::sync::atomic::Ordering::Relaxed)
    ));
    let _ = std::fs::remove_file(&db_path);
    let conn = rusqlite::Connection::open(&db_path).expect("creating the fixture database");
    conn.execute_batch(include_str!("fixtures/sqlite_schema.sql"))
        .expect("applying the fixture DDL");
    drop(conn);

    let mut config =
        Config::load(manifest_path("tests/fixtures/sqlite.toml")).expect("fixture config");
    config.url = Some(format!("sqlite://{}", db_path.display()));
    let files = keelson_gen::generate(&config).expect("generation");
    let _ = std::fs::remove_file(&db_path);
    files
}

#[test]
fn the_same_schema_generates_byte_identical_output_twice() {
    assert_eq!(generate(), generate());
}

#[test]
fn the_output_matches_the_checked_in_fixture() {
    let files = generate();
    let dir = manifest_path("tests/generated/sqlite");

    if std::env::var_os("KEELSON_GEN_BLESS").is_some() {
        keelson_gen::write_files(&dir, &files).expect("blessing the fixture");
        return;
    }

    for (name, contents) in &files {
        let checked_in = std::fs::read_to_string(dir.join(name))
            .unwrap_or_else(|e| panic!("{name}: reading the checked-in fixture: {e}"));
        assert_eq!(
            &checked_in, contents,
            "{name} drifted from the checked-in fixture; \
             regenerate with KEELSON_GEN_BLESS=1"
        );
    }
    // And nothing stale lingers in the fixture directory.
    let mut on_disk: Vec<String> = std::fs::read_dir(&dir)
        .expect("fixture dir")
        .map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
        .filter(|n| n.ends_with(".rs"))
        .collect();
    on_disk.sort();
    let mut expected: Vec<String> = files.iter().map(|(n, _)| n.clone()).collect();
    expected.sort();
    assert_eq!(on_disk, expected);
}

/// The thin binary over [`keelson_gen::run`]: config file + command-line
/// overrides in, the same files on disk out — and a stale generated file
/// from an earlier run is cleaned up, while a hand-written bystander is not.
#[test]
fn the_cli_writes_the_out_directory_and_cleans_only_its_own_leftovers() {
    let scratch = std::env::temp_dir().join(format!("keelson-gen-cli-{}", std::process::id()));
    let _ = std::fs::remove_dir_all(&scratch);
    std::fs::create_dir_all(&scratch).unwrap();

    let db_path = scratch.join("schema.db");
    let conn = rusqlite::Connection::open(&db_path).unwrap();
    conn.execute_batch(include_str!("fixtures/sqlite_schema.sql"))
        .unwrap();
    drop(conn);

    let out = scratch.join("models");
    std::fs::create_dir_all(&out).unwrap();
    // A leftover from a hypothetical earlier run, and a hand-written file.
    std::fs::write(
        out.join("dropped_table.rs"),
        "// @generated by keelson-gen. DO NOT EDIT.\n",
    )
    .unwrap();
    std::fs::write(out.join("hand_written.rs"), "// mine\n").unwrap();

    let status = std::process::Command::new(env!("CARGO_BIN_EXE_keelson-gen"))
        .args([
            "--config",
            manifest_path("tests/fixtures/sqlite.toml")
                .to_str()
                .unwrap(),
            "--url",
            &format!("sqlite://{}", db_path.display()),
            "--out",
            out.to_str().unwrap(),
        ])
        .status()
        .expect("running the keelson-gen binary");
    assert!(status.success());

    let users = std::fs::read_to_string(out.join("users.rs")).expect("users.rs written");
    let expected = generate();
    assert_eq!(
        users,
        expected.iter().find(|(n, _)| n == "users.rs").unwrap().1,
        "the CLI writes exactly what the library generates"
    );
    assert!(out.join("mod.rs").exists());
    assert!(
        !out.join("dropped_table.rs").exists(),
        "a generated leftover is removed"
    );
    assert!(
        out.join("hand_written.rs").exists(),
        "a hand-written file is never deleted"
    );

    let _ = std::fs::remove_dir_all(&scratch);
}