Skip to main content

ralph_core/
urgent_steer.rs

1use serde::{Deserialize, Serialize};
2use std::fs;
3use std::io;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct UrgentSteerRecord {
8    pub messages: Vec<String>,
9    pub created_at: String,
10}
11
12impl UrgentSteerRecord {
13    pub fn new(message: impl Into<String>) -> Self {
14        Self {
15            messages: vec![message.into()],
16            created_at: chrono::Utc::now().to_rfc3339(),
17        }
18    }
19}
20
21#[derive(Debug, Clone)]
22pub struct UrgentSteerStore {
23    path: PathBuf,
24}
25
26impl UrgentSteerStore {
27    pub fn new(path: impl Into<PathBuf>) -> Self {
28        Self { path: path.into() }
29    }
30
31    pub fn path(&self) -> &Path {
32        &self.path
33    }
34
35    pub fn load(&self) -> io::Result<Option<UrgentSteerRecord>> {
36        if !self.path.exists() {
37            return Ok(None);
38        }
39
40        let content = fs::read_to_string(&self.path)?;
41        let record = serde_json::from_str::<UrgentSteerRecord>(&content)
42            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
43        Ok(Some(record))
44    }
45
46    pub fn append_message(&self, message: impl Into<String>) -> io::Result<UrgentSteerRecord> {
47        let message = message.into();
48        let mut record = self
49            .load()?
50            .unwrap_or_else(|| UrgentSteerRecord::new(message.clone()));
51
52        if record.messages.last() != Some(&message) {
53            record.messages.push(message);
54        }
55
56        self.write(&record)?;
57        Ok(record)
58    }
59
60    pub fn write(&self, record: &UrgentSteerRecord) -> io::Result<()> {
61        if let Some(parent) = self.path.parent() {
62            fs::create_dir_all(parent)?;
63        }
64
65        let payload = serde_json::to_string(record)
66            .map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err))?;
67        fs::write(&self.path, payload)
68    }
69
70    pub fn clear(&self) -> io::Result<()> {
71        match fs::remove_file(&self.path) {
72            Ok(()) => Ok(()),
73            Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(()),
74            Err(err) => Err(err),
75        }
76    }
77
78    pub fn take(&self) -> io::Result<Option<UrgentSteerRecord>> {
79        let record = self.load()?;
80        self.clear()?;
81        Ok(record)
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn append_message_creates_and_reloads_record() {
91        let temp_dir = tempfile::tempdir().expect("temp dir");
92        let store = UrgentSteerStore::new(temp_dir.path().join("urgent-steer.json"));
93
94        let record = store.append_message("check tests first").expect("append");
95        assert_eq!(record.messages, vec!["check tests first"]);
96
97        let loaded = store.load().expect("load").expect("record");
98        assert_eq!(loaded.messages, vec!["check tests first"]);
99    }
100
101    #[test]
102    fn take_returns_record_and_clears_file() {
103        let temp_dir = tempfile::tempdir().expect("temp dir");
104        let store = UrgentSteerStore::new(temp_dir.path().join("urgent-steer.json"));
105        store.append_message("steer now").expect("append");
106
107        let record = store.take().expect("take").expect("record");
108        assert_eq!(record.messages, vec!["steer now"]);
109        assert!(store.load().expect("load after take").is_none());
110    }
111}