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