1use thiserror::Error;
12
13use crate::diagnostic::{Diagnostic, Reason};
14
15#[derive(Debug, Error)]
17pub enum RkError {
18 #[error("usage: {0}")]
20 Usage(String),
21
22 #[error("no {kind} named '{name}'; run with --list to see the valid names")]
25 NotFound {
26 kind: &'static str,
29 name: String,
31 },
32
33 #[error("{0}")]
36 Refused(String),
37
38 #[error("{}", .0.message)]
42 Refusal(Box<Diagnostic>),
43
44 #[error("{}", .0.message)]
48 Missing(Box<Diagnostic>),
49
50 #[error("{}", .0.message)]
53 CheckFailed(Box<Diagnostic>),
54
55 #[error("{}", .0.message)]
58 Subprocess(Box<Diagnostic>),
59
60 #[error("io: {0}")]
62 Io(#[from] std::io::Error),
63
64 #[error(transparent)]
66 Other(#[from] anyhow::Error),
67}
68
69impl RkError {
70 #[must_use]
72 pub fn refusal(diagnostic: Diagnostic) -> Self {
73 Self::Refusal(Box::new(diagnostic))
74 }
75
76 #[must_use]
78 pub fn missing(diagnostic: Diagnostic) -> Self {
79 Self::Missing(Box::new(diagnostic))
80 }
81
82 #[must_use]
84 pub fn check_failed(diagnostic: Diagnostic) -> Self {
85 Self::CheckFailed(Box::new(diagnostic))
86 }
87
88 #[must_use]
90 pub fn subprocess(diagnostic: Diagnostic) -> Self {
91 Self::Subprocess(Box::new(diagnostic))
92 }
93
94 #[must_use]
99 pub fn exit_code(&self) -> u8 {
100 match self {
101 Self::Usage(_) => 64,
102 Self::NotFound { .. } | Self::Missing(_) => 66,
103 Self::CheckFailed(_) => 1,
104 Self::Refused(_) | Self::Refusal(_) => 73,
105 Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
106 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
107 Self::Io(_) => 74,
108 Self::Subprocess(_) | Self::Other(_) => 70,
109 }
110 }
111
112 #[must_use]
115 pub fn reason(&self) -> Reason {
116 match self {
117 Self::Usage(_) | Self::NotFound { .. } => Reason::Usage,
118 Self::Refused(_) => Reason::StateDrift,
119 Self::Refusal(diagnostic)
120 | Self::Missing(diagnostic)
121 | Self::CheckFailed(diagnostic)
122 | Self::Subprocess(diagnostic) => diagnostic.reason,
123 Self::Io(_) => Reason::Io,
124 Self::Other(_) => Reason::Internal,
125 }
126 }
127
128 #[must_use]
130 pub fn diagnostic(&self) -> Diagnostic {
131 match self {
132 Self::Refusal(diagnostic)
133 | Self::Missing(diagnostic)
134 | Self::CheckFailed(diagnostic)
135 | Self::Subprocess(diagnostic) => (**diagnostic).clone(),
136 _ => Diagnostic::new(self.reason(), self.to_string()),
137 }
138 }
139}
140
141#[cfg(test)]
142mod tests {
143 use super::RkError;
144 use crate::diagnostic::{Diagnostic, Reason};
145
146 #[test]
147 fn exit_code_matrix() {
148 let cases: Vec<(RkError, u8)> = vec![
149 (RkError::Usage("bad".into()), 64),
150 (
151 RkError::NotFound {
152 kind: "chapter",
153 name: "nope".into(),
154 },
155 66,
156 ),
157 (RkError::Refused("left unchanged".into()), 73),
158 (
159 RkError::refusal(Diagnostic::new(Reason::TargetNotFound, "no target")),
160 73,
161 ),
162 (
163 RkError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone")),
164 66,
165 ),
166 (
167 RkError::Io(std::io::Error::new(
168 std::io::ErrorKind::PermissionDenied,
169 "denied",
170 )),
171 77,
172 ),
173 (RkError::Io(std::io::Error::other("disk fell over")), 74),
174 (RkError::Other(anyhow::anyhow!("unclassified")), 70),
175 ];
176 for (err, code) in cases {
177 assert_eq!(err.exit_code(), code, "wrong exit code for {err:?}");
178 }
179 }
180
181 #[test]
184 fn every_error_carries_a_reason() {
185 assert_eq!(RkError::Usage("bad".into()).reason(), Reason::Usage);
186 assert_eq!(
187 RkError::Refused("left unchanged".into()).reason(),
188 Reason::StateDrift
189 );
190 assert_eq!(
191 RkError::refusal(Diagnostic::new(Reason::JournalUnavailable, "no journal")).reason(),
192 Reason::JournalUnavailable
193 );
194 assert_eq!(
195 RkError::Other(anyhow::anyhow!("unclassified")).reason(),
196 Reason::Internal
197 );
198 }
199}