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:
7//! "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> = Box<dyn Fn(&serde_json::Value, &mut C) -> O + Send + Sync + 'static>;
31
32/// Composition of multiple `SealedDispatcher`s over disjoint
33/// variant universes. Operates on `serde_json::Value` variants
34/// keyed by the `"kind"` field. Use `.add(...)` to extend.
35pub struct ComposedDispatcher<C, O> {
36    helpers: BTreeMap<String, ComposedHelper<C, O>>,
37    strategy: MergeStrategy,
38    /// Per-source labels mapped to the set of kinds they contribute.
39    /// Surfaces the composition shape for diagnostic queries
40    /// without requiring callers to track the input dispatchers.
41    sources: Vec<ComposedSource>,
42}
43
44/// One source dispatcher's contribution to the composition.
45#[derive(Debug, Clone)]
46pub struct ComposedSource {
47    /// Caller-supplied label (e.g. `"gen.cargo"`, `"gen.helm"`).
48    pub label: String,
49    /// Kinds the source contributed.
50    pub kinds: Vec<String>,
51}
52
53impl<C, O> Default for ComposedDispatcher<C, O> {
54    fn default() -> Self {
55        Self {
56            helpers: BTreeMap::new(),
57            strategy: MergeStrategy::default(),
58            sources: Vec::new(),
59        }
60    }
61}
62
63impl<C, O> ComposedDispatcher<C, O>
64where
65    O: Default,
66{
67    /// Start a fresh composition with the default strategy.
68    #[must_use]
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Override the merge strategy.
74    #[must_use]
75    pub fn with_strategy(mut self, strategy: MergeStrategy) -> Self {
76        self.strategy = strategy;
77        self
78    }
79
80    /// The strategy this composition uses.
81    #[must_use]
82    pub fn strategy(&self) -> MergeStrategy {
83        self.strategy
84    }
85
86    /// Extend with another sealed dispatcher. Kinds must be
87    /// disjoint from any already-added source — overlap raises
88    /// `DuplicateHelper` (which here means "the composition's
89    /// kind universes overlap; pick one source for each kind").
90    ///
91    /// `label` is a fleet-unique tag for the source (typically
92    /// matches the `register_dispatcher!` label).
93    ///
94    /// # Errors
95    /// Returns `DispatcherError::DuplicateHelper` if any of the
96    /// added dispatcher's kinds is already covered.
97    pub fn add<V>(
98        mut self,
99        label: &str,
100        source: SealedDispatcher<V, C, O>,
101    ) -> Result<Self, DispatcherError>
102    where
103        V: TypedDispatcher + serde::Serialize + serde::de::DeserializeOwned + 'static,
104        C: 'static,
105        O: 'static,
106    {
107        let kinds = source
108            .saturation_witness()
109            .into_iter()
110            .map(String::from)
111            .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.get("kind").and_then(|k| k.as_str()).ok_or_else(|| {
179                DispatcherError::UnknownKind {
180                    kind: "<no kind field>".to_string(),
181                }
182            })?;
183            let Some(h) = self.helpers.get(kind) else {
184                return Err(DispatcherError::UnknownKind {
185                    kind: kind.to_string(),
186                });
187            };
188            out.push(h(v, ctx));
189        }
190        Ok(out)
191    }
192
193    /// Number of helpers in the composition (sum of source
194    /// helpers; equal to the disjoint union of source variant
195    /// universes).
196    #[must_use]
197    pub fn helper_count(&self) -> usize {
198        self.helpers.len()
199    }
200
201    /// Read-only view of every kind covered by the composition,
202    /// in canonical sorted order.
203    #[must_use]
204    pub fn covered_kinds(&self) -> Vec<&str> {
205        self.helpers.keys().map(String::as_str).collect()
206    }
207
208    /// Read-only view of the sources contributing to the
209    /// composition (label + kinds each contributed).
210    #[must_use]
211    pub fn sources(&self) -> &[ComposedSource] {
212        &self.sources
213    }
214}