a3s-vec 0.1.8

Native Rust in-process vector database with zvec-compatible capabilities
Documentation
//! Error types used by `a3s-vec`.
//!
//! The public error intentionally mirrors zvec's status taxonomy while still
//! carrying a useful, typed Rust error.  Keeping the status code at the API
//! boundary makes it possible for adapters (CLI, HTTP, and A3S Code) to map
//! failures without parsing strings.

use std::fmt;

/// Stable status categories exposed by the collection API.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum ErrorCode {
    NotFound = 1,
    AlreadyExists = 2,
    InvalidArgument = 3,
    PermissionDenied = 4,
    FailedPrecondition = 5,
    ResourceExhausted = 6,
    Unavailable = 7,
    InternalError = 8,
    NotSupported = 9,
    Unknown = 10,
}

impl From<u32> for ErrorCode {
    fn from(value: u32) -> Self {
        match value {
            1 => Self::NotFound,
            2 => Self::AlreadyExists,
            3 => Self::InvalidArgument,
            4 => Self::PermissionDenied,
            5 => Self::FailedPrecondition,
            6 => Self::ResourceExhausted,
            7 => Self::Unavailable,
            8 => Self::InternalError,
            9 => Self::NotSupported,
            _ => Self::Unknown,
        }
    }
}

impl From<ErrorCode> for u32 {
    fn from(value: ErrorCode) -> Self {
        value as u32
    }
}

impl fmt::Display for ErrorCode {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let name = match self {
            Self::NotFound => "NotFound",
            Self::AlreadyExists => "AlreadyExists",
            Self::InvalidArgument => "InvalidArgument",
            Self::PermissionDenied => "PermissionDenied",
            Self::FailedPrecondition => "FailedPrecondition",
            Self::ResourceExhausted => "ResourceExhausted",
            Self::Unavailable => "Unavailable",
            Self::InternalError => "InternalError",
            Self::NotSupported => "NotSupported",
            Self::Unknown => "Unknown",
        };
        f.write_str(name)
    }
}

/// An error returned by a3s-vec.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Error {
    /// Stable machine-readable category.
    pub code: ErrorCode,
    /// Human-readable context.  Messages never contain a secret by design.
    pub message: String,
}

impl Error {
    /// Creates an error with a stable code and context.
    pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
        Self {
            code,
            message: message.into(),
        }
    }

    pub fn not_found(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::NotFound, message)
    }

    pub fn already_exists(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::AlreadyExists, message)
    }

    pub fn invalid_argument(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::InvalidArgument, message)
    }

    pub fn permission_denied(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::PermissionDenied, message)
    }

    pub fn failed_precondition(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::FailedPrecondition, message)
    }

    pub fn internal(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::InternalError, message)
    }

    pub fn resource_exhausted(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::ResourceExhausted, message)
    }

    pub fn not_supported(message: impl Into<String>) -> Self {
        Self::new(ErrorCode::NotSupported, message)
    }

    pub fn is_not_found(&self) -> bool {
        self.code == ErrorCode::NotFound
    }

    pub fn is_already_exists(&self) -> bool {
        self.code == ErrorCode::AlreadyExists
    }

    pub fn is_invalid_argument(&self) -> bool {
        self.code == ErrorCode::InvalidArgument
    }
}

impl fmt::Display for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "a3s-vec error {}: {}", self.code, self.message)
    }
}

impl std::error::Error for Error {}

/// Specialized result type for all public operations.
pub type Result<T> = std::result::Result<T, Error>;

impl From<std::io::Error> for Error {
    fn from(value: std::io::Error) -> Self {
        let code = match value.kind() {
            std::io::ErrorKind::NotFound => ErrorCode::NotFound,
            std::io::ErrorKind::PermissionDenied => ErrorCode::PermissionDenied,
            std::io::ErrorKind::AlreadyExists => ErrorCode::AlreadyExists,
            _ => ErrorCode::InternalError,
        };
        Self::new(code, value.to_string())
    }
}

impl From<serde_json::Error> for Error {
    fn from(value: serde_json::Error) -> Self {
        Self::new(ErrorCode::InternalError, format!("JSON error: {value}"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn status_codes_are_stable() {
        for code in [
            ErrorCode::NotFound,
            ErrorCode::AlreadyExists,
            ErrorCode::InvalidArgument,
            ErrorCode::PermissionDenied,
            ErrorCode::FailedPrecondition,
            ErrorCode::ResourceExhausted,
            ErrorCode::Unavailable,
            ErrorCode::InternalError,
            ErrorCode::NotSupported,
            ErrorCode::Unknown,
        ] {
            assert_eq!(ErrorCode::from(u32::from(code)), code);
            assert!(!format!("{code}").is_empty());
        }
        assert_eq!(u32::from(ErrorCode::InvalidArgument), 3);
        assert_eq!(ErrorCode::from(99), ErrorCode::Unknown);
    }

    #[test]
    fn helpers_preserve_context() {
        let error = Error::not_found("document x");
        assert!(error.is_not_found());
        assert!(error.to_string().contains("document x"));
        assert_eq!(Error::already_exists("dup").code, ErrorCode::AlreadyExists);
        assert_eq!(
            Error::invalid_argument("bad").code,
            ErrorCode::InvalidArgument
        );
        assert_eq!(
            Error::permission_denied("no").code,
            ErrorCode::PermissionDenied
        );
        assert_eq!(
            Error::failed_precondition("pre").code,
            ErrorCode::FailedPrecondition
        );
        assert_eq!(
            Error::resource_exhausted("oom").code,
            ErrorCode::ResourceExhausted
        );
        assert_eq!(Error::internal("boom").code, ErrorCode::InternalError);
        assert_eq!(Error::not_supported("yet").code, ErrorCode::NotSupported);
        assert_eq!(
            Error::new(ErrorCode::Unavailable, "down").code,
            ErrorCode::Unavailable
        );
    }

    #[test]
    fn json_errors_map_to_internal() {
        let err: Error = serde_json::from_str::<serde_json::Value>("{")
            .unwrap_err()
            .into();
        assert_eq!(err.code, ErrorCode::InternalError);
        assert!(err.message.contains("JSON"));
    }

    #[test]
    fn io_and_predicate_helpers_cover_surface() {
        let not_found: Error = std::io::Error::new(std::io::ErrorKind::NotFound, "gone").into();
        assert!(not_found.is_not_found());
        let exists: Error = std::io::Error::new(std::io::ErrorKind::AlreadyExists, "here").into();
        assert!(exists.is_already_exists());
        let denied: Error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "no").into();
        assert_eq!(denied.code, ErrorCode::PermissionDenied);
        let other: Error = std::io::Error::other("boom").into();
        assert_eq!(other.code, ErrorCode::InternalError);
        assert!(Error::invalid_argument("x").is_invalid_argument());
        assert!(!Error::not_found("y").is_invalid_argument());
    }
}