xls-rs 0.1.3

A powerful CLI tool and library for spreadsheet manipulation with pandas-style operations. Supports CSV, Excel (XLSX, XLS, ODS), Parquet, and Avro formats with formula evaluation, data transformation, and comprehensive analytics capabilities.
Documentation
//! Enhanced error types with context information

use std::fmt;
use thiserror::Error;

/// Error with file and location context
#[derive(Error, Debug)]
pub struct XlsRsError {
    pub kind: ErrorKind,
    pub context: ErrorContext,
}

impl fmt::Display for XlsRsError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.kind)?;
        if let Some(file) = &self.context.file {
            write!(f, " in file '{}'", file)?;
        }
        if let Some(row) = self.context.row {
            write!(f, " at row {}", row + 1)?; // 1-indexed for users
        }
        if let Some(col) = self.context.column {
            write!(f, ", column {}", col + 1)?;
        }
        if let Some(cell) = &self.context.cell_ref {
            write!(f, " (cell {})", cell)?;
        }
        Ok(())
    }
}


/// Error context with location information
#[derive(Debug, Default, Clone)]
pub struct ErrorContext {
    /// File path
    pub file: Option<String>,
    /// Row number (0-indexed)
    pub row: Option<usize>,
    /// Column number (0-indexed)
    pub column: Option<usize>,
    /// Cell reference (e.g., "A1")
    pub cell_ref: Option<String>,
    /// Column name
    pub column_name: Option<String>,
}

impl ErrorContext {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn with_file(mut self, file: &str) -> Self {
        self.file = Some(file.to_string());
        self
    }

    pub fn with_row(mut self, row: usize) -> Self {
        self.row = Some(row);
        self
    }

    pub fn with_column(mut self, col: usize) -> Self {
        self.column = Some(col);
        self
    }

    pub fn with_cell_ref(mut self, cell: &str) -> Self {
        self.cell_ref = Some(cell.to_string());
        self
    }

    pub fn with_column_name(mut self, name: &str) -> Self {
        self.column_name = Some(name.to_string());
        self
    }
}

/// Types of errors
#[derive(Error, Debug)]
pub enum ErrorKind {
    #[error("Column '{0}' not found")]
    ColumnNotFound(String),

    #[error("Invalid cell reference '{0}'")]
    InvalidCellRef(String),

    #[error("Invalid value '{0}' - expected {1}")]
    InvalidValue(String, String),

    #[error("Type conversion failed: cannot convert '{0}' to {1}")]
    TypeConversion(String, String),

    #[error("Division by zero")]
    DivisionByZero,

    #[error("Invalid formula: {0}")]
    InvalidFormula(String),

    #[error("File not found: {0}")]
    FileNotFound(String),

    #[error("Unsupported file format: {0}")]
    UnsupportedFormat(String),

    #[error("Parse error: {0}")]
    ParseError(String),

    #[error("Invalid date format: {0}")]
    InvalidDateFormat(String),

    #[error("Invalid regex pattern: {0}")]
    InvalidRegex(String),

    #[error("IO error: {0}")]
    IoError(String),

    #[error("{0}")]
    Other(String),
}

impl ErrorKind {
    /// Stable error code for programmatic matching across CLI and MCP.
    pub fn code(&self) -> &'static str {
        match self {
            ErrorKind::ColumnNotFound(_) => "column_not_found",
            ErrorKind::InvalidCellRef(_) => "invalid_cell_ref",
            ErrorKind::InvalidValue(_, _) => "invalid_value",
            ErrorKind::TypeConversion(_, _) => "type_conversion",
            ErrorKind::DivisionByZero => "division_by_zero",
            ErrorKind::InvalidFormula(_) => "invalid_formula",
            ErrorKind::FileNotFound(_) => "file_not_found",
            ErrorKind::UnsupportedFormat(_) => "unsupported_format",
            ErrorKind::ParseError(_) => "parse_error",
            ErrorKind::InvalidDateFormat(_) => "invalid_date_format",
            ErrorKind::InvalidRegex(_) => "invalid_regex",
            ErrorKind::IoError(_) => "io_error",
            ErrorKind::Other(_) => "other",
        }
    }
}

impl XlsRsError {
    pub fn column_not_found(name: &str) -> Self {
        Self {
            kind: ErrorKind::ColumnNotFound(name.to_string()),
            context: ErrorContext::new(),
        }
    }

    pub fn invalid_value(value: &str, expected: &str) -> Self {
        Self {
            kind: ErrorKind::InvalidValue(value.to_string(), expected.to_string()),
            context: ErrorContext::new(),
        }
    }

    pub fn type_conversion(value: &str, target_type: &str) -> Self {
        Self {
            kind: ErrorKind::TypeConversion(value.to_string(), target_type.to_string()),
            context: ErrorContext::new(),
        }
    }

    pub fn with_context(mut self, context: ErrorContext) -> Self {
        self.context = context;
        self
    }

    /// Stable error code for programmatic matching across CLI and MCP.
    pub fn code(&self) -> &'static str {
        self.kind.code()
    }
}

/// Result type alias for xls-rs operations
pub type XlsRsResult<T> = Result<T, XlsRsError>;

/// Extension trait for adding context to anyhow errors
pub trait ResultExt<T> {
    fn with_file_context(self, file: &str) -> anyhow::Result<T>;
    fn with_row_context(self, file: &str, row: usize) -> anyhow::Result<T>;
    fn with_cell_context(self, file: &str, row: usize, col: usize) -> anyhow::Result<T>;
}

impl<T, E: std::error::Error + Send + Sync + 'static> ResultExt<T> for Result<T, E> {
    fn with_file_context(self, file: &str) -> anyhow::Result<T> {
        self.map_err(|e| anyhow::anyhow!("{} in file '{}'", e, file))
    }

    fn with_row_context(self, file: &str, row: usize) -> anyhow::Result<T> {
        self.map_err(|e| anyhow::anyhow!("{} in file '{}' at row {}", e, file, row + 1))
    }

    fn with_cell_context(self, file: &str, row: usize, col: usize) -> anyhow::Result<T> {
        self.map_err(|e| {
            anyhow::anyhow!(
                "{} in file '{}' at row {}, column {}",
                e,
                file,
                row + 1,
                col + 1
            )
        })
    }
}