zenkey_fleet/report/interface.rs
1//! The interface plane: payload types and the carriers that move them —
2//! one type's producers, subjects and media streams in one document.
3
4use super::asked::Asked;
5use super::schema::{SchemaDrift, SchemaRow};
6use serde::Serialize;
7
8#[derive(Debug, Clone, Serialize)]
9pub struct InterfaceTypeRow {
10 pub name: String,
11 pub carriers: usize,
12}
13
14#[derive(Debug, Clone, Serialize)]
15pub struct InterfaceList {
16 pub types: Vec<InterfaceTypeRow>,
17}
18
19#[derive(Debug, Clone, Serialize)]
20pub struct CarrierRow {
21 pub producer: String,
22 pub class: String,
23 pub path: String,
24}
25
26#[derive(Debug, Clone, Serialize)]
27pub struct InterfaceShow {
28 pub type_name: String,
29 pub carriers: Vec<CarrierRow>,
30 /// What each producer serving this type name says its schema is
31 /// (issue #51). `NotAsked` = `--schema` was not passed, so the bus was
32 /// never asked; `Asked(vec![])` = asked and no carrier served one — the
33 /// empty `Vec` used to conflate the two (RFC 09 §5.1 O4, review finding
34 /// R4). Two rows with different hashes *is* the RFC 08 §7 drift finding,
35 /// visible right here rather than only in `doctor`.
36 #[serde(skip_serializing_if = "Asked::is_not_asked", default)]
37 pub schemas: Asked<Vec<SchemaRow>>,
38 /// The engine's verdict on whether the carriers agree about this type —
39 /// [`schema_drift`](crate::model::decode::schema_drift) over the
40 /// describe sweep, filtered to `type_name` (#410).
41 ///
42 /// Not derivable from `schemas`: a [`SchemaRow`] names a producer and
43 /// no origin, and flattens an unserved hash to `""`, so two rows can
44 /// neither show two hosts of one producer disagreeing nor tell "both
45 /// said nothing" from "both said the same" (the O4 bug #370 fixed in the
46 /// doctor). The renderer used to recompute drift over the rows and got
47 /// exactly those two cases wrong; this field is the one implementation's
48 /// answer, and the note reads it.
49 ///
50 /// Empty when nothing disagrees — and empty when `--schema` was not
51 /// passed, which `schemas` already says (`NotAsked`); omitted from the
52 /// document in both cases, so an unasked run does not sprout a field
53 /// that reads as "asked, clean".
54 #[serde(default, skip_serializing_if = "Vec::is_empty")]
55 pub drift: Vec<SchemaDrift>,
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61 use crate::report::{DriftVerdict, SchemaServer};
62
63 fn show(drift: Vec<SchemaDrift>) -> InterfaceShow {
64 InterfaceShow {
65 type_name: "Health".into(),
66 carriers: vec![CarrierRow {
67 producer: "sysinfo".into(),
68 class: "state".into(),
69 path: "health".into(),
70 }],
71 schemas: Asked::Asked(vec![SchemaRow {
72 producer: "sysinfo".into(),
73 type_name: "Health".into(),
74 kind: "json-schema".into(),
75 hash: "sha256:abc".into(),
76 document: None,
77 }]),
78 drift,
79 }
80 }
81
82 /// `drift` is additive to the pinned document (#410): absent when
83 /// nothing disagrees, so a script keyed on the pre-#410 shape reads a
84 /// clean run unchanged, and present as the engine's own `SchemaDrift`
85 /// when something does.
86 #[test]
87 fn interface_show_json_shape_is_pinned_and_drift_is_absent_when_empty() {
88 assert_eq!(
89 serde_json::to_value(show(vec![])).expect("serialize"),
90 serde_json::json!({
91 "type_name": "Health",
92 "carriers": [{"producer": "sysinfo", "class": "state", "path": "health"}],
93 "schemas": [{
94 "producer": "sysinfo",
95 "type_name": "Health",
96 "kind": "json-schema",
97 "hash": "sha256:abc",
98 }],
99 })
100 );
101 let drifted = show(vec![SchemaDrift {
102 type_name: "Health".into(),
103 servers: vec![
104 SchemaServer {
105 producer: "sysinfo".into(),
106 origin: "h-3fa9c2d41b7e".into(),
107 hash: Asked::Asked("sha256:abc".into()),
108 },
109 SchemaServer {
110 producer: "sysinfo".into(),
111 origin: "h-8b1e07af22c9".into(),
112 hash: Asked::Asked("sha256:def".into()),
113 },
114 ],
115 verdict: DriftVerdict::Disagree,
116 }]);
117 let value = serde_json::to_value(drifted).expect("serialize");
118 assert_eq!(
119 value["drift"],
120 serde_json::json!([{
121 "type_name": "Health",
122 "servers": [
123 {"producer": "sysinfo", "origin": "h-3fa9c2d41b7e", "hash": "sha256:abc"},
124 {"producer": "sysinfo", "origin": "h-8b1e07af22c9", "hash": "sha256:def"},
125 ],
126 "verdict": "disagree",
127 }])
128 );
129 }
130}