1use std::{error::Error, fmt};
2
3pub type Result<T> = std::result::Result<T, TuffError>;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum ErrorKind {
13 Usage,
15 NotFound,
17 Refused,
19 Drift,
21 Source,
23 Corrupt,
25 Unsupported,
28 Io,
30 Internal,
32}
33
34impl ErrorKind {
35 pub fn as_str(self) -> &'static str {
37 match self {
38 Self::Usage => "usage",
39 Self::NotFound => "not_found",
40 Self::Refused => "refused",
41 Self::Drift => "drift",
42 Self::Source => "source",
43 Self::Corrupt => "corrupt",
44 Self::Unsupported => "unsupported",
45 Self::Io => "io",
46 Self::Internal => "internal",
47 }
48 }
49
50 pub fn exit_code(self) -> i32 {
57 match self {
58 Self::Usage => 2,
59 Self::Internal => 70,
60 _ => 1,
61 }
62 }
63}
64
65impl fmt::Display for ErrorKind {
66 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67 f.write_str(self.as_str())
68 }
69}
70
71#[derive(Debug)]
74pub struct TuffError {
75 kind: ErrorKind,
76 message: String,
77 hint: Option<String>,
78 source: Option<Box<dyn Error + Send + Sync>>,
79}
80
81impl TuffError {
82 pub fn of(kind: ErrorKind, message: impl Into<String>) -> Self {
84 Self {
85 kind,
86 message: message.into(),
87 hint: None,
88 source: None,
89 }
90 }
91
92 pub fn new(message: impl Into<String>) -> Self {
98 Self::of(ErrorKind::Internal, message)
99 }
100
101 pub fn usage(message: impl Into<String>) -> Self {
102 Self::of(ErrorKind::Usage, message)
103 }
104
105 pub fn not_found(message: impl Into<String>) -> Self {
106 Self::of(ErrorKind::NotFound, message)
107 }
108
109 pub fn refused(message: impl Into<String>) -> Self {
110 Self::of(ErrorKind::Refused, message)
111 }
112
113 pub fn drift(message: impl Into<String>) -> Self {
114 Self::of(ErrorKind::Drift, message)
115 }
116
117 pub fn source_failed(message: impl Into<String>) -> Self {
118 Self::of(ErrorKind::Source, message)
119 }
120
121 pub fn corrupt(message: impl Into<String>) -> Self {
122 Self::of(ErrorKind::Corrupt, message)
123 }
124
125 pub fn unsupported(message: impl Into<String>) -> Self {
126 Self::of(ErrorKind::Unsupported, message)
127 }
128
129 #[must_use]
133 pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
134 self.hint = Some(hint.into());
135 self
136 }
137
138 #[must_use]
140 pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
141 self.source = Some(Box::new(source));
142 self
143 }
144
145 pub fn kind(&self) -> ErrorKind {
146 self.kind
147 }
148
149 pub fn message(&self) -> &str {
150 &self.message
151 }
152
153 pub fn hint(&self) -> Option<&str> {
154 self.hint.as_deref()
155 }
156
157 pub fn exit_code(&self) -> i32 {
158 self.kind.exit_code()
159 }
160}
161
162impl fmt::Display for TuffError {
163 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164 f.write_str(&self.message)
165 }
166}
167
168impl Error for TuffError {
169 fn source(&self) -> Option<&(dyn Error + 'static)> {
170 self.source
171 .as_ref()
172 .map(|source| source.as_ref() as &(dyn Error + 'static))
173 }
174}
175
176impl From<std::io::Error> for TuffError {
177 fn from(error: std::io::Error) -> Self {
178 Self::of(ErrorKind::Io, error.to_string()).with_source(error)
179 }
180}
181
182impl From<serde_json::Error> for TuffError {
183 fn from(error: serde_json::Error) -> Self {
184 Self::of(ErrorKind::Corrupt, error.to_string()).with_source(error)
185 }
186}
187
188impl From<toml::de::Error> for TuffError {
189 fn from(error: toml::de::Error) -> Self {
190 Self::of(
191 ErrorKind::Corrupt,
192 format!("invalid capability manifest TOML: {error}"),
193 )
194 .with_source(error)
195 }
196}
197
198impl From<toml::ser::Error> for TuffError {
199 fn from(error: toml::ser::Error) -> Self {
200 Self::of(
201 ErrorKind::Internal,
202 format!("could not serialize TOML: {error}"),
203 )
204 .with_source(error)
205 }
206}
207
208#[cfg(test)]
209mod tests {
210 use super::*;
211
212 #[test]
213 fn tuff_error_displays_message() {
214 let err = TuffError::new("test error");
215 assert_eq!(format!("{}", err), "test error");
216 }
217
218 #[test]
219 fn tuff_error_from_io_error() {
220 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
221 let err: TuffError = io_err.into();
222 assert!(format!("{}", err).contains("file not found"));
223 assert_eq!(err.kind(), ErrorKind::Io);
224 assert!(err.source().is_some(), "the cause chain is preserved");
225 }
226
227 #[test]
228 fn tuff_error_from_serde_json_error() {
229 let json_err = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
230 let err: TuffError = json_err.into();
231 assert!(!format!("{}", err).is_empty());
232 assert_eq!(err.kind(), ErrorKind::Corrupt);
233 }
234
235 #[test]
236 fn exit_codes_separate_the_three_audiences() {
237 assert_eq!(TuffError::usage("bad flag").exit_code(), 2);
240 assert_eq!(TuffError::new("invariant broken").exit_code(), 70);
241 for error in [
242 TuffError::not_found("x"),
243 TuffError::refused("x"),
244 TuffError::drift("x"),
245 TuffError::source_failed("x"),
246 TuffError::corrupt("x"),
247 TuffError::unsupported("x"),
248 ] {
249 assert_eq!(error.exit_code(), 1, "{:?}", error.kind());
250 }
251 }
252
253 #[test]
254 fn a_hint_is_carried_separately_from_the_message() {
255 let error =
256 TuffError::drift("'x' has local changes").with_hint("use --force to replace it");
257 assert_eq!(format!("{error}"), "'x' has local changes");
258 assert_eq!(error.hint(), Some("use --force to replace it"));
259 assert_eq!(error.kind().as_str(), "drift");
260 }
261}