use somatize_core::error::{Result, SomaError};
use somatize_core::study::Study;
use std::path::Path;
pub trait StudyIo: Sized {
fn save(&self, path: impl AsRef<Path>) -> Result<()>;
fn load(path: impl AsRef<Path>) -> Result<Self>;
}
impl StudyIo for Study {
fn save(&self, path: impl AsRef<Path>) -> Result<()> {
let json =
serde_json::to_vec_pretty(self).map_err(|e| SomaError::Serialization(e.to_string()))?;
std::fs::write(path, json)?;
Ok(())
}
fn load(path: impl AsRef<Path>) -> Result<Self> {
let bytes = std::fs::read(path)?;
serde_json::from_slice(&bytes).map_err(|e| SomaError::Serialization(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use somatize_core::search::SearchSpace;
use somatize_core::study::{Direction, Objective, SearchStrategy, Study};
fn study() -> Study {
Study::new(
"persisted",
SearchSpace::new(),
SearchStrategy::Grid { points_per_dim: 3 },
vec![Objective {
metric: "f1".into(),
direction: Direction::Maximize,
}],
)
}
#[test]
fn a_study_survives_a_round_trip_through_a_file() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("study.json");
let mut original = study();
original.tags = vec!["mos".into()];
original.planned_trials = Some(6);
original.git_sha = Some("abc123".into());
original.save(&path).unwrap();
let back = Study::load(&path).unwrap();
assert_eq!(back.name, "persisted");
assert_eq!(back.tags, vec!["mos"]);
assert_eq!(back.planned_trials, Some(6));
assert_eq!(back.git_sha.as_deref(), Some("abc123"));
assert!(back.created_at.is_some());
}
#[test]
fn load_errors_are_typed() {
let missing = Study::load("/nonexistent/dir/study.json");
assert!(matches!(missing, Err(SomaError::Io(_))), "{missing:?}");
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("study.json");
std::fs::write(&path, "{not json").unwrap();
let corrupt = Study::load(&path);
assert!(
matches!(corrupt, Err(SomaError::Serialization(_))),
"{corrupt:?}"
);
}
}