1use thiserror::Error;
7
8#[derive(Debug, Error)]
10pub enum ErroSshCli {
11 #[error("erro de I/O: {0}")]
13 Io(#[from] std::io::Error),
14
15 #[error("erro de JSON: {0}")]
17 Json(#[from] serde_json::Error),
18
19 #[error("erro de TOML (leitura): {0}")]
21 TomlDe(#[from] toml::de::Error),
22
23 #[error("erro de TOML (escrita): {0}")]
25 TomlSer(#[from] toml::ser::Error),
26
27 #[error("erro de conexão SSH: {0}")]
29 ConexaoSsh(String),
30
31 #[error("erro de autenticação SSH: {0}")]
33 AutenticacaoSsh(String),
34
35 #[error("conexão SSH falhou: {0}")]
37 ConexaoFalhou(String),
38
39 #[error("autenticação SSH falhou")]
41 AutenticacaoFalhou,
42
43 #[error("canal SSH falhou: {0}")]
45 CanalFalhou(String),
46
47 #[error("timeout SSH após {0}ms")]
49 TimeoutSsh(u64),
50
51 #[error("comando falhou com exit code {exit_code}: {stderr}")]
53 ComandoFalhou {
54 exit_code: i32,
56 stderr: String,
58 },
59
60 #[error("VPS '{0}' não encontrada no registro")]
62 VpsNaoEncontrada(String),
63
64 #[error("VPS '{0}' já existe no registro")]
66 VpsDuplicada(String),
67
68 #[error("arquivo não encontrado: {0}")]
70 ArquivoNaoEncontrado(String),
71
72 #[error("argumento inválido: {0}")]
74 ArgumentoInvalido(String),
75
76 #[error("timeout excedido após {0}ms")]
78 Timeout(u64),
79
80 #[error("diretório de configuração indisponível")]
82 DiretorioXdg,
83
84 #[error("versão de schema incompatível: esperada {esperada}, encontrada {encontrada}")]
86 SchemaIncompativel {
87 esperada: u32,
89 encontrada: u32,
91 },
92
93 #[error("erro: {0}")]
95 Generico(String),
96}
97
98pub mod exit_codes {
100 pub const EX_OK: i32 = 0;
102 pub const EX_GENERAL: i32 = 1;
104 pub const EX_USAGE: i32 = 64;
106 pub const EX_DATAERR: i32 = 65;
108 pub const EX_NOINPUT: i32 = 66;
110 pub const EX_CANTCREAT: i32 = 73;
112 pub const EX_IOERR: i32 = 74;
114 pub const EX_NOPERM: i32 = 77;
116 pub const EX_SIGINT: i32 = 130;
118 pub const EX_SIGTERM: i32 = 143;
120}
121
122impl ErroSshCli {
123 #[must_use]
125 pub fn exit_code(&self) -> i32 {
126 match self {
127 Self::Io(_) => exit_codes::EX_IOERR,
128 Self::Json(_) => exit_codes::EX_DATAERR,
129 Self::TomlDe(_) => exit_codes::EX_DATAERR,
130 Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
131 Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
132 Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
133 Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
134 Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
135 Self::CanalFalhou(_) => exit_codes::EX_IOERR,
136 Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
137 Self::ComandoFalhou { exit_code, .. } => *exit_code,
138 Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
139 Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
140 Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
141 Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
142 Self::Timeout(_) => exit_codes::EX_IOERR,
143 Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
144 Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
145 Self::Generico(_) => exit_codes::EX_GENERAL,
146 }
147 }
148}
149
150pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
152
153#[cfg(test)]
154mod testes {
155 use super::*;
156
157 #[test]
158 fn vps_nao_encontrada_mensagem_contem_nome() {
159 let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
160 assert!(erro.to_string().contains("producao"));
161 }
162
163 #[test]
164 fn vps_duplicada_mensagem_contem_nome() {
165 let erro = ErroSshCli::VpsDuplicada("vps-1".into());
166 let msg = erro.to_string();
167 assert!(msg.contains("vps-1"));
168 assert!(msg.contains("já existe"));
169 }
170
171 #[test]
172 fn erro_io_exibe_mensagem() {
173 let erro = ErroSshCli::from(std::io::Error::new(
174 std::io::ErrorKind::NotFound,
175 "arquivo nao encontrado",
176 ));
177 let msg = erro.to_string();
178 assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
179 }
180
181 #[test]
182 fn erro_toml_de_exibe_mensagem() {
183 let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
184 let erro = ErroSshCli::TomlDe(toml_err);
185 let msg = erro.to_string();
186 assert!(msg.contains("TOML") || msg.contains("leitura"));
187 }
188
189 #[test]
190 fn erro_tipo_servidor_vps_nao_encontrada() {
191 let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
192 let msg = erro.to_string();
193 assert!(msg.contains("servidor-x"));
194 assert!(msg.contains("não encontrada") || msg.contains("not found"));
195 }
196
197 #[test]
198 fn exit_code_io_retorna_ioerr() {
199 let e = ErroSshCli::Io(std::io::Error::other("teste"));
200 assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
201 }
202
203 #[test]
204 fn exit_code_autenticacao_falhou_retorna_noperm() {
205 assert_eq!(
206 ErroSshCli::AutenticacaoFalhou.exit_code(),
207 exit_codes::EX_NOPERM
208 );
209 }
210
211 #[test]
212 fn exit_code_vps_nao_encontrada_retorna_noinput() {
213 let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
214 assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
215 }
216
217 #[test]
218 fn exit_code_comando_falhou_propaga_exit_code_remoto() {
219 let e = ErroSshCli::ComandoFalhou {
220 exit_code: 127,
221 stderr: "not found".to_string(),
222 };
223 assert_eq!(e.exit_code(), 127);
224 }
225
226 #[test]
227 fn exit_code_argumento_invalido_retorna_usage() {
228 let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
229 assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
230 }
231
232 #[test]
233 fn exit_code_diretorio_xdg_retorna_cantcreat() {
234 assert_eq!(
235 ErroSshCli::DiretorioXdg.exit_code(),
236 exit_codes::EX_CANTCREAT
237 );
238 }
239}