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("io: {0}")]
48 Io(#[from] std::io::Error),
49
50 #[error(transparent)]
52 Other(#[from] anyhow::Error),
53}
54
55impl AppError {
56 #[must_use]
61 pub fn exit_code(&self) -> u8 {
62 match self {
63 Self::Violations { .. } => 1,
64 Self::Usage(_) => 64,
65 Self::ManifestInvalid(_) | Self::Marker(_) => 65,
66 Self::ManifestMissing(_) => 66,
67 Self::Refused(_) => 73,
68 Self::Io(e) if e.kind() == std::io::ErrorKind::NotFound => 66,
69 Self::Io(e) if e.kind() == std::io::ErrorKind::PermissionDenied => 77,
70 Self::Io(_) => 74,
71 Self::Other(_) => 70,
72 }
73 }
74
75 #[must_use]
77 pub const fn kind(&self) -> &'static str {
78 match self {
79 Self::Usage(_) => "Usage",
80 Self::Violations { .. } => "Violations",
81 Self::ManifestMissing(_) => "ManifestMissing",
82 Self::ManifestInvalid(_) => "ManifestInvalid",
83 Self::Marker(_) => "Marker",
84 Self::Refused(_) => "Refused",
85 Self::Io(_) => "Io",
86 Self::Other(_) => "Other",
87 }
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 fn io(kind: std::io::ErrorKind) -> AppError {
96 AppError::Io(std::io::Error::from(kind))
97 }
98
99 #[test]
100 fn violations_are_one() {
101 assert_eq!(AppError::Violations { count: 3 }.exit_code(), 1);
102 }
103
104 #[test]
105 fn usage_is_sixty_four() {
106 assert_eq!(AppError::Usage("bad target".into()).exit_code(), 64);
107 }
108
109 #[test]
110 fn invalid_manifest_is_sixty_five() {
111 assert_eq!(
112 AppError::ManifestInvalid("schema_version 3".into()).exit_code(),
113 65
114 );
115 }
116
117 #[test]
118 fn malformed_marker_is_sixty_five() {
119 assert_eq!(AppError::Marker(MarkerError::Malformed).exit_code(), 65);
120 }
121
122 #[test]
123 fn missing_manifest_is_sixty_six() {
124 assert_eq!(
125 AppError::ManifestMissing("x/.spec-driven-docs/manifest.json".into()).exit_code(),
126 66
127 );
128 }
129
130 #[test]
131 fn refused_is_seventy_three() {
132 assert_eq!(
133 AppError::Refused("apply aborted; the target was restored".into()).exit_code(),
134 73
135 );
136 }
137
138 #[test]
139 fn not_found_io_is_sixty_six() {
140 assert_eq!(io(std::io::ErrorKind::NotFound).exit_code(), 66);
141 }
142
143 #[test]
144 fn permission_denied_io_is_seventy_seven() {
145 assert_eq!(io(std::io::ErrorKind::PermissionDenied).exit_code(), 77);
146 }
147
148 #[test]
149 fn other_io_is_seventy_four() {
150 assert_eq!(io(std::io::ErrorKind::BrokenPipe).exit_code(), 74);
151 }
152
153 #[test]
154 fn anyhow_is_seventy() {
155 assert_eq!(AppError::Other(anyhow::anyhow!("boom")).exit_code(), 70);
156 }
157}