descry-tool-core 0.3.1

Core traits and types for descry-tool framework
Documentation
//! Tool error types
//!
//! Provides structured error handling with thiserror.

use thiserror::Error;

/// Tool execution error
///
/// Uses `#[non_exhaustive]` to allow future expansion without breaking changes.
/// Supports error chaining via `#[source]`.
#[non_exhaustive]
#[derive(Error, Debug)]
pub enum ToolError {
    /// Invalid parameters
    #[error("Invalid parameters: {message}")]
    InvalidParams {
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Tool not found
    #[error("Tool not found: {0}")]
    NotFound(String),

    /// Unauthorized access
    #[error("Unauthorized: {0}")]
    Unauthorized(String),

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

    /// Timeout
    #[error("Timeout after {timeout_ms}ms: {message}")]
    Timeout {
        timeout_ms: u64,
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Internal error
    #[error("Internal error: {message}")]
    Internal {
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Custom error with code
    #[error("[{code}] {message}")]
    Custom {
        code: String,
        message: String,
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

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

    /// IO error
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),
}

impl ToolError {
    /// Create invalid parameters error
    pub fn invalid_params(message: impl Into<String>) -> Self {
        Self::InvalidParams {
            message: message.into(),
            source: None,
        }
    }

    /// Create invalid parameters error with source
    pub fn invalid_params_with_source(
        message: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::InvalidParams {
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }

    /// Create tool not found error
    pub fn not_found(name: impl Into<String>) -> Self {
        Self::NotFound(name.into())
    }

    /// Create unauthorized error
    pub fn unauthorized(message: impl Into<String>) -> Self {
        Self::Unauthorized(message.into())
    }

    /// Create forbidden error
    pub fn forbidden(message: impl Into<String>) -> Self {
        Self::Forbidden(message.into())
    }

    /// Create timeout error
    pub fn timeout(timeout_ms: u64, message: impl Into<String>) -> Self {
        Self::Timeout {
            timeout_ms,
            message: message.into(),
            source: None,
        }
    }

    /// Create timeout error with source
    pub fn timeout_with_source(
        timeout_ms: u64,
        message: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::Timeout {
            timeout_ms,
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }

    /// Create internal error
    pub fn internal(message: impl Into<String>) -> Self {
        Self::Internal {
            message: message.into(),
            source: None,
        }
    }

    /// Create internal error with source
    pub fn internal_with_source(
        message: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::Internal {
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }

    /// Create custom error
    pub fn custom(code: impl Into<String>, message: impl Into<String>) -> Self {
        Self::Custom {
            code: code.into(),
            message: message.into(),
            source: None,
        }
    }

    /// Create custom error with source
    pub fn custom_with_source(
        code: impl Into<String>,
        message: impl Into<String>,
        source: impl std::error::Error + Send + Sync + 'static,
    ) -> Self {
        Self::Custom {
            code: code.into(),
            message: message.into(),
            source: Some(Box::new(source)),
        }
    }

    /// Get error code
    pub fn code(&self) -> &str {
        match self {
            Self::InvalidParams { .. } => "INVALID_PARAMS",
            Self::NotFound(_) => "NOT_FOUND",
            Self::Unauthorized(_) => "UNAUTHORIZED",
            Self::Forbidden(_) => "FORBIDDEN",
            Self::Timeout { .. } => "TIMEOUT",
            Self::Internal { .. } => "INTERNAL_ERROR",
            Self::Custom { code, .. } => code,
            Self::Json(_) => "JSON_ERROR",
            Self::Io(_) => "IO_ERROR",
        }
    }

    /// Check if error has source
    pub fn has_source(&self) -> bool {
        match self {
            Self::InvalidParams { source, .. } => source.is_some(),
            Self::Timeout { source, .. } => source.is_some(),
            Self::Internal { source, .. } => source.is_some(),
            Self::Custom { source, .. } => source.is_some(),
            _ => false,
        }
    }
}

impl From<String> for ToolError {
    fn from(message: String) -> Self {
        Self::internal(message)
    }
}

impl From<&str> for ToolError {
    fn from(message: &str) -> Self {
        Self::internal(message)
    }
}

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

    #[test]
    fn test_error_creation() {
        let err = ToolError::internal("Test error");
        assert_eq!(err.code(), "INTERNAL_ERROR");
    }

    #[test]
    fn test_error_with_source() {
        let source = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let err = ToolError::internal_with_source("Failed to read file", source);
        assert!(err.has_source());
    }

    #[test]
    fn test_predefined_errors() {
        let err = ToolError::invalid_params("Parameter error");
        assert_eq!(err.code(), "INVALID_PARAMS");

        let err = ToolError::not_found("shell");
        assert_eq!(err.code(), "NOT_FOUND");

        let err = ToolError::unauthorized("Not logged in");
        assert_eq!(err.code(), "UNAUTHORIZED");

        let err = ToolError::forbidden("No permission");
        assert_eq!(err.code(), "FORBIDDEN");

        let err = ToolError::timeout(5000, "Operation timed out");
        assert_eq!(err.code(), "TIMEOUT");
    }

    #[test]
    fn test_custom_error() {
        let err = ToolError::custom("E001", "Custom error");
        assert_eq!(err.code(), "E001");
        assert_eq!(err.to_string(), "[E001] Custom error");
    }

    #[test]
    fn test_error_from_string() {
        let err: ToolError = "Error message".into();
        assert_eq!(err.code(), "INTERNAL_ERROR");
    }

    #[test]
    fn test_error_from_json() {
        let json_err = serde_json::from_str::<i32>("invalid");
        assert!(json_err.is_err());

        let tool_err: ToolError = json_err.unwrap_err().into();
        assert_eq!(tool_err.code(), "JSON_ERROR");
    }

    #[test]
    fn test_error_from_io() {
        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
        let tool_err: ToolError = io_err.into();
        assert_eq!(tool_err.code(), "IO_ERROR");
    }

    #[test]
    fn test_error_display() {
        let err = ToolError::invalid_params("Parameter error");
        assert_eq!(format!("{}", err), "Invalid parameters: Parameter error");
    }
}