spec_driven_docs/
error.rs1use camino::Utf8PathBuf;
10use thiserror::Error;
11
12use crate::domain::debt::DebtError;
13use crate::domain::marker::MarkerError;
14
15#[derive(Debug, Error)]
17pub enum AppError {
18 #[error("usage: {0}")]
20 Usage(String),
21
22 #[error("{count} violation(s) found")]
25 Violations {
26 count: usize,
28 },
29
30 #[error("missing manifest: {0}")]
32 ManifestMissing(Utf8PathBuf),
33
34 #[error("invalid manifest: {0}")]
36 ManifestInvalid(String),
37
38 #[error(transparent)]
40 Marker(#[from] MarkerError),
41
42 #[error(transparent)]
45 Debt(#[from] DebtError),
46
47 #[error("{0}")]
50 Refused(String),
51
52 #[error("busy: {0}")]
54 Busy(String),
55
56 #[error("unrecovered: {0}")]
60 Unrecovered(String),
61
62 #[error("receipt: {0}")]
66 Receipt(String),
67
68 #[error("git: {message}")]
71 Git {
72 message: String,
74 code: u8,
76 },
77
78 #[error("io: {0}")]
80 Io(#[from] std::io::Error),
81
82 #[error(transparent)]
84 Other(#[from] anyhow::Error),
85}
86
87impl AppError {
88 #[must_use]
93 pub fn exit_code(&self) -> u8 {
94 match self {
95 Self::Violations { .. } => 1,
96 Self::Usage(_) => 64,
97 Self::ManifestInvalid(_)
98 | Self::Marker(_)
99 | Self::Debt(
100 DebtError::Shape(_) | DebtError::Malformed { .. } | DebtError::TwoFormats,
101 ) => 65,
102 Self::ManifestMissing(_) => 66,
103 Self::Refused(_)
104 | Self::Busy(_)
105 | Self::Unrecovered(_)
106 | Self::Receipt(_)
107 | Self::Debt(_) => 73,
108 Self::Git { code, .. } => *code,
109 Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
110 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
111 Self::Io(_) => 74,
112 Self::Other(_) => 70,
113 }
114 }
115
116 #[must_use]
118 pub const fn kind(&self) -> &'static str {
119 match self {
120 Self::Usage(_) => "Usage",
121 Self::Violations { .. } => "Violations",
122 Self::ManifestMissing(_) => "ManifestMissing",
123 Self::ManifestInvalid(_) => "ManifestInvalid",
124 Self::Marker(_) => "Marker",
125 Self::Debt(_) => "Debt",
126 Self::Refused(_) => "Refused",
127 Self::Busy(_) => "Busy",
128 Self::Unrecovered(_) => "Unrecovered",
129 Self::Receipt(_) => "Receipt",
130 Self::Git { .. } => "Git",
131 Self::Io(_) => "Io",
132 Self::Other(_) => "Other",
133 }
134 }
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn io(kind: std::io::ErrorKind) -> AppError {
142 AppError::Io(std::io::Error::from(kind))
143 }
144
145 #[test]
146 fn violations_are_one() {
147 assert_eq!(AppError::Violations { count: 3 }.exit_code(), 1);
148 }
149
150 #[test]
151 fn usage_is_sixty_four() {
152 assert_eq!(AppError::Usage("bad target".into()).exit_code(), 64);
153 }
154
155 #[test]
156 fn invalid_manifest_is_sixty_five() {
157 assert_eq!(
158 AppError::ManifestInvalid("schema_version 3".into()).exit_code(),
159 65
160 );
161 }
162
163 #[test]
164 fn malformed_marker_is_sixty_five() {
165 assert_eq!(AppError::Marker(MarkerError::Malformed).exit_code(), 65);
166 }
167
168 #[test]
169 fn a_malformed_debt_file_is_sixty_five_and_a_refused_debt_verb_is_seventy_three() {
170 assert_eq!(
171 AppError::Debt(DebtError::Shape("not yaml".into())).exit_code(),
172 65
173 );
174 assert_eq!(AppError::Debt(DebtError::TwoFormats).exit_code(), 65);
175 assert_eq!(AppError::Debt(DebtError::AlreadyBaselined).exit_code(), 73);
176 assert_eq!(AppError::Debt(DebtError::NothingToMigrate).exit_code(), 73);
177 }
178
179 #[test]
180 fn missing_manifest_is_sixty_six() {
181 assert_eq!(
182 AppError::ManifestMissing("x/.spec-driven-docs/manifest.json".into()).exit_code(),
183 66
184 );
185 }
186
187 #[test]
188 fn refused_is_seventy_three() {
189 assert_eq!(
190 AppError::Refused("apply aborted; the target was restored".into()).exit_code(),
191 73
192 );
193 }
194
195 #[test]
196 fn not_found_io_is_sixty_six() {
197 assert_eq!(io(std::io::ErrorKind::NotFound).exit_code(), 66);
198 }
199
200 #[test]
201 fn permission_denied_io_is_seventy_seven() {
202 assert_eq!(io(std::io::ErrorKind::PermissionDenied).exit_code(), 77);
203 }
204
205 #[test]
206 fn other_io_is_seventy_four() {
207 assert_eq!(io(std::io::ErrorKind::BrokenPipe).exit_code(), 74);
208 }
209
210 #[test]
211 fn anyhow_is_seventy() {
212 assert_eq!(AppError::Other(anyhow::anyhow!("boom")).exit_code(), 70);
213 }
214}