1use 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 JsonText,
31}
32
33pub const CANONICAL_REALISATION_LISP: &str =
34 include_str!("../specs/realisation.lisp");
35
36pub fn load_canonical() -> Result<Vec<RealisationFormat>, SpecError> {
42 crate::loader::load_all::<RealisationFormat>(CANONICAL_REALISATION_LISP)
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ParsedRealisation {
51 pub id: String,
53 pub out_path: String,
55 pub signatures: Vec<String>,
57 pub dependent_realisations: Vec<String>,
59}
60
61pub 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 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}