1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum ErrorCode {
8 InvalidInput,
10 Unauthenticated,
12 Forbidden,
14 NotFound,
16 Conflict,
18 FailedPrecondition,
20 RateLimited,
22 Internal,
24 NotImplemented,
26 Unavailable,
28}
29
30impl ErrorCode {
31 pub fn http_status(&self) -> u16 {
33 match self {
34 ErrorCode::InvalidInput => 400,
35 ErrorCode::Unauthenticated => 401,
36 ErrorCode::Forbidden => 403,
37 ErrorCode::NotFound => 404,
38 ErrorCode::Conflict => 409,
39 ErrorCode::FailedPrecondition => 422,
40 ErrorCode::RateLimited => 429,
41 ErrorCode::Internal => 500,
42 ErrorCode::NotImplemented => 501,
43 ErrorCode::Unavailable => 503,
44 }
45 }
46
47 pub fn exit_code(&self) -> i32 {
49 match self {
50 ErrorCode::NotFound => 1,
51 ErrorCode::InvalidInput => 2,
52 ErrorCode::Unauthenticated | ErrorCode::Forbidden => 3,
53 ErrorCode::Conflict | ErrorCode::FailedPrecondition => 4,
54 ErrorCode::RateLimited => 5,
55 ErrorCode::Internal | ErrorCode::Unavailable => 1,
56 ErrorCode::NotImplemented => 1,
57 }
58 }
59
60 pub fn grpc_code(&self) -> &'static str {
62 match self {
63 ErrorCode::InvalidInput => "INVALID_ARGUMENT",
64 ErrorCode::Unauthenticated => "UNAUTHENTICATED",
65 ErrorCode::Forbidden => "PERMISSION_DENIED",
66 ErrorCode::NotFound => "NOT_FOUND",
67 ErrorCode::Conflict => "ALREADY_EXISTS",
68 ErrorCode::FailedPrecondition => "FAILED_PRECONDITION",
69 ErrorCode::RateLimited => "RESOURCE_EXHAUSTED",
70 ErrorCode::Internal => "INTERNAL",
71 ErrorCode::NotImplemented => "UNIMPLEMENTED",
72 ErrorCode::Unavailable => "UNAVAILABLE",
73 }
74 }
75
76 pub fn infer_from_name(name: &str) -> Self {
78 let name_lower = name.to_lowercase();
79
80 if name_lower.contains("notfound")
81 || name_lower.contains("not_found")
82 || name_lower.contains("missing")
83 {
84 ErrorCode::NotFound
85 } else if name_lower.contains("invalid")
86 || name_lower.contains("validation")
87 || name_lower.contains("parse")
88 {
89 ErrorCode::InvalidInput
90 } else if name_lower.contains("unauthorized") || name_lower.contains("unauthenticated") {
91 ErrorCode::Unauthenticated
92 } else if name_lower.contains("forbidden")
93 || name_lower.contains("permission")
94 || name_lower.contains("denied")
95 {
96 ErrorCode::Forbidden
97 } else if name_lower.contains("conflict")
98 || name_lower.contains("exists")
99 || name_lower.contains("duplicate")
100 {
101 ErrorCode::Conflict
102 } else if name_lower.contains("ratelimit")
103 || name_lower.contains("rate_limit")
104 || name_lower.contains("throttle")
105 {
106 ErrorCode::RateLimited
107 } else if name_lower.contains("unavailable") || name_lower.contains("temporarily") {
108 ErrorCode::Unavailable
109 } else if name_lower.contains("unimplemented") || name_lower.contains("not_implemented") {
110 ErrorCode::NotImplemented
111 } else {
112 ErrorCode::Internal
113 }
114 }
115}
116
117pub trait IntoErrorCode {
121 fn error_code(&self) -> ErrorCode;
123
124 fn message(&self) -> String;
126}
127
128impl IntoErrorCode for std::io::Error {
130 fn error_code(&self) -> ErrorCode {
131 match self.kind() {
132 std::io::ErrorKind::NotFound => ErrorCode::NotFound,
133 std::io::ErrorKind::PermissionDenied => ErrorCode::Forbidden,
134 std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
135 ErrorCode::InvalidInput
136 }
137 _ => ErrorCode::Internal,
138 }
139 }
140
141 fn message(&self) -> String {
142 self.to_string()
143 }
144}
145
146#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
148pub struct ErrorResponse {
149 pub code: String,
150 pub message: String,
151 #[serde(skip_serializing_if = "Option::is_none")]
152 pub details: Option<serde_json::Value>,
153}
154
155impl ErrorResponse {
156 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
157 Self {
158 code: format!("{:?}", code).to_uppercase(),
159 message: message.into(),
160 details: None,
161 }
162 }
163
164 pub fn with_details(mut self, details: serde_json::Value) -> Self {
165 self.details = Some(details);
166 self
167 }
168}
169
170impl fmt::Display for ErrorResponse {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(f, "{}: {}", self.code, self.message)
173 }
174}
175
176impl std::error::Error for ErrorResponse {}
177
178#[derive(Debug, Clone)]
182pub struct SchemaValidationError {
183 pub schema_type: String,
185 pub missing_lines: Vec<String>,
187 pub extra_lines: Vec<String>,
189}
190
191impl SchemaValidationError {
192 pub fn new(schema_type: impl Into<String>) -> Self {
194 Self {
195 schema_type: schema_type.into(),
196 missing_lines: Vec::new(),
197 extra_lines: Vec::new(),
198 }
199 }
200
201 pub fn add_missing(&mut self, line: impl Into<String>) {
203 self.missing_lines.push(line.into());
204 }
205
206 pub fn add_extra(&mut self, line: impl Into<String>) {
208 self.extra_lines.push(line.into());
209 }
210
211 pub fn has_differences(&self) -> bool {
213 !self.missing_lines.is_empty() || !self.extra_lines.is_empty()
214 }
215}
216
217impl fmt::Display for SchemaValidationError {
218 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
219 writeln!(f, "{} schema validation failed:", self.schema_type)?;
220
221 if !self.missing_lines.is_empty() {
222 writeln!(f, "\nExpected methods/messages not found in generated:")?;
223 for line in &self.missing_lines {
224 writeln!(f, " - {}", line)?;
225 }
226 }
227
228 if !self.extra_lines.is_empty() {
229 writeln!(f, "\nGenerated methods/messages not in expected:")?;
230 for line in &self.extra_lines {
231 writeln!(f, " + {}", line)?;
232 }
233 }
234
235 writeln!(f)?;
237 writeln!(f, "Hints:")?;
238
239 if !self.missing_lines.is_empty() && !self.extra_lines.is_empty() {
240 writeln!(
241 f,
242 " - Method signature or type may have changed. Check parameter names and types."
243 )?;
244 }
245
246 if !self.missing_lines.is_empty() {
247 writeln!(
248 f,
249 " - Missing items may indicate removed or renamed methods in Rust code."
250 )?;
251 }
252
253 if !self.extra_lines.is_empty() {
254 writeln!(
255 f,
256 " - Extra items may indicate new methods added. Update the schema file."
257 )?;
258 }
259
260 writeln!(
261 f,
262 " - Run `write_{schema}()` to regenerate the schema file.",
263 schema = self.schema_type.to_lowercase()
264 )?;
265
266 Ok(())
267 }
268}
269
270impl std::error::Error for SchemaValidationError {}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275
276 #[test]
277 fn test_error_code_inference() {
278 assert_eq!(ErrorCode::infer_from_name("NotFound"), ErrorCode::NotFound);
279 assert_eq!(
280 ErrorCode::infer_from_name("UserNotFound"),
281 ErrorCode::NotFound
282 );
283 assert_eq!(
284 ErrorCode::infer_from_name("InvalidEmail"),
285 ErrorCode::InvalidInput
286 );
287 assert_eq!(
288 ErrorCode::infer_from_name("Forbidden"),
289 ErrorCode::Forbidden
290 );
291 assert_eq!(
292 ErrorCode::infer_from_name("AlreadyExists"),
293 ErrorCode::Conflict
294 );
295 }
296
297 #[test]
298 fn test_http_status_codes() {
299 assert_eq!(ErrorCode::NotFound.http_status(), 404);
300 assert_eq!(ErrorCode::InvalidInput.http_status(), 400);
301 assert_eq!(ErrorCode::Internal.http_status(), 500);
302 }
303}