Skip to main content

gen_platform/
composed.rs

1//! `ComposedDispatcher` — closure-under-composition for the typed
2//! dispatcher catamorphism. Given N sealed dispatchers over
3//! disjoint variant universes, produce a single dispatcher that
4//! handles any variant from any of them by serde-tag dispatch.
5//!
6//! Proves the algebraic law from `theory/QUIRK-APPLIER.md`
7//! §IV-bis.3.d: "the combinator is closed under composition;
8//! polyglot caixa packages (Rust+Python+Helm) compose multiple
9//! ecosystem appliers in sequence into a unified override stack."
10//!
11//! ## Type erasure
12//!
13//! The composed view operates on `serde_json::Value` variants
14//! (each carrying a `"kind"` field). Source dispatchers stay
15//! typed (`SealedDispatcher<V, C, O>`); composition walks the
16//! per-kind helpers through a thin adapter that round-trips
17//! variants through serde_json. This is the same shape the
18//! substrate's `mk-typed-dispatcher.nix` consumes (untyped JSON
19//! values keyed by `kind`), so composed Rust dispatchers and
20//! composed Nix dispatchers share semantics by construction.
21
22use std::collections::BTreeMap;
23
24use crate::dispatcher::{DispatcherError, SealedDispatcher};
25use crate::merge::MergeStrategy;
26use gen_types::TypedDispatcher;
27
28/// One per-kind helper as installed in the composed dispatcher.
29/// Box of Fn over the untyped JSON variant + context.
30type ComposedHelper<C, O> =
31    Box<dyn Fn(&serde_json::Value, &mut C) -> O + Send + Sync + 'static>;
32
33/// Composition of multiple `SealedDispatcher`s over disjoint
34/// variant universes. Operates on `serde_json::Value` variants
35/// keyed by the `"kind"` field. Use `.add(...)` to extend.
36pub struct ComposedDispatcher<C, O> {
37    helpers: BTreeMap<String, ComposedHelper<C, O>>,
38    strategy: MergeStrategy,
39    /// Per-source labels mapped to the set of kinds they contribute.
40    /// Surfaces the composition shape for diagnostic queries
41    /// without requiring callers to track the input dispatchers.
42    sources: Vec<ComposedSource>,
43}
44
45/// One source dispatcher's contribution to the composition.
46#[derive(Debug, Clone)]
47pub struct ComposedSource {
48    /// Caller-supplied label (e.g. `"gen.cargo"`, `"gen.helm"`).
49    pub label: String,
50    /// Kinds the source contributed.
51    pub kinds: Vec<String>,
52}
53
54impl<C, O> Default for ComposedDispatcher<C, O> {
55    fn default() -> Self {
56        Self {
57            helpers: BTreeMap::new(),
58            strategy: MergeStrategy::default(),
59            sources: Vec::new(),
60        }
61    }
62}
63
64impl<C, O> ComposedDispatcher<C, O>
65where
66    O: Default,
67{
68    /// Start a fresh composition with the default strategy.
69    #[must_use]
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Override the merge strategy.
75    #[must_use]
76    pub fn with_strategy(mut self, strategy: MergeStrategy) -> Self {
77        self.strategy = strategy;
78        self
79    }
80
81    /// The strategy this composition uses.
82    #[must_use]
83    pub fn strategy(&self) -> MergeStrategy {
84        self.strategy
85    }
86
87    /// Extend with another sealed dispatcher. Kinds must be
88    /// disjoint from any already-added source — overlap raises
89    /// `DuplicateHelper` (which here means "the composition's
90    /// kind universes overlap; pick one source for each kind").
91    ///
92    /// `label` is a fleet-unique tag for the source (typically
93    /// matches the `register_dispatcher!` label).
94    ///
95    /// # Errors
96    /// Returns `DispatcherError::DuplicateHelper` if any of the
97    /// added dispatcher's kinds is already covered.
98    pub fn add<V>(
99        mut self,
100        label: &str,
101        source: SealedDispatcher<V, C, O>,
102    ) -> Result<Self, DispatcherError>
103    where
104        V: TypedDispatcher
105            + serde::Serialize
106            + serde::de::DeserializeOwned
107            + 'static,
108        C: 'static,
109        O: 'static,
110    {
111        let kinds = source.saturation_witness().into_iter().map(String::from).collect::<Vec<_>>();
112        for k in &kinds {
113            if self.helpers.contains_key(k) {
114                return Err(DispatcherError::DuplicateHelper { kind: k.clone() });
115            }
116        }
117        // Convert the source's typed helpers into untyped JSON
118        // helpers. One closure per kind that round-trips
119        // serde_json::Value into V and dispatches through the
120        // typed dispatcher.
121        let arc = std::sync::Arc::new(source);
122        for k in &kinds {
123            let arc2 = arc.clone();
124            let kind_owned = k.clone();
125            self.helpers.insert(
126                kind_owned,
127                Box::new(move |value: &serde_json::Value, ctx: &mut C| {
128                    let typed: V = match serde_json::from_value(value.clone()) {
129                        Ok(t) => t,
130                        Err(_) => return O::default(),
131                    };
132                    let r = arc2.apply_each(&[typed], ctx);
133                    r.into_iter().next().unwrap_or_default()
134                }),
135            );
136        }
137        self.sources.push(ComposedSource {
138            label: label.to_string(),
139            kinds,
140        });
141        Ok(self)
142    }
143
144    /// Apply each `serde_json::Value` variant against the
145    /// composed helpers. Unknown kinds skip silently — use
146    /// `try_apply_each` for typed `UnknownKind` reporting.
147    pub fn apply_each(&self, variants: &[serde_json::Value], ctx: &mut C) -> Vec<O>
148    where
149        O: Default,
150    {
151        let mut out = Vec::with_capacity(variants.len());
152        for v in variants {
153            let Some(kind) = v.get("kind").and_then(|k| k.as_str()) else {
154                continue;
155            };
156            if let Some(h) = self.helpers.get(kind) {
157                out.push(h(v, ctx));
158            }
159        }
160        out
161    }
162
163    /// `apply_each` with explicit error surfacing.
164    ///
165    /// # Errors
166    /// Returns `DispatcherError::UnknownKind` if a variant's kind
167    /// has no registered helper across the composition.
168    pub fn try_apply_each(
169        &self,
170        variants: &[serde_json::Value],
171        ctx: &mut C,
172    ) -> Result<Vec<O>, DispatcherError>
173    where
174        O: Default,
175    {
176        let mut out = Vec::with_capacity(variants.len());
177        for v in variants {
178            let kind = v
179                .get("kind")
180                .and_then(|k| k.as_str())
181                .ok_or_else(|| DispatcherError::UnknownKind {
182                    kind: "<no kind field>".to_string(),
183                })?;
184            let Some(h) = self.helpers.get(kind) else {
185                return Err(DispatcherError::UnknownKind {
186                    kind: kind.to_string(),
187                });
188            };
189            out.push(h(v, ctx));
190        }
191        Ok(out)
192    }
193
194    /// Number of helpers in the composition (sum of source
195    /// helpers; equal to the disjoint union of source variant
196    /// universes).
197    #[must_use]
198    pub fn helper_count(&self) -> usize {
199        self.helpers.len()
200    }
201
202    /// Read-only view of every kind covered by the composition,
203    /// in canonical sorted order.
204    #[must_use]
205    pub fn covered_kinds(&self) -> Vec<&str> {
206        self.helpers.keys().map(String::as_str).collect()
207    }
208
209    /// Read-only view of the sources contributing to the
210    /// composition (label + kinds each contributed).
211    #[must_use]
212    pub fn sources(&self) -> &[ComposedSource] {
213        &self.sources
214    }
215}