udb 0.3.5

Universal Data Broker — a Rust gRPC broker over multiple databases (Postgres, MySQL, SQLite, MongoDB, ClickHouse, Cassandra, MSSQL, Redis, Qdrant, S3, Neo4j, …) with per-tenant RLS, 2PC, sagas, and CDC.
Documentation
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MutationKind {
    CreateDir,
    CreateFile,
    ManagedBlock,
    OverlayFile,
    VerifyOnly,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MutationAction {
    Create,
    Merge,
    Skip,
    Warn,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WarningLevel {
    None,
    Info,
    Warning,
    Error,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PlanMutation {
    pub path: PathBuf,
    pub kind: MutationKind,
    pub before_hash: Option<String>,
    pub action: MutationAction,
    pub idempotency_key: String,
    pub warning_level: WarningLevel,
    pub summary: String,
}

impl PlanMutation {
    pub fn new(
        root: &Path,
        path: impl Into<PathBuf>,
        kind: MutationKind,
        action: MutationAction,
        idempotency_key: impl Into<String>,
        warning_level: WarningLevel,
        summary: impl Into<String>,
    ) -> Self {
        let path = path.into();
        let before_hash = fs::read(root.join(&path)).ok().map(|bytes| {
            let digest = Sha256::digest(bytes);
            format!("{digest:x}")
        });
        Self {
            path,
            kind,
            before_hash,
            action,
            idempotency_key: idempotency_key.into(),
            warning_level,
            summary: summary.into(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::{self, File};
    use std::io::Write;
    use std::time::{SystemTime, UNIX_EPOCH};

    #[test]
    fn existing_file_mutation_captures_hash() {
        let root = temp_root("hash");
        let mut file = File::create(root.join(".env.example")).unwrap();
        writeln!(file, "UDB_ENDPOINT=http://localhost:9090").unwrap();

        let mutation = PlanMutation::new(
            &root,
            ".env.example",
            MutationKind::ManagedBlock,
            MutationAction::Merge,
            "env-example",
            WarningLevel::Info,
            "merge env example block",
        );
        assert!(mutation.before_hash.is_some());

        fs::remove_dir_all(root).unwrap();
    }

    fn temp_root(name: &str) -> PathBuf {
        let suffix = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .unwrap()
            .as_nanos();
        let root = std::env::temp_dir().join(format!("udb-init-mutation-{name}-{suffix}"));
        fs::create_dir_all(&root).unwrap();
        root
    }
}