meld 1.1.5

Deterministic filesystem state management using Merkle trees
Documentation
use crate::error::ApiError;
use crate::provider::profile::ProviderConfig;
use crate::provider::storage::{ProviderStorage, StoredProviderConfig};

pub struct XdgProviderStorage;

impl XdgProviderStorage {
    pub fn new() -> Self {
        Self
    }
}

impl Default for XdgProviderStorage {
    fn default() -> Self {
        Self::new()
    }
}

impl ProviderStorage for XdgProviderStorage {
    fn list(&self) -> Result<Vec<StoredProviderConfig>, ApiError> {
        let providers_dir = crate::config::xdg::providers_dir()?;
        if !providers_dir.exists() {
            return Ok(Vec::new());
        }

        let entries = std::fs::read_dir(&providers_dir).map_err(|e| {
            ApiError::ConfigError(format!(
                "Failed to read providers directory {}: {}",
                providers_dir.display(),
                e
            ))
        })?;

        let mut loaded = Vec::new();
        for entry in entries {
            let entry = match entry {
                Ok(e) => e,
                Err(e) => {
                    tracing::warn!(
                        "Failed to read directory entry in {}: {}",
                        providers_dir.display(),
                        e
                    );
                    continue;
                }
            };

            let path = entry.path();
            if path.extension() != Some(std::ffi::OsStr::new("toml")) {
                continue;
            }

            let provider_name = match path.file_stem().and_then(|s| s.to_str()) {
                Some(name) => name.to_string(),
                None => {
                    tracing::warn!("Invalid provider filename non UTF8: {:?}", path);
                    continue;
                }
            };

            let content = match std::fs::read_to_string(&path) {
                Ok(c) => c,
                Err(e) => {
                    tracing::error!("Failed to read provider config {}: {}", path.display(), e);
                    continue;
                }
            };

            let mut config: ProviderConfig = match toml::from_str(&content) {
                Ok(config) => config,
                Err(e) => {
                    tracing::error!("Failed to parse provider config {}: {}", path.display(), e);
                    continue;
                }
            };

            if let Some(config_name) = &config.provider_name {
                if config_name != &provider_name {
                    tracing::warn!(
                        "Provider name mismatch in {}: filename={}, config={}",
                        path.display(),
                        provider_name,
                        config_name
                    );
                }
            }

            if config.provider_name.is_none() {
                config.provider_name = Some(provider_name.clone());
            }

            config.normalize_endpoint_in_place();

            if let Err(e) = config.validate() {
                tracing::error!("Invalid provider config {}: {}", path.display(), e);
                continue;
            }

            loaded.push(StoredProviderConfig {
                provider_name,
                config,
                path,
            });
        }

        Ok(loaded)
    }

    fn path_for(&self, provider_name: &str) -> Result<std::path::PathBuf, ApiError> {
        let providers_dir = crate::config::xdg::providers_dir()?;
        Ok(providers_dir.join(format!("{}.toml", provider_name)))
    }

    fn save(&self, provider_name: &str, config: &ProviderConfig) -> Result<(), ApiError> {
        let config_path = self.path_for(provider_name)?;

        let providers_dir = crate::config::xdg::providers_dir()?;
        std::fs::create_dir_all(&providers_dir).map_err(|e| {
            ApiError::ConfigError(format!(
                "Failed to create providers directory {}: {}",
                providers_dir.display(),
                e
            ))
        })?;

        let mut config = config.clone();
        if config.provider_name.is_none() {
            config.provider_name = Some(provider_name.to_string());
        }
        config.normalize_endpoint_in_place();

        let toml_content = toml::to_string_pretty(&config).map_err(|e| {
            ApiError::ConfigError(format!("Failed to serialize provider config: {}", e))
        })?;

        std::fs::write(&config_path, toml_content).map_err(|e| {
            ApiError::ConfigError(format!(
                "Failed to write provider config to {}: {}",
                config_path.display(),
                e
            ))
        })?;

        Ok(())
    }

    fn delete(&self, provider_name: &str) -> Result<(), ApiError> {
        let config_path = self.path_for(provider_name)?;
        if !config_path.exists() {
            return Err(ApiError::ConfigError(format!(
                "Provider config file not found: {}",
                config_path.display()
            )));
        }

        std::fs::remove_file(&config_path).map_err(|e| {
            ApiError::ConfigError(format!(
                "Failed to delete provider config file {}: {}",
                config_path.display(),
                e
            ))
        })
    }
}