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    /// Another identity already holds the issue.
17    ClaimConflict {
18        /// The issue that is already claimed.
19        id: String,
20        /// Who holds it.
21        holder: String,
22        /// When the claim was stamped, if the heading recorded it.
23        claimed_at: Option<String>,
24    },
25    /// The edge would close a loop in the blocker graph.
26    BlockerCycle {
27        /// The prospective prerequisite.
28        blocker: String,
29        /// The issue that would wait on it.
30        issue: String,
31    },
32    /// The issue is in a state that cannot be claimed.
33    InvalidState {
34        /// The issue that was refused.
35        id: String,
36        /// The heading state at the time of the refusal.
37        state: String,
38    },
39    /// Any other failure, usually I/O or a parse problem.
40    Other(anyhow::Error),
41}
42
43impl fmt::Display for Error {
44    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Error::IssueNotFound { id } => write!(f, "issue {id} not found"),
47            Error::ClaimConflict {
48                id,
49                holder,
50                claimed_at,
51            } => write!(
52                f,
53                "{id} is claimed by {holder} since {}; pass --force to take it over",
54                claimed_at.as_deref().unwrap_or("an unknown time")
55            ),
56            Error::BlockerCycle { blocker, issue } if blocker == issue => {
57                write!(f, "issue {issue} cannot block itself")
58            }
59            Error::BlockerCycle { blocker, issue } => {
60                write!(
61                    f,
62                    "adding {blocker} -> {issue} would create a blocker cycle"
63                )
64            }
65            Error::InvalidState { id, state } => {
66                write!(f, "{id} is already {state}; cannot claim")
67            }
68            Error::Other(err) => write!(f, "{err}"),
69        }
70    }
71}
72
73impl std::error::Error for Error {
74    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
75        match self {
76            Error::Other(err) => Some(err.as_ref()),
77            _ => None,
78        }
79    }
80}
81
82impl From<anyhow::Error> for Error {
83    fn from(err: anyhow::Error) -> Self {
84        match err.downcast::<Error>() {
85            Ok(typed) => typed,
86            Err(other) => Error::Other(other),
87        }
88    }
89}
90
91impl From<std::io::Error> for Error {
92    fn from(err: std::io::Error) -> Self {
93        Error::Other(err.into())
94    }
95}
96
97impl From<fmt::Error> for Error {
98    fn from(err: fmt::Error) -> Self {
99        Error::Other(err.into())
100    }
101}
102
103impl From<serde_json::Error> for Error {
104    fn from(err: serde_json::Error) -> Self {
105        Error::Other(err.into())
106    }
107}