Skip to main content

sui_spec/
realisation.rs

1//! Typed border for content-addressed derivation realisations.
2//!
3//! When a CA-drv is built, its output's actual store path depends
4//! on the realised content's hash.  The mapping from drv path +
5//! output name to realised store path is the *realisation*.
6//! cppnix serialises realisations as JSON to `/nix/var/nix/realisations/`
7//! and serves them via the substituter protocol alongside narinfo
8//! files.
9//!
10//! This module names the realisation format as a typed Lisp spec.
11
12use serde::{Deserialize, Serialize};
13use tatara_lisp::DeriveTataraDomain;
14
15use crate::SpecError;
16
17#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
18#[tatara(keyword = "defrealisation-format")]
19pub struct RealisationFormat {
20    pub name: String,
21    pub version: u32,
22    pub encoding: RealisationEncoding,
23    #[serde(rename = "requiredFields")]
24    pub required_fields: Vec<String>,
25}
26
27#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
28pub enum RealisationEncoding {
29    /// JSON file format used by cppnix.
30    JsonText,
31}
32
33pub const CANONICAL_REALISATION_LISP: &str =
34    include_str!("../specs/realisation.lisp");
35
36/// Compile every authored realisation format.
37///
38/// # Errors
39///
40/// Returns an error if the Lisp source fails to parse.
41pub fn load_canonical() -> Result<Vec<RealisationFormat>, SpecError> {
42    crate::loader::load_all::<RealisationFormat>(CANONICAL_REALISATION_LISP)
43}
44
45// ── M3.0 realisation parser ────────────────────────────────────────
46
47/// Parsed CA-drv realisation record.  The mapping from
48/// `<drv-path>!<output-name>` to the realised store path.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ParsedRealisation {
51    /// `<drv-path>!<output-name>` identifier.
52    pub id: String,
53    /// Realised store path (the actual output bytes' location).
54    pub out_path: String,
55    /// Signatures attesting to this realisation.
56    pub signatures: Vec<String>,
57    /// Dependent realisations this one references.
58    pub dependent_realisations: Vec<String>,
59}
60
61/// Parse a realisation JSON record against a format spec.
62///
63/// # Errors
64///
65/// - `realisation-parse` for malformed JSON.
66/// - `realisation-missing-required` for absent required fields.
67pub fn parse(text: &str, format: &RealisationFormat) -> Result<ParsedRealisation, SpecError> {
68    let value: serde_json::Value = serde_json::from_str(text).map_err(|e| SpecError::Interp {
69        phase: "realisation-parse".into(),
70        message: format!("malformed JSON: {e}"),
71    })?;
72    let obj = value.as_object().ok_or_else(|| SpecError::Interp {
73        phase: "realisation-parse".into(),
74        message: "top-level value is not a JSON object".into(),
75    })?;
76    for field in &format.required_fields {
77        if !obj.contains_key(field) {
78            return Err(SpecError::Interp {
79                phase: "realisation-missing-required".into(),
80                message: format!(
81                    "realisation missing required field `{field}` per format `{}`",
82                    format.name,
83                ),
84            });
85        }
86    }
87    Ok(ParsedRealisation {
88        id: obj.get("id").and_then(|v| v.as_str()).unwrap_or_default().into(),
89        out_path: obj.get("outPath").and_then(|v| v.as_str()).unwrap_or_default().into(),
90        signatures: obj
91            .get("signatures")
92            .and_then(|v| v.as_array())
93            .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
94            .unwrap_or_default(),
95        dependent_realisations: obj
96            .get("dependentRealisations")
97            .and_then(|v| v.as_array())
98            .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
99            .unwrap_or_default(),
100    })
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn canonical_realisation_parses() {
109        let formats = load_canonical().unwrap();
110        assert!(!formats.is_empty());
111    }
112
113    #[test]
114    fn cppnix_realisation_v1_has_essential_fields() {
115        let formats = load_canonical().unwrap();
116        let v1 = formats
117            .iter()
118            .find(|f| f.name == "cppnix-realisation-v1")
119            .unwrap();
120        for required in ["id", "outPath", "signatures", "dependentRealisations"] {
121            assert!(
122                v1.required_fields.iter().any(|f| f == required),
123                "realisation v1 missing field {required}",
124            );
125        }
126    }
127
128    // ── M3.0 parser tests ──────────────────────────────────────
129
130    fn fmt() -> RealisationFormat {
131        load_canonical().unwrap().into_iter()
132            .find(|f| f.name == "cppnix-realisation-v1").unwrap()
133    }
134
135    #[test]
136    fn parse_canonical_realisation() {
137        let json = r#"{
138            "id": "sha256:abc!out",
139            "outPath": "/nix/store/abc-hello",
140            "signatures": ["cache.nixos.org-1:sig"],
141            "dependentRealisations": ["sha256:def!out"]
142        }"#;
143        let parsed = parse(json, &fmt()).unwrap();
144        assert_eq!(parsed.id, "sha256:abc!out");
145        assert_eq!(parsed.out_path, "/nix/store/abc-hello");
146        assert_eq!(parsed.signatures.len(), 1);
147        assert_eq!(parsed.dependent_realisations, vec!["sha256:def!out".to_string()]);
148    }
149
150    #[test]
151    fn missing_required_field_errors() {
152        let json = r#"{ "id": "sha256:abc!out" }"#;
153        let err = parse(json, &fmt()).unwrap_err();
154        match err {
155            SpecError::Interp { phase, .. } => assert_eq!(phase, "realisation-missing-required"),
156            _ => panic!("expected missing-required"),
157        }
158    }
159
160    #[test]
161    fn malformed_json_errors() {
162        let err = parse("not json", &fmt()).unwrap_err();
163        match err {
164            SpecError::Interp { phase, .. } => assert_eq!(phase, "realisation-parse"),
165            _ => panic!("expected realisation-parse"),
166        }
167    }
168}