Skip to main content

clap_noun_verb/
error.rs

1// Copyright (c) 2024 Sean Chatman
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Error types for clap-noun-verb
5
6use thiserror::Error;
7
8/// Errors that can occur in the noun-verb CLI framework
9#[non_exhaustive]
10#[derive(Error, Debug)]
11pub enum NounVerbError {
12    /// Command not found
13    #[error("Command '{noun}' not found{suggestion}")]
14    CommandNotFound { noun: String, suggestion: String },
15
16    /// Verb not found for a given noun
17    #[error("Verb '{verb}' not found for noun '{noun}'{suggestion}")]
18    VerbNotFound { noun: String, verb: String, suggestion: String },
19
20    /// Invalid command structure
21    #[error("Invalid command structure: {message}")]
22    InvalidStructure { message: String },
23
24    /// Command execution error
25    #[error("Command execution failed: {message}")]
26    ExecutionError { message: String },
27
28    /// Argument parsing error
29    #[error("Argument parsing failed: {message}")]
30    ArgumentError { message: String },
31
32    /// Plugin-related error
33    #[error("Plugin error: {0}")]
34    PluginError(String),
35
36    /// Validation failed
37    #[error("Validation failed: {0}")]
38    ValidationFailed(String),
39
40    /// Middleware error
41    #[error("Middleware error: {0}")]
42    MiddlewareError(String),
43
44    /// Telemetry error
45    #[error("Telemetry error: {0}")]
46    TelemetryError(String),
47
48    /// Generic error wrapper
49    #[error("Error: {0}")]
50    Generic(String),
51}
52
53/// Helper function to calculate Levenshtein distance
54fn levenshtein_distance(a: &str, b: &str) -> usize {
55    let a_chars: Vec<char> = a.chars().collect();
56    let b_chars: Vec<char> = b.chars().collect();
57    let a_len = a_chars.len();
58    let b_len = b_chars.len();
59
60    if a_len == 0 {
61        return b_len;
62    }
63    if b_len == 0 {
64        return a_len;
65    }
66
67    let mut cache: Vec<usize> = (1..=b_len).collect();
68    let mut dist = 0;
69
70    for (i, &ca) in a_chars.iter().enumerate() {
71        let mut result = i;
72        dist = i + 1;
73        for (j, &cb) in b_chars.iter().enumerate() {
74            let temp = result;
75            result = cache[j];
76            dist =
77                if ca == cb { temp } else { std::cmp::min(std::cmp::min(result, dist), temp) + 1 };
78            cache[j] = dist;
79        }
80    }
81
82    dist
83}
84
85/// Find best suggestions from candidates based on Levenshtein distance
86pub fn find_best_matches<'a>(input: &str, candidates: &[&'a str]) -> Vec<&'a str> {
87    let mut with_distances: Vec<(&str, usize)> = candidates
88        .iter()
89        .map(|&c| (c, levenshtein_distance(input, c)))
90        .filter(|&(_, dist)| dist <= 3 && dist < input.len())
91        .collect();
92
93    with_distances.sort_by(|a, b| a.1.cmp(&b.1).then_with(|| a.0.cmp(b.0)));
94    with_distances.into_iter().map(|(c, _)| c).collect()
95}
96
97impl NounVerbError {
98    /// Enhance error with recovery suggestions from RDF guard validation
99    ///
100    /// Attempts to provide helpful suggestions using the RDF ontology and SPARQL queries.
101    ///
102    /// FUTURE: v5.1 - Complete RDF recovery suggestions
103    pub fn with_recovery_suggestions(self) -> String {
104        // RDF-control feature deferred to v5.1
105        self.to_string()
106    }
107
108    /// Create a command not found error
109    pub fn command_not_found(noun: impl Into<String>) -> Self {
110        Self::CommandNotFound { noun: noun.into(), suggestion: String::new() }
111    }
112
113    /// Create a command not found error with suggestion candidates
114    pub fn command_not_found_with_candidates(noun: impl Into<String>, candidates: &[&str]) -> Self {
115        let noun_str = noun.into();
116        let matches = find_best_matches(&noun_str, candidates);
117        let suggestion = if matches.is_empty() {
118            String::new()
119        } else {
120            let suggestions_str = matches
121                .iter()
122                .map(|s| format!("\x1b[1m\x1b[33m{}\x1b[0m", s))
123                .collect::<Vec<_>>()
124                .join(", ");
125            format!(". Did you mean: {}?", suggestions_str)
126        };
127        Self::CommandNotFound { noun: noun_str, suggestion }
128    }
129
130    /// Create a verb not found error
131    pub fn verb_not_found(noun: impl Into<String>, verb: impl Into<String>) -> Self {
132        Self::VerbNotFound { noun: noun.into(), verb: verb.into(), suggestion: String::new() }
133    }
134
135    /// Create a verb not found error with suggestion candidates
136    pub fn verb_not_found_with_candidates(
137        noun: impl Into<String>,
138        verb: impl Into<String>,
139        candidates: &[&str],
140    ) -> Self {
141        let verb_str = verb.into();
142        let matches = find_best_matches(&verb_str, candidates);
143        let suggestion = if matches.is_empty() {
144            String::new()
145        } else {
146            let suggestions_str = matches
147                .iter()
148                .map(|s| format!("\x1b[1m\x1b[33m{}\x1b[0m", s))
149                .collect::<Vec<_>>()
150                .join(", ");
151            format!(". Did you mean: {}?", suggestions_str)
152        };
153        Self::VerbNotFound { noun: noun.into(), verb: verb_str, suggestion }
154    }
155
156    /// Create an invalid structure error
157    pub fn invalid_structure(message: impl Into<String>) -> Self {
158        Self::InvalidStructure { message: message.into() }
159    }
160
161    /// Create an execution error
162    pub fn execution_error(message: impl Into<String>) -> Self {
163        Self::ExecutionError { message: message.into() }
164    }
165
166    /// Create an argument error
167    pub fn argument_error(message: impl Into<String>) -> Self {
168        Self::ArgumentError { message: message.into() }
169    }
170
171    /// Create a missing argument error (helper for common case)
172    pub fn missing_argument(name: impl Into<String>) -> Self {
173        Self::ArgumentError { message: format!("Required argument '{}' is missing", name.into()) }
174    }
175
176    /// Create a validation error with constraints
177    pub fn validation_error(
178        name: impl Into<String>,
179        value: impl Into<String>,
180        constraints: Option<&str>,
181    ) -> Self {
182        let name = name.into();
183        let value = value.into();
184        if let Some(constraints) = constraints {
185            Self::ArgumentError {
186                message: format!(
187                    "Invalid value '{}' for argument '{}'. {}",
188                    value, name, constraints
189                ),
190            }
191        } else {
192            Self::ArgumentError {
193                message: format!("Invalid value '{}' for argument '{}'", value, name),
194            }
195        }
196    }
197
198    /// Create a validation error with range constraints
199    pub fn validation_range_error(
200        name: impl Into<String>,
201        value: impl Into<String>,
202        min: Option<&str>,
203        max: Option<&str>,
204    ) -> Self {
205        let name = name.into();
206        let value = value.into();
207        let constraint_msg = match (min, max) {
208            (Some(min), Some(max)) => format!("Must be between {} and {}", min, max),
209            (Some(min), None) => format!("Must be >= {}", min),
210            (None, Some(max)) => format!("Must be <= {}", max),
211            (None, None) => "Invalid value".to_string(),
212        };
213        Self::validation_error(name, value, Some(&constraint_msg))
214    }
215
216    /// Create a validation error with length constraints
217    pub fn validation_length_error(
218        name: impl Into<String>,
219        value: impl Into<String>,
220        min: Option<usize>,
221        max: Option<usize>,
222    ) -> Self {
223        let name = name.into();
224        let value = value.into();
225        let constraint_msg = match (min, max) {
226            (Some(min), Some(max)) => {
227                format!("Length must be between {} and {} characters", min, max)
228            }
229            (Some(min), None) => format!("Length must be at least {} characters", min),
230            (None, Some(max)) => format!("Length must be at most {} characters", max),
231            (None, None) => "Invalid length".to_string(),
232        };
233        Self::validation_error(name, value, Some(&constraint_msg))
234    }
235}
236
237impl From<std::io::Error> for NounVerbError {
238    fn from(err: std::io::Error) -> Self {
239        Self::ExecutionError { message: err.to_string() }
240    }
241}
242
243/// Result type alias for noun-verb operations
244pub type Result<T> = std::result::Result<T, NounVerbError>;
245
246/// MAPE-K Error Kinds
247#[non_exhaustive]
248#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
249pub enum ErrorKind {
250    /// Input arguments or structure were invalid.
251    InvalidInput,
252    /// The operation was not permitted.
253    PermissionDenied,
254    /// A required invariant was violated.
255    InvariantBreach,
256    /// A deadline or timeout budget was exceeded.
257    DeadlineExceeded,
258    /// A resource guard limit was exceeded.
259    GuardExceeded,
260    /// The requested noun/command was not found.
261    CommandNotFound,
262    /// The requested verb was not found for a noun.
263    VerbNotFound,
264    /// Execution of the command failed.
265    ExecutionError,
266    /// An internal framework error occurred.
267    InternalError,
268}
269
270/// Severity level of the error
271#[non_exhaustive]
272#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
273pub enum Severity {
274    /// Non-fatal condition; execution may continue.
275    Warning,
276    /// A recoverable error occurred.
277    Error,
278    /// A severe error requiring immediate attention.
279    Critical,
280}
281
282/// Recovery Action templates proposed by the MAPE-K recovery layer
283#[non_exhaustive]
284#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
285#[serde(untagged)]
286pub enum ActionTemplate {
287    /// Suggests increasing the timeout/deadline budget.
288    TimeoutAdjustment {
289        /// Recommended new timeout in milliseconds.
290        suggested_timeout_ms: u64,
291        /// Human-readable rationale for the adjustment.
292        reason: String,
293    },
294    /// Suggests a corrected command to run.
295    CommandFix {
296        /// The corrected command string to use.
297        suggested_command: String,
298        /// Human-readable rationale for the correction.
299        reason: String,
300    },
301}
302
303/// Machine-readable, uniform structured error format for autonomic MAPE-K loops
304#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
305pub struct StructuredError {
306    /// Classification of the error.
307    pub kind: ErrorKind,
308    /// Severity level of the error.
309    pub severity: Severity,
310    /// Human-readable error message.
311    pub message: String,
312    /// Additional structured details keyed by field name.
313    pub details: std::collections::HashMap<String, serde_json::Value>,
314    /// Recovery actions proposed for this error.
315    pub action_templates: Vec<ActionTemplate>,
316}
317
318impl StructuredError {
319    /// Create a deadline exceeded error format with actual and target latency details
320    pub fn deadline_exceeded(deadline_ms: u64, actual_ms: u64) -> Self {
321        let mut details = std::collections::HashMap::new();
322        details.insert("deadline_ms".to_string(), serde_json::json!(deadline_ms));
323        details.insert("actual_ms".to_string(), serde_json::json!(actual_ms));
324
325        Self {
326            kind: ErrorKind::DeadlineExceeded,
327            severity: Severity::Critical,
328            message: format!("Deadline {}ms exceeded, took {}ms", deadline_ms, actual_ms),
329            details,
330            action_templates: vec![ActionTemplate::TimeoutAdjustment {
331                suggested_timeout_ms: actual_ms + 100,
332                reason: "Increase deadline budget to match observed latency".to_string(),
333            }],
334        }
335    }
336
337    /// Map a standard NounVerbError to StructuredError format
338    pub fn from_error(err: &NounVerbError) -> Self {
339        let mut details = std::collections::HashMap::new();
340        let mut action_templates = Vec::new();
341        let mut severity = Severity::Error;
342
343        let kind = match err {
344            NounVerbError::CommandNotFound { noun, suggestion } => {
345                details.insert("noun".to_string(), serde_json::Value::String(noun.clone()));
346                if !suggestion.is_empty() {
347                    details.insert(
348                        "suggestion".to_string(),
349                        serde_json::Value::String(suggestion.clone()),
350                    );
351                    let clean = suggestion
352                        .replace("\x1b[1m\x1b[33m", "")
353                        .replace("\x1b[0m", "")
354                        .replace(". Did you mean: ", "")
355                        .replace("?", "");
356                    let candidates: Vec<&str> = clean.split(", ").collect();
357                    if let Some(first) = candidates.first() {
358                        if !first.is_empty() {
359                            action_templates.push(ActionTemplate::CommandFix {
360                                suggested_command: first.to_string(),
361                                reason: format!(
362                                    "Suggested correction for misspelled command '{}'",
363                                    noun
364                                ),
365                            });
366                        }
367                    }
368                }
369                ErrorKind::CommandNotFound
370            }
371            NounVerbError::VerbNotFound { noun, verb, suggestion } => {
372                details.insert("noun".to_string(), serde_json::Value::String(noun.clone()));
373                details.insert("verb".to_string(), serde_json::Value::String(verb.clone()));
374                if !suggestion.is_empty() {
375                    details.insert(
376                        "suggestion".to_string(),
377                        serde_json::Value::String(suggestion.clone()),
378                    );
379                    let clean = suggestion
380                        .replace("\x1b[1m\x1b[33m", "")
381                        .replace("\x1b[0m", "")
382                        .replace(". Did you mean: ", "")
383                        .replace("?", "");
384                    let candidates: Vec<&str> = clean.split(", ").collect();
385                    if let Some(first) = candidates.first() {
386                        if !first.is_empty() {
387                            action_templates.push(ActionTemplate::CommandFix {
388                                suggested_command: format!("{} {}", noun, first),
389                                reason: format!(
390                                    "Suggested correction for misspelled verb '{}'",
391                                    verb
392                                ),
393                            });
394                        }
395                    }
396                }
397                ErrorKind::VerbNotFound
398            }
399            NounVerbError::InvalidStructure { message } => {
400                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
401                ErrorKind::InvalidInput
402            }
403            NounVerbError::ExecutionError { message } => {
404                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
405                if message.to_lowercase().contains("deadline")
406                    || message.to_lowercase().contains("timeout")
407                    || message.to_lowercase().contains("budget exceeded")
408                {
409                    severity = Severity::Critical;
410                    action_templates.push(ActionTemplate::TimeoutAdjustment {
411                        suggested_timeout_ms: 1000,
412                        reason: "Increase deadline budget due to execution timeout".to_string(),
413                    });
414                    ErrorKind::DeadlineExceeded
415                } else {
416                    ErrorKind::ExecutionError
417                }
418            }
419            NounVerbError::ArgumentError { message } => {
420                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
421                ErrorKind::InvalidInput
422            }
423            NounVerbError::PluginError(message) => {
424                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
425                ErrorKind::InternalError
426            }
427            NounVerbError::ValidationFailed(message) => {
428                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
429                ErrorKind::InvariantBreach
430            }
431            NounVerbError::MiddlewareError(message) => {
432                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
433                ErrorKind::InternalError
434            }
435            NounVerbError::TelemetryError(message) => {
436                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
437                ErrorKind::InternalError
438            }
439            NounVerbError::Generic(message) => {
440                details.insert("message".to_string(), serde_json::Value::String(message.clone()));
441                ErrorKind::InternalError
442            }
443        };
444
445        StructuredError { kind, severity, message: err.to_string(), details, action_templates }
446    }
447}