Skip to main content

modkit_canonical_errors/
error.rs

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