ralph_core/
urgent_steer.rs1use 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}