use crate::{config::Config, session::Session, Result, TenxError};
use fs_err as fs;
use std::path::{Path, PathBuf};
pub fn path_to_filename(path: &Path) -> String {
path.to_string_lossy()
.replace(['/', '\\'], "_")
.replace([':', '<', '>', '"', '|', '?', '*'], "")
}
pub fn load_session<P: AsRef<Path>>(path: P) -> Result<Session> {
let path = path.as_ref();
if !path.exists() {
return Err(TenxError::SessionStore(format!(
"No such session: {}",
path.display()
)));
}
let serialized = fs::read_to_string(path)
.map_err(|e| TenxError::SessionStore(format!("Failed to read session: {}", e)))?;
serde_json::from_str(&serialized)
.map_err(|e| TenxError::SessionStore(format!("Failed to parse session: {}", e)))
}
pub struct SessionStore {
base_dir: PathBuf,
}
impl SessionStore {
pub fn open(base_dir: PathBuf) -> Result<Self> {
fs::create_dir_all(&base_dir)?;
Ok(Self { base_dir })
}
pub fn save(&self, name: &str, state: &Session) -> Result<()> {
let file_path = self.base_dir.join(name);
let serialized = serde_json::to_string(state)
.map_err(|e| TenxError::SessionStore(format!("serialization failed: {}", e)))?;
fs::write(&file_path, serialized)?;
Ok(())
}
pub fn save_current(&self, config: &Config, state: &Session) -> Result<()> {
let file_name = path_to_filename(&config.project_root());
self.save(&file_name, state)
}
pub fn load<S: AsRef<str>>(&self, name: S) -> Result<Session> {
let file_path = self.base_dir.join(name.as_ref());
load_session(file_path)
}
pub fn list(&self) -> Result<Vec<String>> {
let mut sessions = Vec::new();
for entry in fs::read_dir(&self.base_dir)
.map_err(|e| TenxError::SessionStore(format!("Failed to read directory: {}", e)))?
{
let entry = entry
.map_err(|e| TenxError::SessionStore(format!("Failed to read entry: {}", e)))?;
if entry
.file_type()
.map_err(|e| TenxError::SessionStore(format!("Failed to get file type: {}", e)))?
.is_file()
{
if let Some(name) = entry.file_name().to_str() {
sessions.push(name.to_string());
}
}
}
Ok(sessions)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::Project;
use tempfile::TempDir;
#[test]
fn test_normalize_path() {
assert_eq!(path_to_filename(Path::new("/foo/bar")), "_foo_bar");
assert_eq!(
path_to_filename(Path::new("C:\\Windows\\System32")),
"C_Windows_System32"
);
assert_eq!(path_to_filename(Path::new("file:name.txt")), "filename.txt");
}
#[test]
fn test_state_store() -> Result<()> {
let temp_dir = TempDir::new().unwrap();
let config = Config {
project: Project {
root: temp_dir.path().into(),
..Default::default()
},
..Default::default()
};
let state_store = SessionStore::open(temp_dir.path().into()).unwrap();
let state = Session::default();
state_store.save_current(&config, &state).unwrap();
state_store.save("test_session", &state).unwrap();
let name = path_to_filename(&config.project_root());
let _ = state_store.load(&name)?;
let sessions = state_store.list()?;
assert_eq!(sessions.len(), 2);
assert!(sessions.contains(&name));
assert!(sessions.contains(&"test_session".to_string()));
Ok(())
}
}