use crate::{
OverrideEntry,
backup::BackupManager,
error::{OverrideError, Result},
};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageFormat {
version: u32,
overrides: IndexMap<String, OverrideEntry>,
}
impl Default for StorageFormat {
fn default() -> Self {
Self {
version: 1,
overrides: IndexMap::new(),
}
}
}
pub struct OverrideStorage {
storage_path: PathBuf,
#[allow(dead_code)]
workspace_path: PathBuf,
cache: RwLock<StorageFormat>,
backup_manager: Arc<BackupManager>,
hierarchy: Option<raz_config::ConfigHierarchy>,
}
impl OverrideStorage {
#[cfg(test)]
pub fn new_for_test(storage_path: &Path) -> Result<Self> {
let storage_file = storage_path.join("overrides.toml");
if let Some(parent) = storage_file.parent() {
std::fs::create_dir_all(parent)?;
}
let cache = if storage_file.exists() {
let content = std::fs::read_to_string(&storage_file)?;
toml::from_str(&content)?
} else {
StorageFormat::default()
};
let backup_manager = Arc::new(BackupManager::new(storage_path, 5)?);
Ok(Self {
storage_path: storage_file,
workspace_path: storage_path.to_path_buf(),
cache: RwLock::new(cache),
backup_manager,
hierarchy: None,
})
}
pub fn new(workspace_path: &Path) -> Result<Self> {
let hierarchy = raz_config::ConfigHierarchy::discover(workspace_path).map_err(|e| {
OverrideError::StorageError(format!("Failed to discover config hierarchy: {e}"))
})?;
let storage_path = hierarchy.get_override_storage_path().map_err(|e| {
OverrideError::StorageError(format!("Failed to get override storage path: {e}"))
})?;
if let Some(parent) = storage_path.parent() {
std::fs::create_dir_all(parent)?;
}
let cache = if storage_path.exists() {
let content = std::fs::read_to_string(&storage_path)?;
toml::from_str(&content)?
} else {
StorageFormat::default()
};
let backup_manager = Arc::new(BackupManager::new(workspace_path, 5)?);
Ok(Self {
storage_path,
workspace_path: workspace_path.to_path_buf(),
cache: RwLock::new(cache),
backup_manager,
hierarchy: Some(hierarchy),
})
}
pub fn save(&self, entry: &OverrideEntry) -> Result<()> {
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
let _ = self
.backup_manager
.create_backup(&*cache)
.map_err(|e| log::warn!("Failed to create backup: {e}"));
cache
.overrides
.insert(entry.key.primary.clone(), entry.clone());
self.persist(&cache)?;
Ok(())
}
pub fn save_with_validation(
&self,
entry: &OverrideEntry,
validate_fn: impl FnOnce(&OverrideEntry) -> Result<()>,
) -> Result<()> {
validate_fn(entry)?;
self.save(entry)
}
pub fn get_by_primary_key(&self, key: &str) -> Result<Option<OverrideEntry>> {
let cache = self
.cache
.read()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
Ok(cache.overrides.get(key).cloned())
}
pub fn get_by_key(&self, key: &str) -> Result<Option<OverrideEntry>> {
let cache = self
.cache
.read()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
if let Some(entry) = cache.overrides.get(key) {
return Ok(Some(entry.clone()));
}
for entry in cache.overrides.values() {
if entry.key.fallbacks.contains(&key.to_string()) {
return Ok(Some(entry.clone()));
}
}
Ok(None)
}
pub fn get_by_file(&self, file_path: &Path) -> Result<Vec<OverrideEntry>> {
let cache = self
.cache
.read()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
let normalized_path = file_path.to_string_lossy().replace('\\', "/");
Ok(cache
.overrides
.values()
.filter(|entry| {
let entry_path = entry
.metadata
.file_path
.to_string_lossy()
.replace('\\', "/");
entry_path == normalized_path
})
.cloned()
.collect())
}
pub fn delete(&self, key: &str) -> Result<bool> {
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
let removed = cache.overrides.shift_remove(key).is_some();
if removed {
self.persist(&cache)?;
}
Ok(removed)
}
pub fn list_all(&self) -> Result<Vec<OverrideEntry>> {
let cache = self
.cache
.read()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
Ok(cache.overrides.values().cloned().collect())
}
pub fn list_all_hierarchical(
&self,
) -> Result<Vec<(raz_config::ConfigLevel, Vec<OverrideEntry>)>> {
let mut results = Vec::new();
if let Some(ref hierarchy) = self.hierarchy {
for location in hierarchy.locations() {
if location.exists {
let override_path = location.path.join("overrides.toml");
if override_path.exists() {
if let Ok(content) = std::fs::read_to_string(&override_path) {
if let Ok(storage_format) = toml::from_str::<StorageFormat>(&content) {
let entries: Vec<OverrideEntry> =
storage_format.overrides.values().cloned().collect();
if !entries.is_empty() {
results.push((location.level.clone(), entries));
}
}
}
}
}
}
} else {
results.push((raz_config::ConfigLevel::Project, self.list_all()?));
}
Ok(results)
}
pub fn clear(&self) -> Result<()> {
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
cache.overrides.clear();
self.persist(&cache)?;
Ok(())
}
pub fn clear_by_file(&self, file_path: &Path) -> Result<usize> {
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
let normalized_path = file_path.to_string_lossy().replace('\\', "/");
let keys_to_remove: Vec<String> = cache
.overrides
.iter()
.filter(|(_, entry)| {
let entry_path = entry
.metadata
.file_path
.to_string_lossy()
.replace('\\', "/");
entry_path == normalized_path
})
.map(|(key, _)| key.clone())
.collect();
let count = keys_to_remove.len();
for key in keys_to_remove {
cache.overrides.shift_remove(&key);
}
if count > 0 {
self.persist(&cache)?;
}
Ok(count)
}
pub fn migrate_from_legacy(&self, legacy_data: &str) -> Result<Vec<String>> {
let mut migrated_keys = Vec::new();
if let Ok(legacy_map) = toml::from_str::<toml::Value>(legacy_data) {
if let Some(overrides) = legacy_map.get("overrides").and_then(|v| v.as_table()) {
for (old_key, _value) in overrides {
log::info!("Migrating override: {old_key}");
migrated_keys.push(old_key.clone());
}
}
}
Ok(migrated_keys)
}
fn persist(&self, cache: &StorageFormat) -> Result<()> {
let content = toml::to_string_pretty(cache)?;
std::fs::write(&self.storage_path, content)?;
Ok(())
}
pub fn update_execution_status(&self, key: &str, success: bool) -> Result<()> {
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
if let Some(entry) = cache.overrides.get_mut(key) {
let now = chrono::Utc::now();
entry.metadata.last_execution_time = Some(now);
entry.metadata.last_execution_success = Some(success);
if success {
entry.metadata.validation_status = crate::ValidationStatus::Validated;
entry.metadata.failure_count = 0;
} else {
entry.metadata.failure_count += 1;
entry.metadata.validation_status =
crate::ValidationStatus::Failed(entry.metadata.failure_count);
}
entry.metadata.modified_at = now;
self.persist(&cache)?;
}
Ok(())
}
pub fn rollback_to_last_backup(&self) -> Result<()> {
if let Some(backup_info) = self.backup_manager.get_last_backup()? {
let restored: StorageFormat = self.backup_manager.restore_backup(&backup_info.path)?;
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
*cache = restored;
self.persist(&cache)?;
log::info!("Rolled back to backup from {}", backup_info.created_at);
Ok(())
} else {
Err(OverrideError::StorageError(
"No backup available for rollback".to_string(),
))
}
}
pub fn backup_manager(&self) -> &BackupManager {
&self.backup_manager
}
pub fn export(&self) -> Result<String> {
let cache = self
.cache
.read()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
Ok(toml::to_string_pretty(&*cache)?)
}
pub fn import(&self, data: &str) -> Result<usize> {
let imported: StorageFormat = toml::from_str(data)?;
let mut cache = self
.cache
.write()
.map_err(|e| OverrideError::StorageError(format!("Lock poisoned: {e}")))?;
let count = imported.overrides.len();
for (key, entry) in imported.overrides {
cache.overrides.insert(key, entry);
}
self.persist(&cache)?;
Ok(count)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CommandOverride, FunctionContext, OverrideKey, OverrideMetadata};
use tempfile::TempDir;
#[test]
fn test_storage_operations() {
let temp_dir = TempDir::new().unwrap();
let storage = OverrideStorage::new_for_test(temp_dir.path()).unwrap();
let context = FunctionContext {
file_path: PathBuf::from("src/test.rs"),
function_name: Some("test_func".to_string()),
line_number: 10,
context: None,
};
let key = OverrideKey::new(&context).unwrap();
let entry = OverrideEntry {
key: key.clone(),
override_config: CommandOverride::new("test".to_string()),
metadata: OverrideMetadata {
created_at: chrono::Utc::now(),
modified_at: chrono::Utc::now(),
file_path: context.file_path.clone(),
function_name: context.function_name.clone(),
original_line: Some(context.line_number),
notes: None,
validation_status: crate::ValidationStatus::Pending,
last_execution_time: None,
last_execution_success: None,
failure_count: 0,
},
};
storage.save(&entry).unwrap();
let retrieved = storage.get_by_primary_key(&key.primary).unwrap();
assert!(retrieved.is_some());
assert_eq!(retrieved.unwrap().key.primary, key.primary);
let file_overrides = storage.get_by_file(&context.file_path).unwrap();
assert_eq!(file_overrides.len(), 1);
assert!(storage.delete(&key.primary).unwrap());
assert!(storage.get_by_primary_key(&key.primary).unwrap().is_none());
}
#[test]
fn test_persistence() {
let temp_dir = TempDir::new().unwrap();
let storage_path = temp_dir.path();
{
let storage = OverrideStorage::new_for_test(storage_path).unwrap();
let context = FunctionContext {
file_path: PathBuf::from("src/main.rs"),
function_name: Some("main".to_string()),
line_number: 1,
context: None,
};
let key = OverrideKey::new(&context).unwrap();
let entry = OverrideEntry {
key,
override_config: CommandOverride::new("run".to_string()),
metadata: OverrideMetadata {
created_at: chrono::Utc::now(),
modified_at: chrono::Utc::now(),
file_path: context.file_path,
function_name: context.function_name,
original_line: Some(context.line_number),
notes: Some("Test override".to_string()),
validation_status: crate::ValidationStatus::Pending,
last_execution_time: None,
last_execution_success: None,
failure_count: 0,
},
};
storage.save(&entry).unwrap();
}
{
let storage = OverrideStorage::new_for_test(storage_path).unwrap();
let all = storage.list_all().unwrap();
assert_eq!(all.len(), 1);
assert_eq!(all[0].metadata.notes.as_deref(), Some("Test override"));
}
}
}