Skip to main content

auths_cli/commands/
error_lookup.rs

1use clap::{Parser, Subcommand};
2
3use crate::config::CliConfig;
4use crate::errors::registry;
5
6/// Look up error codes and their explanations.
7///
8/// Usage:
9/// ```ignore
10/// auths error show AUTHS-E3001
11/// auths error list
12/// ```
13#[derive(Parser, Debug, Clone)]
14#[command(
15    about = "Look up an error code or list all known codes",
16    after_help = "Examples:
17  auths error list          # List all known error codes
18  auths error show AUTHS-E3001  # Show details for a specific code
19  auths error AUTHS-E3001   # Short form (same as show)
20
21Related:
22  auths doctor  — Run health checks to diagnose issues
23  auths status  — Check your identity and device status"
24)]
25pub struct ErrorLookupCommand {
26    #[command(subcommand)]
27    pub subcommand: Option<ErrorSubcommand>,
28
29    /// The error code to look up (e.g. AUTHS-E3001). Deprecated: use `auths error show CODE`.
30    pub code: Option<String>,
31
32    /// List all known error codes. Deprecated: use `auths error list`.
33    #[arg(long)]
34    pub list: bool,
35}
36
37#[derive(Subcommand, Debug, Clone)]
38pub enum ErrorSubcommand {
39    /// List all known error codes
40    List,
41    /// Show explanation for an error code
42    Show { code: String },
43}
44
45impl ErrorLookupCommand {
46    pub fn execute(&self, ctx: &CliConfig) -> anyhow::Result<()> {
47        // Handle new subcommand path
48        if let Some(ref subcommand) = self.subcommand {
49            match subcommand {
50                ErrorSubcommand::List => return list_codes(ctx),
51                ErrorSubcommand::Show { code } => return explain_code(code, ctx),
52            }
53        }
54
55        // Handle legacy flag/positional path
56        if self.list {
57            return list_codes(ctx);
58        }
59
60        match &self.code {
61            Some(code) => explain_code(code, ctx),
62            None => {
63                // No subcommand, no flag, no code → show help
64                list_codes(ctx)
65            }
66        }
67    }
68}
69
70/// Normalize a user-typed error code to its canonical `AUTHS-E####` form.
71///
72/// Accepts the fully-qualified form and the shorthands people actually type:
73/// `E4203`, `4203`, `auths-e4203` all resolve to `AUTHS-E4203`. Anything that is
74/// not a bare numeric code is upper-cased and passed through unchanged.
75///
76/// Args:
77/// * `code`: the raw code string from the CLI.
78///
79/// Usage:
80/// ```ignore
81/// assert_eq!(normalize_error_code("E4203"), "AUTHS-E4203");
82/// ```
83pub fn normalize_error_code(code: &str) -> String {
84    let upper = code.trim().to_uppercase();
85    let without_prefix = upper.strip_prefix("AUTHS-").unwrap_or(&upper);
86    let digits = without_prefix.strip_prefix('E').unwrap_or(without_prefix);
87    if !digits.is_empty() && digits.bytes().all(|b| b.is_ascii_digit()) {
88        format!("AUTHS-E{digits}")
89    } else {
90        upper
91    }
92}
93
94fn explain_code(code: &str, ctx: &CliConfig) -> anyhow::Result<()> {
95    let normalized = normalize_error_code(code);
96
97    if ctx.is_json() {
98        match registry::explain(&normalized) {
99            Some(text) => {
100                let json = serde_json::json!({
101                    "code": normalized,
102                    "explanation": text,
103                });
104                println!("{}", serde_json::to_string_pretty(&json)?);
105            }
106            None => {
107                let json = serde_json::json!({
108                    "error": { "message": format!("Unknown error code: {normalized}") }
109                });
110                println!("{}", serde_json::to_string_pretty(&json)?);
111            }
112        }
113    } else {
114        match registry::explain(&normalized) {
115            Some(text) => println!("{text}"),
116            None => {
117                eprintln!("Unknown error code: {normalized}");
118                eprintln!();
119                // Provide helpful suggestion if they try to use the old --list flag
120                if normalized == "LIST" {
121                    eprintln!("Did you mean: auths error list");
122                } else {
123                    eprintln!("Run `auths error list` to see all known codes.");
124                }
125                std::process::exit(1);
126            }
127        }
128    }
129    Ok(())
130}
131
132fn list_codes(ctx: &CliConfig) -> anyhow::Result<()> {
133    let codes = registry::all_codes();
134
135    if ctx.is_json() {
136        let json = serde_json::json!({ "codes": codes });
137        println!("{}", serde_json::to_string_pretty(&json)?);
138    } else {
139        for code in codes {
140            println!("{code}");
141        }
142        eprintln!("\n{} error codes registered", codes.len());
143        eprintln!("Run `auths error show CODE` to see details for a specific code.");
144    }
145    Ok(())
146}
147
148#[cfg(test)]
149mod tests {
150    use super::normalize_error_code;
151    use crate::errors::registry;
152
153    #[test]
154    fn bare_and_prefixed_codes_resolve_the_same() {
155        // A user who types the short form must reach the same doc as the canonical
156        // one — `E4203`, `4203`, `auths-e4203` all mean AUTHS-E4203.
157        assert_eq!(normalize_error_code("E4203"), "AUTHS-E4203");
158        assert_eq!(normalize_error_code("4203"), "AUTHS-E4203");
159        assert_eq!(normalize_error_code("auths-e4203"), "AUTHS-E4203");
160        assert_eq!(normalize_error_code("AUTHS-E4203"), "AUTHS-E4203");
161        assert_eq!(
162            registry::explain(&normalize_error_code("E4203")),
163            registry::explain("AUTHS-E4203")
164        );
165        assert!(registry::explain(&normalize_error_code("E4203")).is_some());
166    }
167
168    #[test]
169    fn non_numeric_input_passes_through_uppercased() {
170        // The `LIST` shorthand (and any unknown token) must not be mangled into a
171        // fake code.
172        assert_eq!(normalize_error_code("list"), "LIST");
173    }
174}