Skip to main content

balls/
error.rs

1use std::fmt;
2use std::io;
3use std::path::PathBuf;
4
5#[derive(Debug)]
6pub enum BallError {
7    Io(io::Error),
8    Json(serde_json::Error),
9    Git(String),
10    TaskNotFound(String),
11    InvalidTask(String),
12    NotInitialized,
13    NotARepo,
14    AlreadyClaimed(String),
15    DepsUnmet(String),
16    NotClaimable(String),
17    Cycle(String),
18    WorktreeExists(PathBuf),
19    Conflict(String),
20    Other(String),
21}
22
23impl fmt::Display for BallError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            BallError::Io(e) => write!(f, "io error: {e}"),
27            BallError::Json(e) => write!(f, "json error: {e}"),
28            BallError::Git(s) => write!(f, "git error: {s}"),
29            BallError::TaskNotFound(id) => write!(f, "task not found: {id}"),
30            BallError::InvalidTask(s) => write!(f, "invalid task: {s}"),
31            BallError::NotInitialized => {
32                write!(f, "not initialized. Run `bl init`")
33            }
34            BallError::NotARepo => write!(f, "not a git repository"),
35            BallError::AlreadyClaimed(id) => write!(f, "task {id} is already claimed"),
36            BallError::DepsUnmet(id) => write!(f, "task {id} has unmet dependencies"),
37            BallError::NotClaimable(id) => write!(f, "task {id} is not claimable"),
38            BallError::Cycle(s) => write!(f, "dependency cycle: {s}"),
39            BallError::WorktreeExists(p) => {
40                write!(f, "worktree already exists: {} (try `bl drop`)", p.display())
41            }
42            BallError::Conflict(s) => write!(f, "conflict: {s}"),
43            BallError::Other(s) => write!(f, "{s}"),
44        }
45    }
46}
47
48impl std::error::Error for BallError {}
49
50impl From<io::Error> for BallError {
51    fn from(e: io::Error) -> Self {
52        BallError::Io(e)
53    }
54}
55
56impl From<serde_json::Error> for BallError {
57    fn from(e: serde_json::Error) -> Self {
58        BallError::Json(e)
59    }
60}
61
62pub type Result<T> = std::result::Result<T, BallError>;
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn display_all_variants() {
70        let cases: Vec<BallError> = vec![
71            BallError::Io(io::Error::other("boom")),
72            BallError::Json(serde_json::from_str::<i32>("x").unwrap_err()),
73            BallError::Git("fatal".into()),
74            BallError::TaskNotFound("bl-x".into()),
75            BallError::InvalidTask("bad".into()),
76            BallError::NotInitialized,
77            BallError::NotARepo,
78            BallError::AlreadyClaimed("bl-x".into()),
79            BallError::DepsUnmet("bl-x".into()),
80            BallError::NotClaimable("bl-x".into()),
81            BallError::Cycle("loop".into()),
82            BallError::WorktreeExists(PathBuf::from("/tmp/x")),
83            BallError::Conflict("merge".into()),
84            BallError::Other("misc".into()),
85        ];
86        for e in &cases {
87            let s = format!("{e}");
88            assert!(!s.is_empty());
89        }
90    }
91
92    #[test]
93    fn from_io_error() {
94        let e: BallError = io::Error::other("x").into();
95        assert!(matches!(e, BallError::Io(_)));
96    }
97
98    #[test]
99    fn from_json_error() {
100        let e: BallError = serde_json::from_str::<i32>("oops").unwrap_err().into();
101        assert!(matches!(e, BallError::Json(_)));
102    }
103
104    #[test]
105    fn error_is_std_error() {
106        let e = BallError::NotARepo;
107        let s: &dyn std::error::Error = &e;
108        assert!(!s.to_string().is_empty());
109    }
110}