1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
//! The crate's error type.
// @zen-component: KIT-Error
use std::{fmt, io, path::PathBuf};
use crate::harness::Scope;
/// Everything that can go wrong in the library. Every variant but [`Error::Io`]
/// and [`Error::Internal`] is a refusal: `install` returns it before any
/// write.
// @zen-impl: KIT-16_AC-1
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
/// A scope name that is not `project` or `user`.
UnknownScope {
/// The name given.
name: String,
},
/// A hook event name that is not one of [`Event`](crate::hook::Event)'s.
UnknownEvent {
/// The name given.
name: String,
},
/// A harness id the tool does not support.
UnknownHarness {
/// The harness id given.
harness: String,
},
/// A harness that has no files at the scope.
UnsupportedScope {
/// The harness id.
harness: String,
/// The scope given.
scope: Scope,
},
/// A part name (e.g. in `--without`) no named harness's profile has.
UnknownPart {
/// The harness, or the harnesses named, comma-separated.
harness: String,
/// The part name given.
part: String,
},
/// Something a harness cannot do, e.g. an answer it cannot express for
/// an event.
Unsupported {
/// The harness id.
harness: String,
/// What it cannot do, e.g. `answer deny at stop`.
what: String,
},
/// A file the library must read does not parse or has the wrong shape.
File {
/// The file, as the tool shows it.
file: String,
/// What is wrong with it.
message: String,
},
/// A refusal from the tool's own [`ExternalPart`] or [`DeclinedStore`].
///
/// [`ExternalPart`]: crate::harness::ExternalPart
/// [`DeclinedStore`]: crate::harness::DeclinedStore
Refused(String),
/// An I/O failure on `path`.
Io {
/// The path being operated on.
path: PathBuf,
/// The underlying I/O error.
source: io::Error,
},
/// A bug, or an inconsistent integration or harness.
Internal(String),
}
impl Error {
/// An [`Error::Io`] for `path` from an [`io::Error`].
pub fn io(path: impl Into<PathBuf>, source: io::Error) -> Self {
Error::Io {
path: path.into(),
source,
}
}
/// An [`Error::File`]: `file` (as the tool shows it) and what is wrong.
pub fn file(file: impl Into<String>, message: impl Into<String>) -> Self {
Error::File {
file: file.into(),
message: message.into(),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::UnknownScope { name } => write!(f, "no scope named `{name}`"),
Error::UnknownEvent { name } => write!(f, "no hook event named `{name}`"),
Error::UnknownHarness { harness } => write!(f, "no harness named `{harness}`"),
Error::UnsupportedScope { harness, scope } => {
write!(f, "harness `{harness}` has no {scope} scope")
}
Error::UnknownPart { harness, part } => {
write!(f, "harness `{harness}` has no part named `{part}`")
}
Error::Unsupported { harness, what } => {
write!(f, "harness `{harness}` cannot {what}")
}
Error::File { file, message } => write!(f, "{file}: {message}"),
Error::Refused(message) => f.write_str(message),
Error::Io { path, source } => write!(f, "{}: {source}", path.display()),
Error::Internal(message) => write!(f, "internal: {message}"),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Io { source, .. } => Some(source),
_ => None,
}
}
}
/// The crate's result type.
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
// @zen-test: KIT-16_AC-1
#[test]
fn each_kind_displays_one_line() {
let e = Error::io(
"/tmp/x",
io::Error::new(io::ErrorKind::PermissionDenied, "denied"),
);
let msg = e.to_string();
assert!(msg.contains("/tmp/x") && msg.contains("denied"), "{msg}");
assert!(std::error::Error::source(&e).is_some());
let cases = [
(
Error::UnknownScope { name: "g".into() },
"no scope named `g`",
),
(
Error::UnknownEvent { name: "e".into() },
"no hook event named `e`",
),
(
Error::UnknownHarness {
harness: "x".into(),
},
"no harness named `x`",
),
(
Error::UnsupportedScope {
harness: "cursor".into(),
scope: Scope::Local,
},
"harness `cursor` has no local scope",
),
(
Error::UnknownPart {
harness: "claude".into(),
part: "p".into(),
},
"harness `claude` has no part named `p`",
),
(
Error::Unsupported {
harness: "copilot".into(),
what: "answer context at stop".into(),
},
"harness `copilot` cannot answer context at stop",
),
(
Error::file("a.json", "is not a JSON object"),
"a.json: is not a JSON object",
),
(Error::Refused("could not run x".into()), "could not run x"),
(Error::Internal("bug".into()), "internal: bug"),
];
for (e, text) in cases {
assert_eq!(e.to_string(), text);
assert!(std::error::Error::source(&e).is_none());
}
}
}