Skip to main content

modkit_canonical_errors/
builder.rs

1use crate::context::{
2    Aborted, AlreadyExists, Cancelled, DataLoss, DeadlineExceeded, FailedPrecondition,
3    FieldViolation, Internal, InvalidArgument, NotFound, OutOfRange, PermissionDenied,
4    PreconditionViolation, QuotaViolation, ResourceExhausted, ServiceUnavailable, Unauthenticated,
5    Unimplemented, Unknown,
6};
7use crate::error::CanonicalError;
8
9// ---------------------------------------------------------------------------
10// Resource markers
11// ---------------------------------------------------------------------------
12
13#[doc(hidden)]
14pub struct ResourceAbsent;
15#[doc(hidden)]
16pub struct ResourceOptional;
17#[doc(hidden)]
18pub struct ResourceMissing;
19#[doc(hidden)]
20pub struct ResourceSet(String);
21
22// ---------------------------------------------------------------------------
23// Context markers
24// ---------------------------------------------------------------------------
25
26#[doc(hidden)]
27pub struct NoContext;
28#[doc(hidden)]
29pub struct NeedsFieldViolation;
30#[doc(hidden)]
31pub struct HasFieldViolations(Vec<FieldViolation>);
32#[doc(hidden)]
33pub struct NeedsPreconditionViolation;
34#[doc(hidden)]
35pub struct HasPreconditionViolations(Vec<PreconditionViolation>);
36#[doc(hidden)]
37pub struct NeedsQuotaViolation;
38#[doc(hidden)]
39pub struct HasQuotaViolations(Vec<QuotaViolation>);
40#[doc(hidden)]
41pub struct HasFormatMessage(String);
42#[doc(hidden)]
43pub struct HasConstraintMessage(String);
44#[doc(hidden)]
45pub struct NeedsReason;
46#[doc(hidden)]
47pub struct HasReason(String);
48
49// ---------------------------------------------------------------------------
50// Traits gating build()
51// ---------------------------------------------------------------------------
52
53#[doc(hidden)]
54pub trait ResourceResolved {
55    fn resolve(self) -> Option<String>;
56}
57
58impl ResourceResolved for ResourceAbsent {
59    fn resolve(self) -> Option<String> {
60        None
61    }
62}
63
64impl ResourceResolved for ResourceOptional {
65    fn resolve(self) -> Option<String> {
66        None
67    }
68}
69
70impl ResourceResolved for ResourceSet {
71    fn resolve(self) -> Option<String> {
72        Some(self.0)
73    }
74}
75
76#[doc(hidden)]
77pub struct ContextData {
78    pub field_violations: Vec<FieldViolation>,
79    pub precondition_violations: Vec<PreconditionViolation>,
80    pub quota_violations: Vec<QuotaViolation>,
81    pub format_message: Option<String>,
82    pub constraint_message: Option<String>,
83    pub reason: String,
84}
85
86#[doc(hidden)]
87pub trait ContextResolved {
88    fn into_context_data(self) -> ContextData;
89}
90
91impl ContextResolved for NoContext {
92    fn into_context_data(self) -> ContextData {
93        ContextData {
94            field_violations: Vec::new(),
95            precondition_violations: Vec::new(),
96            quota_violations: Vec::new(),
97            format_message: None,
98            constraint_message: None,
99            reason: String::new(),
100        }
101    }
102}
103
104impl ContextResolved for HasFieldViolations {
105    fn into_context_data(self) -> ContextData {
106        ContextData {
107            field_violations: self.0,
108            precondition_violations: Vec::new(),
109            quota_violations: Vec::new(),
110            format_message: None,
111            constraint_message: None,
112            reason: String::new(),
113        }
114    }
115}
116
117impl ContextResolved for HasFormatMessage {
118    fn into_context_data(self) -> ContextData {
119        ContextData {
120            field_violations: Vec::new(),
121            precondition_violations: Vec::new(),
122            quota_violations: Vec::new(),
123            format_message: Some(self.0),
124            constraint_message: None,
125            reason: String::new(),
126        }
127    }
128}
129
130impl ContextResolved for HasConstraintMessage {
131    fn into_context_data(self) -> ContextData {
132        ContextData {
133            field_violations: Vec::new(),
134            precondition_violations: Vec::new(),
135            quota_violations: Vec::new(),
136            format_message: None,
137            constraint_message: Some(self.0),
138            reason: String::new(),
139        }
140    }
141}
142
143impl ContextResolved for HasPreconditionViolations {
144    fn into_context_data(self) -> ContextData {
145        ContextData {
146            field_violations: Vec::new(),
147            precondition_violations: self.0,
148            quota_violations: Vec::new(),
149            format_message: None,
150            constraint_message: None,
151            reason: String::new(),
152        }
153    }
154}
155
156impl ContextResolved for HasQuotaViolations {
157    fn into_context_data(self) -> ContextData {
158        ContextData {
159            field_violations: Vec::new(),
160            precondition_violations: Vec::new(),
161            quota_violations: self.0,
162            format_message: None,
163            constraint_message: None,
164            reason: String::new(),
165        }
166    }
167}
168
169impl ContextResolved for HasReason {
170    fn into_context_data(self) -> ContextData {
171        ContextData {
172            field_violations: Vec::new(),
173            precondition_violations: Vec::new(),
174            quota_violations: Vec::new(),
175            format_message: None,
176            constraint_message: None,
177            reason: self.0,
178        }
179    }
180}
181
182// ---------------------------------------------------------------------------
183// Error variant discriminant
184// ---------------------------------------------------------------------------
185
186#[doc(hidden)]
187#[derive(Debug, Clone, Copy)]
188pub enum ErrorVariant {
189    Cancelled,
190    Unknown,
191    InvalidArgument,
192    DeadlineExceeded,
193    NotFound,
194    AlreadyExists,
195    PermissionDenied,
196    ResourceExhausted,
197    FailedPrecondition,
198    Aborted,
199    OutOfRange,
200    Unimplemented,
201    Internal,
202    DataLoss,
203    Unauthenticated,
204}
205
206// ---------------------------------------------------------------------------
207// ResourceErrorBuilder
208// ---------------------------------------------------------------------------
209
210pub struct ResourceErrorBuilder<Resource, Context> {
211    resource_type: Option<&'static str>,
212    detail: String,
213    variant: ErrorVariant,
214    resource: Resource,
215    context: Context,
216}
217
218// ---------------------------------------------------------------------------
219// #[doc(hidden)] constructors — called by the macro
220// ---------------------------------------------------------------------------
221
222impl ResourceErrorBuilder<ResourceMissing, NoContext> {
223    #[doc(hidden)]
224    pub fn __not_found(resource_type: &'static str, detail: impl Into<String>) -> Self {
225        ResourceErrorBuilder {
226            resource_type: Some(resource_type),
227            detail: detail.into(),
228            variant: ErrorVariant::NotFound,
229            resource: ResourceMissing,
230            context: NoContext,
231        }
232    }
233
234    #[doc(hidden)]
235    pub fn __already_exists(resource_type: &'static str, detail: impl Into<String>) -> Self {
236        ResourceErrorBuilder {
237            resource_type: Some(resource_type),
238            detail: detail.into(),
239            variant: ErrorVariant::AlreadyExists,
240            resource: ResourceMissing,
241            context: NoContext,
242        }
243    }
244
245    #[doc(hidden)]
246    pub fn __data_loss(resource_type: &'static str, detail: impl Into<String>) -> Self {
247        ResourceErrorBuilder {
248            resource_type: Some(resource_type),
249            detail: detail.into(),
250            variant: ErrorVariant::DataLoss,
251            resource: ResourceMissing,
252            context: NoContext,
253        }
254    }
255}
256
257impl ResourceErrorBuilder<ResourceOptional, NeedsReason> {
258    #[doc(hidden)]
259    pub fn __aborted(resource_type: &'static str, detail: impl Into<String>) -> Self {
260        ResourceErrorBuilder {
261            resource_type: Some(resource_type),
262            detail: detail.into(),
263            variant: ErrorVariant::Aborted,
264            resource: ResourceOptional,
265            context: NeedsReason,
266        }
267    }
268}
269
270impl ResourceErrorBuilder<ResourceOptional, NoContext> {
271    #[doc(hidden)]
272    pub fn __unknown(resource_type: &'static str, detail: impl Into<String>) -> Self {
273        ResourceErrorBuilder {
274            resource_type: Some(resource_type),
275            detail: detail.into(),
276            variant: ErrorVariant::Unknown,
277            resource: ResourceOptional,
278            context: NoContext,
279        }
280    }
281
282    #[doc(hidden)]
283    pub fn __deadline_exceeded(resource_type: &'static str, detail: impl Into<String>) -> Self {
284        ResourceErrorBuilder {
285            resource_type: Some(resource_type),
286            detail: detail.into(),
287            variant: ErrorVariant::DeadlineExceeded,
288            resource: ResourceOptional,
289            context: NoContext,
290        }
291    }
292
293    #[doc(hidden)]
294    pub fn __unimplemented(resource_type: &'static str, detail: impl Into<String>) -> Self {
295        ResourceErrorBuilder {
296            resource_type: Some(resource_type),
297            detail: detail.into(),
298            variant: ErrorVariant::Unimplemented,
299            resource: ResourceOptional,
300            context: NoContext,
301        }
302    }
303}
304
305impl ResourceErrorBuilder<ResourceAbsent, NeedsReason> {
306    #[doc(hidden)]
307    pub fn __permission_denied(resource_type: &'static str, detail: impl Into<String>) -> Self {
308        ResourceErrorBuilder {
309            resource_type: Some(resource_type),
310            detail: detail.into(),
311            variant: ErrorVariant::PermissionDenied,
312            resource: ResourceAbsent,
313            context: NeedsReason,
314        }
315    }
316}
317
318impl ResourceErrorBuilder<ResourceAbsent, NoContext> {
319    #[doc(hidden)]
320    pub fn __cancelled(resource_type: &'static str, detail: impl Into<String>) -> Self {
321        ResourceErrorBuilder {
322            resource_type: Some(resource_type),
323            detail: detail.into(),
324            variant: ErrorVariant::Cancelled,
325            resource: ResourceAbsent,
326            context: NoContext,
327        }
328    }
329}
330
331impl ResourceErrorBuilder<ResourceOptional, NeedsFieldViolation> {
332    #[doc(hidden)]
333    pub fn __invalid_argument(resource_type: &'static str, detail: impl Into<String>) -> Self {
334        ResourceErrorBuilder {
335            resource_type: Some(resource_type),
336            detail: detail.into(),
337            variant: ErrorVariant::InvalidArgument,
338            resource: ResourceOptional,
339            context: NeedsFieldViolation,
340        }
341    }
342
343    #[doc(hidden)]
344    pub fn __out_of_range(resource_type: &'static str, detail: impl Into<String>) -> Self {
345        ResourceErrorBuilder {
346            resource_type: Some(resource_type),
347            detail: detail.into(),
348            variant: ErrorVariant::OutOfRange,
349            resource: ResourceOptional,
350            context: NeedsFieldViolation,
351        }
352    }
353}
354
355impl ResourceErrorBuilder<ResourceOptional, NeedsQuotaViolation> {
356    #[doc(hidden)]
357    pub fn __resource_exhausted(resource_type: &'static str, detail: impl Into<String>) -> Self {
358        ResourceErrorBuilder {
359            resource_type: Some(resource_type),
360            detail: detail.into(),
361            variant: ErrorVariant::ResourceExhausted,
362            resource: ResourceOptional,
363            context: NeedsQuotaViolation,
364        }
365    }
366}
367
368impl ResourceErrorBuilder<ResourceOptional, NeedsPreconditionViolation> {
369    #[doc(hidden)]
370    pub fn __failed_precondition(resource_type: &'static str, detail: impl Into<String>) -> Self {
371        ResourceErrorBuilder {
372            resource_type: Some(resource_type),
373            detail: detail.into(),
374            variant: ErrorVariant::FailedPrecondition,
375            resource: ResourceOptional,
376            context: NeedsPreconditionViolation,
377        }
378    }
379}
380
381// ---------------------------------------------------------------------------
382// with_resource() — available for ResourceMissing and ResourceOptional
383// ---------------------------------------------------------------------------
384
385impl<Context> ResourceErrorBuilder<ResourceMissing, Context> {
386    #[must_use]
387    pub fn with_resource(
388        self,
389        resource: impl Into<String>,
390    ) -> ResourceErrorBuilder<ResourceSet, Context> {
391        ResourceErrorBuilder {
392            resource_type: self.resource_type,
393            detail: self.detail,
394            variant: self.variant,
395            resource: ResourceSet(resource.into()),
396            context: self.context,
397        }
398    }
399}
400
401impl<Context> ResourceErrorBuilder<ResourceOptional, Context> {
402    #[must_use]
403    pub fn with_resource(
404        self,
405        resource: impl Into<String>,
406    ) -> ResourceErrorBuilder<ResourceSet, Context> {
407        ResourceErrorBuilder {
408            resource_type: self.resource_type,
409            detail: self.detail,
410            variant: self.variant,
411            resource: ResourceSet(resource.into()),
412            context: self.context,
413        }
414    }
415}
416
417// ---------------------------------------------------------------------------
418// with_field_violation() — NeedsFieldViolation → HasFieldViolations, then self
419// ---------------------------------------------------------------------------
420
421impl<Resource> ResourceErrorBuilder<Resource, NeedsFieldViolation> {
422    #[must_use]
423    pub fn with_field_violation(
424        self,
425        field: impl Into<String>,
426        description: impl Into<String>,
427        reason: impl Into<String>,
428    ) -> ResourceErrorBuilder<Resource, HasFieldViolations> {
429        ResourceErrorBuilder {
430            resource_type: self.resource_type,
431            detail: self.detail,
432            variant: self.variant,
433            resource: self.resource,
434            context: HasFieldViolations(vec![FieldViolation::new(field, description, reason)]),
435        }
436    }
437
438    #[must_use]
439    pub fn with_format(
440        self,
441        message: impl Into<String>,
442    ) -> ResourceErrorBuilder<Resource, HasFormatMessage> {
443        let msg = message.into();
444        ResourceErrorBuilder {
445            resource_type: self.resource_type,
446            detail: msg.clone(),
447            variant: self.variant,
448            resource: self.resource,
449            context: HasFormatMessage(msg),
450        }
451    }
452
453    #[must_use]
454    pub fn with_constraint(
455        self,
456        message: impl Into<String>,
457    ) -> ResourceErrorBuilder<Resource, HasConstraintMessage> {
458        let msg = message.into();
459        ResourceErrorBuilder {
460            resource_type: self.resource_type,
461            detail: msg.clone(),
462            variant: self.variant,
463            resource: self.resource,
464            context: HasConstraintMessage(msg),
465        }
466    }
467}
468
469impl<Resource> ResourceErrorBuilder<Resource, HasFieldViolations> {
470    #[must_use]
471    pub fn with_field_violation(
472        mut self,
473        field: impl Into<String>,
474        description: impl Into<String>,
475        reason: impl Into<String>,
476    ) -> Self {
477        self.context
478            .0
479            .push(FieldViolation::new(field, description, reason));
480        self
481    }
482}
483
484// ---------------------------------------------------------------------------
485// with_precondition_violation() — NeedsPreconditionViolation → HasPreconditionViolations
486// ---------------------------------------------------------------------------
487
488impl<Resource> ResourceErrorBuilder<Resource, NeedsPreconditionViolation> {
489    #[must_use]
490    pub fn with_precondition_violation(
491        self,
492        subject: impl Into<String>,
493        description: impl Into<String>,
494        type_: impl Into<String>,
495    ) -> ResourceErrorBuilder<Resource, HasPreconditionViolations> {
496        ResourceErrorBuilder {
497            resource_type: self.resource_type,
498            detail: self.detail,
499            variant: self.variant,
500            resource: self.resource,
501            context: HasPreconditionViolations(vec![PreconditionViolation::new(
502                type_,
503                subject,
504                description,
505            )]),
506        }
507    }
508}
509
510impl<Resource> ResourceErrorBuilder<Resource, HasPreconditionViolations> {
511    #[must_use]
512    pub fn with_precondition_violation(
513        mut self,
514        subject: impl Into<String>,
515        description: impl Into<String>,
516        type_: impl Into<String>,
517    ) -> Self {
518        self.context
519            .0
520            .push(PreconditionViolation::new(type_, subject, description));
521        self
522    }
523}
524
525// ---------------------------------------------------------------------------
526// with_quota_violation() — NeedsQuotaViolation → HasQuotaViolations
527// ---------------------------------------------------------------------------
528
529impl<Resource> ResourceErrorBuilder<Resource, NeedsQuotaViolation> {
530    #[must_use]
531    pub fn with_quota_violation(
532        self,
533        subject: impl Into<String>,
534        description: impl Into<String>,
535    ) -> ResourceErrorBuilder<Resource, HasQuotaViolations> {
536        ResourceErrorBuilder {
537            resource_type: self.resource_type,
538            detail: self.detail,
539            variant: self.variant,
540            resource: self.resource,
541            context: HasQuotaViolations(vec![QuotaViolation::new(subject, description)]),
542        }
543    }
544}
545
546impl<Resource> ResourceErrorBuilder<Resource, HasQuotaViolations> {
547    #[must_use]
548    pub fn with_quota_violation(
549        mut self,
550        subject: impl Into<String>,
551        description: impl Into<String>,
552    ) -> Self {
553        self.context
554            .0
555            .push(QuotaViolation::new(subject, description));
556        self
557    }
558}
559
560// ---------------------------------------------------------------------------
561// with_reason() — NeedsReason → HasReason
562// ---------------------------------------------------------------------------
563
564impl<Resource> ResourceErrorBuilder<Resource, NeedsReason> {
565    #[must_use]
566    pub fn with_reason(
567        self,
568        reason: impl Into<String>,
569    ) -> ResourceErrorBuilder<Resource, HasReason> {
570        ResourceErrorBuilder {
571            resource_type: self.resource_type,
572            detail: self.detail,
573            variant: self.variant,
574            resource: self.resource,
575            context: HasReason(reason.into()),
576        }
577    }
578}
579
580// ---------------------------------------------------------------------------
581// Public builder-returning constructors on CanonicalError (non-macro categories)
582// ---------------------------------------------------------------------------
583
584impl CanonicalError {
585    #[must_use]
586    pub fn internal(detail: impl Into<String>) -> ResourceErrorBuilder<ResourceAbsent, NoContext> {
587        ResourceErrorBuilder {
588            resource_type: None,
589            detail: detail.into(),
590            variant: ErrorVariant::Internal,
591            resource: ResourceAbsent,
592            context: NoContext,
593        }
594    }
595
596    #[must_use]
597    pub fn service_unavailable() -> ServiceUnavailableBuilder {
598        ServiceUnavailableBuilder {
599            retry_after_seconds: None,
600        }
601    }
602
603    #[must_use]
604    pub fn unauthenticated() -> ResourceErrorBuilder<ResourceAbsent, NeedsReason> {
605        ResourceErrorBuilder {
606            resource_type: None,
607            detail: String::from("Authentication required"),
608            variant: ErrorVariant::Unauthenticated,
609            resource: ResourceAbsent,
610            context: NeedsReason,
611        }
612    }
613}
614
615// ---------------------------------------------------------------------------
616// create() — gated by Resource + Context Resolved traits
617// ---------------------------------------------------------------------------
618
619impl<Resource, Context> ResourceErrorBuilder<Resource, Context>
620where
621    Resource: ResourceResolved,
622    Context: ContextResolved,
623{
624    #[must_use]
625    pub fn create(self) -> CanonicalError {
626        let resource_name = self.resource.resolve();
627        let ctx_data = self.context.into_context_data();
628
629        let err = match self.variant {
630            ErrorVariant::NotFound => CanonicalError::__not_found(NotFound::new()),
631            ErrorVariant::AlreadyExists => CanonicalError::__already_exists(AlreadyExists::new()),
632            ErrorVariant::Aborted => CanonicalError::__aborted(Aborted::new(&ctx_data.reason)),
633            ErrorVariant::Unknown => CanonicalError::__unknown(Unknown::new(&self.detail)),
634            ErrorVariant::DeadlineExceeded => {
635                CanonicalError::__deadline_exceeded(DeadlineExceeded::new())
636            }
637            ErrorVariant::PermissionDenied => {
638                CanonicalError::__permission_denied(PermissionDenied::new(&ctx_data.reason))
639            }
640            ErrorVariant::InvalidArgument => {
641                let ctx = if let Some(fmt) = ctx_data.format_message {
642                    InvalidArgument::format(fmt)
643                } else if let Some(cst) = ctx_data.constraint_message {
644                    InvalidArgument::constraint(cst)
645                } else {
646                    InvalidArgument::fields(ctx_data.field_violations)
647                };
648                CanonicalError::__invalid_argument(ctx)
649            }
650            ErrorVariant::OutOfRange => {
651                CanonicalError::__out_of_range(OutOfRange::new(ctx_data.field_violations))
652            }
653            ErrorVariant::ResourceExhausted => CanonicalError::__resource_exhausted(
654                ResourceExhausted::new(ctx_data.quota_violations),
655            ),
656            ErrorVariant::FailedPrecondition => CanonicalError::__failed_precondition(
657                FailedPrecondition::new(ctx_data.precondition_violations),
658            ),
659            ErrorVariant::Cancelled => CanonicalError::__cancelled(Cancelled::new()),
660            ErrorVariant::Unimplemented => CanonicalError::__unimplemented(Unimplemented::new()),
661            ErrorVariant::Internal => CanonicalError::__internal(Internal::new(&self.detail)),
662            ErrorVariant::DataLoss => CanonicalError::__data_loss(DataLoss::new()),
663            ErrorVariant::Unauthenticated => {
664                let mut ctx = Unauthenticated::new();
665                if !ctx_data.reason.is_empty() {
666                    ctx = ctx.with_reason(ctx_data.reason);
667                }
668                CanonicalError::__unauthenticated(ctx)
669            }
670        };
671
672        let mut err = if matches!(
673            err,
674            CanonicalError::Internal { .. } | CanonicalError::Unknown { .. }
675        ) {
676            err
677        } else {
678            err.with_detail(&self.detail)
679        };
680
681        if let Some(rt) = self.resource_type {
682            err = err.with_resource_type(rt);
683        }
684
685        if let Some(rn) = resource_name {
686            err.with_resource(rn)
687        } else {
688            err
689        }
690    }
691}
692
693// ---------------------------------------------------------------------------
694// ServiceUnavailableBuilder — dedicated builder for ServiceUnavailable
695// ---------------------------------------------------------------------------
696
697pub struct ServiceUnavailableBuilder {
698    retry_after_seconds: Option<u64>,
699}
700
701impl ServiceUnavailableBuilder {
702    #[must_use]
703    pub fn with_retry_after_seconds(mut self, seconds: u64) -> Self {
704        self.retry_after_seconds = Some(seconds);
705        self
706    }
707
708    #[must_use]
709    pub fn create(self) -> CanonicalError {
710        CanonicalError::__service_unavailable(ServiceUnavailable::new(self.retry_after_seconds))
711            .with_detail("Service temporarily unavailable")
712    }
713}