transfer_family_cli 0.1.0

TUI to browse and transfer files via AWS Transfer Family connector
Documentation
//! Error types for the Transfer Family Connector TUI CLI.
//!
//! Errors are designed for two audiences:
//! - **Machines**: Flat, actionable kinds and explicit retryability (`ErrorKind`, `ErrorStatus`).
//! - **Humans**: Rich context at failure points (operation, path, id) via the context list.
//!
//! Convention: when constructing or propagating an error, attach context (e.g. `.with("remote_path", path)`)
//! so logs and UI can say "listing /foo/ failed" or "get file.txt (`transfer_id` x) timed out".

use std::error::Error as StdError;
use std::fmt;
use std::sync::Arc;

pub type Result<T> = std::result::Result<T, Error>;

/// What the caller can do about the error (action-oriented, not origin-oriented).
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorKind {
    NotFound,
    Timeout,
    InvalidInput,
    PermissionDenied,
    Io,
    Api,
    Parse,
}

/// Whether the operation is safe to retry.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ErrorStatus {
    Permanent,
    Temporary,
}

/// Single error type with kind, status, message, context, and optional source.
#[non_exhaustive]
#[derive(Clone, Debug)]
pub struct Error {
    pub kind: ErrorKind,
    pub status: ErrorStatus,
    pub message: String,
    pub context: Vec<(&'static str, String)>,
    pub source: Option<Arc<dyn StdError + Send + Sync>>,
}

impl Error {
    /// Builder: add a context key-value pair. Consumes self and returns self for chaining.
    pub fn with(mut self, key: &'static str, value: impl Into<String>) -> Self {
        self.context.push((key, value.into()));
        self
    }

    /// User-facing display: message plus context (e.g. "timeout (path: /foo/, `listing_id`: xyz)").
    #[must_use]
    pub fn display_for_user(&self) -> String {
        if self.context.is_empty() {
            return self.message.clone();
        }
        let ctx: String = self
            .context
            .iter()
            .map(|(k, v)| format!("{k}: {v}"))
            .collect::<Vec<_>>()
            .join(", ");
        format!("{} ({})", self.message, ctx)
    }

    /// Whether this error is safe to retry.
    #[must_use]
    pub fn is_retryable(&self) -> bool {
        self.status == ErrorStatus::Temporary
    }

    // --- Constructors (kind + status) ---

    pub fn not_found(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::NotFound,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn timeout(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Timeout,
            status: ErrorStatus::Temporary,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn invalid_input(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::InvalidInput,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn permission_denied(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::PermissionDenied,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn io(message: impl Into<String>, source: impl StdError + Send + Sync + 'static) -> Self {
        Self {
            kind: ErrorKind::Io,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: Some(Arc::new(source)),
        }
    }

    pub fn api(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Api,
            status: ErrorStatus::Temporary,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn api_permanent(message: impl Into<String>) -> Self {
        Self {
            kind: ErrorKind::Api,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: None,
        }
    }

    pub fn parse(
        message: impl Into<String>,
        source: impl StdError + Send + Sync + 'static,
    ) -> Self {
        Self {
            kind: ErrorKind::Parse,
            status: ErrorStatus::Permanent,
            message: message.into(),
            context: Vec::new(),
            source: Some(Arc::new(source)),
        }
    }
}

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

impl StdError for Error {
    fn source(&self) -> Option<&(dyn StdError + 'static)> {
        None
    }
}

impl From<serde_json::Error> for Error {
    fn from(e: serde_json::Error) -> Self {
        Error::parse("parse error", e)
    }
}

impl From<std::io::Error> for Error {
    fn from(e: std::io::Error) -> Self {
        Error::io("IO error", e)
    }
}