1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum ErrorCode {
12 InvalidInput,
14 Unauthenticated,
16 Forbidden,
18 NotFound,
20 Conflict,
22 UnprocessableEntity,
24 RateLimited,
26 Internal,
28 NotImplemented,
30 Unavailable,
32}
33
34impl ErrorCode {
35 pub fn http_status(&self) -> u16 {
37 match self {
38 ErrorCode::InvalidInput => 400,
39 ErrorCode::Unauthenticated => 401,
40 ErrorCode::Forbidden => 403,
41 ErrorCode::NotFound => 404,
42 ErrorCode::Conflict => 409,
43 ErrorCode::UnprocessableEntity => 422,
44 ErrorCode::RateLimited => 429,
45 ErrorCode::Internal => 500,
46 ErrorCode::NotImplemented => 501,
47 ErrorCode::Unavailable => 503,
48 }
49 }
50
51 pub fn exit_code(&self) -> i32 {
53 match self {
54 ErrorCode::NotFound => 1,
55 ErrorCode::InvalidInput => 2,
56 ErrorCode::Unauthenticated | ErrorCode::Forbidden => 3,
57 ErrorCode::Conflict | ErrorCode::UnprocessableEntity => 4,
58 ErrorCode::RateLimited => 5,
59 ErrorCode::Internal | ErrorCode::Unavailable => 1,
60 ErrorCode::NotImplemented => 1,
61 }
62 }
63
64 pub fn grpc_code(&self) -> &'static str {
66 match self {
67 ErrorCode::InvalidInput => "INVALID_ARGUMENT",
68 ErrorCode::Unauthenticated => "UNAUTHENTICATED",
69 ErrorCode::Forbidden => "PERMISSION_DENIED",
70 ErrorCode::NotFound => "NOT_FOUND",
71 ErrorCode::Conflict => "ALREADY_EXISTS",
72 ErrorCode::UnprocessableEntity => "FAILED_PRECONDITION",
73 ErrorCode::RateLimited => "RESOURCE_EXHAUSTED",
74 ErrorCode::Internal => "INTERNAL",
75 ErrorCode::NotImplemented => "UNIMPLEMENTED",
76 ErrorCode::Unavailable => "UNAVAILABLE",
77 }
78 }
79
80 pub fn jsonrpc_code(&self) -> i32 {
86 match self {
87 ErrorCode::InvalidInput => -32602,
88 ErrorCode::Unauthenticated => -32000,
89 ErrorCode::Forbidden => -32001,
90 ErrorCode::NotFound => -32002,
91 ErrorCode::Conflict => -32003,
92 ErrorCode::UnprocessableEntity => -32004,
93 ErrorCode::RateLimited => -32005,
94 ErrorCode::Internal => -32603,
95 ErrorCode::NotImplemented => -32601,
96 ErrorCode::Unavailable => -32006,
97 }
98 }
99
100 pub fn infer_from_name(name: &str) -> Self {
102 let name_lower = name.to_lowercase();
103
104 if name_lower.contains("notfound")
105 || name_lower.contains("not_found")
106 || name_lower.contains("missing")
107 {
108 ErrorCode::NotFound
109 } else if name_lower.contains("invalid")
110 || name_lower.contains("validation")
111 || name_lower.contains("parse")
112 {
113 ErrorCode::InvalidInput
114 } else if name_lower.contains("unauthorized") || name_lower.contains("unauthenticated") {
115 ErrorCode::Unauthenticated
116 } else if name_lower.contains("forbidden")
117 || name_lower.contains("permission")
118 || name_lower.contains("denied")
119 {
120 ErrorCode::Forbidden
121 } else if name_lower.contains("conflict")
122 || name_lower.contains("exists")
123 || name_lower.contains("duplicate")
124 {
125 ErrorCode::Conflict
126 } else if name_lower.contains("ratelimit")
127 || name_lower.contains("rate_limit")
128 || name_lower.contains("throttle")
129 {
130 ErrorCode::RateLimited
131 } else if name_lower.contains("unavailable") || name_lower.contains("temporarily") {
132 ErrorCode::Unavailable
133 } else if name_lower.contains("unimplemented") || name_lower.contains("not_implemented") {
134 ErrorCode::NotImplemented
135 } else {
136 ErrorCode::Internal
137 }
138 }
139}
140
141impl fmt::Display for ErrorCode {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 let s = match self {
144 ErrorCode::InvalidInput => "INVALID_INPUT",
145 ErrorCode::Unauthenticated => "UNAUTHENTICATED",
146 ErrorCode::Forbidden => "FORBIDDEN",
147 ErrorCode::NotFound => "NOT_FOUND",
148 ErrorCode::Conflict => "CONFLICT",
149 ErrorCode::UnprocessableEntity => "UNPROCESSABLE_ENTITY",
150 ErrorCode::RateLimited => "RATE_LIMITED",
151 ErrorCode::Internal => "INTERNAL",
152 ErrorCode::NotImplemented => "NOT_IMPLEMENTED",
153 ErrorCode::Unavailable => "UNAVAILABLE",
154 };
155 f.write_str(s)
156 }
157}
158
159pub trait IntoErrorCode {
163 fn error_code(&self) -> ErrorCode;
165
166 fn message(&self) -> String;
168
169 fn jsonrpc_code(&self) -> i32 {
174 self.error_code().jsonrpc_code()
175 }
176}
177
178#[doc(hidden)]
189pub trait HttpStatusFallback {
190 fn http_status_code(&self) -> u16;
192}
193
194#[doc(hidden)]
208pub struct HttpStatusHelper<'a, T>(pub &'a T);
209
210impl<T: IntoErrorCode> HttpStatusHelper<'_, T> {
211 pub fn http_status_code(&self) -> u16 {
213 self.0.error_code().http_status()
214 }
215}
216
217impl<T> HttpStatusFallback for HttpStatusHelper<'_, T> {
218 fn http_status_code(&self) -> u16 {
221 500
222 }
223}
224
225impl IntoErrorCode for std::io::Error {
227 fn error_code(&self) -> ErrorCode {
228 match self.kind() {
229 std::io::ErrorKind::NotFound => ErrorCode::NotFound,
230 std::io::ErrorKind::PermissionDenied => ErrorCode::Forbidden,
231 std::io::ErrorKind::InvalidInput | std::io::ErrorKind::InvalidData => {
232 ErrorCode::InvalidInput
233 }
234 _ => ErrorCode::Internal,
235 }
236 }
237
238 fn message(&self) -> String {
239 self.to_string()
240 }
241}
242
243impl IntoErrorCode for String {
244 fn error_code(&self) -> ErrorCode {
245 ErrorCode::Internal
246 }
247
248 fn message(&self) -> String {
249 self.clone()
250 }
251}
252
253impl IntoErrorCode for &str {
254 fn error_code(&self) -> ErrorCode {
255 ErrorCode::Internal
256 }
257
258 fn message(&self) -> String {
259 self.to_string()
260 }
261}
262
263impl IntoErrorCode for Box<dyn std::error::Error> {
264 fn error_code(&self) -> ErrorCode {
265 ErrorCode::Internal
266 }
267
268 fn message(&self) -> String {
269 self.to_string()
270 }
271}
272
273impl IntoErrorCode for Box<dyn std::error::Error + Send + Sync> {
274 fn error_code(&self) -> ErrorCode {
275 ErrorCode::Internal
276 }
277
278 fn message(&self) -> String {
279 self.to_string()
280 }
281}
282
283#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
288pub struct ErrorResponse {
289 pub code: String,
291 pub message: String,
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub details: Option<serde_json::Value>,
296}
297
298impl ErrorResponse {
299 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
301 Self {
302 code: code.to_string(),
303 message: message.into(),
304 details: None,
305 }
306 }
307
308 pub fn with_details(mut self, details: serde_json::Value) -> Self {
310 self.details = Some(details);
311 self
312 }
313}
314
315impl fmt::Display for ErrorResponse {
316 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
317 write!(f, "{}: {}", self.code, self.message)
318 }
319}
320
321impl std::error::Error for ErrorResponse {}
322
323#[derive(Debug, Clone)]
327pub struct SchemaValidationError {
328 pub schema_type: String,
330 pub missing_lines: Vec<String>,
332 pub extra_lines: Vec<String>,
334}
335
336impl SchemaValidationError {
337 pub fn new(schema_type: impl Into<String>) -> Self {
339 Self {
340 schema_type: schema_type.into(),
341 missing_lines: Vec::new(),
342 extra_lines: Vec::new(),
343 }
344 }
345
346 pub fn add_missing(&mut self, line: impl Into<String>) {
348 self.missing_lines.push(line.into());
349 }
350
351 pub fn add_extra(&mut self, line: impl Into<String>) {
353 self.extra_lines.push(line.into());
354 }
355
356 pub fn has_differences(&self) -> bool {
358 !self.missing_lines.is_empty() || !self.extra_lines.is_empty()
359 }
360}
361
362impl fmt::Display for SchemaValidationError {
363 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
364 writeln!(f, "{} schema validation failed:", self.schema_type)?;
365
366 if !self.missing_lines.is_empty() {
367 writeln!(f, "\nExpected methods/messages not found in generated:")?;
368 for line in &self.missing_lines {
369 writeln!(f, " - {}", line)?;
370 }
371 }
372
373 if !self.extra_lines.is_empty() {
374 writeln!(f, "\nGenerated methods/messages not in expected:")?;
375 for line in &self.extra_lines {
376 writeln!(f, " + {}", line)?;
377 }
378 }
379
380 writeln!(f)?;
382 writeln!(f, "Hints:")?;
383
384 if !self.missing_lines.is_empty() && !self.extra_lines.is_empty() {
385 writeln!(
386 f,
387 " - Method signature or type may have changed. Check parameter names and types."
388 )?;
389 }
390
391 if !self.missing_lines.is_empty() {
392 writeln!(
393 f,
394 " - Missing items may indicate removed or renamed methods in Rust code."
395 )?;
396 }
397
398 if !self.extra_lines.is_empty() {
399 writeln!(
400 f,
401 " - Extra items may indicate new methods added. Update the schema file."
402 )?;
403 }
404
405 writeln!(
406 f,
407 " - Run `write_{schema}()` to regenerate the schema file.",
408 schema = self.schema_type.to_lowercase()
409 )?;
410
411 Ok(())
412 }
413}
414
415impl std::error::Error for SchemaValidationError {}
416
417#[cfg(test)]
418mod tests {
419 use super::*;
420
421 #[test]
422 fn test_error_code_inference() {
423 assert_eq!(ErrorCode::infer_from_name("NotFound"), ErrorCode::NotFound);
424 assert_eq!(
425 ErrorCode::infer_from_name("UserNotFound"),
426 ErrorCode::NotFound
427 );
428 assert_eq!(
429 ErrorCode::infer_from_name("InvalidEmail"),
430 ErrorCode::InvalidInput
431 );
432 assert_eq!(
433 ErrorCode::infer_from_name("Forbidden"),
434 ErrorCode::Forbidden
435 );
436 assert_eq!(
437 ErrorCode::infer_from_name("AlreadyExists"),
438 ErrorCode::Conflict
439 );
440 }
441
442 #[test]
443 fn test_http_status_codes() {
444 assert_eq!(ErrorCode::NotFound.http_status(), 404);
445 assert_eq!(ErrorCode::InvalidInput.http_status(), 400);
446 assert_eq!(ErrorCode::Internal.http_status(), 500);
447 }
448}