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 jsonrpc_code(&self) -> i32 {
82 match self {
83 ErrorCode::InvalidInput => -32602,
84 ErrorCode::Unauthenticated => -32000,
85 ErrorCode::Forbidden => -32001,
86 ErrorCode::NotFound => -32002,
87 ErrorCode::Conflict => -32003,
88 ErrorCode::FailedPrecondition => -32004,
89 ErrorCode::RateLimited => -32005,
90 ErrorCode::Internal => -32603,
91 ErrorCode::NotImplemented => -32601,
92 ErrorCode::Unavailable => -32006,
93 }
94 }
95
96 pub fn infer_from_name(name: &str) -> Self {
98 let name_lower = name.to_lowercase();
99
100 if name_lower.contains("notfound")
101 || name_lower.contains("not_found")
102 || name_lower.contains("missing")
103 {
104 ErrorCode::NotFound
105 } else if name_lower.contains("invalid")
106 || name_lower.contains("validation")
107 || name_lower.contains("parse")
108 {
109 ErrorCode::InvalidInput
110 } else if name_lower.contains("unauthorized") || name_lower.contains("unauthenticated") {
111 ErrorCode::Unauthenticated
112 } else if name_lower.contains("forbidden")
113 || name_lower.contains("permission")
114 || name_lower.contains("denied")
115 {
116 ErrorCode::Forbidden
117 } else if name_lower.contains("conflict")
118 || name_lower.contains("exists")
119 || name_lower.contains("duplicate")
120 {
121 ErrorCode::Conflict
122 } else if name_lower.contains("ratelimit")
123 || name_lower.contains("rate_limit")
124 || name_lower.contains("throttle")
125 {
126 ErrorCode::RateLimited
127 } else if name_lower.contains("unavailable") || name_lower.contains("temporarily") {
128 ErrorCode::Unavailable
129 } else if name_lower.contains("unimplemented") || name_lower.contains("not_implemented") {
130 ErrorCode::NotImplemented
131 } else {
132 ErrorCode::Internal
133 }
134 }
135}
136
137pub trait IntoErrorCode {
141 fn error_code(&self) -> ErrorCode;
143
144 fn message(&self) -> String;
146
147 fn jsonrpc_code(&self) -> i32 {
152 self.error_code().jsonrpc_code()
153 }
154}
155
156pub trait HttpStatusFallback {
167 fn http_status_code(&self) -> u16;
169}
170
171pub struct HttpStatusHelper<'a, T>(pub &'a T);
185
186impl<T: IntoErrorCode> HttpStatusHelper<'_, T> {
187 pub fn http_status_code(&self) -> u16 {
189 self.0.error_code().http_status()
190 }
191}
192
193impl<T> HttpStatusFallback for HttpStatusHelper<'_, T> {
194 fn http_status_code(&self) -> u16 {
197 500
198 }
199}
200
201impl IntoErrorCode for std::io::Error {
203 fn error_code(&self) -> ErrorCode {
204 match self.kind() {
205 std::io::ErrorKind::NotFound => ErrorCode::NotFound,
206 std::io::ErrorKind::PermissionDenied => ErrorCode::Forbidden,
207 std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
208 ErrorCode::InvalidInput
209 }
210 _ => ErrorCode::Internal,
211 }
212 }
213
214 fn message(&self) -> String {
215 self.to_string()
216 }
217}
218
219impl IntoErrorCode for String {
220 fn error_code(&self) -> ErrorCode {
221 ErrorCode::Internal
222 }
223
224 fn message(&self) -> String {
225 self.clone()
226 }
227}
228
229impl IntoErrorCode for &str {
230 fn error_code(&self) -> ErrorCode {
231 ErrorCode::Internal
232 }
233
234 fn message(&self) -> String {
235 self.to_string()
236 }
237}
238
239impl IntoErrorCode for Box<dyn std::error::Error> {
240 fn error_code(&self) -> ErrorCode {
241 ErrorCode::Internal
242 }
243
244 fn message(&self) -> String {
245 self.to_string()
246 }
247}
248
249impl IntoErrorCode for Box<dyn std::error::Error + Send + Sync> {
250 fn error_code(&self) -> ErrorCode {
251 ErrorCode::Internal
252 }
253
254 fn message(&self) -> String {
255 self.to_string()
256 }
257}
258
259#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
261pub struct ErrorResponse {
262 pub code: String,
263 pub message: String,
264 #[serde(skip_serializing_if = "Option::is_none")]
265 pub details: Option<serde_json::Value>,
266}
267
268impl ErrorResponse {
269 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
270 Self {
271 code: format!("{:?}", code).to_uppercase(),
272 message: message.into(),
273 details: None,
274 }
275 }
276
277 pub fn with_details(mut self, details: serde_json::Value) -> Self {
278 self.details = Some(details);
279 self
280 }
281}
282
283impl fmt::Display for ErrorResponse {
284 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
285 write!(f, "{}: {}", self.code, self.message)
286 }
287}
288
289impl std::error::Error for ErrorResponse {}
290
291#[derive(Debug, Clone)]
295pub struct SchemaValidationError {
296 pub schema_type: String,
298 pub missing_lines: Vec<String>,
300 pub extra_lines: Vec<String>,
302}
303
304impl SchemaValidationError {
305 pub fn new(schema_type: impl Into<String>) -> Self {
307 Self {
308 schema_type: schema_type.into(),
309 missing_lines: Vec::new(),
310 extra_lines: Vec::new(),
311 }
312 }
313
314 pub fn add_missing(&mut self, line: impl Into<String>) {
316 self.missing_lines.push(line.into());
317 }
318
319 pub fn add_extra(&mut self, line: impl Into<String>) {
321 self.extra_lines.push(line.into());
322 }
323
324 pub fn has_differences(&self) -> bool {
326 !self.missing_lines.is_empty() || !self.extra_lines.is_empty()
327 }
328}
329
330impl fmt::Display for SchemaValidationError {
331 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
332 writeln!(f, "{} schema validation failed:", self.schema_type)?;
333
334 if !self.missing_lines.is_empty() {
335 writeln!(f, "\nExpected methods/messages not found in generated:")?;
336 for line in &self.missing_lines {
337 writeln!(f, " - {}", line)?;
338 }
339 }
340
341 if !self.extra_lines.is_empty() {
342 writeln!(f, "\nGenerated methods/messages not in expected:")?;
343 for line in &self.extra_lines {
344 writeln!(f, " + {}", line)?;
345 }
346 }
347
348 writeln!(f)?;
350 writeln!(f, "Hints:")?;
351
352 if !self.missing_lines.is_empty() && !self.extra_lines.is_empty() {
353 writeln!(
354 f,
355 " - Method signature or type may have changed. Check parameter names and types."
356 )?;
357 }
358
359 if !self.missing_lines.is_empty() {
360 writeln!(
361 f,
362 " - Missing items may indicate removed or renamed methods in Rust code."
363 )?;
364 }
365
366 if !self.extra_lines.is_empty() {
367 writeln!(
368 f,
369 " - Extra items may indicate new methods added. Update the schema file."
370 )?;
371 }
372
373 writeln!(
374 f,
375 " - Run `write_{schema}()` to regenerate the schema file.",
376 schema = self.schema_type.to_lowercase()
377 )?;
378
379 Ok(())
380 }
381}
382
383impl std::error::Error for SchemaValidationError {}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
390 fn test_error_code_inference() {
391 assert_eq!(ErrorCode::infer_from_name("NotFound"), ErrorCode::NotFound);
392 assert_eq!(
393 ErrorCode::infer_from_name("UserNotFound"),
394 ErrorCode::NotFound
395 );
396 assert_eq!(
397 ErrorCode::infer_from_name("InvalidEmail"),
398 ErrorCode::InvalidInput
399 );
400 assert_eq!(
401 ErrorCode::infer_from_name("Forbidden"),
402 ErrorCode::Forbidden
403 );
404 assert_eq!(
405 ErrorCode::infer_from_name("AlreadyExists"),
406 ErrorCode::Conflict
407 );
408 }
409
410 #[test]
411 fn test_http_status_codes() {
412 assert_eq!(ErrorCode::NotFound.http_status(), 404);
413 assert_eq!(ErrorCode::InvalidInput.http_status(), 400);
414 assert_eq!(ErrorCode::Internal.http_status(), 500);
415 }
416}