ledvar-diff 0.1.0

Optional companion standard for the Ledvar protocol: representing the comparison (diff) of two snapshots.
Documentation
//! The result representation of a comparison (DIFF.md §3).

use crate::status::StateStatus;
use ledvar_core::Node;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;

/// One node of a comparison result.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiffNode {
    /// The node (from `current`, or carried from `previous` for `Removed`).
    pub node: Node,
    /// The node's identity, from the core.
    pub identity_key: String,
    /// The node's content hash, from the core.
    pub content_hash: String,
    /// How this node relates to its prior state.
    pub state_status: StateStatus,
}

/// A computed comparison: the `current` snapshot's metadata plus the classified nodes.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DiffResult {
    #[allow(missing_docs)]
    pub protocol_version: String,
    #[allow(missing_docs)]
    pub origin_id: String,
    #[allow(missing_docs)]
    pub provider_name: String,
    #[allow(missing_docs)]
    pub timestamp: i64,
    #[allow(missing_docs)]
    pub fingerprint: Option<String>,
    #[allow(missing_docs)]
    pub parent_origin_id: Option<String>,
    #[allow(missing_docs)]
    pub labels: BTreeMap<String, String>,
    /// The classified nodes (current nodes in order, then any removed nodes).
    pub nodes: Vec<DiffNode>,
}

impl DiffResult {
    /// True if any node is `Added`, `Removed` or `Modified`.
    pub fn has_drift(&self) -> bool {
        self.nodes.iter().any(|n| n.state_status.is_drift())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use ledvar_core::Node;

    #[test]
    fn wire_vocabulary_is_stable() {
        // DIFF.md §3: this shape IS the companion standard's wire vocabulary — a consumer reads
        // these exact field names and status strings back. Pin them via serde_json so a field or
        // variant rename cannot slip through silently.
        let result = DiffResult {
            protocol_version: "0.1.0".to_string(),
            origin_id: "o".to_string(),
            provider_name: "p".to_string(),
            timestamp: 1,
            fingerprint: None,
            parent_origin_id: None,
            labels: BTreeMap::new(),
            nodes: vec![DiffNode {
                node: Node {
                    path: vec!["a".into()],
                    content: Default::default(),
                    labels: Default::default(),
                    refs: Vec::new(),
                },
                identity_key: "i".to_string(),
                content_hash: "c".to_string(),
                state_status: StateStatus::Added,
            }],
        };
        let v = serde_json::to_value(&result).unwrap();
        assert_eq!(v["protocol_version"], "0.1.0");
        assert_eq!(v["nodes"][0]["state_status"], "Added");
        assert_eq!(v["nodes"][0]["identity_key"], "i");
        assert_eq!(v["nodes"][0]["content_hash"], "c");
        assert_eq!(v["nodes"][0]["node"]["path"][0], "a");

        let back: DiffResult = serde_json::from_value(v).unwrap();
        assert_eq!(back, result, "the wire form round-trips losslessly");
    }
}