Skip to main content

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}