use serde::{Deserialize, Serialize};
use tatara_lisp::DeriveTataraDomain;
use crate::SpecError;
#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
#[tatara(keyword = "defflake-shape")]
pub struct FlakeShape {
pub name: String,
#[serde(rename = "typeMarker")]
pub type_marker: String,
#[serde(default, rename = "requiredKeys")]
pub required_keys: Vec<String>,
#[serde(default, rename = "spreadFromOutputFn")]
pub spread_from_output_fn: bool,
#[serde(default, rename = "neverLeakFromFlakeBody")]
pub never_leak_from_flake_body: Vec<String>,
}
impl FlakeShape {
#[must_use]
pub fn allow_body_key(&self, key: &str) -> bool {
!self.never_leak_from_flake_body.iter().any(|k| k == key)
}
#[must_use]
pub fn spreads_output_fn(&self) -> bool {
self.spread_from_output_fn
}
}
pub const CPPNIX_FLAKE_SHAPE_LISP: &str = include_str!("../specs/flake.lisp");
pub fn load_canonical() -> Result<FlakeShape, SpecError> {
let mut compiled = tatara_lisp::compile_typed::<FlakeShape>(
CPPNIX_FLAKE_SHAPE_LISP,
)?;
compiled.pop().ok_or_else(|| SpecError::Load(
"no (defflake-shape ...) forms found in canonical spec".into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonical_spec_parses() {
let shape = load_canonical().expect("canonical flake shape must compile");
assert_eq!(shape.name, "cppnix");
assert_eq!(shape.type_marker, "flake");
assert!(shape.spread_from_output_fn);
}
#[test]
fn description_does_not_leak_from_body() {
let shape = load_canonical().unwrap();
assert!(!shape.allow_body_key("description"),
"description must not leak — CppNix does not expose it at the top level");
assert!(!shape.allow_body_key("nixConfig"),
"nixConfig must not leak — CppNix does not expose it at the top level");
}
#[test]
fn inputs_and_outputs_reach_the_top() {
let shape = load_canonical().unwrap();
assert!(shape.required_keys.iter().any(|k| k == "inputs"));
assert!(shape.required_keys.iter().any(|k| k == "outputs"));
assert!(shape.required_keys.iter().any(|k| k == "_type"));
}
}