#![allow(dead_code)]
use std::path::PathBuf;
use thiserror::Error;
pub type TwinResult<T> = Result<T, TwinError>;
#[derive(Error, Debug)]
pub enum TwinError {
#[error("Git error: {message}")]
Git {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Symlink error: {message}")]
Symlink {
message: String,
path: Option<PathBuf>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Config error: {message}")]
Config {
message: String,
path: Option<PathBuf>,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Environment error: {message}")]
Environment {
message: String,
agent_name: Option<String>,
},
#[error("IO error: {message}")]
Io {
message: String,
path: Option<PathBuf>,
#[source]
source: Option<std::io::Error>,
},
#[error("Lock error: {message}")]
Lock {
message: String,
lock_path: Option<PathBuf>,
},
#[error("Hook execution failed: {message}")]
Hook {
message: String,
hook_type: String,
exit_code: Option<i32>,
},
#[error("{resource} already exists: {name}")]
AlreadyExists { resource: String, name: String },
#[error("{resource} not found: {name}")]
NotFound { resource: String, name: String },
#[error("Invalid argument: {message}")]
InvalidArgument { message: String },
#[error("{0}")]
Other(String),
}
impl TwinError {
pub fn git(message: impl Into<String>) -> Self {
Self::Git {
message: message.into(),
source: None,
}
}
pub fn symlink(message: impl Into<String>, path: Option<PathBuf>) -> Self {
Self::Symlink {
message: message.into(),
path,
source: None,
}
}
pub fn environment(message: impl Into<String>, agent_name: Option<String>) -> Self {
Self::Environment {
message: message.into(),
agent_name,
}
}
pub fn already_exists(resource: impl Into<String>, name: impl Into<String>) -> Self {
Self::AlreadyExists {
resource: resource.into(),
name: name.into(),
}
}
pub fn not_found(resource: impl Into<String>, name: impl Into<String>) -> Self {
Self::NotFound {
resource: resource.into(),
name: name.into(),
}
}
}
impl From<std::io::Error> for TwinError {
fn from(err: std::io::Error) -> Self {
Self::Io {
message: err.to_string(),
path: None,
source: Some(err),
}
}
}
impl From<git2::Error> for TwinError {
fn from(err: git2::Error) -> Self {
Self::Git {
message: err.to_string(),
source: Some(Box::new(err)),
}
}
}
impl From<anyhow::Error> for TwinError {
fn from(err: anyhow::Error) -> Self {
Self::Other(err.to_string())
}
}
impl From<toml::de::Error> for TwinError {
fn from(err: toml::de::Error) -> Self {
Self::Config {
message: format!("Failed to parse TOML: {err}"),
path: None,
source: Some(Box::new(err)),
}
}
}
impl From<toml::ser::Error> for TwinError {
fn from(err: toml::ser::Error) -> Self {
Self::Config {
message: format!("Failed to serialize TOML: {err}"),
path: None,
source: Some(Box::new(err)),
}
}
}
impl From<serde_json::Error> for TwinError {
fn from(err: serde_json::Error) -> Self {
Self::Config {
message: format!("Failed to parse/serialize JSON: {err}"),
path: None,
source: Some(Box::new(err)),
}
}
}
impl TwinError {
pub fn config(message: impl Into<String>, path: Option<PathBuf>) -> Self {
Self::Config {
message: message.into(),
path,
source: None,
}
}
pub fn io(message: impl Into<String>, path: Option<PathBuf>) -> Self {
Self::Io {
message: message.into(),
path,
source: None,
}
}
pub fn lock(message: impl Into<String>, lock_path: Option<PathBuf>) -> Self {
Self::Lock {
message: message.into(),
lock_path,
}
}
pub fn hook(
message: impl Into<String>,
hook_type: impl Into<String>,
exit_code: Option<i32>,
) -> Self {
Self::Hook {
message: message.into(),
hook_type: hook_type.into(),
exit_code,
}
}
pub fn invalid_argument(message: impl Into<String>) -> Self {
Self::InvalidArgument {
message: message.into(),
}
}
pub fn other(message: impl Into<String>) -> Self {
Self::Other(message.into())
}
pub fn is_retryable(&self) -> bool {
matches!(self, Self::Lock { .. } | Self::Io { .. })
}
pub fn is_fatal(&self) -> bool {
!matches!(self, Self::Hook { .. } | Self::Lock { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io;
#[test]
fn test_twin_error_git() {
let error = TwinError::git("Failed to checkout branch");
match &error {
TwinError::Git { message, source } => {
assert_eq!(message, "Failed to checkout branch");
assert!(source.is_none());
}
_ => panic!("Expected Git error"),
}
let display_str = format!("{error}");
assert!(display_str.contains("Git error"));
assert!(display_str.contains("Failed to checkout branch"));
}
#[test]
fn test_twin_error_symlink() {
let path = PathBuf::from("/tmp/test.txt");
let error = TwinError::symlink("Failed to create symlink", Some(path.clone()));
match error {
TwinError::Symlink {
message,
path: p,
source,
} => {
assert_eq!(message, "Failed to create symlink");
assert_eq!(p, Some(path));
assert!(source.is_none());
}
_ => panic!("Expected Symlink error"),
}
}
#[test]
fn test_twin_error_config() {
let path = PathBuf::from("config.toml");
let error = TwinError::Config {
message: "Invalid TOML".to_string(),
path: Some(path.clone()),
source: None,
};
match &error {
TwinError::Config {
message,
path: p,
source,
} => {
assert_eq!(message, "Invalid TOML");
assert_eq!(p, &Some(path));
assert!(source.is_none());
}
_ => panic!("Expected Config error"),
}
let display_str = format!("{error}");
assert!(display_str.contains("Config error"));
assert!(display_str.contains("Invalid TOML"));
}
#[test]
fn test_twin_error_display() {
let errors = vec![
(TwinError::git("git error"), "Git error: git error"),
(
TwinError::symlink("symlink error", None),
"Symlink error: symlink error",
),
(
TwinError::Environment {
message: "env error".to_string(),
agent_name: Some("agent1".to_string()),
},
"Environment error: env error",
),
(
TwinError::Hook {
message: "hook failed".to_string(),
hook_type: "pre_create".to_string(),
exit_code: Some(1),
},
"Hook execution failed: hook failed",
),
(
TwinError::AlreadyExists {
resource: "Environment".to_string(),
name: "test".to_string(),
},
"Environment already exists: test",
),
(
TwinError::NotFound {
resource: "Branch".to_string(),
name: "feature".to_string(),
},
"Branch not found: feature",
),
(
TwinError::InvalidArgument {
message: "invalid arg".to_string(),
},
"Invalid argument: invalid arg",
),
(TwinError::Other("other error".to_string()), "other error"),
];
for (error, expected) in errors {
let display_str = format!("{error}");
assert_eq!(display_str, expected);
}
}
#[test]
fn test_twin_error_from_io() {
let io_error = io::Error::new(io::ErrorKind::NotFound, "File not found");
let twin_error = TwinError::from(io_error);
match twin_error {
TwinError::Io {
message,
path,
source,
} => {
assert!(message.contains("not found") || message.contains("File not found"));
assert!(path.is_none());
assert!(source.is_some());
}
_ => panic!("Expected Io error"),
}
}
}