use serde::{Deserialize, Serialize};
use super::asked::{Asked, u64_is_zero};
use super::diff::{ByteDiff, ValueDiff};
use super::judgement::Judgement;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ZsnapHeader {
pub zsnap: u32,
pub selectors: Vec<String>,
pub base: String,
pub collected_at: String,
pub collection_span_s: f64,
pub asked: u64,
pub answered: u64,
#[serde(default, skip_serializing_if = "u64_is_zero")]
pub elided: u64,
#[serde(default, skip_serializing_if = "u64_is_zero")]
pub errors: u64,
#[serde(default, skip_serializing_if = "u64_is_zero")]
pub superseded: u64,
#[serde(default, skip_serializing_if = "Asked::is_not_asked")]
pub roster: Asked<usize>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SnapshotRow {
pub key: String,
pub delete: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub encoding: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub stamper: Option<StamperWire>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_zid: Option<String>,
pub registration: RegistrationWire,
pub verdict: VerdictWire,
pub holder: Holder,
}
impl SnapshotRow {
pub fn payload(&self) -> Option<Vec<u8>> {
use base64::Engine as _;
if self.delete {
return None;
}
base64::engine::general_purpose::STANDARD
.decode(self.bytes.as_deref()?)
.ok()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum StamperWire {
SelfStamped,
Foreign { id: String },
Unattributable { id: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RegistrationWire {
Registered,
Unregistered,
NoSliceForProducer,
NotADataClass,
NotV1,
NotUnderBase,
RegistryNotLoaded,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum VerdictWire {
Valid,
Invalid { violations: Vec<String> },
NotValidated { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Holder {
Live {
origin: String,
answered_by: AnsweredBy,
},
StorageOnly { origin: String },
Unattributed { reason: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AnsweredBy {
Stamper,
Other,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Snapshot {
pub header: ZsnapHeader,
pub rows: Vec<SnapshotRow>,
}
#[derive(Debug, Clone, PartialEq, Serialize)]
pub struct SnapshotReport {
pub header: ZsnapHeader,
#[serde(skip_serializing_if = "Option::is_none")]
pub out: Option<String>,
pub live: u64,
pub storage_only: u64,
pub unattributed: u64,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub incomplete: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SnapshotDiff {
pub a: ZsnapHeader,
pub b: ZsnapHeader,
pub added: Vec<String>,
pub removed: Vec<String>,
pub changed: Vec<KeyChange>,
pub unchanged: u64,
#[serde(default, skip_serializing_if = "u64_is_zero")]
pub truncated: u64,
#[serde(default, skip_serializing_if = "Asked::is_not_asked")]
pub origin_map: Asked<Vec<OriginPair>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unmapped: Vec<Unmapped>,
#[serde(default, skip_serializing_if = "Asked::is_not_asked")]
pub by_subject: Asked<Vec<SubjectDelta>>,
}
impl SnapshotDiff {
pub fn refused(&self) -> bool {
self.origin_map.is_asked() && !self.unmapped.is_empty()
}
pub fn differs(&self) -> bool {
!self.added.is_empty()
|| !self.removed.is_empty()
|| !self.changed.is_empty()
|| self.truncated > 0
}
pub fn to_judgement(&self) -> Judgement {
if self.refused() {
Judgement::Unobservable {
reason: format!(
"{} origin(s) could not be paired; the comparison was not made",
self.unmapped.len()
),
}
} else if self.differs() {
Judgement::Established
} else {
Judgement::NotEstablished {
reason: "the two snapshots are identical on every facet".into(),
}
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct KeyChange {
pub key: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<ValueDiff>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bytes: Option<ByteDiff>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verdict: Option<(VerdictWire, VerdictWire)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub registration: Option<(RegistrationWire, RegistrationWire)>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub holder: Option<(Holder, Holder)>,
pub timestamp: (Option<String>, Option<String>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OriginPair {
pub a: String,
pub b: String,
pub evidence: MapEvidence,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MapEvidence {
Explicit,
Label { source: String },
ProducerSet,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Unmapped {
pub origin: String,
pub side: Side,
pub reason: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Side {
A,
B,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SubjectDelta {
pub subject: String,
pub compared: u64,
pub differing: u64,
pub only_in_a: u64,
pub only_in_b: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub example: Option<KeyChange>,
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn header() -> ZsnapHeader {
ZsnapHeader {
zsnap: 1,
selectors: vec!["acme/v1/**".into()],
base: "acme".into(),
collected_at: "2026-09-06T00:00:00Z".into(),
collection_span_s: 1.25,
asked: 1,
answered: 2,
elided: 0,
errors: 0,
superseded: 0,
roster: Asked::NotAsked,
}
}
#[test]
fn a_header_omits_zero_counters_and_an_unasked_roster() {
let h = header();
let v = serde_json::to_value(&h).unwrap();
assert_eq!(
v,
json!({
"zsnap": 1,
"selectors": ["acme/v1/**"],
"base": "acme",
"collected_at": "2026-09-06T00:00:00Z",
"collection_span_s": 1.25,
"asked": 1,
"answered": 2,
})
);
let back: ZsnapHeader = serde_json::from_value(v).unwrap();
assert_eq!(back, h);
let asked = ZsnapHeader {
elided: 3,
superseded: 1,
roster: Asked::Asked(2),
..header()
};
let v = serde_json::to_value(&asked).unwrap();
assert_eq!(v["elided"], 3);
assert_eq!(v["superseded"], 1);
assert_eq!(v["roster"], 2);
assert!(v.get("errors").is_none(), "still zero, still absent");
let back: ZsnapHeader = serde_json::from_value(v).unwrap();
assert_eq!(back, asked);
}
#[test]
fn the_tagged_vocabularies_spell_snake_case_kinds() {
assert_eq!(
serde_json::to_value(StamperWire::Foreign { id: "ab12".into() }).unwrap(),
json!({"kind": "foreign", "id": "ab12"})
);
assert_eq!(
serde_json::to_value(StamperWire::SelfStamped).unwrap(),
json!({"kind": "self_stamped"})
);
assert_eq!(
serde_json::to_value(VerdictWire::NotValidated {
reason: "no_registry".into()
})
.unwrap(),
json!({"state": "not_validated", "reason": "no_registry"})
);
assert_eq!(
serde_json::to_value(VerdictWire::Invalid {
violations: vec!["/status: not one of …".into()]
})
.unwrap(),
json!({"state": "invalid", "violations": ["/status: not one of …"]})
);
assert_eq!(
serde_json::to_value(Holder::Live {
origin: "h-3fa9c2d41b7e".into(),
answered_by: AnsweredBy::Stamper,
})
.unwrap(),
json!({"kind": "live", "origin": "h-3fa9c2d41b7e", "answered_by": "stamper"})
);
assert_eq!(
serde_json::to_value(Holder::Unattributed {
reason: "roster not asked".into()
})
.unwrap(),
json!({"kind": "unattributed", "reason": "roster not asked"})
);
assert_eq!(
serde_json::to_value(RegistrationWire::RegistryNotLoaded).unwrap(),
json!("registry_not_loaded")
);
assert_eq!(
serde_json::to_value(MapEvidence::Label {
source: "pve".into()
})
.unwrap(),
json!({"kind": "label", "source": "pve"})
);
assert_eq!(serde_json::to_value(Side::A).unwrap(), json!("a"));
}
#[test]
fn a_delete_row_carries_no_payload_and_no_null() {
let row = SnapshotRow {
key: "acme/v1/h-3fa9c2d41b7e/state/sysinfo/health".into(),
delete: true,
bytes: None,
encoding: None,
timestamp: None,
stamper: None,
source: None,
source_zid: None,
registration: RegistrationWire::Registered,
verdict: VerdictWire::NotValidated {
reason: "tombstone".into(),
},
holder: Holder::StorageOnly {
origin: "h-3fa9c2d41b7e".into(),
},
};
assert_eq!(
serde_json::to_value(&row).unwrap(),
json!({
"key": "acme/v1/h-3fa9c2d41b7e/state/sysinfo/health",
"delete": true,
"registration": "registered",
"verdict": {"state": "not_validated", "reason": "tombstone"},
"holder": {"kind": "storage_only", "origin": "h-3fa9c2d41b7e"},
})
);
}
#[test]
fn an_unasked_alignment_is_absent_from_a_diff() {
let d = SnapshotDiff {
a: header(),
b: header(),
added: vec![],
removed: vec![],
changed: vec![],
unchanged: 4,
truncated: 0,
origin_map: Asked::NotAsked,
unmapped: vec![],
by_subject: Asked::NotAsked,
};
let v = serde_json::to_value(&d).unwrap();
for absent in ["truncated", "origin_map", "unmapped", "by_subject"] {
assert!(v.get(absent).is_none(), "{absent} should be absent: {v}");
}
assert_eq!(v["unchanged"], 4);
assert!(!d.differs());
assert_eq!(crate::report::judgement_exit_code(&d.to_judgement()), 0);
}
}