1use std::collections::BTreeMap;
20use std::path::Path;
21
22use chrono::{DateTime, Utc};
23use serde::{Deserialize, Serialize};
24
25use crate::content::ContentHash;
26use crate::error::NapError;
27use crate::types::EntityType;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct Manifest {
48 pub id: String,
51
52 pub name: String,
54
55 pub entity_type: EntityType,
57
58 #[serde(default)]
60 pub version: u64,
61
62 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub principals: Option<Principal>,
65
66 #[serde(default)]
71 pub properties: BTreeMap<String, serde_yaml::Value>,
72
73 #[serde(default)]
76 pub representations: BTreeMap<String, Representation>,
77
78 #[serde(default)]
81 pub references: BTreeMap<String, serde_yaml::Value>,
82
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub provenance: Option<Provenance>,
86
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub head: Option<String>,
90
91 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
93 pub metadata: BTreeMap<String, serde_yaml::Value>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Principal {
99 #[serde(default)]
101 pub owners: Vec<String>,
102
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub maintainers: Vec<String>,
106
107 #[serde(default, skip_serializing_if = "Vec::is_empty")]
109 pub publishers: Vec<String>,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Representation {
115 pub hash: String,
117
118 pub format: String,
120
121 #[serde(default, skip_serializing_if = "Option::is_none")]
123 pub uri: Option<String>,
124
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub tier: Option<String>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct Provenance {
133 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub model: Option<String>,
136
137 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub prompt_hash: Option<String>,
140
141 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub seed: Option<String>,
144
145 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
147 pub parameters: BTreeMap<String, String>,
148
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub derived_from: Option<String>,
152
153 #[serde(default, skip_serializing_if = "Option::is_none")]
155 pub created_at: Option<DateTime<Utc>>,
156}
157
158impl Manifest {
159 pub fn new(universe: &str, entity_type: EntityType, entity_id: &str, name: &str) -> Self {
161 let id = match entity_type {
162 EntityType::World => format!("nap://{universe}/world/{universe}"),
163 _ => format!("nap://{universe}/{entity_type}/{entity_id}"),
164 };
165
166 Self {
167 id,
168 name: name.to_string(),
169 entity_type,
170 version: 0,
171 principals: None,
172 properties: BTreeMap::new(),
173 representations: BTreeMap::new(),
174 references: BTreeMap::new(),
175 provenance: None,
176 head: None,
177 metadata: BTreeMap::new(),
178 }
179 }
180
181 pub fn to_yaml(&self) -> Result<String, NapError> {
183 serde_yaml::to_string(self).map_err(|e| NapError::ManifestValidationError(e.to_string()))
184 }
185
186 pub fn from_yaml(yaml: &str) -> Result<Self, NapError> {
188 serde_yaml::from_str(yaml).map_err(|e| NapError::ManifestParseError {
189 path: "<string>".to_string(),
190 source: e,
191 })
192 }
193
194 pub fn from_file(path: &Path) -> Result<Self, NapError> {
196 let content = std::fs::read_to_string(path)?;
197 serde_yaml::from_str(&content).map_err(|e| NapError::ManifestParseError {
198 path: path.display().to_string(),
199 source: e,
200 })
201 }
202
203 pub fn to_file(&self, path: &Path) -> Result<(), NapError> {
205 let yaml = self.to_yaml()?;
206 if let Some(parent) = path.parent() {
207 std::fs::create_dir_all(parent)?;
208 }
209 std::fs::write(path, yaml).map_err(|e| NapError::ManifestWriteError {
210 path: path.display().to_string(),
211 source: e,
212 })
213 }
214
215 pub fn to_value(&self) -> Result<serde_yaml::Value, NapError> {
217 serde_yaml::to_value(self).map_err(|e| NapError::ManifestValidationError(e.to_string()))
218 }
219
220 pub fn to_json_value(&self) -> Result<serde_json::Value, NapError> {
222 serde_json::to_value(self).map_err(|e| NapError::ManifestValidationError(e.to_string()))
223 }
224
225 pub fn content_hash(&self) -> Result<ContentHash, NapError> {
227 let yaml = self.to_yaml()?;
228 Ok(ContentHash::from_str_content(&yaml))
229 }
230
231 pub fn set_representation(&mut self, key: &str, repr: Representation) {
233 self.representations.insert(key.to_string(), repr);
234 }
235
236 pub fn set_property(&mut self, key: &str, value: serde_yaml::Value) {
238 self.properties.insert(key.to_string(), value);
239 }
240
241 pub fn add_reference(&mut self, key: &str, value: serde_yaml::Value) {
243 self.references.insert(key.to_string(), value);
244 }
245
246 pub fn bump_version(&mut self) {
248 self.version += 1;
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_manifest_new() {
258 let manifest = Manifest::new(
259 "starwars",
260 EntityType::Character,
261 "lukeskywalker",
262 "Luke Skywalker",
263 );
264 assert_eq!(manifest.id, "nap://starwars/character/lukeskywalker");
265 assert_eq!(manifest.name, "Luke Skywalker");
266 assert_eq!(manifest.entity_type, EntityType::Character);
267 assert_eq!(manifest.version, 0);
268 }
269
270 #[test]
271 fn test_manifest_yaml_roundtrip() {
272 let mut manifest = Manifest::new(
273 "starwars",
274 EntityType::Character,
275 "lukeskywalker",
276 "Luke Skywalker",
277 );
278 manifest.set_property("species", serde_yaml::Value::String("human".to_string()));
279 manifest.set_representation(
280 "reference_image",
281 Representation {
282 hash: "blake3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
283 .to_string(),
284 format: "png".to_string(),
285 uri: Some("gs://assets/luke/ref.png".to_string()),
286 tier: Some("production".to_string()),
287 },
288 );
289
290 let yaml = manifest.to_yaml().unwrap();
291 let parsed = Manifest::from_yaml(&yaml).unwrap();
292
293 assert_eq!(parsed.id, manifest.id);
294 assert_eq!(parsed.name, manifest.name);
295 assert!(parsed.properties.contains_key("species"));
296 assert!(parsed.representations.contains_key("reference_image"));
297 }
298
299 #[test]
300 fn test_manifest_world_uri() {
301 let manifest = Manifest::new(
302 "starwars",
303 EntityType::World,
304 "starwars",
305 "Star Wars Universe",
306 );
307 assert_eq!(manifest.id, "nap://starwars/world/starwars");
308 }
309
310 #[test]
311 fn test_manifest_content_hash_deterministic() {
312 let manifest = Manifest::new(
313 "starwars",
314 EntityType::Character,
315 "lukeskywalker",
316 "Luke Skywalker",
317 );
318 let hash_a = manifest.content_hash().unwrap();
319 let hash_b = manifest.content_hash().unwrap();
320 assert_eq!(hash_a, hash_b);
321 }
322}