use crate::core::error::Result;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use uuid::Uuid;
pub type SessionId = Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Session {
pub id: SessionId,
pub name: String,
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub last_accessed: DateTime<Utc>,
pub config: SessionConfig,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionConfig {
pub working_dir: Option<PathBuf>,
pub env_vars: HashMap<String, String>,
pub default_timeout: Option<u64>,
pub verbose: bool,
}
impl Default for SessionConfig {
fn default() -> Self {
Self {
working_dir: None,
env_vars: HashMap::new(),
default_timeout: Some(60),
verbose: false,
}
}
}
#[derive(Debug)]
pub struct SessionManager {
data_dir: PathBuf,
sessions: HashMap<SessionId, Session>,
}
impl SessionManager {
pub fn with_default_storage() -> Result<Self> {
let data_dir = crate::cli::default_data_dir();
Self::new(data_dir)
}
pub fn new(data_dir: PathBuf) -> Result<Self> {
if !data_dir.exists() {
std::fs::create_dir_all(&data_dir)?;
}
let mut manager = Self {
data_dir,
sessions: HashMap::new(),
};
manager.load_sessions()?;
Ok(manager)
}
pub fn create_session(
&mut self,
name: String,
description: Option<String>,
config: Option<SessionConfig>,
) -> Result<SessionId> {
let id = SessionId::new_v4();
let now = Utc::now();
let session = Session {
id,
name,
description,
created_at: now,
last_accessed: now,
config: config.unwrap_or_default(),
metadata: HashMap::new(),
};
self.save_session(&session)?;
self.sessions.insert(id, session);
Ok(id)
}
pub fn get_session(&mut self, id: SessionId) -> Result<Option<&Session>> {
if let Some(session) = self.sessions.get(&id) {
let mut updated_session = session.clone();
updated_session.last_accessed = Utc::now();
self.save_session(&updated_session)?;
self.sessions.insert(id, updated_session);
return Ok(self.sessions.get(&id));
}
Ok(None)
}
pub fn get_session_by_name(&mut self, name: &str) -> Result<Option<&Session>> {
let id = self
.sessions
.values()
.find(|session| session.name == name)
.map(|session| session.id);
if let Some(id) = id {
self.get_session(id)
} else {
Ok(None)
}
}
pub fn list_sessions(&self) -> Vec<&Session> {
self.sessions.values().collect()
}
pub fn get_current_session_id(&self) -> Option<SessionId> {
self.sessions
.values()
.max_by_key(|session| session.last_accessed)
.map(|session| session.id)
}
pub fn get_current_session(&self) -> Option<&Session> {
self.sessions
.values()
.max_by_key(|session| session.last_accessed)
}
pub fn switch_to_session(&mut self, id: SessionId) -> Result<()> {
if let Some(session) = self.sessions.get_mut(&id) {
session.last_accessed = Utc::now();
let session_to_save = session.clone();
let _ = session; self.save_session(&session_to_save)?;
}
Ok(())
}
pub fn delete_session(&mut self, id: SessionId) -> Result<bool> {
if self.sessions.remove(&id).is_some() {
let session_file = self.session_file_path(id);
if session_file.exists() {
std::fs::remove_file(session_file)?;
}
Ok(true)
} else {
Ok(false)
}
}
pub fn update_session_metadata(
&mut self,
id: SessionId,
key: String,
value: String,
) -> Result<()> {
if let Some(session) = self.sessions.get_mut(&id) {
session.metadata.insert(key, value);
session.last_accessed = Utc::now();
let session_clone = session.clone();
self.save_session(&session_clone)?;
}
Ok(())
}
fn load_sessions(&mut self) -> Result<()> {
let sessions_dir = self.data_dir.join("sessions");
if !sessions_dir.exists() {
std::fs::create_dir_all(&sessions_dir)?;
return Ok(());
}
for entry in std::fs::read_dir(&sessions_dir)? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|s| s.to_str()) == Some("json") {
if let Ok(session) = self.load_session_from_file(&path) {
self.sessions.insert(session.id, session);
}
}
}
Ok(())
}
fn load_session_from_file(&self, path: &Path) -> Result<Session> {
let content = std::fs::read_to_string(path)?;
let session: Session = serde_json::from_str(&content)?;
Ok(session)
}
fn save_session(&self, session: &Session) -> Result<()> {
let sessions_dir = self.data_dir.join("sessions");
if !sessions_dir.exists() {
std::fs::create_dir_all(&sessions_dir)?;
}
let session_file = self.session_file_path(session.id);
let content = serde_json::to_string_pretty(session)?;
std::fs::write(session_file, content)?;
Ok(())
}
fn session_file_path(&self, id: SessionId) -> PathBuf {
self.data_dir.join("sessions").join(format!("{}.json", id))
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn create_test_manager() -> (SessionManager, TempDir) {
let temp_dir = TempDir::new().unwrap();
let manager = SessionManager::new(temp_dir.path().to_path_buf()).unwrap();
(manager, temp_dir)
}
#[test]
fn test_create_session() {
let (mut manager, _temp_dir) = create_test_manager();
let session_id = manager
.create_session("test-session".to_string(), None, None)
.unwrap();
assert!(manager.sessions.contains_key(&session_id));
let session = manager.get_session(session_id).unwrap().unwrap();
assert_eq!(session.name, "test-session");
assert!(session.description.is_none());
}
#[test]
fn test_get_session_by_name() {
let (mut manager, _temp_dir) = create_test_manager();
let _session_id = manager
.create_session("test-session".to_string(), None, None)
.unwrap();
let session = manager
.get_session_by_name("test-session")
.unwrap()
.unwrap();
assert_eq!(session.name, "test-session");
let not_found = manager.get_session_by_name("nonexistent").unwrap();
assert!(not_found.is_none());
}
#[test]
fn test_list_sessions() {
let (mut manager, _temp_dir) = create_test_manager();
let _id1 = manager
.create_session("session1".to_string(), None, None)
.unwrap();
let _id2 = manager
.create_session("session2".to_string(), None, None)
.unwrap();
let sessions = manager.list_sessions();
assert_eq!(sessions.len(), 2);
let names: Vec<&str> = sessions.iter().map(|s| s.name.as_str()).collect();
assert!(names.contains(&"session1"));
assert!(names.contains(&"session2"));
}
#[test]
fn test_delete_session() {
let (mut manager, _temp_dir) = create_test_manager();
let session_id = manager
.create_session("test-session".to_string(), None, None)
.unwrap();
assert!(manager.sessions.contains_key(&session_id));
let deleted = manager.delete_session(session_id).unwrap();
assert!(deleted);
assert!(!manager.sessions.contains_key(&session_id));
let deleted_again = manager.delete_session(session_id).unwrap();
assert!(!deleted_again);
}
#[test]
fn test_session_persistence() {
let temp_dir = TempDir::new().unwrap();
let data_dir = temp_dir.path().to_path_buf();
let session_id = {
let mut manager = SessionManager::new(data_dir.clone()).unwrap();
manager
.create_session("persistent-session".to_string(), None, None)
.unwrap()
};
let mut manager2 = SessionManager::new(data_dir).unwrap();
let session = manager2.get_session(session_id).unwrap().unwrap();
assert_eq!(session.name, "persistent-session");
}
#[test]
fn test_update_session_metadata() {
let (mut manager, _temp_dir) = create_test_manager();
let session_id = manager
.create_session("test-session".to_string(), None, None)
.unwrap();
manager
.update_session_metadata(session_id, "key1".to_string(), "value1".to_string())
.unwrap();
let session = manager.get_session(session_id).unwrap().unwrap();
assert_eq!(session.metadata.get("key1"), Some(&"value1".to_string()));
}
}