Skip to main content

looprs_core/adapters/
fs_session_store.rs

1//! FsSessionStore adapter — `SessionStore` backed by a JSONL file on disk.
2
3use std::fs::{self, OpenOptions};
4use std::io::Write;
5use std::path::{Path, PathBuf};
6
7use anyhow::Context as _;
8use chrono::Utc;
9use serde::Serialize;
10use uuid::Uuid;
11
12use crate::ports::session_store::{SessionEvent, SessionStore};
13
14/// Filesystem-backed session store that appends JSONL lines to a dated log file.
15pub struct FsSessionStore {
16    session_id: String,
17    path: PathBuf,
18}
19
20impl FsSessionStore {
21    /// Create a new store under `sessions_dir`, generating a unique session id.
22    pub fn new(sessions_dir: PathBuf) -> Result<Self, anyhow::Error> {
23        let session_id = format!("sess-{}", Uuid::new_v4());
24        let date = Utc::now().format("%Y-%m-%d");
25        let filename = format!("{}-{}.jsonl", date, session_id);
26        fs::create_dir_all(&sessions_dir).with_context(|| {
27            format!("failed to create sessions dir: {}", sessions_dir.display())
28        })?;
29        let path = sessions_dir.join(filename);
30        Ok(Self { session_id, path })
31    }
32}
33
34impl SessionStore for FsSessionStore {
35    fn log(&mut self, event: SessionEvent) -> Result<(), anyhow::Error> {
36        #[derive(Serialize)]
37        struct LogLine<'a> {
38            ts: String,
39            session_id: &'a str,
40            #[serde(flatten)]
41            event: &'a SessionEvent,
42        }
43        let line = LogLine {
44            ts: Utc::now().to_rfc3339(),
45            session_id: &self.session_id,
46            event: &event,
47        };
48        let mut json = serde_json::to_string(&line).context("failed to serialize log line")?;
49        json.push('\n');
50        let mut file = OpenOptions::new()
51            .create(true)
52            .append(true)
53            .open(&self.path)
54            .with_context(|| format!("failed to open log file: {}", self.path.display()))?;
55        file.write_all(json.as_bytes())
56            .context("failed to write log line")?;
57        Ok(())
58    }
59
60    fn path(&self) -> Option<&Path> {
61        Some(&self.path)
62    }
63
64    fn session_id(&self) -> &str {
65        &self.session_id
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn conformance() {
75        let dir = tempfile::tempdir().expect("failed to create tempdir");
76        let mut store =
77            FsSessionStore::new(dir.path().join("sessions")).expect("failed to create store");
78        crate::ports::test_contracts::assert_session_store_contract(&mut store);
79    }
80}