zenkey_fleet/report/asked.rs
1//! The absence vocabulary every domain below shares.
2//!
3//! [`Asked`] carries one distinction and nothing else: was the question put?
4//! It is here rather than in a domain file because every domain needs it and
5//! none of them owns it — a `topic` field and a `doctor` field must spell
6//! "not asked" the same way or the RFC 09 §5.1 O4 split is only local.
7//!
8//! [`u64_is_zero`] keeps company with it for the same reason: it is the
9//! `skip_serializing_if` predicate behind "absent at zero", the append rule
10//! that lets a counter be added to a shipped document without changing it
11//! for consumers who never see the counter fire.
12
13use serde::Serialize;
14
15/// "Was the question even put?" — the RFC 09 §5.1 O4 split (#246 / P1),
16/// made nominal (RFC 13, v1.24).
17///
18/// A generation of report fields spelled "not asked" as `Option::None`,
19/// which conflated it with every *other* absence the moment a field also had
20/// an asked-but-absent reading. This type carries exactly the not-asked
21/// distinction and nothing else:
22///
23/// * [`Asked::NotAsked`] — the flag was not passed, the sweep was not made,
24/// the question does not exist for this subject. On the wire it is
25/// **absence** (the field carries `skip_serializing_if` +
26/// `default`), byte-identical to the `Option` it replaced.
27/// * [`Asked::Asked`] — the question was put; the payload is the answer,
28/// serialized transparently (again exactly as `Some` did).
29///
30/// The split is the point: a field whose `None` means *asked and nothing
31/// was there* (an unstamped sample's age, a producer with no served slice)
32/// **stays `Option`** — wrapping it here would re-conflate in the other
33/// direction.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
35pub enum Asked<T> {
36 /// The question was not put. Serializes as absence — not zero, not null,
37 /// not `[]` (RFC 09 §5.1 O4).
38 #[default]
39 NotAsked,
40 /// The question was put, and this is what came back — an empty answer
41 /// (`Asked(vec![])`, `Asked(0)`) is a real answer, distinct from
42 /// `NotAsked` on the wire and in the type.
43 Asked(T),
44}
45
46impl<T> Asked<T> {
47 /// The `skip_serializing_if` predicate: not-asked is absence.
48 pub fn is_not_asked(&self) -> bool {
49 matches!(self, Asked::NotAsked)
50 }
51
52 pub fn is_asked(&self) -> bool {
53 !self.is_not_asked()
54 }
55
56 /// The answer, if the question was put.
57 pub fn as_option(&self) -> Option<&T> {
58 match self {
59 Asked::NotAsked => None,
60 Asked::Asked(v) => Some(v),
61 }
62 }
63
64 pub fn into_option(self) -> Option<T> {
65 match self {
66 Asked::NotAsked => None,
67 Asked::Asked(v) => Some(v),
68 }
69 }
70
71 /// `Asked<Vec<T>>` → `Option<&[T]>` and friends, mirroring
72 /// `Option::as_deref`.
73 pub fn as_deref(&self) -> Option<&T::Target>
74 where
75 T: std::ops::Deref,
76 {
77 self.as_option().map(|v| v.deref())
78 }
79}
80
81impl<T: Copy> Asked<T> {
82 /// The answer by value, for `Copy` payloads.
83 pub fn get(&self) -> Option<T> {
84 self.as_option().copied()
85 }
86}
87
88/// `Option`'s not-asked reading, named: `None` → `NotAsked`, `Some` →
89/// `Asked` — the mechanical migration step for gated facts built with
90/// `flag.then(...)`.
91impl<T> From<Option<T>> for Asked<T> {
92 fn from(o: Option<T>) -> Asked<T> {
93 match o {
94 None => Asked::NotAsked,
95 Some(v) => Asked::Asked(v),
96 }
97 }
98}
99
100impl<T: Serialize> Serialize for Asked<T> {
101 /// `Asked` is transparent; `NotAsked` serializes as `null` — reached
102 /// only if a field forgets its `skip_serializing_if`, in which case it
103 /// degrades exactly as the `Option` it replaced would have.
104 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
105 match self {
106 Asked::NotAsked => s.serialize_none(),
107 Asked::Asked(v) => v.serialize(s),
108 }
109 }
110}
111
112/// `skip_serializing_if` helper: a zero here is "nothing dropped", which the
113/// absent field already says.
114pub(crate) fn u64_is_zero(n: &u64) -> bool {
115 *n == 0
116}