Skip to main content

sui_spec/
spec_trait.rs

1//! The [`Spec`] trait — the unifying abstraction across every
2//! authored sui-spec domain.
3//!
4//! Each domain in sui-spec is a typed Lisp surface: one keyword,
5//! one embedded `.lisp` source, one `load_canonical()` function.
6//! Until now those load functions were free functions with the
7//! same shape but no type-level relationship — every domain
8//! reinvented the same three lines.  This trait names the contract
9//! once.  Generic code over `T: Spec` can:
10//!
11//! - load every domain's canonical corpus uniformly,
12//! - iterate the substrate by type-erased trait objects when
13//!   needed (the `SpecHandle` enum below carries the type-erasure),
14//! - enforce at compile time that new domains expose the API
15//!   (omitting it breaks downstream `T: Spec` consumers).
16//!
17//! Per the prime directive: solve once, in one place, stand on a
18//! solid abstraction.
19
20use serde::de::DeserializeOwned;
21use tatara_lisp::TataraDomain;
22
23use crate::SpecError;
24
25/// The contract every typed sui-spec domain satisfies.
26///
27/// `TataraDomain` is the upstream marker the derive macro emits;
28/// `DeserializeOwned` lets the trait load self from JSON when the
29/// tatara compile pipeline returns deserialised values.  The
30/// const `CANONICAL_LISP` carries the embedded source so generic
31/// code doesn't need to know the file path.
32pub trait Spec: TataraDomain + DeserializeOwned + Sized {
33    /// The embedded canonical Lisp source for this domain.
34    /// Wrapped over `include_str!` in the domain's own module.
35    const CANONICAL_LISP: &'static str;
36
37    /// Compile the canonical source and return every authored
38    /// instance.  Default impl threads through
39    /// `crate::loader::load_all`; domains generally don't need to
40    /// override.
41    ///
42    /// # Errors
43    ///
44    /// Returns an error if the Lisp source fails to parse under
45    /// the domain's typed schema.
46    fn load_canonical_all() -> Result<Vec<Self>, SpecError> {
47        crate::loader::load_all::<Self>(Self::CANONICAL_LISP)
48    }
49
50    /// Look up the canonical instance with the given `name`.
51    /// Default impl scans `load_canonical_all` linearly; domains
52    /// that store many instances may override with a hashmap.
53    ///
54    /// # Errors
55    ///
56    /// Returns `SpecError::Load` if the source fails to parse or no
57    /// instance matches `name`.
58    fn load_named(name: &str) -> Result<Self, SpecError>
59    where
60        Self: HasName,
61    {
62        Self::load_canonical_all()?
63            .into_iter()
64            .find(|s| s.name() == name)
65            .ok_or_else(|| {
66                SpecError::Load(format!(
67                    "no ({}) with :name {name:?}",
68                    Self::KEYWORD,
69                ))
70            })
71    }
72}
73
74/// Sub-trait for spec types that carry a `name: String` field.
75/// Most do; a few (derivation Phase, fetcher phase) don't because
76/// they're sub-structs inside a parent that has the name.
77pub trait HasName {
78    fn name(&self) -> &str;
79}
80
81// ── Blanket impls — every existing domain plugs in by adding one ──
82//    line in its module.  See sui-spec/src/derivation.rs etc.
83
84impl Spec for crate::derivation::DerivationAlgorithm {
85    const CANONICAL_LISP: &'static str = crate::derivation::CPPNIX_INPUT_ADDRESSED_LISP;
86}
87impl HasName for crate::derivation::DerivationAlgorithm {
88    fn name(&self) -> &str { &self.name }
89}
90
91impl Spec for crate::flake::FlakeShape {
92    const CANONICAL_LISP: &'static str = crate::flake::CPPNIX_FLAKE_SHAPE_LISP;
93}
94impl HasName for crate::flake::FlakeShape {
95    fn name(&self) -> &str { &self.name }
96}
97
98impl Spec for crate::probe::Probe {
99    const CANONICAL_LISP: &'static str = crate::probe::CANONICAL_PROBES_LISP;
100}
101impl HasName for crate::probe::Probe {
102    fn name(&self) -> &str { &self.name }
103}
104
105impl Spec for crate::rebuild::RebuildProbe {
106    const CANONICAL_LISP: &'static str = crate::rebuild::CANONICAL_REBUILD_PROBES_LISP;
107}
108impl HasName for crate::rebuild::RebuildProbe {
109    fn name(&self) -> &str { &self.name }
110}
111
112impl Spec for crate::module_system::ModuleEvalAlgorithm {
113    const CANONICAL_LISP: &'static str = crate::module_system::CANONICAL_MODULE_SYSTEM_LISP;
114}
115impl HasName for crate::module_system::ModuleEvalAlgorithm {
116    fn name(&self) -> &str { &self.name }
117}
118
119impl Spec for crate::module_system::OptionTypeSpec {
120    const CANONICAL_LISP: &'static str = crate::module_system::CANONICAL_MODULE_SYSTEM_LISP;
121}
122impl HasName for crate::module_system::OptionTypeSpec {
123    fn name(&self) -> &str { &self.name }
124}
125
126impl Spec for crate::module_system::PriorityRank {
127    const CANONICAL_LISP: &'static str = crate::module_system::CANONICAL_MODULE_SYSTEM_LISP;
128}
129impl HasName for crate::module_system::PriorityRank {
130    fn name(&self) -> &str { &self.name }
131}
132
133impl Spec for crate::activation_script::ActivationScriptAlgorithm {
134    const CANONICAL_LISP: &'static str = crate::activation_script::CANONICAL_ACTIVATION_LISP;
135}
136impl HasName for crate::activation_script::ActivationScriptAlgorithm {
137    fn name(&self) -> &str { &self.name }
138}
139
140impl Spec for crate::fetcher::FetcherSpec {
141    const CANONICAL_LISP: &'static str = crate::fetcher::CANONICAL_FETCHERS_LISP;
142}
143impl HasName for crate::fetcher::FetcherSpec {
144    fn name(&self) -> &str { &self.name }
145}
146
147impl Spec for crate::substituter::SubstituterSpec {
148    const CANONICAL_LISP: &'static str = crate::substituter::CANONICAL_SUBSTITUTERS_LISP;
149}
150impl HasName for crate::substituter::SubstituterSpec {
151    fn name(&self) -> &str { &self.name }
152}
153
154impl Spec for crate::sandbox::SandboxSpec {
155    const CANONICAL_LISP: &'static str = crate::sandbox::CANONICAL_SANDBOX_LISP;
156}
157impl HasName for crate::sandbox::SandboxSpec {
158    fn name(&self) -> &str { &self.name }
159}
160
161impl Spec for crate::store_layout::StoreLayout {
162    const CANONICAL_LISP: &'static str = crate::store_layout::CANONICAL_STORE_LAYOUT_LISP;
163}
164impl HasName for crate::store_layout::StoreLayout {
165    fn name(&self) -> &str { &self.name }
166}
167
168impl Spec for crate::gc::GcAlgorithm {
169    const CANONICAL_LISP: &'static str = crate::gc::CANONICAL_GC_LISP;
170}
171impl HasName for crate::gc::GcAlgorithm {
172    fn name(&self) -> &str { &self.name }
173}
174
175impl Spec for crate::hash::HashAlgorithm {
176    const CANONICAL_LISP: &'static str = crate::hash::CANONICAL_HASH_LISP;
177}
178impl HasName for crate::hash::HashAlgorithm {
179    fn name(&self) -> &str { &self.name }
180}
181
182impl Spec for crate::hash::HashEncoding {
183    const CANONICAL_LISP: &'static str = crate::hash::CANONICAL_HASH_LISP;
184}
185impl HasName for crate::hash::HashEncoding {
186    fn name(&self) -> &str { &self.name }
187}
188
189impl Spec for crate::nar::NarFormat {
190    const CANONICAL_LISP: &'static str = crate::nar::CANONICAL_NAR_LISP;
191}
192impl HasName for crate::nar::NarFormat {
193    fn name(&self) -> &str { &self.name }
194}
195
196impl Spec for crate::narinfo::NarinfoFormat {
197    const CANONICAL_LISP: &'static str = crate::narinfo::CANONICAL_NARINFO_LISP;
198}
199impl HasName for crate::narinfo::NarinfoFormat {
200    fn name(&self) -> &str { &self.name }
201}
202
203impl Spec for crate::eval_cache::EvalCacheFormat {
204    const CANONICAL_LISP: &'static str = crate::eval_cache::CANONICAL_EVAL_CACHE_LISP;
205}
206impl HasName for crate::eval_cache::EvalCacheFormat {
207    fn name(&self) -> &str { &self.name }
208}
209
210impl Spec for crate::profile::ProfileFormat {
211    const CANONICAL_LISP: &'static str = crate::profile::CANONICAL_PROFILE_LISP;
212}
213impl HasName for crate::profile::ProfileFormat {
214    fn name(&self) -> &str { &self.name }
215}
216
217impl Spec for crate::realisation::RealisationFormat {
218    const CANONICAL_LISP: &'static str = crate::realisation::CANONICAL_REALISATION_LISP;
219}
220impl HasName for crate::realisation::RealisationFormat {
221    fn name(&self) -> &str { &self.name }
222}
223
224impl Spec for crate::lock_file::LockFileFormat {
225    const CANONICAL_LISP: &'static str = crate::lock_file::CANONICAL_LOCK_FILE_LISP;
226}
227impl HasName for crate::lock_file::LockFileFormat {
228    fn name(&self) -> &str { &self.name }
229}
230
231impl Spec for crate::registry::RegistryFormat {
232    const CANONICAL_LISP: &'static str = crate::registry::CANONICAL_REGISTRY_LISP;
233}
234impl HasName for crate::registry::RegistryFormat {
235    fn name(&self) -> &str { &self.name }
236}
237
238impl Spec for crate::trust_model::TrustModel {
239    const CANONICAL_LISP: &'static str = crate::trust_model::CANONICAL_TRUST_LISP;
240}
241impl HasName for crate::trust_model::TrustModel {
242    fn name(&self) -> &str { &self.name }
243}
244
245impl Spec for crate::worker_protocol::WorkerProtocol {
246    const CANONICAL_LISP: &'static str = crate::worker_protocol::CANONICAL_WORKER_PROTOCOL_LISP;
247}
248impl HasName for crate::worker_protocol::WorkerProtocol {
249    fn name(&self) -> &str { &self.name }
250}
251
252impl Spec for crate::worker_protocol::WorkerOpcode {
253    const CANONICAL_LISP: &'static str = crate::worker_protocol::CANONICAL_WORKER_PROTOCOL_LISP;
254}
255impl HasName for crate::worker_protocol::WorkerOpcode {
256    fn name(&self) -> &str { &self.name }
257}
258
259impl Spec for crate::catalog::SubstrateDomain {
260    const CANONICAL_LISP: &'static str = crate::catalog::CANONICAL_CATALOG_LISP;
261}
262impl HasName for crate::catalog::SubstrateDomain {
263    fn name(&self) -> &str { &self.name }
264}
265
266impl Spec for crate::cli_coverage::SuiCommand {
267    const CANONICAL_LISP: &'static str = crate::cli_coverage::CANONICAL_CLI_COVERAGE_LISP;
268}
269impl HasName for crate::cli_coverage::SuiCommand {
270    fn name(&self) -> &str { &self.name }
271}
272
273impl Spec for crate::store_ops::StoreSlice {
274    const CANONICAL_LISP: &'static str = crate::store_ops::CANONICAL_STORE_OPS_LISP;
275}
276impl HasName for crate::store_ops::StoreSlice {
277    fn name(&self) -> &str { &self.name }
278}
279
280impl Spec for crate::store_inventory::StoreInventoryProfile {
281    const CANONICAL_LISP: &'static str = crate::store_inventory::CANONICAL_STORE_INVENTORY_LISP;
282}
283impl HasName for crate::store_inventory::StoreInventoryProfile {
284    fn name(&self) -> &str { &self.name }
285}
286
287impl Spec for crate::store_transform::StoreTransform {
288    const CANONICAL_LISP: &'static str = crate::store_transform::CANONICAL_STORE_TRANSFORMS_LISP;
289}
290impl HasName for crate::store_transform::StoreTransform {
291    fn name(&self) -> &str { &self.name }
292}
293
294impl Spec for crate::store_recipe::StoreRecipe {
295    const CANONICAL_LISP: &'static str = crate::store_recipe::CANONICAL_STORE_RECIPES_LISP;
296}
297impl HasName for crate::store_recipe::StoreRecipe {
298    fn name(&self) -> &str { &self.name }
299}
300
301impl Spec for crate::store_query::StoreQuery {
302    const CANONICAL_LISP: &'static str = crate::store_query::CANONICAL_STORE_QUERIES_LISP;
303}
304impl HasName for crate::store_query::StoreQuery {
305    fn name(&self) -> &str { &self.name }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use crate::probe::Probe;
312
313    #[test]
314    fn spec_load_canonical_all_works_through_trait() {
315        let probes = Probe::load_canonical_all().expect("load via trait");
316        assert!(!probes.is_empty());
317    }
318
319    #[test]
320    fn spec_load_named_finds_known_probe() {
321        let p = Probe::load_named("getflake-outPath").expect("load via trait");
322        assert_eq!(p.name(), "getflake-outPath");
323    }
324
325    #[test]
326    fn spec_load_named_errors_on_missing() {
327        let err = Probe::load_named("no-such-probe").expect_err("must error");
328        match err {
329            SpecError::Load(msg) => {
330                assert!(msg.contains("no-such-probe"));
331                assert!(msg.contains("defprobe"),
332                    "error must mention the keyword");
333            }
334            _ => panic!("expected SpecError::Load"),
335        }
336    }
337
338    #[test]
339    fn spec_keyword_matches_attribute() {
340        // The trait's KEYWORD comes from #[tatara(keyword=...)].
341        assert_eq!(Probe::KEYWORD, "defprobe");
342    }
343
344    /// Generic function over Spec — proves the trait composes.
345    /// Counts the canonical instances of any Spec type.
346    fn count_canonical<S: Spec>() -> usize {
347        S::load_canonical_all().map(|v| v.len()).unwrap_or(0)
348    }
349
350    #[test]
351    fn generic_count_over_spec_trait() {
352        let derivation_count = count_canonical::<crate::derivation::DerivationAlgorithm>();
353        let fetcher_count = count_canonical::<crate::fetcher::FetcherSpec>();
354        let catalog_count = count_canonical::<crate::catalog::SubstrateDomain>();
355        // Substrate has at least one derivation algo, five fetchers,
356        // and the full catalog.
357        assert!(derivation_count >= 1);
358        assert!(fetcher_count >= 5);
359        assert!(catalog_count >= 15);
360    }
361}