Skip to main content

ledvar_diff/
result.rs

1//! The result representation of a comparison (DIFF.md §3).
2
3use crate::status::StateStatus;
4use ledvar_core::Node;
5use serde::{Deserialize, Serialize};
6use std::collections::BTreeMap;
7
8/// One node of a comparison result.
9#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub struct DiffNode {
11    /// The node (from `current`, or carried from `previous` for `Removed`).
12    pub node: Node,
13    /// The node's identity, from the core.
14    pub identity_key: String,
15    /// The node's content hash, from the core.
16    pub content_hash: String,
17    /// How this node relates to its prior state.
18    pub state_status: StateStatus,
19}
20
21/// A computed comparison: the `current` snapshot's metadata plus the classified nodes.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct DiffResult {
24    #[allow(missing_docs)]
25    pub protocol_version: String,
26    #[allow(missing_docs)]
27    pub origin_id: String,
28    #[allow(missing_docs)]
29    pub provider_name: String,
30    #[allow(missing_docs)]
31    pub timestamp: i64,
32    #[allow(missing_docs)]
33    pub fingerprint: Option<String>,
34    #[allow(missing_docs)]
35    pub parent_origin_id: Option<String>,
36    #[allow(missing_docs)]
37    pub labels: BTreeMap<String, String>,
38    /// The classified nodes (current nodes in order, then any removed nodes).
39    pub nodes: Vec<DiffNode>,
40}
41
42impl DiffResult {
43    /// True if any node is `Added`, `Removed` or `Modified`.
44    pub fn has_drift(&self) -> bool {
45        self.nodes.iter().any(|n| n.state_status.is_drift())
46    }
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52    use ledvar_core::Node;
53
54    #[test]
55    fn wire_vocabulary_is_stable() {
56        // DIFF.md §3: this shape IS the companion standard's wire vocabulary — a consumer reads
57        // these exact field names and status strings back. Pin them via serde_json so a field or
58        // variant rename cannot slip through silently.
59        let result = DiffResult {
60            protocol_version: "0.1.0".to_string(),
61            origin_id: "o".to_string(),
62            provider_name: "p".to_string(),
63            timestamp: 1,
64            fingerprint: None,
65            parent_origin_id: None,
66            labels: BTreeMap::new(),
67            nodes: vec![DiffNode {
68                node: Node {
69                    path: vec!["a".into()],
70                    content: Default::default(),
71                    labels: Default::default(),
72                    refs: Vec::new(),
73                },
74                identity_key: "i".to_string(),
75                content_hash: "c".to_string(),
76                state_status: StateStatus::Added,
77            }],
78        };
79        let v = serde_json::to_value(&result).unwrap();
80        assert_eq!(v["protocol_version"], "0.1.0");
81        assert_eq!(v["nodes"][0]["state_status"], "Added");
82        assert_eq!(v["nodes"][0]["identity_key"], "i");
83        assert_eq!(v["nodes"][0]["content_hash"], "c");
84        assert_eq!(v["nodes"][0]["node"]["path"][0], "a");
85
86        let back: DiffResult = serde_json::from_value(v).unwrap();
87        assert_eq!(back, result, "the wire form round-trips losslessly");
88    }
89}