flowglad 0.1.0

Rust SDK for FlowGlad - Open source billing infrastructure
Documentation
//! Error types for the FlowGlad SDK
//!
//! This module defines all error types that can be returned by the SDK.
//! Errors are designed to be informative and actionable.

use thiserror::Error;

/// The main error type for the FlowGlad SDK
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum Error {
    /// An error returned by the FlowGlad API
    #[error("API error (status {status}): {message}")]
    Api {
        /// HTTP status code
        status: u16,
        /// Error message from the API
        message: String,
        /// Optional error code from the API
        code: Option<String>,
    },

    /// Network or connection error
    #[error("Network error: {0}")]
    Network(#[from] reqwest::Error),

    /// JSON serialization/deserialization error
    #[error("Serialization error: {0}")]
    Serialization(#[from] serde_json::Error),

    /// Invalid configuration
    #[error("Configuration error: {0}")]
    Config(String),

    /// Authentication failed
    #[error("Authentication failed: {0}")]
    Authentication(String),

    /// Rate limit exceeded
    #[error("Rate limit exceeded, retry after {retry_after:?}")]
    RateLimit {
        /// Duration to wait before retrying
        retry_after: Option<std::time::Duration>,
    },

    /// Invalid URL
    #[error("Invalid URL: {0}")]
    Url(#[from] url::ParseError),

    /// Request timeout
    #[error("Request timed out")]
    Timeout,

    /// Unknown or unexpected error
    #[error("Unknown error: {0}")]
    Unknown(String),
}

impl Error {
    /// Returns true if this error is retryable
    ///
    /// Retryable errors include network errors, timeouts, and server errors (5xx).
    pub fn is_retryable(&self) -> bool {
        match self {
            Error::Network(_) => true,
            Error::Timeout => true,
            Error::RateLimit { .. } => true,
            Error::Api { status, .. } => *status >= 500 && *status < 600,
            _ => false,
        }
    }

    /// Returns true if this error is an authentication error
    pub fn is_auth_error(&self) -> bool {
        matches!(
            self,
            Error::Authentication(_)
                | Error::Api {
                    status: 401 | 403,
                    ..
                }
        )
    }

    /// Create an API error from status code and message
    pub(crate) fn api_error(status: u16, message: impl Into<String>) -> Self {
        Error::Api {
            status,
            message: message.into(),
            code: None,
        }
    }

    /// Create an API error with an error code
    pub(crate) fn api_error_with_code(
        status: u16,
        message: impl Into<String>,
        code: impl Into<String>,
    ) -> Self {
        Error::Api {
            status,
            message: message.into(),
            code: Some(code.into()),
        }
    }
}

/// A specialized Result type for FlowGlad operations
pub type Result<T, E = Error> = std::result::Result<T, E>;

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

    #[test]
    fn test_is_retryable() {
        assert!(Error::Timeout.is_retryable());
        assert!(Error::RateLimit { retry_after: None }.is_retryable());
        assert!(Error::api_error(500, "Server error").is_retryable());
        assert!(Error::api_error(503, "Service unavailable").is_retryable());

        assert!(!Error::api_error(400, "Bad request").is_retryable());
        assert!(!Error::api_error(404, "Not found").is_retryable());
        assert!(!Error::Config("Invalid config".into()).is_retryable());
    }

    #[test]
    fn test_is_auth_error() {
        assert!(Error::Authentication("Invalid key".into()).is_auth_error());
        assert!(Error::api_error(401, "Unauthorized").is_auth_error());
        assert!(Error::api_error(403, "Forbidden").is_auth_error());

        assert!(!Error::api_error(404, "Not found").is_auth_error());
        assert!(!Error::Timeout.is_auth_error());
    }

    #[test]
    fn test_error_display() {
        let err = Error::api_error(404, "Customer not found");
        assert_eq!(
            err.to_string(),
            "API error (status 404): Customer not found"
        );

        let err = Error::Config("Missing API key".into());
        assert_eq!(err.to_string(), "Configuration error: Missing API key");
    }
}