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)]
264pub struct ErrorResponse {
265 pub code: String,
267 pub message: String,
269 #[serde(skip_serializing_if = "Option::is_none")]
271 pub details: Option<serde_json::Value>,
272}
273
274impl ErrorResponse {
275 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
277 Self {
278 code: format!("{:?}", code).to_uppercase(),
279 message: message.into(),
280 details: None,
281 }
282 }
283
284 pub fn with_details(mut self, details: serde_json::Value) -> Self {
286 self.details = Some(details);
287 self
288 }
289}
290
291impl fmt::Display for ErrorResponse {
292 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293 write!(f, "{}: {}", self.code, self.message)
294 }
295}
296
297impl std::error::Error for ErrorResponse {}
298
299#[derive(Debug, Clone)]
303pub struct SchemaValidationError {
304 pub schema_type: String,
306 pub missing_lines: Vec<String>,
308 pub extra_lines: Vec<String>,
310}
311
312impl SchemaValidationError {
313 pub fn new(schema_type: impl Into<String>) -> Self {
315 Self {
316 schema_type: schema_type.into(),
317 missing_lines: Vec::new(),
318 extra_lines: Vec::new(),
319 }
320 }
321
322 pub fn add_missing(&mut self, line: impl Into<String>) {
324 self.missing_lines.push(line.into());
325 }
326
327 pub fn add_extra(&mut self, line: impl Into<String>) {
329 self.extra_lines.push(line.into());
330 }
331
332 pub fn has_differences(&self) -> bool {
334 !self.missing_lines.is_empty() || !self.extra_lines.is_empty()
335 }
336}
337
338impl fmt::Display for SchemaValidationError {
339 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
340 writeln!(f, "{} schema validation failed:", self.schema_type)?;
341
342 if !self.missing_lines.is_empty() {
343 writeln!(f, "\nExpected methods/messages not found in generated:")?;
344 for line in &self.missing_lines {
345 writeln!(f, " - {}", line)?;
346 }
347 }
348
349 if !self.extra_lines.is_empty() {
350 writeln!(f, "\nGenerated methods/messages not in expected:")?;
351 for line in &self.extra_lines {
352 writeln!(f, " + {}", line)?;
353 }
354 }
355
356 writeln!(f)?;
358 writeln!(f, "Hints:")?;
359
360 if !self.missing_lines.is_empty() && !self.extra_lines.is_empty() {
361 writeln!(
362 f,
363 " - Method signature or type may have changed. Check parameter names and types."
364 )?;
365 }
366
367 if !self.missing_lines.is_empty() {
368 writeln!(
369 f,
370 " - Missing items may indicate removed or renamed methods in Rust code."
371 )?;
372 }
373
374 if !self.extra_lines.is_empty() {
375 writeln!(
376 f,
377 " - Extra items may indicate new methods added. Update the schema file."
378 )?;
379 }
380
381 writeln!(
382 f,
383 " - Run `write_{schema}()` to regenerate the schema file.",
384 schema = self.schema_type.to_lowercase()
385 )?;
386
387 Ok(())
388 }
389}
390
391impl std::error::Error for SchemaValidationError {}
392
393#[cfg(test)]
394mod tests {
395 use super::*;
396
397 #[test]
398 fn test_error_code_inference() {
399 assert_eq!(ErrorCode::infer_from_name("NotFound"), ErrorCode::NotFound);
400 assert_eq!(
401 ErrorCode::infer_from_name("UserNotFound"),
402 ErrorCode::NotFound
403 );
404 assert_eq!(
405 ErrorCode::infer_from_name("InvalidEmail"),
406 ErrorCode::InvalidInput
407 );
408 assert_eq!(
409 ErrorCode::infer_from_name("Forbidden"),
410 ErrorCode::Forbidden
411 );
412 assert_eq!(
413 ErrorCode::infer_from_name("AlreadyExists"),
414 ErrorCode::Conflict
415 );
416 }
417
418 #[test]
419 fn test_http_status_codes() {
420 assert_eq!(ErrorCode::NotFound.http_status(), 404);
421 assert_eq!(ErrorCode::InvalidInput.http_status(), 400);
422 assert_eq!(ErrorCode::Internal.http_status(), 500);
423 }
424}