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::ConfigInvalid, "bad config")),
160 73,
161 ),
162 (
163 RkError::refusal(Diagnostic::new(Reason::TargetNotFound, "no target")),
164 73,
165 ),
166 (
167 RkError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "gone")),
168 66,
169 ),
170 (
171 RkError::Io(std::io::Error::new(
172 std::io::ErrorKind::PermissionDenied,
173 "denied",
174 )),
175 77,
176 ),
177 (RkError::Io(std::io::Error::other("disk fell over")), 74),
178 (RkError::Other(anyhow::anyhow!("unclassified")), 70),
179 ];
180 for (err, code) in cases {
181 assert_eq!(err.exit_code(), code, "wrong exit code for {err:?}");
182 }
183 }
184
185 #[test]
188 fn every_error_carries_a_reason() {
189 assert_eq!(RkError::Usage("bad".into()).reason(), Reason::Usage);
190 assert_eq!(
191 RkError::Refused("left unchanged".into()).reason(),
192 Reason::StateDrift
193 );
194 assert_eq!(
195 RkError::refusal(Diagnostic::new(Reason::JournalUnavailable, "no journal")).reason(),
196 Reason::JournalUnavailable
197 );
198 assert_eq!(
199 RkError::Other(anyhow::anyhow!("unclassified")).reason(),
200 Reason::Internal
201 );
202 }
203}