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}