spec_driven_docs/
error.rs1use camino::Utf8PathBuf;
10use thiserror::Error;
11
12use crate::domain::marker::MarkerError;
13
14#[derive(Debug, Error)]
16pub enum AppError {
17 #[error("usage: {0}")]
19 Usage(String),
20
21 #[error("{count} violation(s) found")]
24 Violations {
25 count: usize,
27 },
28
29 #[error("missing manifest: {0}")]
31 ManifestMissing(Utf8PathBuf),
32
33 #[error("invalid manifest: {0}")]
35 ManifestInvalid(String),
36
37 #[error(transparent)]
39 Marker(#[from] MarkerError),
40
41 #[error("{0}")]
44 Refused(String),
45
46 #[error("git: {message}")]
49 Git {
50 message: String,
52 code: u8,
54 },
55
56 #[error("io: {0}")]
58 Io(#[from] std::io::Error),
59
60 #[error(transparent)]
62 Other(#[from] anyhow::Error),
63}
64
65impl AppError {
66 #[must_use]
71 pub fn exit_code(&self) -> u8 {
72 match self {
73 Self::Violations { .. } => 1,
74 Self::Usage(_) => 64,
75 Self::ManifestInvalid(_) | Self::Marker(_) => 65,
76 Self::ManifestMissing(_) => 66,
77 Self::Refused(_) => 73,
78 Self::Git { code, .. } => *code,
79 Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
80 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
81 Self::Io(_) => 74,
82 Self::Other(_) => 70,
83 }
84 }
85
86 #[must_use]
88 pub const fn kind(&self) -> &'static str {
89 match self {
90 Self::Usage(_) => "Usage",
91 Self::Violations { .. } => "Violations",
92 Self::ManifestMissing(_) => "ManifestMissing",
93 Self::ManifestInvalid(_) => "ManifestInvalid",
94 Self::Marker(_) => "Marker",
95 Self::Refused(_) => "Refused",
96 Self::Git { .. } => "Git",
97 Self::Io(_) => "Io",
98 Self::Other(_) => "Other",
99 }
100 }
101}
102
103#[cfg(test)]
104mod tests {
105 use super::*;
106
107 fn io(kind: std::io::ErrorKind) -> AppError {
108 AppError::Io(std::io::Error::from(kind))
109 }
110
111 #[test]
112 fn violations_are_one() {
113 assert_eq!(AppError::Violations { count: 3 }.exit_code(), 1);
114 }
115
116 #[test]
117 fn usage_is_sixty_four() {
118 assert_eq!(AppError::Usage("bad target".into()).exit_code(), 64);
119 }
120
121 #[test]
122 fn invalid_manifest_is_sixty_five() {
123 assert_eq!(
124 AppError::ManifestInvalid("schema_version 3".into()).exit_code(),
125 65
126 );
127 }
128
129 #[test]
130 fn malformed_marker_is_sixty_five() {
131 assert_eq!(AppError::Marker(MarkerError::Malformed).exit_code(), 65);
132 }
133
134 #[test]
135 fn missing_manifest_is_sixty_six() {
136 assert_eq!(
137 AppError::ManifestMissing("x/.spec-driven-docs/manifest.json".into()).exit_code(),
138 66
139 );
140 }
141
142 #[test]
143 fn refused_is_seventy_three() {
144 assert_eq!(
145 AppError::Refused("apply aborted; the target was restored".into()).exit_code(),
146 73
147 );
148 }
149
150 #[test]
151 fn not_found_io_is_sixty_six() {
152 assert_eq!(io(std::io::ErrorKind::NotFound).exit_code(), 66);
153 }
154
155 #[test]
156 fn permission_denied_io_is_seventy_seven() {
157 assert_eq!(io(std::io::ErrorKind::PermissionDenied).exit_code(), 77);
158 }
159
160 #[test]
161 fn other_io_is_seventy_four() {
162 assert_eq!(io(std::io::ErrorKind::BrokenPipe).exit_code(), 74);
163 }
164
165 #[test]
166 fn anyhow_is_seventy() {
167 assert_eq!(AppError::Other(anyhow::anyhow!("boom")).exit_code(), 70);
168 }
169}