serializer/llm/
cache_generator.rs1use crate::llm::convert::{ConvertError, document_to_llm, document_to_machine};
21use crate::llm::human_parser::HumanParser;
22use crate::llm::types::DxDocument;
23use std::fs;
24use std::io;
25use std::path::{Path, PathBuf};
26use thiserror::Error;
27
28#[derive(Debug, Error)]
30pub enum CacheError {
31 #[error("IO error: {0}")]
33 Io(#[from] io::Error),
34
35 #[error("Parse error: {0}")]
37 Parse(String),
38
39 #[error("Conversion error: {0}")]
41 Convert(#[from] ConvertError),
42
43 #[error("Invalid path: {0}")]
45 InvalidPath(String),
46
47 #[error("Cache directory creation failed: {0}")]
49 DirectoryCreation(String),
50}
51
52#[derive(Debug, Clone)]
54pub struct CacheConfig {
55 pub cache_root: PathBuf,
57 pub generate_llm: bool,
59 pub generate_machine: bool,
61 pub atomic_writes: bool,
63}
64
65impl Default for CacheConfig {
66 fn default() -> Self {
67 Self {
68 cache_root: PathBuf::from(".dx/cache"),
69 generate_llm: true,
70 generate_machine: true,
71 atomic_writes: true,
72 }
73 }
74}
75
76impl CacheConfig {
77 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_cache_root(mut self, root: impl Into<PathBuf>) -> Self {
84 self.cache_root = root.into();
85 self
86 }
87
88 pub fn with_llm(mut self, generate: bool) -> Self {
90 self.generate_llm = generate;
91 self
92 }
93
94 pub fn with_machine(mut self, generate: bool) -> Self {
96 self.generate_machine = generate;
97 self
98 }
99
100 pub fn with_atomic_writes(mut self, atomic: bool) -> Self {
102 self.atomic_writes = atomic;
103 self
104 }
105}
106
107pub struct CacheGenerator {
109 config: CacheConfig,
110 parser: HumanParser,
111}
112
113impl CacheGenerator {
114 pub fn new() -> Self {
116 Self {
117 config: CacheConfig::default(),
118 parser: HumanParser::new(),
119 }
120 }
121
122 pub fn with_config(config: CacheConfig) -> Self {
124 Self {
125 config,
126 parser: HumanParser::new(),
127 }
128 }
129
130 pub fn map_path_to_cache(&self, source_path: &Path, base_path: &Path) -> CachePaths {
135 let relative = source_path.strip_prefix(base_path).unwrap_or(source_path);
137
138 let relative_str = relative.to_string_lossy().replace('\\', "/");
140
141 let llm_path = self
143 .config
144 .cache_root
145 .join("llm")
146 .join(&relative_str)
147 .with_extension("dx.llm");
148
149 let machine_path = self
150 .config
151 .cache_root
152 .join("machine")
153 .join(&relative_str)
154 .with_extension("dx.bin");
155
156 CachePaths {
157 source: source_path.to_path_buf(),
158 llm: llm_path,
159 machine: machine_path,
160 }
161 }
162
163 pub fn generate(
165 &self,
166 source_path: &Path,
167 base_path: &Path,
168 ) -> Result<CacheResult, CacheError> {
169 let content = fs::read_to_string(source_path)?;
171
172 let doc = self
174 .parser
175 .parse(&content)
176 .map_err(|e| CacheError::Parse(e.to_string()))?;
177
178 self.generate_from_document(&doc, source_path, base_path)
180 }
181
182 pub fn generate_from_document(
184 &self,
185 doc: &DxDocument,
186 source_path: &Path,
187 base_path: &Path,
188 ) -> Result<CacheResult, CacheError> {
189 let paths = self.map_path_to_cache(source_path, base_path);
190 let mut result = CacheResult {
191 paths: paths.clone(),
192 llm_generated: false,
193 machine_generated: false,
194 };
195
196 if self.config.generate_llm {
198 let llm_content = document_to_llm(doc);
199 self.write_cache_file(&paths.llm, llm_content.as_bytes())?;
200 result.llm_generated = true;
201 }
202
203 if self.config.generate_machine {
205 let machine_content = document_to_machine(doc);
206 self.write_cache_file(&paths.machine, &machine_content.data)?;
207 result.machine_generated = true;
208 }
209
210 Ok(result)
211 }
212
213 fn write_cache_file(&self, path: &Path, content: &[u8]) -> Result<(), CacheError> {
215 if let Some(parent) = path.parent() {
217 fs::create_dir_all(parent).map_err(|e| {
218 CacheError::DirectoryCreation(format!("{}: {}", parent.display(), e))
219 })?;
220 }
221
222 if self.config.atomic_writes {
223 let temp_path = path.with_extension("tmp");
225 fs::write(&temp_path, content)?;
226 fs::rename(&temp_path, path)?;
227 } else {
228 fs::write(path, content)?;
229 }
230
231 Ok(())
232 }
233
234 pub fn config(&self) -> &CacheConfig {
236 &self.config
237 }
238}
239
240impl Default for CacheGenerator {
241 fn default() -> Self {
242 Self::new()
243 }
244}
245
246#[derive(Debug, Clone)]
248pub struct CachePaths {
249 pub source: PathBuf,
251 pub llm: PathBuf,
253 pub machine: PathBuf,
255}
256
257#[derive(Debug)]
259pub struct CacheResult {
260 pub paths: CachePaths,
262 pub llm_generated: bool,
264 pub machine_generated: bool,
266}
267
268#[cfg(test)]
269mod tests {
270 use super::*;
271 use std::path::Path;
272
273 #[test]
274 fn test_cache_config_default() {
275 let config = CacheConfig::default();
276 assert_eq!(config.cache_root, PathBuf::from(".dx/cache"));
277 assert!(config.generate_llm);
278 assert!(config.generate_machine);
279 assert!(config.atomic_writes);
280 }
281
282 #[test]
283 fn test_cache_config_builder() {
284 let config = CacheConfig::new()
285 .with_cache_root("/custom/cache")
286 .with_llm(false)
287 .with_machine(true)
288 .with_atomic_writes(false);
289
290 assert_eq!(config.cache_root, PathBuf::from("/custom/cache"));
291 assert!(!config.generate_llm);
292 assert!(config.generate_machine);
293 assert!(!config.atomic_writes);
294 }
295
296 #[test]
297 fn test_map_path_to_cache_simple() {
298 let generator = CacheGenerator::new();
299 let source = Path::new("config.dx");
300 let base = Path::new(".");
301
302 let paths = generator.map_path_to_cache(source, base);
303
304 assert_eq!(paths.source, PathBuf::from("config.dx"));
305 assert!(paths.llm.to_string_lossy().contains("llm"));
306 assert!(paths.llm.to_string_lossy().contains("config.dx.llm"));
307 assert!(paths.machine.to_string_lossy().contains("machine"));
308 assert!(paths.machine.to_string_lossy().contains("config.dx.bin"));
309 }
310
311 #[test]
312 fn test_map_path_to_cache_nested() {
313 let generator = CacheGenerator::new();
314 let source = Path::new("src/config/data.dx");
315 let base = Path::new(".");
316
317 let paths = generator.map_path_to_cache(source, base);
318
319 let llm_str = paths.llm.to_string_lossy();
321 assert!(llm_str.contains("src") || llm_str.contains("config"));
322 assert!(llm_str.contains("data.dx.llm"));
323 }
324
325 #[test]
326 fn test_map_path_to_cache_with_base() {
327 let generator = CacheGenerator::new();
328 let source = Path::new("/project/src/config/data.dx");
329 let base = Path::new("/project");
330
331 let paths = generator.map_path_to_cache(source, base);
332
333 let llm_str = paths.llm.to_string_lossy();
335 assert!(!llm_str.contains("project") || llm_str.contains("src"));
336 }
337
338 #[test]
339 fn test_cache_generator_from_document() {
340 use crate::llm::types::{DxDocument, DxLlmValue};
341 use std::env;
342
343 let temp_dir = env::temp_dir().join("dx_cache_test_1");
345 let _ = fs::remove_dir_all(&temp_dir); let config = CacheConfig::new()
348 .with_cache_root(temp_dir.join("cache"))
349 .with_atomic_writes(false);
350
351 let generator = CacheGenerator::with_config(config);
352
353 let mut doc = DxDocument::new();
355 doc.context
356 .insert("nm".to_string(), DxLlmValue::Str("Test".to_string()));
357
358 let source = Path::new("test.dx");
359 let base = Path::new(".");
360
361 let result = generator
362 .generate_from_document(&doc, source, base)
363 .unwrap();
364
365 assert!(result.llm_generated);
366 assert!(result.machine_generated);
367 assert!(result.paths.llm.exists());
368 assert!(result.paths.machine.exists());
369
370 let llm_content = fs::read_to_string(&result.paths.llm).unwrap();
372 assert!(llm_content.contains("nm") || llm_content.contains("Test"));
373
374 let _ = fs::remove_dir_all(&temp_dir);
376 }
377
378 #[test]
379 fn test_cache_generator_llm_only() {
380 use crate::llm::types::{DxDocument, DxLlmValue};
381 use std::env;
382
383 let temp_dir = env::temp_dir().join("dx_cache_test_2");
384 let _ = fs::remove_dir_all(&temp_dir);
385
386 let config = CacheConfig::new()
387 .with_cache_root(temp_dir.join("cache"))
388 .with_llm(true)
389 .with_machine(false);
390
391 let generator = CacheGenerator::with_config(config);
392
393 let mut doc = DxDocument::new();
394 doc.context
395 .insert("nm".to_string(), DxLlmValue::Str("Test".to_string()));
396
397 let source = Path::new("test.dx");
398 let base = Path::new(".");
399
400 let result = generator
401 .generate_from_document(&doc, source, base)
402 .unwrap();
403
404 assert!(result.llm_generated);
405 assert!(!result.machine_generated);
406 assert!(result.paths.llm.exists());
407 assert!(!result.paths.machine.exists());
408
409 let _ = fs::remove_dir_all(&temp_dir);
411 }
412
413 #[test]
414 fn test_cache_paths_structure() {
415 let paths = CachePaths {
416 source: PathBuf::from("src/data.dx"),
417 llm: PathBuf::from(".dx/cache/llm/src/data.dx.llm"),
418 machine: PathBuf::from(".dx/cache/machine/src/data.dx.bin"),
419 };
420
421 assert_eq!(paths.source.file_name().unwrap(), "data.dx");
422 assert!(paths.llm.extension().unwrap() == "llm");
423 assert!(paths.machine.extension().unwrap() == "bin");
424 }
425}