use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SearchError {
#[error("No translation files found containing '{text}'.\n\nSearched in: {searched_paths}\n\nTip: Check your project structure or verify translation files exist")]
NoTranslationFiles {
text: String,
searched_paths: String,
},
#[error(
"Failed to parse YAML file {file}:\n{reason}\n\nTip: Verify the YAML syntax is correct"
)]
YamlParseError { file: PathBuf, reason: String },
#[error(
"Failed to parse JSON file {file}:\n{reason}\n\nTip: Verify the JSON syntax is correct"
)]
JsonParseError { file: PathBuf, reason: String },
#[error("Translation key '{key}' found in {file} but no code references detected.\n\nTip: Check if the key is actually used in the codebase")]
NoCodeReferences { key: String, file: PathBuf },
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Failed to execute ripgrep command: {0}")]
RipgrepExecutionFailed(String),
#[error("ripgrep output is not valid UTF-8: {0}")]
InvalidUtf8(#[from] std::string::FromUtf8Error),
#[error("Failed to parse file path: {0}")]
InvalidPath(String),
#[error("{0}")]
Generic(String),
}
impl SearchError {
pub fn no_translation_files(text: impl Into<String>) -> Self {
Self::NoTranslationFiles {
text: text.into(),
searched_paths: "config/locales, src/i18n, locales, i18n".to_string(),
}
}
pub fn no_translation_files_with_paths(
text: impl Into<String>,
paths: impl Into<String>,
) -> Self {
Self::NoTranslationFiles {
text: text.into(),
searched_paths: paths.into(),
}
}
pub fn yaml_parse_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self::YamlParseError {
file: file.into(),
reason: reason.into(),
}
}
pub fn json_parse_error(file: impl Into<PathBuf>, reason: impl Into<String>) -> Self {
Self::JsonParseError {
file: file.into(),
reason: reason.into(),
}
}
pub fn no_code_references(key: impl Into<String>, file: impl Into<PathBuf>) -> Self {
Self::NoCodeReferences {
key: key.into(),
file: file.into(),
}
}
}
pub type Result<T> = std::result::Result<T, SearchError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_translation_files_error() {
let err = SearchError::no_translation_files("add new");
let msg = err.to_string();
assert!(msg.contains("add new"));
assert!(msg.contains("config/locales"));
assert!(msg.contains("Tip:"));
}
#[test]
fn test_no_translation_files_with_custom_paths() {
let err =
SearchError::no_translation_files_with_paths("test", "custom/path1, custom/path2");
let msg = err.to_string();
assert!(msg.contains("test"));
assert!(msg.contains("custom/path1"));
assert!(msg.contains("custom/path2"));
}
#[test]
fn test_yaml_parse_error() {
let err = SearchError::yaml_parse_error("config/en.yml", "unexpected character");
let msg = err.to_string();
assert!(msg.contains("config/en.yml"));
assert!(msg.contains("unexpected character"));
assert!(msg.contains("YAML syntax"));
}
#[test]
fn test_no_code_references_error() {
let err = SearchError::no_code_references("invoice.labels.add_new", "config/en.yml");
let msg = err.to_string();
assert!(msg.contains("invoice.labels.add_new"));
assert!(msg.contains("config/en.yml"));
assert!(msg.contains("Tip:"));
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let search_err: SearchError = io_err.into();
let msg = search_err.to_string();
assert!(msg.contains("IO error"));
assert!(msg.contains("file not found"));
}
#[test]
fn test_ripgrep_execution_failed() {
let err = SearchError::RipgrepExecutionFailed("command failed".to_string());
let msg = err.to_string();
assert!(msg.contains("Failed to execute ripgrep"));
assert!(msg.contains("command failed"));
}
}