Skip to main content

sui_spec/
flake.rs

1//! Flake result shape — declarative policy for `getFlake` / `callFlake`
2//! output assembly.  CppNix parity, authored as Lisp data.
3//!
4//! Before this spec existed, the top-level flake attrset was built
5//! in `sui-eval/src/builtins/flake_eval.rs` by hand: the code
6//! inserted `_type`, `outPath`, `sourceInfo`, `inputs`, `outputs`
7//! explicitly, then iterated `flake_attrs` copying anything not
8//! already present.  That loop silently leaked every top-level
9//! attribute from the parsed `flake.nix` (`description`, `nixConfig`,
10//! ...) — which the first cross-repo sweep caught: CppNix does NOT
11//! expose those keys on the flake result, and every pleme-io flake's
12//! `(getFlake …)` result disagreed.
13//!
14//! This crate encodes the policy once:
15//!
16//! ```lisp
17//! (defflake-shape
18//!   :name                       "cppnix"
19//!   :type-marker                "flake"
20//!   :required-keys              ("_type" "outPath" "sourceInfo"
21//!                                "inputs" "outputs")
22//!   :spread-from-output-fn       t
23//!   :never-leak-from-flake-body ("description" "nixConfig"))
24//! ```
25//!
26//! The Rust caller asks the shape: "should I copy this key from
27//! the flake body?", "what's the type marker string?", "am I
28//! allowed to spread the outputs-fn result?".  Each question is
29//! a one-method call.
30
31use serde::{Deserialize, Serialize};
32use tatara_lisp::DeriveTataraDomain;
33
34use crate::SpecError;
35
36/// Top-level flake result shape policy.
37///
38/// `required_keys` is informational only — the Rust assembler in
39/// `flake_eval.rs` is responsible for *producing* the values (they
40/// all require eval-time state the spec doesn't have access to).
41/// The shape exists to answer yes/no decisions the assembler makes
42/// along the way.
43#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
44#[tatara(keyword = "defflake-shape")]
45pub struct FlakeShape {
46    pub name: String,
47    #[serde(rename = "typeMarker")]
48    pub type_marker: String,
49    #[serde(default, rename = "requiredKeys")]
50    pub required_keys: Vec<String>,
51    #[serde(default, rename = "spreadFromOutputFn")]
52    pub spread_from_output_fn: bool,
53    #[serde(default, rename = "neverLeakFromFlakeBody")]
54    pub never_leak_from_flake_body: Vec<String>,
55}
56
57impl FlakeShape {
58    /// Is `key` allowed to be copied from the parsed flake body
59    /// (`{ description = ...; outputs = ...; }`) onto the top-level
60    /// flake result?
61    ///
62    /// Note: the assembler always has to consult this — the default
63    /// answer under CppNix parity is "no for most keys".  Only
64    /// explicitly-allowed keys (typically `inputs`) should cross
65    /// over, and those are handled by named inserts, not by body
66    /// iteration.
67    #[must_use]
68    pub fn allow_body_key(&self, key: &str) -> bool {
69        !self.never_leak_from_flake_body.iter().any(|k| k == key)
70    }
71
72    /// Whether the outputs-function's returned attrset spreads
73    /// (its keys merge directly) into the top-level flake result.
74    /// CppNix says yes; every other policy is a departure.
75    #[must_use]
76    pub fn spreads_output_fn(&self) -> bool {
77        self.spread_from_output_fn
78    }
79}
80
81// ── Canonical spec ─────────────────────────────────────────────────
82
83pub const CPPNIX_FLAKE_SHAPE_LISP: &str = include_str!("../specs/flake.lisp");
84
85/// Compile the embedded canonical flake-shape spec.
86///
87/// # Errors
88///
89/// Returns an error if the compile-time spec fails to parse or
90/// produces no `(defflake-shape ...)` forms.
91pub fn load_canonical() -> Result<FlakeShape, SpecError> {
92    let mut compiled = tatara_lisp::compile_typed::<FlakeShape>(
93        CPPNIX_FLAKE_SHAPE_LISP,
94    )?;
95    compiled.pop().ok_or_else(|| SpecError::Load(
96        "no (defflake-shape ...) forms found in canonical spec".into(),
97    ))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn canonical_spec_parses() {
106        let shape = load_canonical().expect("canonical flake shape must compile");
107        assert_eq!(shape.name, "cppnix");
108        assert_eq!(shape.type_marker, "flake");
109        assert!(shape.spread_from_output_fn);
110    }
111
112    #[test]
113    fn description_does_not_leak_from_body() {
114        let shape = load_canonical().unwrap();
115        assert!(!shape.allow_body_key("description"),
116            "description must not leak — CppNix does not expose it at the top level");
117        assert!(!shape.allow_body_key("nixConfig"),
118            "nixConfig must not leak — CppNix does not expose it at the top level");
119    }
120
121    #[test]
122    fn inputs_and_outputs_reach_the_top() {
123        let shape = load_canonical().unwrap();
124        // These keys are named-inserts, not body-iterations, so the
125        // question here is simply "does the spec declare them as
126        // required"?
127        assert!(shape.required_keys.iter().any(|k| k == "inputs"));
128        assert!(shape.required_keys.iter().any(|k| k == "outputs"));
129        assert!(shape.required_keys.iter().any(|k| k == "_type"));
130    }
131}