1use std::path::PathBuf;
6use thiserror::Error;
7
8#[derive(Debug, Error)]
13pub enum DynamicCliError {
14 #[error(transparent)]
16 Config(#[from] ConfigError),
17
18 #[error(transparent)]
20 Parse(#[from] ParseError),
21
22 #[error(transparent)]
24 Validation(#[from] ValidationError),
25
26 #[error(transparent)]
28 Execution(#[from] ExecutionError),
29
30 #[error(transparent)]
32 Registry(#[from] RegistryError),
33
34 #[error("I/O error: {0}")]
36 Io(#[from] std::io::Error),
37}
38
39#[derive(Debug, Error)]
48pub enum ConfigError {
49 #[error("Configuration file not found: {path:?}")]
62 FileNotFound { path: PathBuf },
63
64 #[error("Unsupported file format: '{extension}'. Supported: .yaml, .yml, .json")]
68 UnsupportedFormat { extension: String },
69
70 #[error("Failed to parse YAML configuration at line {line:?}, column {column:?}: {source}")]
72 YamlParse {
73 #[source]
74 source: serde_yaml::Error,
75 line: Option<usize>,
77 column: Option<usize>,
78 },
79
80 #[error("Failed to parse JSON configuration at line {line}, column {column}: {source}")]
82 JsonParse {
83 #[source]
84 source: serde_json::Error,
85 line: usize,
87 column: usize,
88 },
89
90 #[error("Invalid configuration schema: {reason} (at {path:?})")]
94 InvalidSchema {
95 reason: String,
96 path: Option<String>,
98 },
99
100 #[error("Duplicate command name or alias: '{name}'")]
102 DuplicateCommand { name: String },
103
104 #[error("Unknown argument type: '{type_name}' in {context}")]
106 UnknownType { type_name: String, context: String },
107
108 #[error("Configuration inconsistency: {details}")]
112 Inconsistency { details: String },
113}
114
115#[derive(Debug, Error)]
124pub enum ParseError {
125 #[error("Unknown command: '{command}'. Type 'help' for available commands.")]
130 UnknownCommand {
131 command: String,
132 suggestions: Vec<String>,
134 },
135
136 #[error("Missing required argument: {argument} for command '{command}'")]
138 MissingArgument { argument: String, command: String },
139
140 #[error("Missing required option: --{option} for command '{command}'")]
142 MissingOption { option: String, command: String },
143
144 #[error("Too many arguments for command '{command}'. Expected {expected}, got {got}")]
146 TooManyArguments {
147 command: String,
148 expected: usize,
149 got: usize,
150 },
151
152 #[error("Unknown option: {flag} for command '{command}'")]
156 UnknownOption {
157 flag: String,
158 command: String,
159 suggestions: Vec<String>,
161 },
162
163 #[error("Failed to parse {arg_name} as {expected_type}: '{value}'{}",
168 .details.as_ref().map(|d| format!(" ({})", d)).unwrap_or_default())]
169 TypeParseError {
170 arg_name: String,
171 expected_type: String,
172 value: String,
173 details: Option<String>,
175 },
176
177 #[error("Invalid value for {arg_name}: '{value}'. Must be one of: {}",
179 .choices.join(", "))]
180 InvalidChoice {
181 arg_name: String,
182 value: String,
183 choices: Vec<String>,
184 },
185
186 #[error("Invalid command syntax: {details}{}",
188 .hint.as_ref().map(|h| format!("\nHint: {}", h)).unwrap_or_default())]
189 InvalidSyntax {
190 details: String,
191 hint: Option<String>,
193 },
194}
195
196#[derive(Debug, Error)]
205pub enum ValidationError {
206 #[error("File not found for argument '{arg_name}': {path:?}")]
208 FileNotFound { path: PathBuf, arg_name: String },
209
210 #[error("Invalid file extension for {arg_name}: {path:?}. Expected: {}",
212 .expected.join(", "))]
213 InvalidExtension {
214 arg_name: String,
215 path: PathBuf,
216 expected: Vec<String>,
217 },
218
219 #[error("{arg_name} must be between {min} and {max}, got {value}")]
221 OutOfRange {
222 arg_name: String,
223 value: f64,
224 min: f64,
225 max: f64,
226 },
227
228 #[error("Validation failed for {arg_name}: {reason}")]
230 CustomConstraint { arg_name: String, reason: String },
231
232 #[error("{arg_name} requires {required_arg} to be specified")]
236 MissingDependency {
237 arg_name: String,
238 required_arg: String,
239 },
240
241 #[error("Options {arg1} and {arg2} cannot be used together")]
245 MutuallyExclusive { arg1: String, arg2: String },
246}
247
248#[derive(Debug, Error)]
256pub enum ExecutionError {
257 #[error("No handler registered for command '{command}' (implementation: '{implementation}')")]
262 HandlerNotFound {
263 command: String,
264 implementation: String,
265 },
266
267 #[error("Failed to downcast execution context to expected type: {expected_type}")]
271 ContextDowncastFailed { expected_type: String },
272
273 #[error("Invalid context state: {reason}")]
275 InvalidContextState { reason: String },
276
277 #[error("Command execution failed: {0}")]
281 CommandFailed(#[source] anyhow::Error),
282
283 #[error("Command interrupted by user")]
287 Interrupted,
288}
289
290#[derive(Debug, Error)]
299pub enum RegistryError {
300 #[error("Command '{name}' is already registered")]
302 DuplicateRegistration { name: String },
303
304 #[error("Alias '{alias}' is already used by command '{existing_command}'")]
306 DuplicateAlias {
307 alias: String,
308 existing_command: String,
309 },
310
311 #[error("No handler provided for command '{command}'")]
316 MissingHandler { command: String },
317}
318
319impl ParseError {
324 pub fn unknown_command_with_suggestions(command: &str, available: &[String]) -> Self {
342 let suggestions = crate::error::find_similar_strings(command, available, 3);
343 Self::UnknownCommand {
344 command: command.to_string(),
345 suggestions,
346 }
347 }
348
349 pub fn unknown_option_with_suggestions(
351 flag: &str,
352 command: &str,
353 available: &[String],
354 ) -> Self {
355 let suggestions = crate::error::find_similar_strings(flag, available, 2);
356 Self::UnknownOption {
357 flag: flag.to_string(),
358 command: command.to_string(),
359 suggestions,
360 }
361 }
362}
363
364impl ConfigError {
365 pub fn yaml_parse_with_location(source: serde_yaml::Error) -> Self {
369 let location = source.location();
370 Self::YamlParse {
371 source,
372 line: location.as_ref().map(|l| l.line()),
373 column: location.map(|l| l.column()),
374 }
375 }
376
377 pub fn json_parse_with_location(source: serde_json::Error) -> Self {
381 Self::JsonParse {
382 line: source.line(),
383 column: source.column(),
384 source,
385 }
386 }
387}
388
389#[cfg(test)]
390mod tests {
391 use super::*;
392
393 #[test]
394 fn test_config_error_display() {
395 let error = ConfigError::FileNotFound {
396 path: PathBuf::from("/path/to/config.yaml"),
397 };
398 let message = format!("{}", error);
399 assert!(message.contains("not found"));
400 assert!(message.contains("config.yaml"));
401 }
402
403 #[test]
404 fn test_parse_error_with_suggestions() {
405 let available = vec!["simulate".to_string(), "validate".to_string()];
406 let error = ParseError::unknown_command_with_suggestions("simulat", &available);
407
408 match error {
409 ParseError::UnknownCommand {
410 command,
411 suggestions,
412 } => {
413 assert_eq!(command, "simulat");
414 assert!(!suggestions.is_empty());
415 assert!(suggestions.contains(&"simulate".to_string()));
416 }
417 _ => panic!("Wrong error type"),
418 }
419 }
420
421 #[test]
422 fn test_validation_error_display() {
423 let error = ValidationError::OutOfRange {
424 arg_name: "percentage".to_string(),
425 value: 150.0,
426 min: 0.0,
427 max: 100.0,
428 };
429 let message = format!("{}", error);
430 assert!(message.contains("percentage"));
431 assert!(message.contains("150"));
432 assert!(message.contains("0"));
433 assert!(message.contains("100"));
434 }
435}