use crate::llm::convert::{ConvertError, document_to_llm, document_to_machine};
use crate::llm::human_parser::HumanParser;
use crate::llm::types::DxDocument;
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum CacheError {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Parse error: {0}")]
Parse(String),
#[error("Conversion error: {0}")]
Convert(#[from] ConvertError),
#[error("Invalid path: {0}")]
InvalidPath(String),
#[error("Cache directory creation failed: {0}")]
DirectoryCreation(String),
}
#[derive(Debug, Clone)]
pub struct CacheConfig {
pub cache_root: PathBuf,
pub generate_llm: bool,
pub generate_machine: bool,
pub atomic_writes: bool,
}
impl Default for CacheConfig {
fn default() -> Self {
Self {
cache_root: PathBuf::from(".dx/cache"),
generate_llm: true,
generate_machine: true,
atomic_writes: true,
}
}
}
impl CacheConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_cache_root(mut self, root: impl Into<PathBuf>) -> Self {
self.cache_root = root.into();
self
}
pub fn with_llm(mut self, generate: bool) -> Self {
self.generate_llm = generate;
self
}
pub fn with_machine(mut self, generate: bool) -> Self {
self.generate_machine = generate;
self
}
pub fn with_atomic_writes(mut self, atomic: bool) -> Self {
self.atomic_writes = atomic;
self
}
}
pub struct CacheGenerator {
config: CacheConfig,
parser: HumanParser,
}
impl CacheGenerator {
pub fn new() -> Self {
Self {
config: CacheConfig::default(),
parser: HumanParser::new(),
}
}
pub fn with_config(config: CacheConfig) -> Self {
Self {
config,
parser: HumanParser::new(),
}
}
pub fn map_path_to_cache(&self, source_path: &Path, base_path: &Path) -> CachePaths {
let relative = source_path.strip_prefix(base_path).unwrap_or(source_path);
let relative_str = relative.to_string_lossy().replace('\\', "/");
let llm_path = self
.config
.cache_root
.join("llm")
.join(&relative_str)
.with_extension("dx.llm");
let machine_path = self
.config
.cache_root
.join("machine")
.join(&relative_str)
.with_extension("dx.bin");
CachePaths {
source: source_path.to_path_buf(),
llm: llm_path,
machine: machine_path,
}
}
pub fn generate(
&self,
source_path: &Path,
base_path: &Path,
) -> Result<CacheResult, CacheError> {
let content = fs::read_to_string(source_path)?;
let doc = self
.parser
.parse(&content)
.map_err(|e| CacheError::Parse(e.to_string()))?;
self.generate_from_document(&doc, source_path, base_path)
}
pub fn generate_from_document(
&self,
doc: &DxDocument,
source_path: &Path,
base_path: &Path,
) -> Result<CacheResult, CacheError> {
let paths = self.map_path_to_cache(source_path, base_path);
let mut result = CacheResult {
paths: paths.clone(),
llm_generated: false,
machine_generated: false,
};
if self.config.generate_llm {
let llm_content = document_to_llm(doc);
self.write_cache_file(&paths.llm, llm_content.as_bytes())?;
result.llm_generated = true;
}
if self.config.generate_machine {
let machine_content = document_to_machine(doc);
self.write_cache_file(&paths.machine, &machine_content.data)?;
result.machine_generated = true;
}
Ok(result)
}
fn write_cache_file(&self, path: &Path, content: &[u8]) -> Result<(), CacheError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|e| {
CacheError::DirectoryCreation(format!("{}: {}", parent.display(), e))
})?;
}
if self.config.atomic_writes {
let temp_path = path.with_extension("tmp");
fs::write(&temp_path, content)?;
fs::rename(&temp_path, path)?;
} else {
fs::write(path, content)?;
}
Ok(())
}
pub fn config(&self) -> &CacheConfig {
&self.config
}
}
impl Default for CacheGenerator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CachePaths {
pub source: PathBuf,
pub llm: PathBuf,
pub machine: PathBuf,
}
#[derive(Debug)]
pub struct CacheResult {
pub paths: CachePaths,
pub llm_generated: bool,
pub machine_generated: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_cache_config_default() {
let config = CacheConfig::default();
assert_eq!(config.cache_root, PathBuf::from(".dx/cache"));
assert!(config.generate_llm);
assert!(config.generate_machine);
assert!(config.atomic_writes);
}
#[test]
fn test_cache_config_builder() {
let config = CacheConfig::new()
.with_cache_root("/custom/cache")
.with_llm(false)
.with_machine(true)
.with_atomic_writes(false);
assert_eq!(config.cache_root, PathBuf::from("/custom/cache"));
assert!(!config.generate_llm);
assert!(config.generate_machine);
assert!(!config.atomic_writes);
}
#[test]
fn test_map_path_to_cache_simple() {
let generator = CacheGenerator::new();
let source = Path::new("config.dx");
let base = Path::new(".");
let paths = generator.map_path_to_cache(source, base);
assert_eq!(paths.source, PathBuf::from("config.dx"));
assert!(paths.llm.to_string_lossy().contains("llm"));
assert!(paths.llm.to_string_lossy().contains("config.dx.llm"));
assert!(paths.machine.to_string_lossy().contains("machine"));
assert!(paths.machine.to_string_lossy().contains("config.dx.bin"));
}
#[test]
fn test_map_path_to_cache_nested() {
let generator = CacheGenerator::new();
let source = Path::new("src/config/data.dx");
let base = Path::new(".");
let paths = generator.map_path_to_cache(source, base);
let llm_str = paths.llm.to_string_lossy();
assert!(llm_str.contains("src") || llm_str.contains("config"));
assert!(llm_str.contains("data.dx.llm"));
}
#[test]
fn test_map_path_to_cache_with_base() {
let generator = CacheGenerator::new();
let source = Path::new("/project/src/config/data.dx");
let base = Path::new("/project");
let paths = generator.map_path_to_cache(source, base);
let llm_str = paths.llm.to_string_lossy();
assert!(!llm_str.contains("project") || llm_str.contains("src"));
}
#[test]
fn test_cache_generator_from_document() {
use crate::llm::types::{DxDocument, DxLlmValue};
use std::env;
let temp_dir = env::temp_dir().join("dx_cache_test_1");
let _ = fs::remove_dir_all(&temp_dir);
let config = CacheConfig::new()
.with_cache_root(temp_dir.join("cache"))
.with_atomic_writes(false);
let generator = CacheGenerator::with_config(config);
let mut doc = DxDocument::new();
doc.context
.insert("nm".to_string(), DxLlmValue::Str("Test".to_string()));
let source = Path::new("test.dx");
let base = Path::new(".");
let result = generator
.generate_from_document(&doc, source, base)
.unwrap();
assert!(result.llm_generated);
assert!(result.machine_generated);
assert!(result.paths.llm.exists());
assert!(result.paths.machine.exists());
let llm_content = fs::read_to_string(&result.paths.llm).unwrap();
assert!(llm_content.contains("nm") || llm_content.contains("Test"));
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_cache_generator_llm_only() {
use crate::llm::types::{DxDocument, DxLlmValue};
use std::env;
let temp_dir = env::temp_dir().join("dx_cache_test_2");
let _ = fs::remove_dir_all(&temp_dir);
let config = CacheConfig::new()
.with_cache_root(temp_dir.join("cache"))
.with_llm(true)
.with_machine(false);
let generator = CacheGenerator::with_config(config);
let mut doc = DxDocument::new();
doc.context
.insert("nm".to_string(), DxLlmValue::Str("Test".to_string()));
let source = Path::new("test.dx");
let base = Path::new(".");
let result = generator
.generate_from_document(&doc, source, base)
.unwrap();
assert!(result.llm_generated);
assert!(!result.machine_generated);
assert!(result.paths.llm.exists());
assert!(!result.paths.machine.exists());
let _ = fs::remove_dir_all(&temp_dir);
}
#[test]
fn test_cache_paths_structure() {
let paths = CachePaths {
source: PathBuf::from("src/data.dx"),
llm: PathBuf::from(".dx/cache/llm/src/data.dx.llm"),
machine: PathBuf::from(".dx/cache/machine/src/data.dx.bin"),
};
assert_eq!(paths.source.file_name().unwrap(), "data.dx");
assert!(paths.llm.extension().unwrap() == "llm");
assert!(paths.machine.extension().unwrap() == "bin");
}
}