cloudiful-config 0.6.1

Small serde-based config helpers for default app TOML/SQL stores, env overrides, and atomic saves.
Documentation
use crate::{read, save, sqlite_store, sqlite_store_with_table, ReadOptions};
use rusqlite::Connection;
use serde::{Deserialize, Serialize};

#[derive(Default, Serialize, Deserialize, Debug, PartialEq)]
struct SqlConf {
    host: String,
    port: u16,
    database: SqlDatabase,
}

#[derive(Default, Serialize, Deserialize, Debug, PartialEq)]
struct SqlDatabase {
    url: String,
    password: String,
}

fn setup_db() -> Connection {
    Connection::open_in_memory().unwrap()
}

#[test]
fn missing_sql_config_is_written_from_default() {
    let conn = setup_db();
    let mut store = sqlite_store(&conn, "stock");

    let conf: SqlConf = read(&mut store, None).unwrap();

    assert_eq!(conf, SqlConf::default());

    let raw: String = conn
        .query_row("SELECT config_json FROM app_configs WHERE app_name = 'stock'", [], |row| row.get(0))
        .unwrap();
    assert!(raw.contains("\"host\""));
}

#[test]
fn save_to_sqlite_overwrites_entire_blob() {
    let conn = setup_db();
    let mut store = sqlite_store(&conn, "stock");

    save(
        &mut store,
        &SqlConf {
            host: "from-file".to_string(),
            port: 8080,
            database: SqlDatabase {
                url: "postgres://db/file".to_string(),
                password: "plain".to_string(),
            },
        },
    )
    .unwrap();

    let mut store = sqlite_store(&conn, "stock");
    save(
        &mut store,
        &SqlConf {
            host: "from-db".to_string(),
            port: 9090,
            database: SqlDatabase {
                url: "postgres://db/new".to_string(),
                password: "plain-2".to_string(),
            },
        },
    )
    .unwrap();

    let mut store = sqlite_store(&conn, "stock");
    let conf: SqlConf = read(&mut store, None).unwrap();
    assert_eq!(conf.host, "from-db");
    assert_eq!(conf.port, 9090);
}

#[test]
fn sqlite_env_and_secret_processing_runs_after_db_load() {
    let conn = setup_db();
    let mut store = sqlite_store(&conn, "stock");

    save(
        &mut store,
        &SqlConf {
            host: "from-db".to_string(),
            port: 8080,
            database: SqlDatabase {
                url: "postgres://db/file".to_string(),
                password: "secret://test?value=db-pass".to_string(),
            },
        },
    )
    .unwrap();

    super::support::with_env_changes(
        &[
            ("APP_PORT", Some("9090")),
            ("APP_DATABASE__URL", Some("\"postgres://db/env\"")),
        ],
        || {
            let mut store = sqlite_store(&conn, "stock");
            let conf: SqlConf =
                read(&mut store, Some(ReadOptions::with_env_prefix("APP_"))).unwrap();

            assert_eq!(conf.port, 9090);
            assert_eq!(conf.database.url, "postgres://db/env");
            assert_eq!(conf.database.password, "db-pass");
        },
    );
}

#[test]
fn sqlite_conflict_detection_rejects_wrong_table_shape() {
    let conn = Connection::open_in_memory().unwrap();
    conn.execute("CREATE TABLE bad_configs (id INTEGER PRIMARY KEY, payload TEXT NOT NULL)", [])
        .unwrap();

    let mut store = sqlite_store_with_table(&conn, "stock", "bad_configs");
    let err = read::<SqlConf>(&mut store, None).unwrap_err();

    assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
    assert!(err.to_string().contains("expected config schema"));
}