tatara_process/tagged_union.rs
1//! `tagged_union::resolve` — the typescape's "exactly-one-Option" pattern,
2//! lifted to one source of truth.
3//!
4//! Several CRD-facing types in this crate ([`crate::intent::Intent`],
5//! [`crate::lifetime::Lifetime`], [`crate::export::ArtifactSource`],
6//! [`crate::export::VectorChannel`], [`crate::encapsulates::EncapsulationKind`])
7//! carry `N` `Option<T>` fields where exactly one is expected to be
8//! populated on the wire. Each previously hand-rolled the same
9//! `count() + if-let-chain + unreachable!()` body — four parallel tables
10//! (the struct fields, an `is_some()` count array, an `if-let-else`
11//! resolution chain, and any sibling projection like `IntentVariant::kind`)
12//! kept coherent only by code review. The `unreachable!()` arm at the
13//! bottom of every chain was a sentinel that fires at runtime if the
14//! parallel tables ever drift.
15//!
16//! This module collapses the resolver to ONE typed sweep over an
17//! `IntoIterator<Item = Option<V>>` of candidate variant projections.
18//! Adding a new tagged-union variant is now ONE additional line at the
19//! callsite — no `unreachable!()` arm to update, no parallel `is_some()`
20//! count array to extend.
21
22/// Outcome of [`resolve`] when the candidate list isn't exactly-one.
23#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum ResolveError {
25 /// No candidate was populated.
26 None,
27 /// More than one candidate was populated.
28 Many,
29}
30
31/// Resolve at most one populated variant from a candidate list.
32///
33/// Each item in `candidates` is the projected borrowed-variant view for
34/// the corresponding `Option<T>` field — `None` when the field is unset,
35/// `Some(V::Variant(...))` when set.
36///
37/// Returns the single populated variant, [`ResolveError::None`] when
38/// none are populated, or [`ResolveError::Many`] when more than one are.
39///
40/// The body is one short-circuiting sweep — `Many` is returned as soon
41/// as the second populated entry is seen, without scanning the rest.
42pub fn resolve<V>(candidates: impl IntoIterator<Item = Option<V>>) -> Result<V, ResolveError> {
43 let mut found: Option<V> = None;
44 for candidate in candidates {
45 if candidate.is_some() {
46 if found.is_some() {
47 return Err(ResolveError::Many);
48 }
49 found = candidate;
50 }
51 }
52 found.ok_or(ResolveError::None)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58
59 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
60 enum V {
61 A,
62 B,
63 C,
64 }
65
66 #[test]
67 fn empty_candidate_list_is_none() {
68 let r: Result<V, _> = resolve(std::iter::empty());
69 assert_eq!(r.unwrap_err(), ResolveError::None);
70 }
71
72 #[test]
73 fn all_none_is_none() {
74 let r: Result<V, _> = resolve([None, None, None]);
75 assert_eq!(r.unwrap_err(), ResolveError::None);
76 }
77
78 #[test]
79 fn single_some_is_resolved_regardless_of_position() {
80 assert_eq!(resolve([Some(V::A), None, None]).unwrap(), V::A);
81 assert_eq!(resolve([None, Some(V::B), None]).unwrap(), V::B);
82 assert_eq!(resolve([None, None, Some(V::C)]).unwrap(), V::C);
83 }
84
85 #[test]
86 fn two_or_more_some_is_many() {
87 assert_eq!(
88 resolve([Some(V::A), Some(V::B), None]).unwrap_err(),
89 ResolveError::Many
90 );
91 assert_eq!(
92 resolve([Some(V::A), None, Some(V::C)]).unwrap_err(),
93 ResolveError::Many
94 );
95 assert_eq!(
96 resolve([None, Some(V::B), Some(V::C)]).unwrap_err(),
97 ResolveError::Many
98 );
99 assert_eq!(
100 resolve([Some(V::A), Some(V::B), Some(V::C)]).unwrap_err(),
101 ResolveError::Many
102 );
103 }
104
105 /// Short-circuit invariant: once `Many` is decided, the sweep does
106 /// NOT inspect further candidates. Encode it as a side-effect probe.
107 #[test]
108 fn many_short_circuits_after_second_some() {
109 let mut visited = 0usize;
110 let candidates = (0..4).map(|i| {
111 visited += 1;
112 // first two are Some, the rest would be Some too if we got there.
113 Some(i)
114 });
115 // We can't actually consume `visited` here because it's borrowed in
116 // the closure — fold the count via the resolver's short-circuit.
117 let _ = resolve(candidates);
118 // The resolver evaluates the iterator lazily up to the second
119 // Some — index 0 (found = Some(0)), index 1 (Many → return).
120 assert_eq!(visited, 2);
121 }
122
123 /// The helper is value-agnostic — works with borrowed enum-view
124 /// types matching the actual on-the-typescape callsites.
125 #[test]
126 fn works_with_borrowed_enum_view() {
127 #[derive(Debug, PartialEq)]
128 enum View<'a> {
129 X(&'a u32),
130 Y(&'a String),
131 }
132 let x = 7u32;
133 let r = resolve([Some(View::X(&x)), None]).unwrap();
134 assert_eq!(r, View::X(&7));
135 }
136}