apollo-errors 0.7.0

Structured error handling with automatic format conversion
Documentation
//! Tests for #[from] attribute support

use apollo_errors::{Error, FormatConfig};
use miette::Diagnostic;

// ============================================================================
// Source error types (must implement Diagnostic for #[source] to work)
// ============================================================================

/// A simple source error type
#[derive(Debug, Error, Diagnostic, Clone)]
#[error("IO error: {message}")]
#[diagnostic(code(source::io))]
pub struct IoError {
    #[extension]
    pub message: String,
}

/// Another source error type for testing multiple From impls
#[derive(Debug, Error, Diagnostic, Clone)]
#[error("Parse error: {details}")]
#[diagnostic(code(source::parse))]
pub struct ParseError {
    #[extension]
    pub details: String,
}

// ============================================================================
// Enum error with #[from] variants
// ============================================================================

#[derive(Debug, Error, Diagnostic)]
#[allow(dead_code)]
pub enum ErrorWithFrom {
    #[error("IO operation failed")]
    #[diagnostic(code(from::io_failed))]
    Io {
        #[from]
        source: IoError,
    },

    #[error(transparent)]
    #[diagnostic(transparent)]
    Parse(#[from] ParseError),

    #[error("Generic error: {message}")]
    #[diagnostic(code(from::generic_error))]
    Generic {
        #[extension]
        message: String,
    },
}

// ============================================================================
// From impl tests
// ============================================================================

#[test]
fn test_from_impl_for_field() {
    let io_error = IoError {
        message: "file not found".to_string(),
    };

    let error: ErrorWithFrom = io_error.into();

    // Check it's the right variant
    match &error {
        ErrorWithFrom::Io { source } => {
            assert_eq!(source.message, "file not found");
        }
        _ => panic!("Expected Io variant"),
    }
}

#[test]
fn test_from_impl_for_transparent_variant() {
    let parse_error = ParseError {
        details: "unexpected token".to_string(),
    };

    let error: ErrorWithFrom = parse_error.into();

    // Check it's the right variant
    match &error {
        ErrorWithFrom::Parse(source) => {
            assert_eq!(source.details, "unexpected token");
        }
        _ => panic!("Expected Parse variant"),
    }
}

#[test]
fn test_question_mark_operator_works() {
    fn may_fail() -> Result<(), IoError> {
        Err(IoError {
            message: "disk full".to_string(),
        })
    }

    fn wrapper() -> Result<(), ErrorWithFrom> {
        may_fail()?;
        Ok(())
    }

    let result = wrapper();
    assert!(result.is_err());

    match result.unwrap_err() {
        ErrorWithFrom::Io { source } => {
            assert_eq!(source.message, "disk full");
        }
        _ => panic!("Expected Io variant"),
    }
}

// ============================================================================
// #[from] implies #[source] tests
// ============================================================================

#[test]
fn test_from_implies_source() {
    let io_error = IoError {
        message: "permission denied".to_string(),
    };
    let error: ErrorWithFrom = io_error.into();

    // Error::source() should return the inner error
    let source = std::error::Error::source(&error);
    assert!(source.is_some());
    assert_eq!(source.unwrap().to_string(), "IO error: permission denied");
}

// ============================================================================
// Format output tests
// ============================================================================

#[test]
fn test_from_variant_display() {
    let error: ErrorWithFrom = IoError {
        message: "read failed".to_string(),
    }
    .into();

    assert_eq!(error.to_string(), "IO operation failed");
}

#[test]
fn test_from_variant_json() {
    use apollo_errors::Error as ErrorTrait;

    let error: ErrorWithFrom = IoError {
        message: "write failed".to_string(),
    }
    .into();

    let json = error.to_json(FormatConfig::default()).unwrap();
    assert_eq!(json["error"], "from::io_failed");
    assert_eq!(json["message"], "IO operation failed");
}

// ============================================================================
// Struct error with #[from]
// ============================================================================

/// Struct error that wraps an IoError via #[from]
#[derive(Debug, Error, Diagnostic)]
#[error("Wrapped IO error")]
#[diagnostic(code(wrapper::io))]
pub struct WrappedIoError {
    #[from]
    source: IoError,
}

// ============================================================================
// Struct #[from] tests
// ============================================================================

#[test]
fn test_struct_from_impl() {
    let io_error = IoError {
        message: "connection reset".to_string(),
    };

    let error: WrappedIoError = io_error.into();
    assert_eq!(error.source.message, "connection reset");
}

#[test]
fn test_struct_from_question_mark() {
    fn may_fail() -> Result<(), IoError> {
        Err(IoError {
            message: "timeout".to_string(),
        })
    }

    fn wrapper() -> Result<(), WrappedIoError> {
        may_fail()?;
        Ok(())
    }

    let result = wrapper();
    assert!(result.is_err());
    assert_eq!(result.unwrap_err().source.message, "timeout");
}

#[test]
fn test_struct_from_implies_source() {
    let error: WrappedIoError = IoError {
        message: "broken pipe".to_string(),
    }
    .into();

    let source = std::error::Error::source(&error);
    assert!(source.is_some());
    assert_eq!(source.unwrap().to_string(), "IO error: broken pipe");
}

#[test]
fn test_struct_from_display() {
    let error: WrappedIoError = IoError {
        message: "any".to_string(),
    }
    .into();

    assert_eq!(error.to_string(), "Wrapped IO error");
}

#[test]
fn test_struct_from_json() {
    use apollo_errors::Error as ErrorTrait;

    let error: WrappedIoError = IoError {
        message: "any".to_string(),
    }
    .into();

    let json = error.to_json(FormatConfig::default()).unwrap();
    assert_eq!(json["error"], "wrapper::io");
    assert_eq!(json["message"], "Wrapped IO error");
}