apif-source-error 0.1.0

Typed errors for CSV/TSV/NDJSON data sources
Documentation
use std::fmt;

#[derive(Debug)]
pub enum SourceError {
    FileOpenFailed(String, std::io::Error),
    EmptyFile(String),
    DuplicateColumn(String),
    FieldCountMismatch {
        row: usize,
        fields: usize,
        expected: usize,
    },
    InvalidJson(usize, String),
    UnknownFormat(String),
    SourceNotFound(String),
    ColumnNotFound(String, String),
}

impl fmt::Display for SourceError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            SourceError::FileOpenFailed(path, err) => {
                write!(f, "failed to open source file '{path}': {err}")
            }
            SourceError::EmptyFile(fmt) => {
                write!(f, "header required but file is empty: {fmt}")
            }
            SourceError::DuplicateColumn(col) => {
                write!(f, "duplicate column name '{col}' in header")
            }
            SourceError::FieldCountMismatch {
                row,
                fields,
                expected,
            } => {
                write!(f, "row {row} has {fields} fields, expected {expected}")
            }
            SourceError::InvalidJson(line, msg) => {
                write!(f, "invalid JSON on line {line}: {msg}")
            }
            SourceError::UnknownFormat(file) => {
                write!(f, "unknown format for file: {file}")
            }
            SourceError::SourceNotFound(name) => {
                write!(f, "source '{name}' not found")
            }
            SourceError::ColumnNotFound(col, source) => {
                write!(f, "column '{col}' not found in source '{source}'")
            }
        }
    }
}

impl std::error::Error for SourceError {}

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

    #[test]
    fn test_source_error_display() {
        let err = SourceError::FileOpenFailed(
            "test.csv".into(),
            std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
        );
        assert!(err.to_string().contains("test.csv"));

        let err = SourceError::EmptyFile("empty.csv".into());
        assert!(err.to_string().contains("empty.csv"));

        let err = SourceError::DuplicateColumn("id".into());
        assert!(err.to_string().contains("id"));

        let err = SourceError::FieldCountMismatch {
            row: 5,
            fields: 2,
            expected: 3,
        };
        assert!(err.to_string().contains("5"));

        let err = SourceError::InvalidJson(10, "bad json".into());
        assert!(err.to_string().contains("bad json"));

        let err = SourceError::UnknownFormat("file.bin".into());
        assert!(err.to_string().contains("file.bin"));

        let err = SourceError::SourceNotFound("users".into());
        assert!(err.to_string().contains("users"));

        let err = SourceError::ColumnNotFound("name".into(), "users".into());
        assert!(err.to_string().contains("name"));
    }

    #[test]
    fn test_source_error_debug() {
        let err = SourceError::EmptyFile("test.csv".into());
        let s = format!("{:?}", err);
        assert!(!s.is_empty());
    }
}