Skip to main content

excel_cli/cli/
error.rs

1use serde_json::{json, Value};
2use std::fmt;
3
4/// Exit codes for the v1.0.0 CLI contract.
5pub const EXIT_SUCCESS: i32 = 0;
6pub const EXIT_CHECK_FINDINGS: i32 = 1;
7pub const EXIT_INVALID_ARGS: i32 = 2;
8pub const EXIT_FILE_ERROR: i32 = 3;
9pub const EXIT_PARSE_ERROR: i32 = 4;
10pub const EXIT_TARGET_NOT_FOUND: i32 = 5;
11pub const EXIT_INVALID_QUERY: i32 = 6;
12pub const EXIT_INTERNAL_ERROR: i32 = 7;
13
14/// Application errors with stable codes and exit code mapping.
15#[derive(Debug, Clone)]
16pub enum AppError {
17    InvalidArgs { message: String },
18    FileError { message: String },
19    ParseError { message: String },
20    TargetNotFound { message: String },
21    InvalidQuery { message: String },
22    InternalError { message: String },
23}
24
25impl AppError {
26    pub fn code(&self) -> &'static str {
27        match self {
28            AppError::InvalidArgs { .. } => "invalid_args",
29            AppError::FileError { .. } => "file_error",
30            AppError::ParseError { .. } => "parse_error",
31            AppError::TargetNotFound { .. } => "target_not_found",
32            AppError::InvalidQuery { .. } => "invalid_query",
33            AppError::InternalError { .. } => "internal_error",
34        }
35    }
36
37    pub fn exit_code(&self) -> i32 {
38        match self {
39            AppError::InvalidArgs { .. } => EXIT_INVALID_ARGS,
40            AppError::FileError { .. } => EXIT_FILE_ERROR,
41            AppError::ParseError { .. } => EXIT_PARSE_ERROR,
42            AppError::TargetNotFound { .. } => EXIT_TARGET_NOT_FOUND,
43            AppError::InvalidQuery { .. } => EXIT_INVALID_QUERY,
44            AppError::InternalError { .. } => EXIT_INTERNAL_ERROR,
45        }
46    }
47
48    pub fn message(&self) -> String {
49        match self {
50            AppError::InvalidArgs { message } => message.clone(),
51            AppError::FileError { message } => message.clone(),
52            AppError::ParseError { message } => message.clone(),
53            AppError::TargetNotFound { message } => message.clone(),
54            AppError::InvalidQuery { message } => message.clone(),
55            AppError::InternalError { message } => message.clone(),
56        }
57    }
58
59    /// Build the standard error envelope as JSON.
60    pub fn to_envelope(&self, command: &str, file_path: &str, file_format: &str) -> Value {
61        json!({
62            "schema_version": "1.0",
63            "command": command,
64            "file": {
65                "path": file_path,
66                "format": file_format,
67            },
68            "error": {
69                "code": self.code(),
70                "message": self.message(),
71                "details": {},
72            },
73        })
74    }
75}
76
77impl fmt::Display for AppError {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.message())
80    }
81}
82
83impl std::error::Error for AppError {}
84
85/// Convert anyhow::Error to AppError by inspecting the message.
86pub fn anyhow_to_app_error(err: anyhow::Error) -> AppError {
87    let msg = err.to_string();
88    let lower = msg.to_lowercase();
89
90    if err
91        .chain()
92        .any(|cause| cause.downcast_ref::<std::io::Error>().is_some())
93    {
94        AppError::FileError { message: msg }
95    } else if lower.contains("unable to parse excel file")
96        || lower.contains("parser panic: malformed workbook data")
97        || lower.contains("no worksheets found")
98    {
99        AppError::ParseError { message: msg }
100    } else if lower.contains("unable to read worksheet")
101        || lower.contains("cannot load sheet")
102        || (lower.contains("sheet") && lower.contains("not found"))
103    {
104        AppError::TargetNotFound { message: msg }
105    } else if lower.contains("invalid")
106        || lower.contains("mutually exclusive")
107        || lower.contains("expected")
108    {
109        AppError::InvalidArgs { message: msg }
110    } else {
111        AppError::InternalError { message: msg }
112    }
113}