Skip to main content

context7_cli/
errors.rs

1/// Error types for the context7-cli library.
2///
3/// [`ErroContext7`] is used for all structured errors within the library.
4/// Binary code uses [`anyhow::Result`] for flexible propagation.
5use thiserror::Error;
6
7/// Structured errors for the Context7 API client.
8#[derive(Debug, Error)]
9pub enum ErroContext7 {
10    /// All available API keys have been exhausted after the given number of attempts.
11    #[error("No valid API key available after {tentativas} attempts")]
12    RetryEsgotado {
13        /// Number of attempts made before exhaustion.
14        tentativas: u32,
15    },
16
17    /// All keys failed due to authentication errors (401/403).
18    #[error("All API keys failed due to authentication errors")]
19    SemChavesApi,
20
21    /// The API returned an unexpected HTTP status code.
22    #[error("Invalid API response: status {status}")]
23    RespostaInvalida {
24        /// HTTP status code returned by the API.
25        status: u16,
26    },
27
28    /// The API returned HTTP 400 with an error message.
29    #[error("API returned error 400: {mensagem}")]
30    ApiRetornou400 {
31        /// Error message returned by the API.
32        mensagem: String,
33    },
34
35    /// The requested library was not found (HTTP 404).
36    #[error("Library not found: {library_id}")]
37    BibliotecaNaoEncontrada {
38        /// Library identifier that was not found.
39        library_id: String,
40    },
41
42    /// A keys operation failed (e.g., invalid index, no keys stored).
43    /// The caller already printed a user-friendly message; this signals exit code 1.
44    #[error("")]
45    OperacaoKeysFalhou,
46}
47
48impl ErroContext7 {
49    /// Maps each error variant to a BSD-style exit code (sysexits.h).
50    ///
51    /// | Code | Constant       | Meaning                          |
52    /// |------|----------------|----------------------------------|
53    /// |   1  | generic        | Unspecified runtime error         |
54    /// |  65  | EX_DATAERR     | Invalid input data               |
55    /// |  66  | EX_NOINPUT     | Requested resource not found     |
56    /// |  69  | EX_UNAVAILABLE | Service unavailable after retry  |
57    /// |  74  | EX_IOERR       | I/O or network error             |
58    /// |  77  | EX_NOPERM      | Permission / authentication denied|
59    pub fn exit_code(&self) -> i32 {
60        match self {
61            Self::RetryEsgotado { .. } => 69,
62            Self::SemChavesApi => 77,
63            Self::RespostaInvalida { .. } => 74,
64            Self::ApiRetornou400 { .. } => 65,
65            Self::BibliotecaNaoEncontrada { .. } => 66,
66            Self::OperacaoKeysFalhou => 1,
67        }
68    }
69}
70
71#[cfg(test)]
72mod testes {
73    use super::*;
74
75    #[test]
76    fn testa_erro_sem_chaves_api_display() {
77        let erro = ErroContext7::SemChavesApi;
78        let mensagem = erro.to_string();
79        assert!(
80            !mensagem.is_empty(),
81            "SemChavesApi deve ter mensagem não-vazia"
82        );
83        assert!(
84            mensagem.to_lowercase().contains("key")
85                || mensagem.to_lowercase().contains("api")
86                || mensagem.to_lowercase().contains("auth"),
87            "Mensagem deve mencionar key/api/auth, obteve: {mensagem}"
88        );
89    }
90
91    #[test]
92    fn testa_erro_retry_esgotado_contem_numero_de_tentativas() {
93        let erro = ErroContext7::RetryEsgotado { tentativas: 3 };
94        let mensagem = erro.to_string();
95        assert!(
96            mensagem.contains('3'),
97            "Mensagem deve conter número de tentativas (3), obteve: {mensagem}"
98        );
99    }
100
101    #[test]
102    fn testa_erro_resposta_invalida_contem_status() {
103        let erro = ErroContext7::RespostaInvalida { status: 500 };
104        let mensagem = erro.to_string();
105        assert!(
106            mensagem.contains("500"),
107            "Mensagem deve conter código de status, obteve: {mensagem}"
108        );
109    }
110
111    #[test]
112    fn testa_erro_api_400_contem_texto_do_erro() {
113        let erro = ErroContext7::ApiRetornou400 {
114            mensagem: "Parâmetro inválido".to_string(),
115        };
116        let mensagem = erro.to_string();
117        assert!(
118            mensagem.contains("Parâmetro inválido"),
119            "Mensagem deve conter texto do erro, obteve: {mensagem}"
120        );
121    }
122
123    #[test]
124    fn testa_resultado_alias_propaga_erro_context7() {
125        fn falha() -> Result<(), ErroContext7> {
126            Err(ErroContext7::SemChavesApi)
127        }
128        let resultado: Result<(), ErroContext7> = falha();
129        assert!(resultado.is_err(), "Resultado deve ser Err");
130        let err = resultado.unwrap_err();
131        assert!(matches!(err, ErroContext7::SemChavesApi));
132    }
133}