Skip to main content

gen_platform/
dispatcher.rs

1//! Runtime `Dispatcher<V, C, O>` — the Rust-side peer of
2//! `mk-typed-dispatcher.nix`. Same catamorphism, native enum types.
3//!
4//! Type parameters:
5//!
6//! - `V`: the typed variant universe (a Rust enum implementing
7//!   `gen_types::TypedDispatcher`). Used for compile-time coverage
8//!   reflection.
9//! - `C`: the runtime context the helpers mutate / read.
10//! - `O`: the override type each helper produces. Folded across
11//!   the variant list per the `MergeStrategy`.
12//!
13//! Sealing: after `Dispatcher::new()` + N `helper(...)` calls, the
14//! consumer calls `into_sealed()`. The seal asserts that every
15//! kind in `V::variant_kinds()` has a registered helper — silent
16//! unknown variants become impossible at the type level. Missing
17//! coverage returns a typed `DispatcherError`.
18//!
19//! Anti-pattern this primitive eliminates: hand-rolled `match`
20//! ladders sprinkled across pleme-io controllers/daemons/migration
21//! runners. Pillar 12 (generation over composition) applied at
22//! the Rust runtime layer.
23
24use std::collections::BTreeMap;
25use std::marker::PhantomData;
26
27use gen_types::TypedDispatcher;
28
29use crate::merge::MergeStrategy;
30
31/// Build-time / seal-time dispatcher error.
32#[derive(Debug, thiserror::Error)]
33pub enum DispatcherError {
34    /// One or more variant kinds reflected by `V::variant_kinds()`
35    /// have no registered helper. Surfaces the exact missing kinds
36    /// so the consumer can fix at compile time.
37    #[error("dispatcher missing helpers for variant kinds: {missing:?}")]
38    MissingCoverage { missing: Vec<String> },
39    /// Two helpers registered for the same kind. The dispatcher
40    /// refuses ambiguity — pick one.
41    #[error("dispatcher already has a helper for kind '{kind}' — register only once")]
42    DuplicateHelper { kind: String },
43    /// An applied variant carried a kind not in the variant
44    /// universe. Can only happen if the variant enum was hand-
45    /// constructed via serde without the typed enum (i.e. the
46    /// reflection contract was violated upstream).
47    #[error("variant kind '{kind}' is unknown to the dispatcher (not in V::variant_kinds())")]
48    UnknownKind { kind: String },
49}
50
51type Helper<V, C, O> = Box<dyn Fn(&V, &mut C) -> O + Send + Sync + 'static>;
52
53/// Unsealed dispatcher builder. Register helpers per variant kind,
54/// then call `into_sealed()` to prove coverage.
55pub struct Dispatcher<V, C, O>
56where
57    V: TypedDispatcher,
58{
59    helpers: BTreeMap<String, Helper<V, C, O>>,
60    strategy: MergeStrategy,
61    _marker: PhantomData<fn() -> V>,
62}
63
64impl<V, C, O> Default for Dispatcher<V, C, O>
65where
66    V: TypedDispatcher,
67{
68    fn default() -> Self {
69        Self {
70            helpers: BTreeMap::new(),
71            strategy: MergeStrategy::default(),
72            _marker: PhantomData,
73        }
74    }
75}
76
77impl<V, C, O> Dispatcher<V, C, O>
78where
79    V: TypedDispatcher,
80{
81    /// Start a fresh dispatcher with the default `OverrideLast`
82    /// merge strategy.
83    #[must_use]
84    pub fn new() -> Self {
85        Self::default()
86    }
87
88    /// Override the merge strategy used by `apply` when folding
89    /// per-variant overrides. Defaults to `OverrideLast`.
90    #[must_use]
91    pub fn with_strategy(mut self, strategy: MergeStrategy) -> Self {
92        self.strategy = strategy;
93        self
94    }
95
96    /// Register a helper for a kind. Returns `DuplicateHelper` if
97    /// the kind already has one — pleme-io's typed-dispatch rule
98    /// is: one kind, one helper.
99    ///
100    /// # Errors
101    /// Returns `DispatcherError::DuplicateHelper` if a helper for
102    /// `kind` has already been registered.
103    pub fn helper<F>(mut self, kind: &str, f: F) -> Result<Self, DispatcherError>
104    where
105        F: Fn(&V, &mut C) -> O + Send + Sync + 'static,
106    {
107        if self.helpers.contains_key(kind) {
108            return Err(DispatcherError::DuplicateHelper {
109                kind: kind.to_string(),
110            });
111        }
112        self.helpers.insert(kind.to_string(), Box::new(f));
113        Ok(self)
114    }
115
116    /// Convenience: register a helper, panic on duplicate. Use only
117    /// when you control both sides of the registration and want
118    /// builder-style ergonomics.
119    #[must_use]
120    pub fn with_helper<F>(self, kind: &str, f: F) -> Self
121    where
122        F: Fn(&V, &mut C) -> O + Send + Sync + 'static,
123    {
124        self.helper(kind, f).expect("duplicate helper")
125    }
126
127    /// Seal the dispatcher — asserts every kind in
128    /// `V::variant_kinds()` has a registered helper. Returns a
129    /// `SealedDispatcher` that can apply variants without runtime
130    /// coverage checks.
131    ///
132    /// # Errors
133    /// Returns `DispatcherError::MissingCoverage` if any kind in
134    /// the variant universe has no registered helper.
135    pub fn into_sealed(self) -> Result<SealedDispatcher<V, C, O>, DispatcherError> {
136        let universe = V::variant_kinds();
137        let mut missing = Vec::new();
138        for k in &universe {
139            if !self.helpers.contains_key(*k) {
140                missing.push((*k).to_string());
141            }
142        }
143        if !missing.is_empty() {
144            return Err(DispatcherError::MissingCoverage { missing });
145        }
146        Ok(SealedDispatcher {
147            helpers: self.helpers,
148            strategy: self.strategy,
149            _marker: PhantomData,
150        })
151    }
152}
153
154/// Post-seal dispatcher — coverage proved at construction. `apply`
155/// folds helpers over the variant list using the configured merge
156/// strategy.
157pub struct SealedDispatcher<V, C, O>
158where
159    V: TypedDispatcher,
160{
161    helpers: BTreeMap<String, Helper<V, C, O>>,
162    strategy: MergeStrategy,
163    _marker: PhantomData<fn() -> V>,
164}
165
166impl<V, C, O> SealedDispatcher<V, C, O>
167where
168    V: TypedDispatcher,
169    O: Default,
170{
171    /// The strategy the dispatcher was sealed with.
172    #[must_use]
173    pub fn strategy(&self) -> MergeStrategy {
174        self.strategy
175    }
176
177    /// Apply each variant via the matching helper and collect every
178    /// produced override into a `Vec<O>`. The caller decides how to
179    /// fold — typical pattern: `MergeStrategy::Accumulate` consumers
180    /// keep the Vec; `MergeStrategy::OverrideLast` consumers
181    /// reduce it to a single `O` via their own `Monoid`-flavored
182    /// reducer (the `O` type's `Default + Add` impl).
183    pub fn apply_each(&self, variants: &[V], ctx: &mut C) -> Vec<O>
184    where
185        V: serde::Serialize,
186    {
187        let mut out = Vec::with_capacity(variants.len());
188        for v in variants {
189            let kind = serde_variant_kind(v);
190            if let Some(h) = self.helpers.get(&kind) {
191                out.push(h(v, ctx));
192            }
193            // Cannot reach here normally: seal guarantees coverage,
194            // and the variant came from a typed V (no string-key
195            // construction). Defensive: skip silently. Consumers
196            // with a stronger contract can use `try_apply_each`
197            // below for typed UnknownKind reporting.
198        }
199        out
200    }
201
202    /// `apply_each` with explicit error surfacing — returns
203    /// `UnknownKind` if a variant's serialized kind isn't in the
204    /// helpers table. In typed pleme-io use this never fires; it's
205    /// available for defensive code paths.
206    ///
207    /// # Errors
208    /// Returns `DispatcherError::UnknownKind` if a variant's
209    /// serialized kind has no registered helper.
210    pub fn try_apply_each(
211        &self,
212        variants: &[V],
213        ctx: &mut C,
214    ) -> Result<Vec<O>, DispatcherError>
215    where
216        V: serde::Serialize,
217    {
218        let mut out = Vec::with_capacity(variants.len());
219        for v in variants {
220            let kind = serde_variant_kind(v);
221            let Some(h) = self.helpers.get(&kind) else {
222                return Err(DispatcherError::UnknownKind { kind });
223            };
224            out.push(h(v, ctx));
225        }
226        Ok(out)
227    }
228
229    /// Saturation witness — list of every kind the dispatcher
230    /// covers, in canonical order. Equal to `V::variant_kinds()`
231    /// by construction (the seal proved this). Useful for catalog
232    /// tooling that wants to expose what a dispatcher accepts
233    /// without recomputing the universe from the trait.
234    #[must_use]
235    pub fn saturation_witness(&self) -> Vec<&str> {
236        self.helpers.keys().map(String::as_str).collect()
237    }
238
239    /// The number of registered helpers. Equal to
240    /// `V::variant_count()` by construction.
241    #[must_use]
242    pub fn helper_count(&self) -> usize {
243        self.helpers.len()
244    }
245}
246
247impl<V, C, O> SealedDispatcher<V, C, O>
248where
249    V: TypedDispatcher + serde::Serialize,
250    C: Clone,
251    O: Default + Clone + PartialEq,
252{
253    /// Determinism law — assert that applying the same variants
254    /// twice (against a clone of the context) produces the same
255    /// output vector. The substrate-wide invariant from
256    /// `theory/QUIRK-APPLIER.md` §IV-bis.2: "determinism — same
257    /// input → same output, property-test."
258    ///
259    /// Returns `true` if the law holds for this input; `false` if
260    /// the helpers carry observable side effects that diverge on
261    /// re-apply. Consumers typically `assert!()` this in tests.
262    pub fn is_deterministic(&self, variants: &[V], ctx: &C) -> bool {
263        let mut a = ctx.clone();
264        let mut b = ctx.clone();
265        let r1 = self.apply_each(variants, &mut a);
266        let r2 = self.apply_each(variants, &mut b);
267        r1 == r2
268    }
269
270    /// Idempotence law — assert that applying the variant list
271    /// twice in sequence produces the same output as applying it
272    /// once (for the `Override`/last-wins merge family). Consumers
273    /// whose helpers accumulate state (counters, append-only logs)
274    /// will return `false` here; that's the diagnostic — they're
275    /// not idempotent in the algebraic sense.
276    ///
277    /// Returns `true` iff `apply_each(variants) == apply_each(variants ++ variants)`.
278    pub fn is_idempotent(&self, variants: &[V], ctx: &C) -> bool
279    where
280        V: Clone,
281    {
282        let mut a = ctx.clone();
283        let r1 = self.apply_each(variants, &mut a);
284
285        let mut b = ctx.clone();
286        let doubled: Vec<V> = variants.iter().chain(variants.iter()).cloned().collect();
287        let r2 = self.apply_each(&doubled, &mut b);
288
289        // For OverrideLast semantics: the second pass replays the
290        // same variants, and the last application of each kind wins
291        // — so r2's tail (last len(variants) entries) should equal
292        // r1.
293        r2.len() == 2 * r1.len()
294            && r2.iter().skip(r1.len()).cloned().collect::<Vec<_>>() == r1
295    }
296}
297
298/// Pull the kebab-case serde tag out of a `#[serde(tag = "kind", ...)]`
299/// variant. Round-trips through `serde_json::Value` once — cheap and
300/// correct independent of the enum's field shape.
301fn serde_variant_kind<V: serde::Serialize>(v: &V) -> String {
302    serde_json::to_value(v)
303        .ok()
304        .and_then(|val| val.get("kind").and_then(|k| k.as_str().map(String::from)))
305        .unwrap_or_default()
306}