use llman::error::{LlmanError, Result};
use std::io;
fn load_i18n_template(key: &str, locale: &str) -> String {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let path = std::path::Path::new(manifest_dir)
.join("locales")
.join("app.yml");
let content = std::fs::read_to_string(path).expect("Failed to read locales/app.yml");
let key_parts: Vec<&str> = key.split('.').collect();
let mut path_stack: Vec<String> = Vec::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
continue;
}
let indent = line.chars().take_while(|c| *c == ' ').count();
if indent % 2 != 0 {
continue;
}
let level = indent / 2;
let Some((raw_key, raw_value)) = trimmed.split_once(':') else {
continue;
};
let current_key = raw_key.trim();
let value = raw_value.trim();
if path_stack.len() > level {
path_stack.truncate(level);
}
path_stack.push(current_key.to_string());
if current_key == locale {
let parent = &path_stack[..path_stack.len().saturating_sub(1)];
if parent == key_parts {
return unquote_yaml_value(value);
}
}
}
panic!("Missing locale '{}' for key '{}'", locale, key);
}
fn unquote_yaml_value(value: &str) -> String {
let trimmed = value.trim();
if trimmed.starts_with('"') && trimmed.ends_with('"') && trimmed.len() >= 2 {
return trimmed[1..trimmed.len() - 1].to_string();
}
if trimmed.starts_with('\'') && trimmed.ends_with('\'') && trimmed.len() >= 2 {
return trimmed[1..trimmed.len() - 1].to_string();
}
trimmed.to_string()
}
fn format_i18n_template(template: &str, replacements: &[(&str, &str)]) -> String {
let mut formatted = template.to_string();
for (key, value) in replacements {
formatted = formatted.replace(&format!("%{{{}}}", key), value);
}
formatted
}
#[test]
fn test_llman_error_variants_creation_and_messages() {
let config_error = LlmanError::Config {
message: "Invalid configuration".to_string(),
};
assert!(
matches!(
config_error,
LlmanError::Config { ref message } if message == "Invalid configuration"
),
"Config error should retain message payload"
);
let app_error = LlmanError::InvalidApp {
app: "testapp".to_string(),
};
assert!(
matches!(
app_error,
LlmanError::InvalidApp { ref app } if app == "testapp"
),
"InvalidApp error should retain app payload"
);
let dir_error = LlmanError::NotProjectDirectory;
assert!(matches!(dir_error, LlmanError::NotProjectDirectory));
let home_error = LlmanError::HomeDirectoryNotAllowed;
assert!(matches!(home_error, LlmanError::HomeDirectoryNotAllowed));
let rule_error = LlmanError::RuleNotFound {
name: "missing_rule".to_string(),
};
assert!(
matches!(
rule_error,
LlmanError::RuleNotFound { ref name } if name == "missing_rule"
),
"RuleNotFound error should retain rule name"
);
let custom_error = LlmanError::Custom("Something went wrong".to_string());
assert!(
matches!(
custom_error,
LlmanError::Custom(ref message) if message == "Something went wrong"
),
"Custom error should retain message payload"
);
}
#[test]
fn test_llman_error_from_conversions() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
let llman_error: LlmanError = io_error.into();
assert!(matches!(llman_error, LlmanError::Io(_)));
fn function_returns_error() -> Result<String> {
Err(LlmanError::Config {
message: "Test error".to_string(),
})
}
let result = function_returns_error();
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, LlmanError::Config { .. }));
}
#[test]
fn test_llman_error_display_localized() {
let locale = "en";
llman::init_locale();
let config_error = LlmanError::Config {
message: "Config issue".to_string(),
};
let localized = config_error.display_localized();
let template = load_i18n_template("errors.config_error", locale);
let expected = format_i18n_template(&template, &[("message", "Config issue")]);
assert_eq!(localized, expected);
let app_error = LlmanError::InvalidApp {
app: "myapp".to_string(),
};
let localized = app_error.display_localized();
let template = load_i18n_template("errors.invalid_app", locale);
let expected = format_i18n_template(&template, &[("app", "myapp")]);
assert_eq!(localized, expected);
let dir_error = LlmanError::NotProjectDirectory;
let localized = dir_error.display_localized();
let expected = load_i18n_template("errors.not_project_directory", locale);
assert_eq!(localized, expected);
let home_error = LlmanError::HomeDirectoryNotAllowed;
let localized = home_error.display_localized();
let expected = load_i18n_template("errors.home_directory_not_allowed", locale);
assert_eq!(localized, expected);
let rule_error = LlmanError::RuleNotFound {
name: "missing_rule".to_string(),
};
let localized = rule_error.display_localized();
let template = load_i18n_template("errors.rule_not_found", locale);
let expected = format_i18n_template(&template, &[("name", "missing_rule")]);
assert_eq!(localized, expected);
}
#[test]
fn test_llman_error_clone_and_equality() {
let error1 = LlmanError::Config {
message: "Test".to_string(),
};
let error2 = LlmanError::Config {
message: "Test".to_string(),
};
let error3 = LlmanError::Config {
message: "Different".to_string(),
};
let message1 = match &error1 {
LlmanError::Config { message } => message,
_ => unreachable!("Expected Config error"),
};
let message2 = match &error2 {
LlmanError::Config { message } => message,
_ => unreachable!("Expected Config error"),
};
let message3 = match &error3 {
LlmanError::Config { message } => message,
_ => unreachable!("Expected Config error"),
};
assert_eq!(message1, message2);
assert_ne!(message1, message3);
}
#[test]
fn test_llman_error_in_error_handling_contexts() {
let result: Result<String> = Ok("success".to_string())
.map_err(|_: LlmanError| LlmanError::Custom("Map error".to_string()));
assert!(result.is_ok());
fn function_that_propagates_error(should_fail: bool) -> Result<String> {
if should_fail {
Err(LlmanError::Custom("Propagation test".to_string()))
} else {
Ok("Success".to_string())
}
}
let success_result = function_that_propagates_error(false);
assert!(success_result.is_ok());
let error_result = function_that_propagates_error(true);
assert!(error_result.is_err());
assert!(
matches!(
error_result.unwrap_err(),
LlmanError::Custom(ref message) if message == "Propagation test"
),
"Expected Custom error with propagation message"
);
}
#[test]
fn test_llman_error_complex_scenarios() {
fn nested_operation(level: u32) -> Result<String> {
match level {
0 => Err(LlmanError::Config {
message: "Level 0 config error".to_string(),
}),
1 => Err(LlmanError::InvalidApp {
app: "level1_app".to_string(),
}),
2 => Ok("Success at level 2".to_string()),
_ => Err(LlmanError::Custom("Unknown level".to_string())),
}
}
let level0_result = nested_operation(0);
assert!(level0_result.is_err());
assert!(
matches!(
level0_result.unwrap_err(),
LlmanError::Config { ref message } if message == "Level 0 config error"
),
"Expected Config error at level 0"
);
let level2_result = nested_operation(2);
assert!(level2_result.is_ok());
assert_eq!(level2_result.unwrap(), "Success at level 2");
let errors: Vec<LlmanError> = vec![
LlmanError::Config {
message: "Error 1".to_string(),
},
LlmanError::InvalidApp {
app: "app1".to_string(),
},
LlmanError::NotProjectDirectory,
];
assert_eq!(errors.len(), 3);
assert!(matches!(errors[0], LlmanError::Config { .. }));
assert!(matches!(errors[1], LlmanError::InvalidApp { .. }));
assert!(matches!(errors[2], LlmanError::NotProjectDirectory));
}