ralph_workflow/config/
path_resolver.rs1use std::collections::HashMap;
37use std::io;
38use std::path::{Path, PathBuf};
39use std::sync::{Arc, RwLock};
40
41pub trait ConfigEnvironment: Send + Sync {
51 fn unified_config_path(&self) -> Option<PathBuf>;
58
59 fn prompt_path(&self) -> PathBuf {
64 PathBuf::from("PROMPT.md")
65 }
66
67 fn file_exists(&self, path: &Path) -> bool;
69
70 fn read_file(&self, path: &Path) -> io::Result<String>;
72
73 fn write_file(&self, path: &Path, content: &str) -> io::Result<()>;
75
76 fn create_dir_all(&self, path: &Path) -> io::Result<()>;
78}
79
80#[derive(Debug, Default, Clone, Copy)]
86pub struct RealConfigEnvironment;
87
88impl ConfigEnvironment for RealConfigEnvironment {
89 fn unified_config_path(&self) -> Option<PathBuf> {
90 super::unified::unified_config_path()
91 }
92
93 fn file_exists(&self, path: &Path) -> bool {
94 path.exists()
95 }
96
97 fn read_file(&self, path: &Path) -> io::Result<String> {
98 std::fs::read_to_string(path)
99 }
100
101 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
102 if let Some(parent) = path.parent() {
103 std::fs::create_dir_all(parent)?;
104 }
105 std::fs::write(path, content)
106 }
107
108 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
109 std::fs::create_dir_all(path)
110 }
111}
112
113#[derive(Debug, Clone, Default)]
137pub struct MemoryConfigEnvironment {
138 unified_config_path: Option<PathBuf>,
139 prompt_path: Option<PathBuf>,
140 files: Arc<RwLock<HashMap<PathBuf, String>>>,
142 dirs: Arc<RwLock<std::collections::HashSet<PathBuf>>>,
144}
145
146impl MemoryConfigEnvironment {
147 pub fn new() -> Self {
149 Self::default()
150 }
151
152 #[must_use]
154 pub fn with_unified_config_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
155 self.unified_config_path = Some(path.into());
156 self
157 }
158
159 #[must_use]
161 pub fn with_prompt_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
162 self.prompt_path = Some(path.into());
163 self
164 }
165
166 #[must_use]
168 pub fn with_file<P: Into<PathBuf>, S: Into<String>>(self, path: P, content: S) -> Self {
169 let path = path.into();
170 self.files.write().unwrap().insert(path, content.into());
171 self
172 }
173
174 pub fn get_file(&self, path: &Path) -> Option<String> {
176 self.files.read().unwrap().get(path).cloned()
177 }
178
179 pub fn was_written(&self, path: &Path) -> bool {
181 self.files.read().unwrap().contains_key(path)
182 }
183}
184
185impl ConfigEnvironment for MemoryConfigEnvironment {
186 fn unified_config_path(&self) -> Option<PathBuf> {
187 self.unified_config_path.clone()
188 }
189
190 fn prompt_path(&self) -> PathBuf {
191 self.prompt_path
192 .clone()
193 .unwrap_or_else(|| PathBuf::from("PROMPT.md"))
194 }
195
196 fn file_exists(&self, path: &Path) -> bool {
197 self.files.read().unwrap().contains_key(path)
198 }
199
200 fn read_file(&self, path: &Path) -> io::Result<String> {
201 self.files
202 .read()
203 .unwrap()
204 .get(path)
205 .cloned()
206 .ok_or_else(|| {
207 io::Error::new(
208 io::ErrorKind::NotFound,
209 format!("File not found: {}", path.display()),
210 )
211 })
212 }
213
214 fn write_file(&self, path: &Path, content: &str) -> io::Result<()> {
215 if let Some(parent) = path.parent() {
217 self.dirs.write().unwrap().insert(parent.to_path_buf());
218 }
219 self.files
220 .write()
221 .unwrap()
222 .insert(path.to_path_buf(), content.to_string());
223 Ok(())
224 }
225
226 fn create_dir_all(&self, path: &Path) -> io::Result<()> {
227 self.dirs.write().unwrap().insert(path.to_path_buf());
228 Ok(())
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 #[test]
237 fn test_real_environment_returns_path() {
238 let env = RealConfigEnvironment;
239 let path = env.unified_config_path();
241 if let Some(p) = path {
242 assert!(p.to_string_lossy().contains("ralph-workflow.toml"));
243 }
244 }
245
246 #[test]
247 fn test_memory_environment_with_custom_paths() {
248 let env = MemoryConfigEnvironment::new()
249 .with_unified_config_path("/custom/config.toml")
250 .with_prompt_path("/custom/PROMPT.md");
251
252 assert_eq!(
253 env.unified_config_path(),
254 Some(PathBuf::from("/custom/config.toml"))
255 );
256 assert_eq!(env.prompt_path(), PathBuf::from("/custom/PROMPT.md"));
257 }
258
259 #[test]
260 fn test_memory_environment_default_prompt_path() {
261 let env = MemoryConfigEnvironment::new();
262 assert_eq!(env.prompt_path(), PathBuf::from("PROMPT.md"));
263 }
264
265 #[test]
266 fn test_memory_environment_no_unified_config() {
267 let env = MemoryConfigEnvironment::new();
268 assert_eq!(env.unified_config_path(), None);
269 }
270
271 #[test]
272 fn test_memory_environment_file_operations() {
273 let env = MemoryConfigEnvironment::new();
274 let path = Path::new("/test/file.txt");
275
276 assert!(!env.file_exists(path));
278
279 env.write_file(path, "test content").unwrap();
281
282 assert!(env.file_exists(path));
284 assert_eq!(env.read_file(path).unwrap(), "test content");
285 assert!(env.was_written(path));
286 }
287
288 #[test]
289 fn test_memory_environment_with_prepopulated_file() {
290 let env =
291 MemoryConfigEnvironment::new().with_file("/test/existing.txt", "existing content");
292
293 assert!(env.file_exists(Path::new("/test/existing.txt")));
294 assert_eq!(
295 env.read_file(Path::new("/test/existing.txt")).unwrap(),
296 "existing content"
297 );
298 }
299
300 #[test]
301 fn test_memory_environment_read_nonexistent_file() {
302 let env = MemoryConfigEnvironment::new();
303 let result = env.read_file(Path::new("/nonexistent"));
304 assert!(result.is_err());
305 assert_eq!(result.unwrap_err().kind(), io::ErrorKind::NotFound);
306 }
307}