reqsign-core 3.0.1

Signing API requests without effort.
Documentation
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

use std::fmt;

/// The error type for reqsign operations
pub struct Error {
    /// The category of error that occurred
    kind: ErrorKind,

    /// Human-readable error message
    message: String,

    /// The underlying error source
    source: Option<anyhow::Error>,

    /// Additional context information for debugging
    context: Vec<String>,

    /// Whether this error is retryable
    retryable: bool,
}

/// The kind of error that occurred
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
    /// Credentials are invalid, expired, or malformed
    /// User action: Check credential format, refresh if expired
    CredentialInvalid,

    /// Permission denied when accessing credentials or resources
    /// User action: Check IAM policies, role trust relationships
    PermissionDenied,

    /// Required configuration is missing or invalid
    /// User action: Check configuration files, environment variables
    ConfigInvalid,

    /// Request cannot be signed or is malformed
    /// User action: Check request parameters, headers
    RequestInvalid,

    /// Rate limit exceeded
    /// User action: Implement backoff, check quotas
    RateLimited,

    /// Unexpected error that doesn't fit other categories
    /// User action: Check logs, report bug if persistent
    Unexpected,
}

impl Error {
    /// Create a new error with the given kind and message
    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
        Self {
            kind,
            message: message.into(),
            source: None,
            context: Vec::new(),
            retryable: kind.default_retryable(),
        }
    }

    /// Add a source error
    pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
        self.source = Some(source.into());
        self
    }

    /// Add context information for debugging
    pub fn with_context(mut self, context: impl fmt::Display) -> Self {
        self.context.push(context.to_string());
        self
    }

    /// Override the retryable status
    pub fn set_retryable(mut self, retryable: bool) -> Self {
        self.retryable = retryable;
        self
    }

    /// Get the error kind
    pub fn kind(&self) -> ErrorKind {
        self.kind
    }

    /// Check if this error is retryable
    pub fn is_retryable(&self) -> bool {
        self.retryable
    }

    /// Get the context information
    pub fn context(&self) -> &[String] {
        &self.context
    }
}

impl ErrorKind {
    /// Default retryable status for each error kind
    fn default_retryable(&self) -> bool {
        matches!(self, ErrorKind::RateLimited)
    }
}

// Convenience constructors
impl Error {
    /// Create a credential invalid error
    pub fn credential_invalid(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::CredentialInvalid, message)
    }

    /// Create a permission denied error
    pub fn permission_denied(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::PermissionDenied, message)
    }

    /// Create a config invalid error
    pub fn config_invalid(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::ConfigInvalid, message)
    }

    /// Create a request invalid error
    pub fn request_invalid(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::RequestInvalid, message)
    }

    /// Create a rate limited error
    pub fn rate_limited(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::RateLimited, message)
    }

    /// Create an unexpected error
    pub fn unexpected(message: impl Into<String>) -> Self {
        Self::new(ErrorKind::Unexpected, message)
    }
}

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

// Custom Debug implementation for better error display
impl fmt::Debug for Error {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut debug = f.debug_struct("Error");
        debug.field("kind", &self.kind);
        debug.field("message", &self.message);

        if !self.context.is_empty() {
            debug.field("context", &self.context);
        }

        if let Some(source) = &self.source {
            debug.field("source", source);
        }

        debug.field("retryable", &self.retryable);
        debug.finish()
    }
}

impl std::error::Error for Error {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.source.as_ref().map(|e| e.as_ref())
    }
}

impl fmt::Display for ErrorKind {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ErrorKind::CredentialInvalid => write!(f, "invalid credentials"),
            ErrorKind::PermissionDenied => write!(f, "permission denied"),
            ErrorKind::ConfigInvalid => write!(f, "invalid configuration"),
            ErrorKind::RequestInvalid => write!(f, "invalid request"),
            ErrorKind::RateLimited => write!(f, "rate limited"),
            ErrorKind::Unexpected => write!(f, "unexpected error"),
        }
    }
}

/// Convenience type alias for Results
pub type Result<T> = std::result::Result<T, Error>;

// Common From implementations for better ergonomics
impl From<anyhow::Error> for Error {
    fn from(err: anyhow::Error) -> Self {
        Self::unexpected(err.to_string()).with_source(err)
    }
}

impl From<fmt::Error> for Error {
    fn from(err: fmt::Error) -> Self {
        Self::unexpected(err.to_string()).with_source(err)
    }
}

impl From<http::Error> for Error {
    fn from(err: http::Error) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

impl From<http::header::InvalidHeaderValue> for Error {
    fn from(err: http::header::InvalidHeaderValue) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

impl From<http::uri::InvalidUri> for Error {
    fn from(err: http::uri::InvalidUri) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

impl From<http::uri::InvalidUriParts> for Error {
    fn from(err: http::uri::InvalidUriParts) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

impl From<std::string::FromUtf8Error> for Error {
    fn from(err: std::string::FromUtf8Error) -> Self {
        Self::unexpected(err.to_string()).with_source(err)
    }
}

impl From<std::io::Error> for Error {
    fn from(err: std::io::Error) -> Self {
        use std::io::ErrorKind;

        let kind = err.kind();
        let message = err.to_string();
        let source = anyhow::Error::from(err);

        match kind {
            ErrorKind::NotFound => Self::config_invalid(message).with_source(source),
            ErrorKind::PermissionDenied => Self::permission_denied(message).with_source(source),
            _ => Self::unexpected(message)
                .with_source(source)
                .set_retryable(matches!(
                    kind,
                    ErrorKind::TimedOut | ErrorKind::Interrupted | ErrorKind::ConnectionRefused
                )),
        }
    }
}

impl From<http::header::InvalidHeaderName> for Error {
    fn from(err: http::header::InvalidHeaderName) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

impl From<http::header::ToStrError> for Error {
    fn from(err: http::header::ToStrError) -> Self {
        Self::request_invalid(err.to_string()).with_source(err)
    }
}

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

    #[test]
    fn test_error_creation() {
        let err = Error::credential_invalid("token expired");
        assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
        assert!(!err.is_retryable());
    }

    #[test]
    fn test_error_with_context() {
        let err = Error::permission_denied("access denied")
            .with_context("role: arn:aws:iam::123456789012:role/MyRole")
            .with_context("operation: AssumeRole");

        assert_eq!(err.context().len(), 2);
        assert_eq!(
            err.context()[0],
            "role: arn:aws:iam::123456789012:role/MyRole"
        );
        assert_eq!(err.context()[1], "operation: AssumeRole");
    }

    #[test]
    fn test_rate_limited_default_retryable() {
        let err = Error::rate_limited("too many requests");
        assert!(err.is_retryable());
    }

    #[test]
    fn test_override_retryable() {
        let err = Error::unexpected("network timeout").set_retryable(true);
        assert!(err.is_retryable());

        let err = Error::rate_limited("quota exceeded").set_retryable(false);
        assert!(!err.is_retryable());
    }

    #[test]
    fn test_error_debug_format() {
        let err = Error::config_invalid("missing region")
            .with_context("file: ~/.aws/config")
            .with_context("profile: default");

        let debug_str = format!("{:?}", err);
        assert!(debug_str.contains("ConfigInvalid"));
        assert!(debug_str.contains("missing region"));
        assert!(debug_str.contains("~/.aws/config"));
    }
}