aix-core 0.1.0

Core abstractions and types for the AIX library
Documentation
//! Comprehensive error handling for the AIX library.
//!
//! This module provides a unified error type that can represent all possible
//! failure modes when interacting with AI providers.

use std::error::Error as StdError;
use std::fmt;
use std::time::Duration;

/// Unified error type for all AIX operations.
#[derive(Debug, Clone)]
pub enum AixError {
    /// Transport layer errors (network, DNS, etc.)
    Transport {
        /// The underlying error
        source: String,
        /// Additional context about the error
        context: String,
    },
    /// Provider-specific errors (API errors, validation, etc.)
    Provider {
        /// Name of the provider that returned the error
        provider: String,
        /// Error code from the provider (if available)
        code: Option<String>,
        /// Human-readable error message
        message: String,
        /// HTTP status code (if applicable)
        status: Option<u16>,
    },
    /// Rate limiting errors
    RateLimit {
        /// Name of the provider that rate limited the request
        provider: String,
        /// Suggested retry delay (if provided by the provider)
        retry_after: Option<Duration>,
        /// Human-readable error message
        message: String,
    },
    /// Serialization/deserialization errors
    Serialization {
        /// The underlying error
        source: String,
        /// Additional context about what was being serialized/deserialized
        context: String,
    },
    /// Configuration errors
    Config {
        /// Human-readable error message
        message: String,
    },
    /// Streaming-related errors
    Stream {
        /// Human-readable error message
        message: String,
        /// The underlying error (if available)
        source: Option<String>,
    },
    /// Safety/content policy violations
    Safety {
        /// Name of the provider that flagged the content
        provider: String,
        /// Category of safety violation
        category: String,
        /// Human-readable error message
        message: String,
    },
    /// Authentication/authorization errors
    Auth {
        /// Name of the provider that rejected the authentication
        provider: String,
        /// Human-readable error message
        message: String,
    },
    /// Timeout errors
    Timeout {
        /// The operation that timed out
        operation: String,
        /// How long we waited before timing out
        duration: Duration,
    },
    /// Catch-all for other errors
    Other {
        /// Human-readable error message
        message: String,
        /// The underlying error (if available)
        source: Option<String>,
    },
}

impl AixError {
    /// Check if this error is retryable.
    ///
    /// Returns true if the error warrants a retry attempt.
    pub fn is_retryable(&self) -> bool {
        match self {
            AixError::Transport { .. } => true,
            AixError::Provider { status, .. } => {
                status.map_or(false, |s| s >= 500 || s == 429)
            }
            AixError::RateLimit { .. } => true,
            AixError::Timeout { .. } => true,
            AixError::Serialization { .. } => false,
            AixError::Config { .. } => false,
            AixError::Stream { .. } => false,
            AixError::Safety { .. } => false,
            AixError::Auth { .. } => false,
            AixError::Other { .. } => false,
        }
    }

    /// Create a new transport error.
    pub fn transport<S: Into<String>, C: Into<String>>(source: S, context: C) -> Self {
        AixError::Transport {
            source: source.into(),
            context: context.into(),
        }
    }

    /// Create a new provider error.
    pub fn provider<P: Into<String>, M: Into<String>>(
        provider: P,
        message: M,
    ) -> Self {
        AixError::Provider {
            provider: provider.into(),
            code: None,
            message: message.into(),
            status: None,
        }
    }

    /// Create a new provider error with status and code.
    pub fn provider_with_details<P: Into<String>, M: Into<String>, C: Into<String>>(
        provider: P,
        message: M,
        status: u16,
        code: C,
    ) -> Self {
        AixError::Provider {
            provider: provider.into(),
            code: Some(code.into()),
            message: message.into(),
            status: Some(status),
        }
    }

    /// Create a new rate limit error.
    pub fn rate_limit<P: Into<String>, M: Into<String>>(
        provider: P,
        message: M,
    ) -> Self {
        AixError::RateLimit {
            provider: provider.into(),
            retry_after: None,
            message: message.into(),
        }
    }

    /// Create a new rate limit error with retry after.
    pub fn rate_limit_with_retry<P: Into<String>, M: Into<String>>(
        provider: P,
        message: M,
        retry_after: Duration,
    ) -> Self {
        AixError::RateLimit {
            provider: provider.into(),
            retry_after: Some(retry_after),
            message: message.into(),
        }
    }

    /// Create a new serialization error.
    pub fn serialization<S: Into<String>, C: Into<String>>(source: S, context: C) -> Self {
        AixError::Serialization {
            source: source.into(),
            context: context.into(),
        }
    }

    /// Create a new config error.
    pub fn config<M: Into<String>>(message: M) -> Self {
        AixError::Config {
            message: message.into(),
        }
    }

