attune-core 0.1.0

Core traits and types for attune: runtime-mutable, persisted, observable configuration.
Documentation
use std::path::PathBuf;

/// Errors returned by storage backends.
///
/// Backend-specific errors are converted into strings so `attune-core` does not
/// depend on a concrete backend implementation's error types.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum BackendError {
    #[error("Failed to open backend: {0}")]
    Open(String),
    #[error("Failed to read from backend: {0}")]
    Read(String),
    #[error("Failed to write to backend: {0}")]
    Write(String),
}

/// Errors returned by generated settings loaders, field setters, and config helpers.
///
/// This enum is structured so callers can match on known failure categories.
/// It is marked `non_exhaustive`, so downstream matches should include a
/// wildcard arm to remain compatible with future versions.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum SettingsError {
    #[error(transparent)]
    Backend(#[from] BackendError),
    #[error("Failed to serialize persisted setting value: {source}")]
    Serialize { source: serde_json::Error },
    #[error("No configuration directory found.")]
    NoConfigDir,
    #[error("Failed to read config file `{path}`: {source}")]
    ConfigRead {
        path: PathBuf,
        source: std::io::Error,
    },
    #[error("Failed to parse config file `{path}`: {source}")]
    ConfigParse {
        path: PathBuf,
        source: toml::de::Error,
    },
    #[error("Failed to parse config value `{key}`: {source}")]
    ConfigValueParse {
        key: String,
        source: toml::de::Error,
    },
    #[error("Failed to parse environment variable `{name}`: {source}")]
    EnvParse {
        name: String,
        source: toml::de::Error,
    },
    #[error("Failed to parse persisted setting `{key}`: {source}")]
    PersistValueParse {
        key: String,
        source: serde_json::Error,
    },
    #[error("Failed to parse persisted setting `{key}` with deserialize fallback: {error}")]
    PersistFallbackParse { key: String, error: String },
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde::{Serialize, Serializer};

    struct FailingSerialize;

    impl Serialize for FailingSerialize {
        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
        where
            S: Serializer,
        {
            Err(serde::ser::Error::custom("intentional serialize failure"))
        }
    }

    #[test]
    fn test_backend_error_open_message_is_correct() {
        let msg = String::from("test_message");
        let error = BackendError::Open(msg.clone());
        assert_eq!(
            error.to_string(),
            format!("Failed to open backend: {}", msg)
        );
    }

    #[test]
    fn test_backend_error_read_message_is_correct() {
        let msg = String::from("test_message");
        let error = BackendError::Read(msg.clone());
        assert_eq!(
            error.to_string(),
            format!("Failed to read from backend: {}", msg)
        );
    }

    #[test]
    fn test_backend_error_write_message_is_correct() {
        let msg = String::from("test_message");
        let error = BackendError::Write(msg.clone());
        assert_eq!(
            error.to_string(),
            format!("Failed to write to backend: {}", msg)
        );
    }

    #[test]
    fn test_settings_error_backend_message_is_correct() {
        let msg = String::from("test_message");
        let b_error = BackendError::Open(msg.clone());
        let s_error = SettingsError::from(b_error);

        assert_eq!(
            s_error.to_string(),
            format!("Failed to open backend: {}", msg)
        );
    }

    #[test]
    fn test_settings_error_serialize_message_is_correct() {
        let source = serde_json::to_string(&FailingSerialize).unwrap_err();
        let set_error = SettingsError::Serialize { source };

        assert!(
            set_error
                .to_string()
                .contains("Failed to serialize persisted setting value:")
        )
    }

    #[test]
    fn test_settings_error_noconfigdir_message_is_correct() {
        let s_error = SettingsError::NoConfigDir;

        assert_eq!(s_error.to_string(), "No configuration directory found.")
    }
}