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]
134 pub fn mensagem_i18n(&self) -> String {
135 use crate::i18n::{t, Mensagem};
136 match self {
137 Self::VpsNaoEncontrada(nome) => t(Mensagem::VpsNaoEncontrada { nome: nome.clone() }),
138 Self::VpsDuplicada(nome) => t(Mensagem::VpsDuplicada { nome: nome.clone() }),
139 Self::ArgumentoInvalido(detalhe) => t(Mensagem::ErroArgumentoInvalido {
140 detalhe: detalhe.clone(),
141 }),
142 Self::ConexaoSsh(detalhe)
143 | Self::AutenticacaoSsh(detalhe)
144 | Self::ConexaoFalhou(detalhe)
145 | Self::CanalFalhou(detalhe) => t(Mensagem::ErroGenerico {
146 detalhe: format!("{}: {detalhe}", t(Mensagem::ErroConexaoSsh)),
147 }),
148 Self::AutenticacaoFalhou => t(Mensagem::ErroConexaoSsh),
149 Self::ComandoFalhou { exit_code, stderr } => t(Mensagem::ErroGenerico {
150 detalhe: format!(
151 "{} (exit={exit_code}): {stderr}",
152 t(Mensagem::ErroComandoFalhou)
153 ),
154 }),
155 Self::TimeoutSsh(ms) | Self::Timeout(ms) => t(Mensagem::ErroGenerico {
156 detalhe: format!("timeout: {ms}ms"),
157 }),
158 _ => self.to_string(),
161 }
162 }
163
164 #[must_use]
166 pub fn exit_code(&self) -> i32 {
167 match self {
168 Self::Io(_) => exit_codes::EX_IOERR,
169 Self::Json(_) => exit_codes::EX_DATAERR,
170 Self::TomlDe(_) => exit_codes::EX_DATAERR,
171 Self::TomlSer(_) => exit_codes::EX_CANTCREAT,
172 Self::ConexaoSsh(_) => exit_codes::EX_IOERR,
173 Self::AutenticacaoSsh(_) => exit_codes::EX_IOERR,
174 Self::ConexaoFalhou(_) => exit_codes::EX_IOERR,
175 Self::AutenticacaoFalhou => exit_codes::EX_NOPERM,
176 Self::CanalFalhou(_) => exit_codes::EX_IOERR,
177 Self::TimeoutSsh(_) => exit_codes::EX_IOERR,
178 Self::ComandoFalhou { exit_code, .. } => *exit_code,
179 Self::VpsNaoEncontrada(_) => exit_codes::EX_NOINPUT,
180 Self::VpsDuplicada(_) => exit_codes::EX_USAGE,
181 Self::ArquivoNaoEncontrado(_) => exit_codes::EX_NOINPUT,
182 Self::ArgumentoInvalido(_) => exit_codes::EX_USAGE,
183 Self::Timeout(_) => exit_codes::EX_IOERR,
184 Self::DiretorioXdg => exit_codes::EX_CANTCREAT,
185 Self::SchemaIncompativel { .. } => exit_codes::EX_DATAERR,
186 Self::Generico(_) => exit_codes::EX_GENERAL,
187 }
188 }
189}
190
191pub type ResultadoSshCli<T> = std::result::Result<T, ErroSshCli>;
193
194#[cfg(test)]
195mod testes {
196 use super::*;
197
198 #[test]
199 fn vps_nao_encontrada_mensagem_contem_nome() {
200 let erro = ErroSshCli::VpsNaoEncontrada("producao".into());
201 assert!(erro.to_string().contains("producao"));
202 }
203
204 #[test]
205 fn vps_duplicada_mensagem_contem_nome() {
206 let erro = ErroSshCli::VpsDuplicada("vps-1".into());
207 let msg = erro.to_string();
208 assert!(msg.contains("vps-1"));
209 assert!(msg.contains("já existe"));
210 }
211
212 #[test]
213 fn erro_io_exibe_mensagem() {
214 let erro = ErroSshCli::from(std::io::Error::new(
215 std::io::ErrorKind::NotFound,
216 "arquivo nao encontrado",
217 ));
218 let msg = erro.to_string();
219 assert!(msg.contains("I/O") || msg.contains("arquivo nao encontrado"));
220 }
221
222 #[test]
223 fn erro_toml_de_exibe_mensagem() {
224 let toml_err = "invalid TOML".parse::<toml::Value>().unwrap_err();
225 let erro = ErroSshCli::TomlDe(toml_err);
226 let msg = erro.to_string();
227 assert!(msg.contains("TOML") || msg.contains("leitura"));
228 }
229
230 #[test]
231 fn erro_tipo_servidor_vps_nao_encontrada() {
232 let erro = ErroSshCli::VpsNaoEncontrada("servidor-x".into());
233 let msg = erro.to_string();
234 assert!(msg.contains("servidor-x"));
235 assert!(msg.contains("não encontrada") || msg.contains("not found"));
236 }
237
238 #[test]
239 fn exit_code_io_retorna_ioerr() {
240 let e = ErroSshCli::Io(std::io::Error::other("teste"));
241 assert_eq!(e.exit_code(), exit_codes::EX_IOERR);
242 }
243
244 #[test]
245 fn exit_code_autenticacao_falhou_retorna_noperm() {
246 assert_eq!(
247 ErroSshCli::AutenticacaoFalhou.exit_code(),
248 exit_codes::EX_NOPERM
249 );
250 }
251
252 #[test]
253 fn exit_code_vps_nao_encontrada_retorna_noinput() {
254 let e = ErroSshCli::VpsNaoEncontrada("teste".to_string());
255 assert_eq!(e.exit_code(), exit_codes::EX_NOINPUT);
256 }
257
258 #[test]
259 fn exit_code_comando_falhou_propaga_exit_code_remoto() {
260 let e = ErroSshCli::ComandoFalhou {
261 exit_code: 127,
262 stderr: "not found".to_string(),
263 };
264 assert_eq!(e.exit_code(), 127);
265 }
266
267 #[test]
268 fn exit_code_argumento_invalido_retorna_usage() {
269 let e = ErroSshCli::ArgumentoInvalido("bad".to_string());
270 assert_eq!(e.exit_code(), exit_codes::EX_USAGE);
271 }
272
273 #[test]
274 fn exit_code_diretorio_xdg_retorna_cantcreat() {
275 assert_eq!(
276 ErroSshCli::DiretorioXdg.exit_code(),
277 exit_codes::EX_CANTCREAT
278 );
279 }
280}