use std::fs;
use std::path::{Path, PathBuf};
use super::learned_component::{
LearnedComponent, LearnedDepGraph, LearnedExploration, LearnedStrategy,
};
use super::scenario_profile::{ScenarioProfile, ScenarioProfileId};
#[derive(Debug, thiserror::Error)]
pub enum ProfileStoreError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Profile not found: {0}")]
NotFound(String),
#[error("Profile already exists: {0}")]
AlreadyExists(String),
}
pub struct ProfileStore {
base_dir: PathBuf,
}
impl ProfileStore {
pub fn new(base_dir: impl AsRef<Path>) -> Self {
Self {
base_dir: base_dir.as_ref().to_path_buf(),
}
}
pub fn default_path() -> Option<Self> {
dirs::home_dir().map(|home| Self::new(home.join(".swarm-engine/profiles")))
}
pub fn base_dir(&self) -> &Path {
&self.base_dir
}
fn profile_dir(&self, id: &ScenarioProfileId) -> PathBuf {
self.base_dir.join(&id.0)
}
pub fn save(&self, profile: &ScenarioProfile) -> Result<(), ProfileStoreError> {
let dir = self.profile_dir(&profile.id);
fs::create_dir_all(&dir)?;
let profile_path = dir.join("profile.json");
let json = serde_json::to_string_pretty(profile)?;
fs::write(profile_path, json)?;
if let Some(dep_graph) = &profile.dep_graph {
self.save_component(&profile.id, dep_graph)?;
}
if let Some(exploration) = &profile.exploration {
self.save_component(&profile.id, exploration)?;
}
if let Some(strategy) = &profile.strategy {
self.save_component(&profile.id, strategy)?;
}
Ok(())
}
pub fn load(&self, id: &ScenarioProfileId) -> Result<ScenarioProfile, ProfileStoreError> {
let dir = self.profile_dir(id);
let profile_path = dir.join("profile.json");
if !profile_path.exists() {
return Err(ProfileStoreError::NotFound(id.0.clone()));
}
let json = fs::read_to_string(profile_path)?;
let mut profile: ScenarioProfile = serde_json::from_str(&json)?;
if let Ok(dep_graph) = self.load_component::<LearnedDepGraph>(id) {
profile.dep_graph = Some(dep_graph);
}
if let Ok(exploration) = self.load_component::<LearnedExploration>(id) {
profile.exploration = Some(exploration);
}
if let Ok(strategy) = self.load_component::<LearnedStrategy>(id) {
profile.strategy = Some(strategy);
}
Ok(profile)
}
pub fn delete(&self, id: &ScenarioProfileId) -> Result<(), ProfileStoreError> {
let dir = self.profile_dir(id);
if dir.exists() {
fs::remove_dir_all(dir)?;
}
Ok(())
}
pub fn exists(&self, id: &ScenarioProfileId) -> bool {
self.profile_dir(id).join("profile.json").exists()
}
pub fn list_ids(&self) -> Result<Vec<ScenarioProfileId>, ProfileStoreError> {
if !self.base_dir.exists() {
return Ok(Vec::new());
}
let mut ids = Vec::new();
for entry in fs::read_dir(&self.base_dir)? {
let entry = entry?;
if entry.file_type()?.is_dir() {
let profile_json = entry.path().join("profile.json");
if profile_json.exists() {
if let Some(name) = entry.file_name().to_str() {
ids.push(ScenarioProfileId::new(name));
}
}
}
}
Ok(ids)
}
pub fn load_all(&self) -> Result<Vec<ScenarioProfile>, ProfileStoreError> {
let ids = self.list_ids()?;
let mut profiles = Vec::new();
for id in ids {
match self.load(&id) {
Ok(profile) => profiles.push(profile),
Err(e) => {
tracing::warn!("Failed to load profile {}: {}", id, e);
}
}
}
Ok(profiles)
}
pub fn save_component<C: LearnedComponent>(
&self,
id: &ScenarioProfileId,
component: &C,
) -> Result<(), ProfileStoreError> {
let dir = self.profile_dir(id);
fs::create_dir_all(&dir)?;
let path = dir.join(format!("{}.json", C::component_id()));
let json = serde_json::to_string_pretty(component)?;
fs::write(path, json)?;
Ok(())
}
pub fn load_component<C: LearnedComponent>(
&self,
id: &ScenarioProfileId,
) -> Result<C, ProfileStoreError> {
let path = self
.profile_dir(id)
.join(format!("{}.json", C::component_id()));
if !path.exists() {
return Err(ProfileStoreError::NotFound(format!(
"{}/{}",
id.0,
C::component_id()
)));
}
let json = fs::read_to_string(path)?;
let component: C = serde_json::from_str(&json)?;
Ok(component)
}
pub fn delete_component<C: LearnedComponent>(
&self,
id: &ScenarioProfileId,
) -> Result<(), ProfileStoreError> {
let path = self
.profile_dir(id)
.join(format!("{}.json", C::component_id()));
if path.exists() {
fs::remove_file(path)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::exploration::DependencyGraph;
use tempfile::TempDir;
fn create_test_store() -> (ProfileStore, TempDir) {
let temp_dir = TempDir::new().unwrap();
let store = ProfileStore::new(temp_dir.path());
(store, temp_dir)
}
#[test]
fn test_save_and_load_profile() {
let (store, _temp) = create_test_store();
let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
store.save(&profile).unwrap();
let loaded = store.load(&profile.id).unwrap();
assert_eq!(loaded.id.0, "test");
}
#[test]
fn test_save_and_load_with_components() {
let (store, _temp) = create_test_store();
let mut profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
let dep_graph = LearnedDepGraph::new(DependencyGraph::new(), vec!["A".to_string()])
.with_confidence(0.8);
profile.dep_graph = Some(dep_graph);
let exploration = LearnedExploration::new(2.0, 0.5, 1.0);
profile.exploration = Some(exploration);
store.save(&profile).unwrap();
let loaded = store.load(&profile.id).unwrap();
assert!(loaded.dep_graph.is_some());
assert!(loaded.exploration.is_some());
assert_eq!(loaded.dep_graph.unwrap().confidence, 0.8);
}
#[test]
fn test_list_ids() {
let (store, _temp) = create_test_store();
let profile1 = ScenarioProfile::from_file("profile1", "/path/to/1.toml");
let profile2 = ScenarioProfile::from_file("profile2", "/path/to/2.toml");
store.save(&profile1).unwrap();
store.save(&profile2).unwrap();
let ids = store.list_ids().unwrap();
assert_eq!(ids.len(), 2);
}
#[test]
fn test_exists() {
let (store, _temp) = create_test_store();
let id = ScenarioProfileId::new("test");
assert!(!store.exists(&id));
let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
store.save(&profile).unwrap();
assert!(store.exists(&id));
}
#[test]
fn test_delete() {
let (store, _temp) = create_test_store();
let profile = ScenarioProfile::from_file("test", "/path/to/scenario.toml");
store.save(&profile).unwrap();
assert!(store.exists(&profile.id));
store.delete(&profile.id).unwrap();
assert!(!store.exists(&profile.id));
}
}