use anyhow::{Context, Result, bail};
use std::{collections::HashMap, fs::File, io::BufReader};
use crate::data::filter::StudySession;
pub trait StudySessionManager {
fn get_study_session(&self, id: &str) -> Option<StudySession>;
fn list_study_sessions(&self) -> Vec<(String, String)>;
}
pub struct LocalStudySessionManager {
pub sessions: HashMap<String, StudySession>,
}
impl LocalStudySessionManager {
fn scan_sessions(session_directory: &str) -> Result<HashMap<String, StudySession>> {
let mut sessions = HashMap::new();
for entry in std::fs::read_dir(session_directory)
.context("Failed to read study session directory")?
{
let entry = entry.context("Failed to read file entry for saved study session")?;
let file = File::open(entry.path()).context(format!(
"Failed to open saved study session file {}",
entry.path().display()
))?;
let reader = BufReader::new(file);
let session: StudySession = serde_json::from_reader(reader).context(format!(
"Failed to parse study session from {}",
entry.path().display()
))?;
if sessions.contains_key(&session.id) {
bail!("Found multiple study sessions with ID {}", session.id);
}
sessions.insert(session.id.clone(), session);
}
Ok(sessions)
}
pub fn new(session_directory: &str) -> Result<LocalStudySessionManager> {
Ok(LocalStudySessionManager {
sessions: LocalStudySessionManager::scan_sessions(session_directory)?,
})
}
}
impl StudySessionManager for LocalStudySessionManager {
fn get_study_session(&self, id: &str) -> Option<StudySession> {
self.sessions.get(id).cloned()
}
fn list_study_sessions(&self) -> Vec<(String, String)> {
let mut sessions: Vec<(String, String)> = self
.sessions
.iter()
.map(|(id, session)| (id.clone(), session.description.clone()))
.collect();
sessions.sort_by(|a, b| a.0.cmp(&b.0));
sessions
}
}
#[cfg(test)]
#[cfg_attr(coverage, coverage(off))]
mod test {
use anyhow::{Ok, Result};
use std::{os::unix::prelude::PermissionsExt, path::Path};
use tempfile::TempDir;
use crate::{
data::filter::StudySession,
study_session_manager::{LocalStudySessionManager, StudySessionManager},
};
fn test_sessions() -> Vec<StudySession> {
vec![
StudySession {
id: "session1".into(),
description: "Session 1".into(),
parts: vec![],
},
StudySession {
id: "session2".into(),
description: "Session 2".into(),
parts: vec![],
},
]
}
fn write_sessions(sessions: Vec<StudySession>, dir: &Path) -> Result<()> {
for session in sessions {
let timestamp_ns = chrono::Utc::now().timestamp_nanos_opt().unwrap_or(0);
let session_path = dir.join(format!("{}_{}.json", session.id, timestamp_ns));
let session_json = serde_json::to_string(&session)?;
std::fs::write(session_path, session_json)?;
}
Ok(())
}
#[test]
fn session_manager() -> Result<()> {
let temp_dir = TempDir::new()?;
let sessions = test_sessions();
write_sessions(sessions.clone(), temp_dir.path())?;
let manager = LocalStudySessionManager::new(temp_dir.path().to_str().unwrap())?;
let session_list = manager.list_study_sessions();
assert_eq!(
session_list,
vec![
("session1".to_string(), "Session 1".to_string()),
("session2".to_string(), "Session 2".to_string())
]
);
for (index, (id, _)) in session_list.iter().enumerate() {
let session = manager.get_study_session(id);
assert!(session.is_some());
let session = session.unwrap();
assert_eq!(sessions[index], session);
}
Ok(())
}
#[test]
fn sessions_repeated_ids() -> Result<()> {
let sessions = vec![
StudySession {
id: "session1".into(),
description: "Session 1".into(),
parts: vec![],
},
StudySession {
id: "session1".into(),
description: "Session 2".into(),
parts: vec![],
},
];
let temp_dir = TempDir::new()?;
write_sessions(sessions.clone(), temp_dir.path())?;
assert!(LocalStudySessionManager::new(temp_dir.path().to_str().unwrap()).is_err());
Ok(())
}
#[test]
fn read_bad_directory() -> Result<()> {
assert!(LocalStudySessionManager::new("bad_directory").is_err());
Ok(())
}
#[test]
fn read_bad_file_format() -> Result<()> {
let temp_dir = TempDir::new()?;
let bad_file = temp_dir.path().join("bad_file.json");
std::fs::write(bad_file, "bad json")?;
assert!(LocalStudySessionManager::new(temp_dir.path().to_str().unwrap()).is_err());
Ok(())
}
#[test]
fn read_bad_file_permissions() -> Result<()> {
let temp_dir = TempDir::new()?;
let bad_file = temp_dir.path().join("bad_file.json");
std::fs::write(bad_file.clone(), "bad json")?;
std::fs::set_permissions(bad_file, std::fs::Permissions::from_mode(0o000))?;
assert!(LocalStudySessionManager::new(temp_dir.path().to_str().unwrap()).is_err());
Ok(())
}
}