1pub 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
7fn 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
30pub 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#[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 #[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 line: usize,
66 column: usize,
68 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#[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 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}