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}