Skip to main content

vissue_core/
error.rs

1//! Matchable errors so callers do not parse [`std::fmt::Display`] text.
2
3use std::fmt;
4
5/// Library result using [`Error`].
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// Recoverable catalog and mutation failures with a stable shape.
9#[derive(Debug)]
10pub enum Error {
11    /// The working directory was taken as the tracker root and holds no
12    /// tracker, so an empty answer would be a wrong answer rather than a
13    /// small one.
14    NotATracker {
15        /// The directory that was taken as the root.
16        root: std::path::PathBuf,
17        /// The project directory that was looked for inside it.
18        prefix: String,
19    },
20    /// No heading in the corpus carries this id.
21    IssueNotFound {
22        /// The id that was looked up.
23        id: String,
24    },
25    /// The same id exists under more than one distinct tracker layout.
26    DuplicateId {
27        /// The id that was found twice.
28        id: String,
29        /// The `issues.org` files that define it.
30        paths: Vec<std::path::PathBuf>,
31    },
32    /// Another identity already holds the issue.
33    ClaimConflict {
34        /// The issue that is already claimed.
35        id: String,
36        /// Who holds it.
37        holder: String,
38        /// When the claim was stamped, if the heading recorded it.
39        claimed_at: Option<String>,
40    },
41    /// The edge would close a loop in the blocker graph.
42    BlockerCycle {
43        /// The prospective prerequisite.
44        blocker: String,
45        /// The issue that would wait on it.
46        issue: String,
47    },
48    /// The issue is in a state that cannot be claimed.
49    InvalidState {
50        /// The issue that was refused.
51        id: String,
52        /// The heading state at the time of the refusal.
53        state: String,
54    },
55    /// A write named a last-seen state or generation that no longer holds.
56    StaleWrite {
57        /// The issue that was refused.
58        id: String,
59        /// State the caller required, when `--if-state` was set.
60        expected_state: Option<String>,
61        /// Heading state at the refusal.
62        actual_state: String,
63        /// Generation the caller required, when `--if-gen` was set.
64        expected_gen: Option<u64>,
65        /// Corpus generation at the refusal.
66        actual_gen: Option<u64>,
67    },
68    /// A second terminal disagrees with the one already on the heading.
69    TerminalConflict {
70        /// The issue that already has a terminal state.
71        id: String,
72        /// The terminal already written.
73        held: String,
74        /// The terminal the caller asked for.
75        attempted: String,
76    },
77    /// Any other failure, usually I/O or a parse problem.
78    Other(anyhow::Error),
79}
80
81impl fmt::Display for Error {
82    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
83        match self {
84            Error::NotATracker { root, prefix } => write!(
85                f,
86                "{} is not a tracker: it holds neither vissue.toml nor {prefix}/. \
87                 Name one with --root, or set VISSUE_ROOT",
88                root.display()
89            ),
90            Error::IssueNotFound { id } => write!(f, "issue {id} not found"),
91            Error::DuplicateId { id, paths } => {
92                let listed = paths
93                    .iter()
94                    .map(|p| p.display().to_string())
95                    .collect::<Vec<_>>()
96                    .join(", ");
97                write!(f, "id {id} is defined in more than one tracker: {listed}")
98            }
99            Error::ClaimConflict {
100                id,
101                holder,
102                claimed_at,
103            } => write!(
104                f,
105                "{id} is claimed by {holder} since {}; pass --force to take it over",
106                claimed_at.as_deref().unwrap_or("an unknown time")
107            ),
108            Error::BlockerCycle { blocker, issue } if blocker == issue => {
109                write!(f, "issue {issue} cannot block itself")
110            }
111            Error::BlockerCycle { blocker, issue } => {
112                write!(
113                    f,
114                    "adding {blocker} -> {issue} would create a blocker cycle"
115                )
116            }
117            Error::InvalidState { id, state } => {
118                write!(f, "{id} is already {state}; cannot claim")
119            }
120            Error::StaleWrite {
121                id,
122                expected_state,
123                actual_state,
124                expected_gen,
125                actual_gen,
126            } => match (expected_state, expected_gen) {
127                (Some(want), Some(want_gen)) => write!(
128                    f,
129                    "{id} is {actual_state} at generation {}, not {want} at {want_gen}; write refused",
130                    actual_gen.map_or_else(|| "?".into(), |g| g.to_string())
131                ),
132                (Some(want), None) => {
133                    write!(f, "{id} is {actual_state}, not {want}; write refused")
134                }
135                (None, Some(want_gen)) => write!(
136                    f,
137                    "{id} generation is {}, not {want_gen}; write refused",
138                    actual_gen.map_or_else(|| "?".into(), |g| g.to_string())
139                ),
140                (None, None) => write!(f, "{id} write refused: stale"),
141            },
142            Error::TerminalConflict {
143                id,
144                held,
145                attempted,
146            } => write!(
147                f,
148                "{id} already closed as {held}; {attempted} kept as a sibling"
149            ),
150            Error::Other(err) => write!(f, "{err}"),
151        }
152    }
153}
154
155impl std::error::Error for Error {
156    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
157        match self {
158            Error::Other(err) => Some(err.as_ref()),
159            _ => None,
160        }
161    }
162}
163
164impl From<anyhow::Error> for Error {
165    fn from(err: anyhow::Error) -> Self {
166        match err.downcast::<Error>() {
167            Ok(typed) => typed,
168            Err(other) => Error::Other(other),
169        }
170    }
171}
172
173impl From<std::io::Error> for Error {
174    fn from(err: std::io::Error) -> Self {
175        Error::Other(err.into())
176    }
177}
178
179impl From<fmt::Error> for Error {
180    fn from(err: fmt::Error) -> Self {
181        Error::Other(err.into())
182    }
183}
184
185impl From<serde_json::Error> for Error {
186    fn from(err: serde_json::Error) -> Self {
187        Error::Other(err.into())
188    }
189}