Skip to main content

toolkit_canonical_errors/
error.rs

1use std::fmt;
2
3// `gts_id!` is the prefix-free GTS-id construction macro (re-exported at the
4// crate root from `gts_macros`). It expands at compile time to a `&'static
5// str` literal with the configured `GTS_ID_PREFIX`
6// prepended, so the canonical error type ids below track the configured
7// prefix without hard-coding the literal prefix.
8use crate::gts_id;
9
10use crate::context::{
11    Aborted, AlreadyExists, Cancelled, DataLoss, DeadlineExceeded, FailedPrecondition, Internal,
12    InvalidArgument, NotFound, OutOfRange, PermissionDenied, ResourceExhausted, ServiceUnavailable,
13    Unauthenticated, Unimplemented, Unknown,
14};
15
16// ---------------------------------------------------------------------------
17// CanonicalError Enum
18// ---------------------------------------------------------------------------
19
20#[derive(Debug, Clone)]
21#[non_exhaustive]
22pub enum CanonicalError {
23    #[non_exhaustive]
24    Cancelled {
25        ctx: Cancelled,
26        detail: String,
27        resource_type: Option<String>,
28        resource_name: Option<String>,
29    },
30    #[non_exhaustive]
31    Unknown {
32        ctx: Unknown,
33        detail: String,
34        resource_type: Option<String>,
35        resource_name: Option<String>,
36    },
37    #[non_exhaustive]
38    InvalidArgument {
39        ctx: InvalidArgument,
40        detail: String,
41        resource_type: Option<String>,
42        resource_name: Option<String>,
43    },
44    #[non_exhaustive]
45    DeadlineExceeded {
46        ctx: DeadlineExceeded,
47        detail: String,
48        resource_type: Option<String>,
49        resource_name: Option<String>,
50    },
51    #[non_exhaustive]
52    NotFound {
53        ctx: NotFound,
54        detail: String,
55        resource_type: Option<String>,
56        resource_name: Option<String>,
57    },
58    #[non_exhaustive]
59    AlreadyExists {
60        ctx: AlreadyExists,
61        detail: String,
62        resource_type: Option<String>,
63        resource_name: Option<String>,
64    },
65    #[non_exhaustive]
66    PermissionDenied {
67        ctx: PermissionDenied,
68        detail: String,
69        resource_type: Option<String>,
70        resource_name: Option<String>,
71    },
72    #[non_exhaustive]
73    ResourceExhausted {
74        ctx: ResourceExhausted,
75        detail: String,
76        resource_type: Option<String>,
77        resource_name: Option<String>,
78    },
79    #[non_exhaustive]
80    FailedPrecondition {
81        ctx: FailedPrecondition,
82        detail: String,
83        resource_type: Option<String>,
84        resource_name: Option<String>,
85    },
86    #[non_exhaustive]
87    Aborted {
88        ctx: Aborted,
89        detail: String,
90        resource_type: Option<String>,
91        resource_name: Option<String>,
92    },
93    #[non_exhaustive]
94    OutOfRange {
95        ctx: OutOfRange,
96        detail: String,
97        resource_type: Option<String>,
98        resource_name: Option<String>,
99    },
100    #[non_exhaustive]
101    Unimplemented {
102        ctx: Unimplemented,
103        detail: String,
104        resource_type: Option<String>,
105        resource_name: Option<String>,
106    },
107    #[non_exhaustive]
108    Internal { ctx: Internal, detail: String },
109    #[non_exhaustive]
110    ServiceUnavailable {
111        ctx: ServiceUnavailable,
112        detail: String,
113        resource_type: Option<String>,
114        resource_name: Option<String>,
115    },
116    #[non_exhaustive]
117    DataLoss {
118        ctx: DataLoss,
119        detail: String,
120        resource_type: Option<String>,
121        resource_name: Option<String>,
122    },
123    #[non_exhaustive]
124    Unauthenticated {
125        ctx: Unauthenticated,
126        detail: String,
127        resource_type: Option<String>,
128        resource_name: Option<String>,
129    },
130}
131
132impl CanonicalError {
133    // --- Ergonomic constructors (one per category) ---
134
135    #[doc(hidden)]
136    #[must_use]
137    pub(crate) fn __cancelled(ctx: Cancelled) -> Self {
138        Self::Cancelled {
139            ctx,
140            detail: String::from("Operation cancelled by the client"),
141            resource_type: None,
142            resource_name: None,
143        }
144    }
145
146    #[doc(hidden)]
147    #[must_use]
148    pub(crate) fn __unknown(ctx: Unknown) -> Self {
149        Self::Unknown {
150            ctx,
151            detail: String::from("An unknown error occurred"),
152            resource_type: None,
153            resource_name: None,
154        }
155    }
156
157    #[doc(hidden)]
158    #[must_use]
159    pub(crate) fn __invalid_argument(ctx: InvalidArgument) -> Self {
160        let detail = match &ctx {
161            InvalidArgument::FieldViolations { .. } => String::from("Request validation failed"),
162            InvalidArgument::Format { format } => format.clone(),
163            InvalidArgument::Constraint { constraint } => constraint.clone(),
164        };
165        Self::InvalidArgument {
166            ctx,
167            detail,
168            resource_type: None,
169            resource_name: None,
170        }
171    }
172
173    #[doc(hidden)]
174    #[must_use]
175    pub(crate) fn __deadline_exceeded(ctx: DeadlineExceeded) -> Self {
176        Self::DeadlineExceeded {
177            ctx,
178            detail: String::from("Operation did not complete within the allowed time"),
179            resource_type: None,
180            resource_name: None,
181        }
182    }
183
184    #[doc(hidden)]
185    #[must_use]
186    pub(crate) fn __not_found(ctx: NotFound) -> Self {
187        Self::NotFound {
188            ctx,
189            detail: String::from("Resource not found"),
190            resource_type: None,
191            resource_name: None,
192        }
193    }
194
195    #[doc(hidden)]
196    #[must_use]
197    pub(crate) fn __already_exists(ctx: AlreadyExists) -> Self {
198        Self::AlreadyExists {
199            ctx,
200            detail: String::from("Resource already exists"),
201            resource_type: None,
202            resource_name: None,
203        }
204    }
205
206    #[doc(hidden)]
207    #[must_use]
208    pub(crate) fn __permission_denied(ctx: PermissionDenied) -> Self {
209        Self::PermissionDenied {
210            ctx,
211            detail: String::from("You do not have permission to perform this operation"),
212            resource_type: None,
213            resource_name: None,
214        }
215    }
216
217    #[doc(hidden)]
218    #[must_use]
219    pub(crate) fn __resource_exhausted(ctx: ResourceExhausted) -> Self {
220        Self::ResourceExhausted {
221            ctx,
222            detail: String::from("Quota exceeded"),
223            resource_type: None,
224            resource_name: None,
225        }
226    }
227
228    #[doc(hidden)]
229    #[must_use]
230    pub(crate) fn __failed_precondition(ctx: FailedPrecondition) -> Self {
231        Self::FailedPrecondition {
232            ctx,
233            detail: String::from("Operation precondition not met"),
234            resource_type: None,
235            resource_name: None,
236        }
237    }
238
239    #[doc(hidden)]
240    #[must_use]
241    pub(crate) fn __aborted(ctx: Aborted) -> Self {
242        Self::Aborted {
243            ctx,
244            detail: String::from("Operation aborted due to concurrency conflict"),
245            resource_type: None,
246            resource_name: None,
247        }
248    }
249
250    #[doc(hidden)]
251    #[must_use]
252    pub(crate) fn __out_of_range(ctx: OutOfRange) -> Self {
253        Self::OutOfRange {
254            ctx,
255            detail: String::from("Value out of range"),
256            resource_type: None,
257            resource_name: None,
258        }
259    }
260
261    #[doc(hidden)]
262    #[must_use]
263    pub(crate) fn __unimplemented(ctx: Unimplemented) -> Self {
264        Self::Unimplemented {
265            ctx,
266            detail: String::from("This operation is not implemented"),
267            resource_type: None,
268            resource_name: None,
269        }
270    }
271
272    #[doc(hidden)]
273    #[must_use]
274    pub(crate) fn __internal(ctx: Internal) -> Self {
275        Self::Internal {
276            ctx,
277            detail: String::from("An internal error occurred. Please retry later."),
278        }
279    }
280
281    #[doc(hidden)]
282    #[must_use]
283    pub(crate) fn __service_unavailable(ctx: ServiceUnavailable) -> Self {
284        Self::ServiceUnavailable {
285            ctx,
286            detail: String::from("Service temporarily unavailable"),
287            resource_type: None,
288            resource_name: None,
289        }
290    }
291
292    #[doc(hidden)]
293    #[must_use]
294    pub(crate) fn __data_loss(ctx: DataLoss) -> Self {
295        Self::DataLoss {
296            ctx,
297            detail: String::from("Data loss detected"),
298            resource_type: None,
299            resource_name: None,
300        }
301    }
302
303    #[doc(hidden)]
304    #[must_use]
305    pub(crate) fn __unauthenticated(ctx: Unauthenticated) -> Self {
306        Self::Unauthenticated {
307            ctx,
308            detail: String::from("Authentication required"),
309            resource_type: None,
310            resource_name: None,
311        }
312    }
313
314    // --- Builder methods ---
315
316    #[must_use]
317    pub(crate) fn with_detail(mut self, msg: impl Into<String>) -> Self {
318        let msg = msg.into();
319        match &mut self {
320            Self::Cancelled { detail, .. }
321            | Self::Unknown { detail, .. }
322            | Self::InvalidArgument { detail, .. }
323            | Self::DeadlineExceeded { detail, .. }
324            | Self::NotFound { detail, .. }
325            | Self::AlreadyExists { detail, .. }
326            | Self::PermissionDenied { detail, .. }
327            | Self::ResourceExhausted { detail, .. }
328            | Self::FailedPrecondition { detail, .. }
329            | Self::Aborted { detail, .. }
330            | Self::OutOfRange { detail, .. }
331            | Self::Unimplemented { detail, .. }
332            | Self::ServiceUnavailable { detail, .. }
333            | Self::DataLoss { detail, .. }
334            | Self::Unauthenticated { detail, .. }
335            | Self::Internal { detail, .. } => *detail = msg,
336        }
337        self
338    }
339
340    #[must_use]
341    pub(crate) fn with_resource_type(mut self, rt: impl Into<String>) -> Self {
342        let rt = Some(rt.into());
343        match &mut self {
344            Self::Cancelled { resource_type, .. }
345            | Self::Unknown { resource_type, .. }
346            | Self::InvalidArgument { resource_type, .. }
347            | Self::DeadlineExceeded { resource_type, .. }
348            | Self::NotFound { resource_type, .. }
349            | Self::AlreadyExists { resource_type, .. }
350            | Self::PermissionDenied { resource_type, .. }
351            | Self::ResourceExhausted { resource_type, .. }
352            | Self::FailedPrecondition { resource_type, .. }
353            | Self::Aborted { resource_type, .. }
354            | Self::OutOfRange { resource_type, .. }
355            | Self::Unimplemented { resource_type, .. }
356            | Self::ServiceUnavailable { resource_type, .. }
357            | Self::DataLoss { resource_type, .. }
358            | Self::Unauthenticated { resource_type, .. } => *resource_type = rt,
359            Self::Internal { .. } => {}
360        }
361        self
362    }
363
364    #[must_use]
365    pub(crate) fn with_resource(mut self, rn: impl Into<String>) -> Self {
366        let rn = Some(rn.into());
367        match &mut self {
368            Self::Cancelled { resource_name, .. }
369            | Self::Unknown { resource_name, .. }
370            | Self::InvalidArgument { resource_name, .. }
371            | Self::DeadlineExceeded { resource_name, .. }
372            | Self::NotFound { resource_name, .. }
373            | Self::AlreadyExists { resource_name, .. }
374            | Self::PermissionDenied { resource_name, .. }
375            | Self::ResourceExhausted { resource_name, .. }
376            | Self::FailedPrecondition { resource_name, .. }
377            | Self::Aborted { resource_name, .. }
378            | Self::OutOfRange { resource_name, .. }
379            | Self::Unimplemented { resource_name, .. }
380            | Self::ServiceUnavailable { resource_name, .. }
381            | Self::DataLoss { resource_name, .. }
382            | Self::Unauthenticated { resource_name, .. } => *resource_name = rn,
383            Self::Internal { .. } => {}
384        }
385        self
386    }
387
388    // --- Accessors ---
389
390    #[must_use]
391    pub fn detail(&self) -> &str {
392        match self {
393            Self::Cancelled { detail, .. }
394            | Self::Unknown { detail, .. }
395            | Self::InvalidArgument { detail, .. }
396            | Self::DeadlineExceeded { detail, .. }
397            | Self::NotFound { detail, .. }
398            | Self::AlreadyExists { detail, .. }
399            | Self::PermissionDenied { detail, .. }
400            | Self::ResourceExhausted { detail, .. }
401            | Self::FailedPrecondition { detail, .. }
402            | Self::Aborted { detail, .. }
403            | Self::OutOfRange { detail, .. }
404            | Self::Unimplemented { detail, .. }
405            | Self::ServiceUnavailable { detail, .. }
406            | Self::DataLoss { detail, .. }
407            | Self::Unauthenticated { detail, .. }
408            | Self::Internal { detail, .. } => detail,
409        }
410    }
411
412    #[must_use]
413    pub fn resource_type(&self) -> Option<&str> {
414        match self {
415            Self::Cancelled { resource_type, .. }
416            | Self::Unknown { resource_type, .. }
417            | Self::InvalidArgument { resource_type, .. }
418            | Self::DeadlineExceeded { resource_type, .. }
419            | Self::NotFound { resource_type, .. }
420            | Self::AlreadyExists { resource_type, .. }
421            | Self::PermissionDenied { resource_type, .. }
422            | Self::ResourceExhausted { resource_type, .. }
423            | Self::FailedPrecondition { resource_type, .. }
424            | Self::Aborted { resource_type, .. }
425            | Self::OutOfRange { resource_type, .. }
426            | Self::Unimplemented { resource_type, .. }
427            | Self::ServiceUnavailable { resource_type, .. }
428            | Self::DataLoss { resource_type, .. }
429            | Self::Unauthenticated { resource_type, .. } => resource_type.as_deref(),
430            Self::Internal { .. } => None,
431        }
432    }
433
434    #[must_use]
435    pub fn resource_name(&self) -> Option<&str> {
436        match self {
437            Self::Cancelled { resource_name, .. }
438            | Self::Unknown { resource_name, .. }
439            | Self::InvalidArgument { resource_name, .. }
440            | Self::DeadlineExceeded { resource_name, .. }
441            | Self::NotFound { resource_name, .. }
442            | Self::AlreadyExists { resource_name, .. }
443            | Self::PermissionDenied { resource_name, .. }
444            | Self::ResourceExhausted { resource_name, .. }
445            | Self::FailedPrecondition { resource_name, .. }
446            | Self::Aborted { resource_name, .. }
447            | Self::OutOfRange { resource_name, .. }
448            | Self::Unimplemented { resource_name, .. }
449            | Self::ServiceUnavailable { resource_name, .. }
450            | Self::DataLoss { resource_name, .. }
451            | Self::Unauthenticated { resource_name, .. } => resource_name.as_deref(),
452            Self::Internal { .. } => None,
453        }
454    }
455
456    /// Returns the internal diagnostic string for `Internal` and `Unknown`
457    /// variants, or `None` for all other categories.
458    ///
459    /// Middleware should call this **before** converting to `Problem` so
460    /// that the real cause can be logged server-side with the `trace_id`.
461    /// The diagnostic is never included in production wire responses.
462    #[must_use]
463    pub fn diagnostic(&self) -> Option<&str> {
464        match self {
465            Self::Internal { ctx, .. } => Some(&ctx.description),
466            Self::Unknown { ctx, .. } => Some(&ctx.description),
467            _ => None,
468        }
469    }
470
471    // --- Metadata accessors (direct match) ---
472
473    #[must_use]
474    pub fn gts_type(&self) -> &'static str {
475        match self {
476            Self::Cancelled { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.cancelled.v1~"),
477            Self::Unknown { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.unknown.v1~"),
478            Self::InvalidArgument { .. } => {
479                gts_id!("cf.core.errors.err.v1~cf.core.err.invalid_argument.v1~")
480            }
481            Self::DeadlineExceeded { .. } => {
482                gts_id!("cf.core.errors.err.v1~cf.core.err.deadline_exceeded.v1~")
483            }
484            Self::NotFound { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.not_found.v1~"),
485            Self::AlreadyExists { .. } => {
486                gts_id!("cf.core.errors.err.v1~cf.core.err.already_exists.v1~")
487            }
488            Self::PermissionDenied { .. } => {
489                gts_id!("cf.core.errors.err.v1~cf.core.err.permission_denied.v1~")
490            }
491            Self::ResourceExhausted { .. } => {
492                gts_id!("cf.core.errors.err.v1~cf.core.err.resource_exhausted.v1~")
493            }
494            Self::FailedPrecondition { .. } => {
495                gts_id!("cf.core.errors.err.v1~cf.core.err.failed_precondition.v1~")
496            }
497            Self::Aborted { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.aborted.v1~"),
498            Self::OutOfRange { .. } => {
499                gts_id!("cf.core.errors.err.v1~cf.core.err.out_of_range.v1~")
500            }
501            Self::Unimplemented { .. } => {
502                gts_id!("cf.core.errors.err.v1~cf.core.err.unimplemented.v1~")
503            }
504            Self::Internal { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.internal.v1~"),
505            Self::ServiceUnavailable { .. } => {
506                gts_id!("cf.core.errors.err.v1~cf.core.err.service_unavailable.v1~")
507            }
508            Self::DataLoss { .. } => gts_id!("cf.core.errors.err.v1~cf.core.err.data_loss.v1~"),
509            Self::Unauthenticated { .. } => {
510                gts_id!("cf.core.errors.err.v1~cf.core.err.unauthenticated.v1~")
511            }
512        }
513    }
514
515    #[must_use]
516    pub fn status_code(&self) -> u16 {
517        match self {
518            Self::InvalidArgument { .. }
519            | Self::FailedPrecondition { .. }
520            | Self::OutOfRange { .. } => 400,
521            Self::Unauthenticated { .. } => 401,
522            Self::PermissionDenied { .. } => 403,
523            Self::NotFound { .. } => 404,
524            Self::AlreadyExists { .. } | Self::Aborted { .. } => 409,
525            Self::ResourceExhausted { .. } => 429,
526            Self::Cancelled { .. } => 499,
527            Self::Unknown { .. } | Self::Internal { .. } | Self::DataLoss { .. } => 500,
528            Self::Unimplemented { .. } => 501,
529            Self::ServiceUnavailable { .. } => 503,
530            Self::DeadlineExceeded { .. } => 504,
531        }
532    }
533
534    #[must_use]
535    pub fn title(&self) -> &'static str {
536        match self {
537            Self::Cancelled { .. } => "Cancelled",
538            Self::Unknown { .. } => "Unknown",
539            Self::InvalidArgument { .. } => "Invalid Argument",
540            Self::DeadlineExceeded { .. } => "Deadline Exceeded",
541            Self::NotFound { .. } => "Not Found",
542            Self::AlreadyExists { .. } => "Already Exists",
543            Self::PermissionDenied { .. } => "Permission Denied",
544            Self::ResourceExhausted { .. } => "Resource Exhausted",
545            Self::FailedPrecondition { .. } => "Failed Precondition",
546            Self::Aborted { .. } => "Aborted",
547            Self::OutOfRange { .. } => "Out of Range",
548            Self::Unimplemented { .. } => "Unimplemented",
549            Self::Internal { .. } => "Internal",
550            Self::ServiceUnavailable { .. } => "Service Unavailable",
551            Self::DataLoss { .. } => "Data Loss",
552            Self::Unauthenticated { .. } => "Unauthenticated",
553        }
554    }
555
556    fn category_name(&self) -> &'static str {
557        match self {
558            Self::Cancelled { .. } => "cancelled",
559            Self::Unknown { .. } => "unknown",
560            Self::InvalidArgument { .. } => "invalid_argument",
561            Self::DeadlineExceeded { .. } => "deadline_exceeded",
562            Self::NotFound { .. } => "not_found",
563            Self::AlreadyExists { .. } => "already_exists",
564            Self::PermissionDenied { .. } => "permission_denied",
565            Self::ResourceExhausted { .. } => "resource_exhausted",
566            Self::FailedPrecondition { .. } => "failed_precondition",
567            Self::Aborted { .. } => "aborted",
568            Self::OutOfRange { .. } => "out_of_range",
569            Self::Unimplemented { .. } => "unimplemented",
570            Self::Internal { .. } => "internal",
571            Self::ServiceUnavailable { .. } => "service_unavailable",
572            Self::DataLoss { .. } => "data_loss",
573            Self::Unauthenticated { .. } => "unauthenticated",
574        }
575    }
576}
577
578impl fmt::Display for CanonicalError {
579    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
580        write!(f, "{}: {}", self.category_name(), self.detail())
581    }
582}
583
584impl std::error::Error for CanonicalError {}
585
586// ---------------------------------------------------------------------------
587// From impls for common library errors (? propagation)
588// ---------------------------------------------------------------------------
589
590// TODO(DE1302): `Internal` only carries a description string today, so these
591// From impls lose the source error chain. Refactor `Internal` to also carry a
592// boxed source so `.source()` returns the original error, then remove these
593// allows.
594#[allow(unknown_lints, de1302_error_from_to_string)]
595impl From<std::io::Error> for CanonicalError {
596    fn from(err: std::io::Error) -> Self {
597        Self::__internal(Internal::new(err.to_string()))
598    }
599}
600
601#[allow(unknown_lints, de1302_error_from_to_string)]
602impl From<serde_json::Error> for CanonicalError {
603    fn from(err: serde_json::Error) -> Self {
604        Self::__invalid_argument(InvalidArgument::format(err.to_string()))
605    }
606}
607
608#[cfg(feature = "sea-orm")]
609#[allow(unknown_lints, de1302_error_from_to_string)]
610impl From<sea_orm::DbErr> for CanonicalError {
611    fn from(err: sea_orm::DbErr) -> Self {
612        Self::__internal(Internal::new(err.to_string()))
613    }
614}