Skip to main content

dkit_core/
error.rs

1/// 지원하는 포맷 목록 (에러 메시지용)
2pub const SUPPORTED_FORMATS: &[&str] = &[
3    "json", "jsonl", "csv", "tsv", "yaml", "yml", "toml", "env", "xml", "msgpack", "xlsx",
4    "sqlite", "parquet", "hcl", "tf", "md", "html", "table",
5];
6
7/// Compute Levenshtein edit distance between two strings.
8fn levenshtein(a: &str, b: &str) -> usize {
9    let a: Vec<char> = a.chars().collect();
10    let b: Vec<char> = b.chars().collect();
11    let m = a.len();
12    let n = b.len();
13    let mut row: Vec<usize> = (0..=n).collect();
14    for i in 1..=m {
15        let mut prev = row[0];
16        row[0] = i;
17        for j in 1..=n {
18            let temp = row[j];
19            row[j] = if a[i - 1] == b[j - 1] {
20                prev
21            } else {
22                1 + prev.min(row[j]).min(row[j - 1])
23            };
24            prev = temp;
25        }
26    }
27    row[n]
28}
29
30/// Return the closest supported format name for "Did you mean?" hints.
31/// Returns `None` if no candidate is close enough (distance > 3).
32pub fn suggest_format(input: &str) -> Option<&'static str> {
33    let input_lower = input.to_lowercase();
34    SUPPORTED_FORMATS
35        .iter()
36        .copied()
37        .filter(|&f| levenshtein(&input_lower, f) <= 3)
38        .min_by_key(|&f| levenshtein(&input_lower, f))
39}
40
41/// Error types for dkit operations.
42///
43/// Covers format parsing, writing, IO, query evaluation, and path navigation.
44/// Uses `thiserror` for automatic `Display` and `Error` implementations.
45#[derive(Debug, thiserror::Error)]
46#[non_exhaustive]
47pub enum DkitError {
48    #[error("Unknown format: '{0}'\n  Supported formats: {}", SUPPORTED_FORMATS.join(", "))]
49    UnknownFormat(String),
50
51    #[error("Failed to parse {format}: {source}\n  Hint: check that the input is valid {format}")]
52    ParseError {
53        format: String,
54        #[source]
55        source: Box<dyn std::error::Error + Send + Sync>,
56    },
57
58    /// Parse error with source location info for rich display (line/column + snippet).
59    #[error("Failed to parse {format} at line {line}, column {column}: {source}")]
60    ParseErrorAt {
61        format: String,
62        #[source]
63        source: Box<dyn std::error::Error + Send + Sync>,
64        /// 1-indexed line number
65        line: usize,
66        /// 1-indexed column number
67        column: usize,
68        /// The text of the line where the error occurred
69        line_text: String,
70    },
71
72    #[error("Failed to write {format}: {source}")]
73    WriteError {
74        format: String,
75        #[source]
76        source: Box<dyn std::error::Error + Send + Sync>,
77    },
78
79    #[error("Failed to detect format: {0}\n  Hint: specify the input format explicitly, e.g. --from json\n  Supported formats: {}", SUPPORTED_FORMATS.join(", "))]
80    FormatDetectionFailed(String),
81
82    #[allow(dead_code)]
83    #[error("Invalid query: {0}\n  Hint: use 'dkit query --help' for query syntax")]
84    QueryError(String),
85
86    #[error("IO error: {0}")]
87    IoError(#[from] std::io::Error),
88
89    #[allow(dead_code)]
90    #[error("Path not found: {0}")]
91    PathNotFound(String),
92}
93
94/// `anyhow` 통합을 위한 `Result` 타입 별칭.
95/// 라이브러리 내부에서는 `DkitError`를 직접 사용하고,
96/// 애플리케이션 레벨에서는 `anyhow::Result`로 변환하여 사용한다.
97#[allow(dead_code)]
98pub type Result<T> = std::result::Result<T, DkitError>;
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[test]
105    fn test_unknown_format_display() {
106        let err = DkitError::UnknownFormat("bin".to_string());
107        let msg = err.to_string();
108        assert!(msg.contains("Unknown format: 'bin'"));
109        assert!(msg.contains("Supported formats:"));
110        assert!(msg.contains("json"));
111        assert!(msg.contains("csv"));
112        assert!(msg.contains("toml"));
113    }
114
115    #[test]
116    fn test_parse_error_display() {
117        let source: Box<dyn std::error::Error + Send + Sync> =
118            "unexpected token".to_string().into();
119        let err = DkitError::ParseError {
120            format: "JSON".to_string(),
121            source,
122        };
123        let msg = err.to_string();
124        assert!(msg.contains("Failed to parse JSON: unexpected token"));
125        assert!(msg.contains("Hint:"));
126    }
127
128    #[test]
129    fn test_write_error_display() {
130        let source: Box<dyn std::error::Error + Send + Sync> =
131            "serialization failed".to_string().into();
132        let err = DkitError::WriteError {
133            format: "TOML".to_string(),
134            source,
135        };
136        assert_eq!(
137            err.to_string(),
138            "Failed to write TOML: serialization failed"
139        );
140    }
141
142    #[test]
143    fn test_query_error_display() {
144        let err = DkitError::QueryError("invalid syntax at position 5".to_string());
145        let msg = err.to_string();
146        assert!(msg.contains("Invalid query: invalid syntax at position 5"));
147        assert!(msg.contains("Hint:"));
148    }
149
150    #[test]
151    fn test_io_error_from() {
152        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
153        let err: DkitError = io_err.into();
154        assert!(matches!(err, DkitError::IoError(_)));
155        assert!(err.to_string().contains("file not found"));
156    }
157
158    #[test]
159    fn test_path_not_found_display() {
160        let err = DkitError::PathNotFound(".users[0].name".to_string());
161        assert_eq!(err.to_string(), "Path not found: .users[0].name");
162    }
163
164    #[test]
165    fn test_anyhow_conversion() {
166        // DkitError는 anyhow::Error로 변환 가능해야 한다
167        let err = DkitError::UnknownFormat("bin".to_string());
168        let anyhow_err: anyhow::Error = err.into();
169        assert!(anyhow_err.to_string().contains("Unknown format: 'bin'"));
170    }
171
172    #[test]
173    fn test_result_type_alias() {
174        let ok: Result<i32> = Ok(42);
175        assert_eq!(ok.unwrap(), 42);
176
177        let err: Result<i32> = Err(DkitError::UnknownFormat("x".to_string()));
178        assert!(err.is_err());
179    }
180}