use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("validation error: {0}")]
Validation(String),
#[error("duplicate detected: {0}")]
Duplicate(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("not found: {0}")]
NotFound(String),
#[error("namespace not resolved: {0}")]
NamespaceError(String),
#[error("limit exceeded: {0}")]
LimitExceeded(String),
#[error("database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("embedding error: {0}")]
Embedding(String),
#[error("sqlite-vec extension failed: {0}")]
VecExtension(String),
#[error("database busy: {0}")]
DbBusy(String),
#[error("falha parcial em batch: {failed} de {total} itens falharam")]
BatchPartialFailure { total: usize, failed: usize },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("internal error: {0}")]
Internal(#[from] anyhow::Error),
#[error("json error: {0}")]
Json(#[from] serde_json::Error),
#[error("lock busy: {0}")]
LockBusy(String),
#[error(
"todos os {max} slots de concorrência ocupados após aguardar {waited_secs}s (exit 75); \
use --max-concurrency ou aguarde outras invocações terminarem"
)]
AllSlotsFull { max: usize, waited_secs: u64 },
#[error(
"memória disponível ({available_mb}MB) abaixo do mínimo requerido ({required_mb}MB) \
para carregar o modelo; aborte outras cargas ou use --skip-memory-guard (exit 77)"
)]
LowMemory { available_mb: u64, required_mb: u64 },
}
impl AppError {
pub fn exit_code(&self) -> i32 {
match self {
Self::Validation(_) => 1,
Self::Duplicate(_) => 2,
Self::Conflict(_) => 3,
Self::NotFound(_) => 4,
Self::NamespaceError(_) => 5,
Self::LimitExceeded(_) => 6,
Self::Database(_) => 10,
Self::Embedding(_) => 11,
Self::VecExtension(_) => 12,
Self::BatchPartialFailure { .. } => crate::constants::BATCH_PARTIAL_FAILURE_EXIT_CODE,
Self::DbBusy(_) => crate::constants::DB_BUSY_EXIT_CODE,
Self::Io(_) => 14,
Self::Internal(_) => 20,
Self::Json(_) => 20,
Self::LockBusy(_) => crate::constants::CLI_LOCK_EXIT_CODE,
Self::AllSlotsFull { .. } => crate::constants::CLI_LOCK_EXIT_CODE,
Self::LowMemory { .. } => crate::constants::LOW_MEMORY_EXIT_CODE,
}
}
}
#[cfg(test)]
mod testes {
use super::*;
use std::io;
#[test]
fn exit_code_validation_retorna_1() {
assert_eq!(AppError::Validation("campo inválido".into()).exit_code(), 1);
}
#[test]
fn exit_code_duplicate_retorna_2() {
assert_eq!(AppError::Duplicate("namespace/nome".into()).exit_code(), 2);
}
#[test]
fn exit_code_conflict_retorna_3() {
assert_eq!(AppError::Conflict("updated_at mudou".into()).exit_code(), 3);
}
#[test]
fn exit_code_not_found_retorna_4() {
assert_eq!(AppError::NotFound("memória ausente".into()).exit_code(), 4);
}
#[test]
fn exit_code_namespace_error_retorna_5() {
assert_eq!(
AppError::NamespaceError("não resolvido".into()).exit_code(),
5
);
}
#[test]
fn exit_code_limit_exceeded_retorna_6() {
assert_eq!(
AppError::LimitExceeded("corpo muito grande".into()).exit_code(),
6
);
}
#[test]
fn exit_code_embedding_retorna_11() {
assert_eq!(
AppError::Embedding("falha de modelo".into()).exit_code(),
11
);
}
#[test]
fn exit_code_vec_extension_retorna_12() {
assert_eq!(
AppError::VecExtension("extensão não carregou".into()).exit_code(),
12
);
}
#[test]
fn exit_code_db_busy_retorna_15() {
assert_eq!(AppError::DbBusy("esgotou retries".into()).exit_code(), 15);
}
#[test]
fn exit_code_batch_partial_failure_retorna_13() {
assert_eq!(
AppError::BatchPartialFailure {
total: 10,
failed: 3
}
.exit_code(),
13
);
}
#[test]
fn display_batch_partial_failure_inclui_contagens() {
let err = AppError::BatchPartialFailure {
total: 50,
failed: 7,
};
let msg = err.to_string();
assert!(msg.contains("7"));
assert!(msg.contains("50"));
assert!(msg.contains("falha parcial"));
}
#[test]
fn exit_code_io_retorna_14() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "arquivo ausente");
assert_eq!(AppError::Io(io_err).exit_code(), 14);
}
#[test]
fn exit_code_internal_retorna_20() {
let anyhow_err = anyhow::anyhow!("erro interno inesperado");
assert_eq!(AppError::Internal(anyhow_err).exit_code(), 20);
}
#[test]
fn exit_code_json_retorna_20() {
let json_err = serde_json::from_str::<serde_json::Value>("json inválido {{").unwrap_err();
assert_eq!(AppError::Json(json_err).exit_code(), 20);
}
#[test]
fn exit_code_lock_busy_retorna_75() {
assert_eq!(
AppError::LockBusy("outra instância ativa".into()).exit_code(),
75
);
}
#[test]
fn display_validation_inclui_mensagem() {
let err = AppError::Validation("cpf inválido".into());
assert!(err.to_string().contains("cpf inválido"));
assert!(err.to_string().contains("validation error"));
}
#[test]
fn display_duplicate_inclui_mensagem() {
let err = AppError::Duplicate("proj/mem".into());
assert!(err.to_string().contains("proj/mem"));
assert!(err.to_string().contains("duplicate detected"));
}
#[test]
fn display_not_found_inclui_mensagem() {
let err = AppError::NotFound("id 42".into());
assert!(err.to_string().contains("id 42"));
assert!(err.to_string().contains("not found"));
}
#[test]
fn display_embedding_inclui_mensagem() {
let err = AppError::Embedding("dimensão errada".into());
assert!(err.to_string().contains("dimensão errada"));
assert!(err.to_string().contains("embedding error"));
}
#[test]
fn display_lock_busy_inclui_mensagem() {
let err = AppError::LockBusy("pid 1234".into());
assert!(err.to_string().contains("pid 1234"));
assert!(err.to_string().contains("lock busy"));
}
#[test]
fn from_io_error_converte_corretamente() {
let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "sem permissão");
let app_err: AppError = io_err.into();
assert_eq!(app_err.exit_code(), 14);
assert!(app_err.to_string().contains("IO error"));
}
#[test]
fn from_anyhow_error_converte_corretamente() {
let anyhow_err = anyhow::anyhow!("detalhe interno");
let app_err: AppError = anyhow_err.into();
assert_eq!(app_err.exit_code(), 20);
assert!(app_err.to_string().contains("internal error"));
}
#[test]
fn from_serde_json_error_converte_corretamente() {
let json_err = serde_json::from_str::<serde_json::Value>("{campo_ruim}").unwrap_err();
let app_err: AppError = json_err.into();
assert_eq!(app_err.exit_code(), 20);
assert!(app_err.to_string().contains("json error"));
}
#[test]
fn exit_code_lock_busy_bate_com_constante() {
assert_eq!(
AppError::LockBusy("test".into()).exit_code(),
crate::constants::CLI_LOCK_EXIT_CODE
);
}
}