    /// Create a new stream error.
    pub fn stream<M: Into<String>>(message: M) -> Self {
        AixError::Stream {
            message: message.into(),
            source: None,
        }
    }

    /// Create a new stream error with source.
    pub fn stream_with_source<M: Into<String>, S: Into<String>>(
        message: M,
        source: S,
    ) -> Self {
        AixError::Stream {
            message: message.into(),
            source: Some(source.into()),
        }
    }

    /// Create a new safety error.
    pub fn safety<P: Into<String>, C: Into<String>, M: Into<String>>(
        provider: P,
        category: C,
        message: M,
    ) -> Self {
        AixError::Safety {
            provider: provider.into(),
            category: category.into(),
            message: message.into(),
        }
    }

    /// Create a new auth error.
    pub fn auth<P: Into<String>, M: Into<String>>(provider: P, message: M) -> Self {
        AixError::Auth {
            provider: provider.into(),
            message: message.into(),
        }
    }

    /// Create a new timeout error.
    pub fn timeout<O: Into<String>>(operation: O, duration: Duration) -> Self {
        AixError::Timeout {
            operation: operation.into(),
            duration,
        }
    }

    /// Create a new other error.
    pub fn other<M: Into<String>>(message: M) -> Self {
        AixError::Other {
            message: message.into(),
            source: None,
        }
    }

    /// Create a new other error with source.
    pub fn other_with_source<M: Into<String>, S: Into<String>>(
        message: M,
        source: S,
    ) -> Self {
        AixError::Other {
            message: message.into(),
            source: Some(source.into()),
        }
    }
}

impl fmt::Display for AixError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AixError::Transport { source, context } => {
                write!(f, "Transport error in {}: {}", context, source)
            }
            AixError::Provider {
                provider,
                code,
                message,
                status,
            } => {
                write!(f, "Provider error from {}", provider)?;
                if let Some(status) = status {
                    write!(f, " (status {})", status)?;
                }
                if let Some(code) = code {
                    write!(f, " (code: {})", code)?;
                }
                write!(f, ": {}", message)
            }
            AixError::RateLimit {
                provider,
                retry_after,
                message,
            } => {
                write!(f, "Rate limit error from {}: {}", provider, message)?;
                if let Some(retry_after) = retry_after {
                    write!(f, " (retry after: {:?})", retry_after)?;
                }
                Ok(())
            }
            AixError::Serialization { source, context } => {
                write!(f, "Serialization error in {}: {}", context, source)
            }
            AixError::Config { message } => {
                write!(f, "Configuration error: {}", message)
            }
            AixError::Stream { message, source } => {
                write!(f, "Stream error: {}", message)?;
                if let Some(source) = source {
                    write!(f, " (source: {})", source)?;
                }
                Ok(())
            }
            AixError::Safety {
                provider,
                category,
                message,
            } => {
                write!(
                    f,
                    "Safety violation from {} (category: {}): {}",
                    provider, category, message
                )
            }
            AixError::Auth { provider, message } => {
                write!(f, "Authentication error from {}: {}", provider, message)
            }
            AixError::Timeout { operation, duration } => {
                write!(
                    f,
                    "Operation '{}' timed out after {:?}",
                    operation, duration
                )
            }
            AixError::Other { message, source } => {
                write!(f, "Error: {}", message)?;
                if let Some(source) = source {
                    write!(f, " (source: {})", source)?;
                }
                Ok(())
            }
        }
    }
}

impl StdError for AixError {}

/// Result type alias for AIX operations.
pub type AixResult<T> = Result<T, AixError>;

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

    #[test]
    fn test_error_retryability() {
        assert!(AixError::transport("network error", "request").is_retryable());
        assert!(AixError::provider_with_details("openai", "error", 500, "internal_error").is_retryable());
        assert!(AixError::provider_with_details("openai", "error", 429, "rate_limit").is_retryable());
        assert!(AixError::rate_limit("openai", "too many requests").is_retryable());
        assert!(AixError::timeout("chat", Duration::from_secs(30)).is_retryable());
        
        assert!(!AixError::provider_with_details("openai", "error", 400, "bad_request").is_retryable());
        assert!(!AixError::config("invalid api key").is_retryable());
        assert!(!AixError::auth("openai", "unauthorized").is_retryable());
        assert!(!AixError::safety("openai", "hate", "content flagged").is_retryable());
    }

    #[test]
    fn test_error_display() {
        let err = AixError::provider_with_details("openai", "invalid request", 400, "invalid_request");
        assert_eq!(err.to_string(), "Provider error from openai (status 400) (code: invalid_request): invalid request");
    }
}