differential_engine/
store.rs1use std::path::{Path, PathBuf};
9
10use serde::{Deserialize, Serialize};
11
12use crate::EngineError;
13use crate::plan;
14use crate::ports;
15use crate::review_state::{Finding, ReviewState};
16
17fn io_err(path: &Path, source: std::io::Error) -> EngineError {
18 EngineError::Cache {
19 path: path.display().to_string(),
20 source,
21 }
22}
23
24#[derive(Serialize, Deserialize)]
27struct Entry {
28 response: String,
29}
30
31pub struct FsGroupingCache {
37 dir: Option<PathBuf>,
38}
39
40impl FsGroupingCache {
41 pub fn for_repo<L: ports::RepoLayout>(layout: &L) -> Result<Self, EngineError> {
43 Ok(FsGroupingCache {
44 dir: Some(plan::grouping_cache_dir(&layout.common_dir()?)),
45 })
46 }
47
48 pub fn at(dir: PathBuf) -> Self {
49 FsGroupingCache { dir: Some(dir) }
50 }
51
52 pub fn disabled() -> Self {
54 FsGroupingCache { dir: None }
55 }
56
57 fn entry_path(dir: &Path, key: &str) -> PathBuf {
58 dir.join(format!("{key}.json"))
59 }
60}
61
62impl ports::GroupingCache for FsGroupingCache {
63 fn get(&self, key: &str) -> Result<Option<String>, EngineError> {
64 let Some(dir) = &self.dir else {
65 return Ok(None);
66 };
67 let path = Self::entry_path(dir, key);
68 match std::fs::read_to_string(&path) {
69 Ok(text) => {
70 let entry: Entry = serde_json::from_str(&text).map_err(|e| EngineError::Cache {
71 path: path.display().to_string(),
72 source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
73 })?;
74 Ok(Some(entry.response))
75 }
76 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
77 Err(e) => Err(io_err(&path, e)),
78 }
79 }
80
81 fn put(&self, key: &str, response: &str) -> Result<(), EngineError> {
82 let Some(dir) = &self.dir else {
83 return Ok(());
84 };
85 std::fs::create_dir_all(dir).map_err(|e| io_err(dir, e))?;
86 let body = serde_json::to_string(&Entry {
87 response: response.to_string(),
88 })
89 .expect("string serialises");
90 let path = Self::entry_path(dir, key);
91 std::fs::write(&path, body).map_err(|e| io_err(&path, e))
92 }
93}
94
95pub struct FsReviewStore {
99 dir: PathBuf,
100}
101
102impl FsReviewStore {
103 pub fn for_review<L: ports::RepoLayout>(
104 layout: &L,
105 base_sha: &str,
106 head_spec: &str,
107 ) -> Result<Self, EngineError> {
108 let dir = plan::review_dir(&layout.common_dir()?, &plan::review_id(base_sha, head_spec));
109 Self::at(dir)
110 }
111
112 pub fn at(dir: PathBuf) -> Result<Self, EngineError> {
114 std::fs::create_dir_all(dir.join("plans")).map_err(|e| io_err(&dir, e))?;
115 Ok(FsReviewStore { dir })
116 }
117
118 pub fn dir(&self) -> &Path {
119 &self.dir
120 }
121}
122
123impl ports::ReviewStore for FsReviewStore {
124 fn save_plan(&self, hash: &str, json: &str) -> Result<(), EngineError> {
125 let path = self.dir.join("plans").join(format!("{hash}.json"));
126 if !path.exists() {
128 std::fs::write(&path, json).map_err(|e| io_err(&path, e))?;
129 }
130 let current = self.dir.join("current");
131 std::fs::write(¤t, hash).map_err(|e| io_err(¤t, e))
132 }
133
134 fn load_state(&self) -> Result<ReviewState, EngineError> {
135 let path = self.dir.join("state.json");
136 match std::fs::read_to_string(&path) {
137 Ok(text) => serde_json::from_str(&text).map_err(|e| EngineError::Cache {
138 path: path.display().to_string(),
139 source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
140 }),
141 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(ReviewState::default()),
142 Err(e) => Err(io_err(&path, e)),
143 }
144 }
145
146 fn save_state(&self, state: &ReviewState) -> Result<(), EngineError> {
147 let path = self.dir.join("state.json");
148 let text = serde_json::to_string_pretty(state).expect("state serialises");
149 std::fs::write(&path, text).map_err(|e| io_err(&path, e))
150 }
151
152 fn load_findings(&self) -> Result<Vec<Finding>, EngineError> {
153 let path = self.dir.join("findings.jsonl");
154 let text = match std::fs::read_to_string(&path) {
155 Ok(t) => t,
156 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()),
157 Err(e) => return Err(io_err(&path, e)),
158 };
159 let mut out = Vec::new();
160 for (n, line) in text.lines().enumerate() {
161 if line.trim().is_empty() {
162 continue;
163 }
164 let f: Finding = serde_json::from_str(line).map_err(|e| EngineError::Cache {
165 path: format!("{}:{}", path.display(), n + 1),
166 source: std::io::Error::new(std::io::ErrorKind::InvalidData, e),
167 })?;
168 out.push(f);
169 }
170 Ok(out)
171 }
172
173 fn save_findings(&self, findings: &[Finding]) -> Result<(), EngineError> {
174 let path = self.dir.join("findings.jsonl");
175 let mut text = String::new();
176 for f in findings {
177 text.push_str(&serde_json::to_string(f).expect("serialises"));
178 text.push('\n');
179 }
180 std::fs::write(&path, text).map_err(|e| io_err(&path, e))
181 }
182}
183
184pub struct OsConfigSource;
189
190impl ports::ConfigSource for OsConfigSource {
191 fn user_config_dir(&self) -> Option<PathBuf> {
192 use etcetera::BaseStrategy;
193 let strategy = etcetera::choose_base_strategy().ok()?;
194 Some(strategy.config_dir())
195 }
196
197 fn read(&self, path: &Path) -> Result<Option<String>, EngineError> {
198 match std::fs::read_to_string(path) {
199 Ok(text) => Ok(Some(text)),
200 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
201 Err(e) => Err(EngineError::Config {
202 path: path.display().to_string(),
203 msg: e.to_string(),
204 }),
205 }
206 }
207
208 fn read_required(&self, path: &Path) -> Result<String, EngineError> {
209 std::fs::read_to_string(path).map_err(|e| EngineError::Config {
213 path: path.display().to_string(),
214 msg: e.to_string(),
215 })
216 }
217}