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("git: {message}")]
55 Git {
56 message: String,
58 code: u8,
60 },
61
62 #[error("io: {0}")]
64 Io(#[from] std::io::Error),
65
66 #[error(transparent)]
68 Other(#[from] anyhow::Error),
69}
70
71impl AppError {
72 #[must_use]
77 pub fn exit_code(&self) -> u8 {
78 match self {
79 Self::Violations { .. } => 1,
80 Self::Usage(_) => 64,
81 Self::ManifestInvalid(_)
82 | Self::Marker(_)
83 | Self::Debt(
84 DebtError::Shape(_) | DebtError::Malformed { .. } | DebtError::TwoFormats,
85 ) => 65,
86 Self::ManifestMissing(_) => 66,
87 Self::Refused(_) | Self::Debt(_) => 73,
88 Self::Git { code, .. } => *code,
89 Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
90 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
91 Self::Io(_) => 74,
92 Self::Other(_) => 70,
93 }
94 }
95
96 #[must_use]
98 pub const fn kind(&self) -> &'static str {
99 match self {
100 Self::Usage(_) => "Usage",
101 Self::Violations { .. } => "Violations",
102 Self::ManifestMissing(_) => "ManifestMissing",
103 Self::ManifestInvalid(_) => "ManifestInvalid",
104 Self::Marker(_) => "Marker",
105 Self::Debt(_) => "Debt",
106 Self::Refused(_) => "Refused",
107 Self::Git { .. } => "Git",
108 Self::Io(_) => "Io",
109 Self::Other(_) => "Other",
110 }
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117
118 fn io(kind: std::io::ErrorKind) -> AppError {
119 AppError::Io(std::io::Error::from(kind))
120 }
121
122 #[test]
123 fn violations_are_one() {
124 assert_eq!(AppError::Violations { count: 3 }.exit_code(), 1);
125 }
126
127 #[test]
128 fn usage_is_sixty_four() {
129 assert_eq!(AppError::Usage("bad target".into()).exit_code(), 64);
130 }
131
132 #[test]
133 fn invalid_manifest_is_sixty_five() {
134 assert_eq!(
135 AppError::ManifestInvalid("schema_version 3".into()).exit_code(),
136 65
137 );
138 }
139
140 #[test]
141 fn malformed_marker_is_sixty_five() {
142 assert_eq!(AppError::Marker(MarkerError::Malformed).exit_code(), 65);
143 }
144
145 #[test]
146 fn a_malformed_debt_file_is_sixty_five_and_a_refused_debt_verb_is_seventy_three() {
147 assert_eq!(
148 AppError::Debt(DebtError::Shape("not yaml".into())).exit_code(),
149 65
150 );
151 assert_eq!(AppError::Debt(DebtError::TwoFormats).exit_code(), 65);
152 assert_eq!(AppError::Debt(DebtError::AlreadyBaselined).exit_code(), 73);
153 assert_eq!(AppError::Debt(DebtError::NothingToMigrate).exit_code(), 73);
154 }
155
156 #[test]
157 fn missing_manifest_is_sixty_six() {
158 assert_eq!(
159 AppError::ManifestMissing("x/.spec-driven-docs/manifest.json".into()).exit_code(),
160 66
161 );
162 }
163
164 #[test]
165 fn refused_is_seventy_three() {
166 assert_eq!(
167 AppError::Refused("apply aborted; the target was restored".into()).exit_code(),
168 73
169 );
170 }
171
172 #[test]
173 fn not_found_io_is_sixty_six() {
174 assert_eq!(io(std::io::ErrorKind::NotFound).exit_code(), 66);
175 }
176
177 #[test]
178 fn permission_denied_io_is_seventy_seven() {
179 assert_eq!(io(std::io::ErrorKind::PermissionDenied).exit_code(), 77);
180 }
181
182 #[test]
183 fn other_io_is_seventy_four() {
184 assert_eq!(io(std::io::ErrorKind::BrokenPipe).exit_code(), 74);
185 }
186
187 #[test]
188 fn anyhow_is_seventy() {
189 assert_eq!(AppError::Other(anyhow::anyhow!("boom")).exit_code(), 70);
190 }
191}