use std::path::Path; use serde::{Deserialize, Serialize}; use crate::{RuntimeState, RuntimeSummary}; pub const SNAPSHOT_FORMAT_VERSION: u32 = 1; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] pub struct RuntimeSnapshotSource { #[serde(default)] pub source_fixture_id: Option, #[serde(default)] pub description: Option, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct RuntimeSnapshotDocument { pub format_version: u32, pub snapshot_id: String, #[serde(default)] pub source: RuntimeSnapshotSource, pub state: RuntimeState, } impl RuntimeSnapshotDocument { pub fn summary(&self) -> RuntimeSummary { RuntimeSummary::from_state(&self.state) } } pub fn validate_runtime_snapshot_document( document: &RuntimeSnapshotDocument, ) -> Result<(), String> { if document.format_version != SNAPSHOT_FORMAT_VERSION { return Err(format!( "unsupported snapshot format_version {} (expected {})", document.format_version, SNAPSHOT_FORMAT_VERSION )); } if document.snapshot_id.trim().is_empty() { return Err("snapshot_id must not be empty".to_string()); } document.state.validate() } pub fn load_runtime_snapshot_document( path: &Path, ) -> Result> { let text = std::fs::read_to_string(path)?; Ok(serde_json::from_str(&text)?) } pub fn save_runtime_snapshot_document( path: &Path, document: &RuntimeSnapshotDocument, ) -> Result<(), Box> { validate_runtime_snapshot_document(document) .map_err(|err| format!("invalid runtime snapshot document: {err}"))?; let bytes = serde_json::to_vec_pretty(document)?; if let Some(parent) = path.parent() { std::fs::create_dir_all(parent)?; } std::fs::write(path, bytes)?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::{ CalendarPoint, RuntimeSaveProfileState, RuntimeServiceState, RuntimeWorldRestoreState, }; use std::collections::BTreeMap; fn snapshot() -> RuntimeSnapshotDocument { RuntimeSnapshotDocument { format_version: SNAPSHOT_FORMAT_VERSION, snapshot_id: "snapshot-smoke".to_string(), source: RuntimeSnapshotSource { source_fixture_id: Some("fixture-smoke".to_string()), description: Some("test snapshot".to_string()), }, state: RuntimeState { calendar: CalendarPoint { year: 1830, month_slot: 0, phase_slot: 0, tick_slot: 0, }, world_flags: BTreeMap::new(), save_profile: RuntimeSaveProfileState::default(), world_restore: RuntimeWorldRestoreState::default(), metadata: BTreeMap::new(), companies: Vec::new(), event_runtime_records: Vec::new(), candidate_availability: BTreeMap::new(), special_conditions: BTreeMap::new(), service_state: RuntimeServiceState::default(), }, } } #[test] fn validates_snapshot_document() { let document = snapshot(); assert!(validate_runtime_snapshot_document(&document).is_ok()); } #[test] fn roundtrips_snapshot_json() { let document = snapshot(); let value = serde_json::to_string_pretty(&document).expect("snapshot should serialize"); let reparsed: RuntimeSnapshotDocument = serde_json::from_str(&value).expect("snapshot should deserialize"); assert_eq!(document, reparsed); } }