use colored::Colorize;
use thiserror::Error;
pub const VALID_PRESETS: &[&str] = &["opensource", "enterprise", "strict"];
#[derive(Error, Debug)]
pub enum RepoLensError {
#[error("Scan error: {0}")]
Scan(#[from] ScanError),
#[error("Config error: {0}")]
Config(#[from] ConfigError),
#[error("Provider error: {0}")]
Provider(#[from] ProviderError),
#[error("Action error: {0}")]
Action(#[from] ActionError),
#[error("Rule error: {0}")]
Rule(#[from] RuleError),
#[error("Cache error: {0}")]
Cache(#[from] CacheError),
}
impl RepoLensError {
pub fn suggestion(&self) -> Option<String> {
match self {
RepoLensError::Config(ConfigError::ConfigNotFound { .. }) => {
Some("Run 'repolens init' to create a configuration file.".to_string())
}
RepoLensError::Config(ConfigError::InvalidPreset { .. }) => {
Some(format!("Valid presets are: {}", VALID_PRESETS.join(", ")))
}
RepoLensError::Config(ConfigError::Parse { .. }) => {
Some("Check your .repolens.toml file for syntax errors.".to_string())
}
RepoLensError::Provider(ProviderError::GitNotRepository { .. }) => {
Some("Run 'git init' to initialize a git repository.".to_string())
}
RepoLensError::Provider(ProviderError::NotAuthenticated) => {
Some("Run 'gh auth login' to authenticate with GitHub.".to_string())
}
RepoLensError::Provider(ProviderError::GitHubCliNotAvailable) => {
Some("Install GitHub CLI from https://cli.github.com/".to_string())
}
RepoLensError::Action(ActionError::FileWrite { path, .. }) => Some(format!(
"Check that you have write permissions for '{}'.",
path
)),
RepoLensError::Action(ActionError::DirectoryCreate { path, .. }) => Some(format!(
"Check that you have permissions to create directories in '{}'.",
path
)),
_ => None,
}
}
pub fn display_formatted(&self) -> String {
let mut output = String::new();
output.push_str(&format!("{} {}\n", "Error:".red().bold(), self));
if let Some(suggestion) = self.suggestion() {
output.push_str(&format!("\n {} {}\n", "Hint:".cyan().bold(), suggestion));
}
output
}
}
#[derive(Error, Debug)]
pub enum ScanError {
#[error("Failed to read file '{path}': {source}")]
FileRead {
path: String,
source: std::io::Error,
},
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[allow(dead_code)]
#[error("Configuration file not found")]
ConfigNotFound {
path: String,
},
#[error("Failed to read configuration file '{path}': {source}")]
FileRead {
path: String,
source: std::io::Error,
},
#[error("Failed to parse configuration file: {message}")]
Parse {
message: String,
},
#[error("Failed to serialize configuration: {message}")]
Serialize {
message: String,
},
#[allow(dead_code)]
#[error("Invalid preset '{name}'")]
InvalidPreset {
name: String,
},
}
impl ConfigError {
#[allow(dead_code)]
pub fn description(&self) -> String {
match self {
ConfigError::ConfigNotFound { path } => {
format!("No .repolens.toml found at '{}'.", path)
}
ConfigError::InvalidPreset { name } => {
format!(
"The preset '{}' is not valid. Valid presets are: {}",
name,
VALID_PRESETS.join(", ")
)
}
_ => self.to_string(),
}
}
}
#[derive(Error, Debug)]
pub enum ProviderError {
#[error("Command execution failed: {command}")]
CommandFailed {
command: String,
},
#[error("Failed to parse JSON response: {message}")]
JsonParse {
message: String,
},
#[error("Not in a GitHub repository or not authenticated")]
NotAuthenticated,
#[allow(dead_code)]
#[error("Not a git repository")]
GitNotRepository {
path: String,
},
#[error("Invalid repository name format: {name}")]
InvalidRepoName {
name: String,
},
#[error("GitHub CLI (gh) is not available or not authenticated")]
GitHubCliNotAvailable,
}
impl ProviderError {
#[allow(dead_code)]
pub fn description(&self) -> String {
match self {
ProviderError::GitNotRepository { path } => {
format!("The directory '{}' is not a git repository.", path)
}
ProviderError::GitHubCliNotAvailable => {
"The GitHub CLI (gh) is not installed or not in PATH.".to_string()
}
ProviderError::NotAuthenticated => "You are not authenticated with GitHub.".to_string(),
ProviderError::CommandFailed { command, .. } => {
format!("The command '{}' failed to execute.", command)
}
_ => self.to_string(),
}
}
}
#[derive(Error, Debug)]
pub enum ActionError {
#[error("Failed to create file '{path}': {source}")]
#[allow(dead_code)]
FileCreate {
path: String,
source: std::io::Error,
},
#[error("Failed to write file '{path}': {source}")]
FileWrite {
path: String,
source: std::io::Error,
},
#[error("Failed to create directory '{path}': {source}")]
DirectoryCreate {
path: String,
source: std::io::Error,
},
#[error("Unknown template: {name}")]
UnknownTemplate {
name: String,
},
#[error("Action execution failed: {message}")]
ExecutionFailed {
message: String,
},
}
#[derive(Error, Debug)]
pub enum RuleError {
#[error("Rule execution failed: {message}")]
ExecutionFailed {
message: String,
},
}
#[derive(Error, Debug)]
#[allow(dead_code)]
pub enum CacheError {
#[error("Failed to read cache file '{path}': {message}")]
FileRead {
path: String,
message: String,
},
#[error("Failed to write cache file '{path}': {message}")]
FileWrite {
path: String,
message: String,
},
#[error("Failed to parse cache file: {message}")]
Parse {
message: String,
},
#[error("Failed to delete cache: {message}")]
Delete {
message: String,
},
}
impl From<std::io::Error> for RepoLensError {
fn from(err: std::io::Error) -> Self {
RepoLensError::Scan(ScanError::FileRead {
path: "unknown".to_string(),
source: err,
})
}
}
impl From<toml::de::Error> for RepoLensError {
fn from(err: toml::de::Error) -> Self {
RepoLensError::Config(ConfigError::Parse {
message: err.to_string(),
})
}
}
impl From<toml::ser::Error> for RepoLensError {
fn from(err: toml::ser::Error) -> Self {
RepoLensError::Config(ConfigError::Serialize {
message: err.to_string(),
})
}
}
impl From<serde_json::Error> for RepoLensError {
fn from(err: serde_json::Error) -> Self {
RepoLensError::Provider(ProviderError::JsonParse {
message: err.to_string(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_scan_error_display() {
let err = ScanError::FileRead {
path: "test.txt".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"),
};
let msg = format!("{}", err);
assert!(msg.contains("test.txt"));
assert!(msg.contains("Failed to read file"));
}
#[test]
fn test_config_error_file_read_display() {
let err = ConfigError::FileRead {
path: "config.toml".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
};
let msg = format!("{}", err);
assert!(msg.contains("config.toml"));
}
#[test]
fn test_config_error_parse_display() {
let err = ConfigError::Parse {
message: "invalid syntax".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("invalid syntax"));
}
#[test]
fn test_config_error_serialize_display() {
let err = ConfigError::Serialize {
message: "serialization failed".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("serialization failed"));
}
#[test]
fn test_config_error_invalid_preset_display() {
let err = ConfigError::InvalidPreset {
name: "unknown".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("unknown"));
}
#[test]
fn test_provider_error_command_failed_display() {
let err = ProviderError::CommandFailed {
command: "gh pr list".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("gh pr list"));
}
#[test]
fn test_provider_error_json_parse_display() {
let err = ProviderError::JsonParse {
message: "unexpected token".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("unexpected token"));
}
#[test]
fn test_provider_error_not_authenticated_display() {
let err = ProviderError::NotAuthenticated;
let msg = format!("{}", err);
assert!(msg.contains("not authenticated") || msg.contains("Not in a GitHub repository"));
}
#[test]
fn test_provider_error_invalid_repo_name_display() {
let err = ProviderError::InvalidRepoName {
name: "invalid-name".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("invalid-name"));
}
#[test]
fn test_provider_error_github_cli_not_available_display() {
let err = ProviderError::GitHubCliNotAvailable;
let msg = format!("{}", err);
assert!(msg.contains("GitHub CLI") || msg.contains("gh"));
}
#[test]
fn test_action_error_file_write_display() {
let err = ActionError::FileWrite {
path: "output.txt".to_string(),
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"),
};
let msg = format!("{}", err);
assert!(msg.contains("output.txt"));
}
#[test]
fn test_action_error_directory_create_display() {
let err = ActionError::DirectoryCreate {
path: "/some/dir".to_string(),
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"),
};
let msg = format!("{}", err);
assert!(msg.contains("/some/dir"));
}
#[test]
fn test_action_error_unknown_template_display() {
let err = ActionError::UnknownTemplate {
name: "my-template".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("my-template"));
}
#[test]
fn test_action_error_execution_failed_display() {
let err = ActionError::ExecutionFailed {
message: "action failed".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("action failed"));
}
#[test]
fn test_rule_error_execution_failed_display() {
let err = RuleError::ExecutionFailed {
message: "rule failed".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("rule failed"));
}
#[test]
fn test_cache_error_file_read_display() {
let err = CacheError::FileRead {
path: "cache.json".to_string(),
message: "read error".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("cache.json"));
}
#[test]
fn test_cache_error_file_write_display() {
let err = CacheError::FileWrite {
path: "cache.json".to_string(),
message: "write error".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("cache.json"));
}
#[test]
fn test_cache_error_parse_display() {
let err = CacheError::Parse {
message: "parse error".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("parse error"));
}
#[test]
fn test_cache_error_delete_display() {
let err = CacheError::Delete {
message: "delete error".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("delete error"));
}
#[test]
fn test_repolens_error_from_scan_error() {
let scan_err = ScanError::FileRead {
path: "test.txt".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
};
let err: RepoLensError = scan_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Scan error"));
}
#[test]
fn test_repolens_error_from_config_error() {
let config_err = ConfigError::Parse {
message: "parse error".to_string(),
};
let err: RepoLensError = config_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Config error"));
}
#[test]
fn test_repolens_error_from_provider_error() {
let provider_err = ProviderError::NotAuthenticated;
let err: RepoLensError = provider_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Provider error"));
}
#[test]
fn test_repolens_error_from_action_error() {
let action_err = ActionError::ExecutionFailed {
message: "failed".to_string(),
};
let err: RepoLensError = action_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Action error"));
}
#[test]
fn test_repolens_error_from_rule_error() {
let rule_err = RuleError::ExecutionFailed {
message: "failed".to_string(),
};
let err: RepoLensError = rule_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Rule error"));
}
#[test]
fn test_repolens_error_from_cache_error() {
let cache_err = CacheError::Parse {
message: "failed".to_string(),
};
let err: RepoLensError = cache_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Cache error"));
}
#[test]
fn test_repolens_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
let err: RepoLensError = io_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Scan error"));
}
#[test]
fn test_repolens_error_from_toml_de_error() {
let toml_err = toml::from_str::<toml::Value>("invalid [[[toml").unwrap_err();
let err: RepoLensError = toml_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Config error"));
}
#[test]
fn test_repolens_error_from_serde_json_error() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let err: RepoLensError = json_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Provider error"));
}
#[test]
fn test_action_error_file_create_display() {
let err = ActionError::FileCreate {
path: "new_file.txt".to_string(),
source: std::io::Error::new(std::io::ErrorKind::AlreadyExists, "already exists"),
};
let msg = format!("{}", err);
assert!(msg.contains("new_file.txt"));
assert!(msg.contains("Failed to create file"));
}
#[test]
fn test_repolens_error_display_variants() {
let scan_err = RepoLensError::Scan(ScanError::FileRead {
path: "test.rs".to_string(),
source: std::io::Error::new(std::io::ErrorKind::NotFound, "not found"),
});
assert!(format!("{}", scan_err).contains("Scan error"));
let rule_err = RepoLensError::Rule(RuleError::ExecutionFailed {
message: "test failure".to_string(),
});
assert!(format!("{}", rule_err).contains("Rule error"));
let provider_err = RepoLensError::Provider(ProviderError::NotAuthenticated);
assert!(format!("{}", provider_err).contains("Provider error"));
}
#[test]
fn test_repolens_error_from_toml_ser_error() {
use std::collections::HashMap;
let mut map = HashMap::new();
map.insert(vec![1, 2, 3], "value");
let ser_err = toml::to_string(&map).unwrap_err();
let err: RepoLensError = ser_err.into();
let msg = format!("{}", err);
assert!(msg.contains("Config error"));
}
#[test]
fn test_config_not_found_error() {
let err = ConfigError::ConfigNotFound {
path: ".repolens.toml".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("Configuration file not found"));
}
#[test]
fn test_config_not_found_description() {
let err = ConfigError::ConfigNotFound {
path: ".repolens.toml".to_string(),
};
let desc = err.description();
assert!(desc.contains(".repolens.toml"));
}
#[test]
fn test_invalid_preset_description() {
let err = ConfigError::InvalidPreset {
name: "foo".to_string(),
};
let desc = err.description();
assert!(desc.contains("foo"));
assert!(desc.contains("opensource"));
assert!(desc.contains("enterprise"));
assert!(desc.contains("strict"));
}
#[test]
fn test_git_not_repository_error() {
let err = ProviderError::GitNotRepository {
path: "/some/path".to_string(),
};
let msg = format!("{}", err);
assert!(msg.contains("Not a git repository"));
}
#[test]
fn test_git_not_repository_description() {
let err = ProviderError::GitNotRepository {
path: "/some/path".to_string(),
};
let desc = err.description();
assert!(desc.contains("/some/path"));
assert!(desc.contains("not a git repository"));
}
#[test]
fn test_repolens_error_suggestion_config_not_found() {
let err = RepoLensError::Config(ConfigError::ConfigNotFound {
path: ".repolens.toml".to_string(),
});
let suggestion = err.suggestion();
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("repolens init"));
}
#[test]
fn test_repolens_error_suggestion_invalid_preset() {
let err = RepoLensError::Config(ConfigError::InvalidPreset {
name: "foo".to_string(),
});
let suggestion = err.suggestion();
assert!(suggestion.is_some());
let s = suggestion.unwrap();
assert!(s.contains("opensource"));
assert!(s.contains("enterprise"));
assert!(s.contains("strict"));
}
#[test]
fn test_repolens_error_suggestion_git_not_repo() {
let err = RepoLensError::Provider(ProviderError::GitNotRepository {
path: ".".to_string(),
});
let suggestion = err.suggestion();
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("git init"));
}
#[test]
fn test_repolens_error_suggestion_not_authenticated() {
let err = RepoLensError::Provider(ProviderError::NotAuthenticated);
let suggestion = err.suggestion();
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("gh auth login"));
}
#[test]
fn test_repolens_error_suggestion_github_cli_not_available() {
let err = RepoLensError::Provider(ProviderError::GitHubCliNotAvailable);
let suggestion = err.suggestion();
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("cli.github.com"));
}
#[test]
fn test_repolens_error_suggestion_file_write() {
let err = RepoLensError::Action(ActionError::FileWrite {
path: "/some/file.txt".to_string(),
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"),
});
let suggestion = err.suggestion();
assert!(suggestion.is_some());
assert!(suggestion.unwrap().contains("/some/file.txt"));
}
#[test]
fn test_repolens_error_no_suggestion() {
let err = RepoLensError::Cache(CacheError::Parse {
message: "some error".to_string(),
});
let suggestion = err.suggestion();
assert!(suggestion.is_none());
}
#[test]
fn test_display_formatted_with_suggestion() {
let err = RepoLensError::Config(ConfigError::ConfigNotFound {
path: ".repolens.toml".to_string(),
});
let formatted = err.display_formatted();
assert!(formatted.contains("Configuration file not found"));
}
#[test]
fn test_display_formatted_without_suggestion() {
let err = RepoLensError::Cache(CacheError::Parse {
message: "parse error".to_string(),
});
let formatted = err.display_formatted();
assert!(formatted.contains("parse error"));
}
#[test]
fn test_valid_presets_constant() {
assert!(VALID_PRESETS.contains(&"opensource"));
assert!(VALID_PRESETS.contains(&"enterprise"));
assert!(VALID_PRESETS.contains(&"strict"));
assert_eq!(VALID_PRESETS.len(), 3);
}
}