rrt/crates/rrt-runtime/src/persistence.rs

126 lines
4.1 KiB
Rust

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<String>,
#[serde(default)]
pub description: Option<String>,
}
#[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<RuntimeSnapshotDocument, Box<dyn std::error::Error>> {
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<dyn std::error::Error>> {
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(),
selected_company_id: None,
players: Vec::new(),
selected_player_id: None,
trains: Vec::new(),
territories: Vec::new(),
company_territory_track_piece_counts: Vec::new(),
company_territory_access: Vec::new(),
packed_event_collection: None,
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);
}
}