Skip to main content

g_err/
gerr.rs

1extern crate alloc;
2
3use alloc::boxed::Box;
4use alloc::format;
5use alloc::string::{String, ToString};
6use alloc::vec::Vec;
7
8use crate::gerr_box::GErrBox;
9use crate::gerr_source::DataSource;
10use crate::gerr_source::GErrSource;
11use crate::gerr_source::IdSource;
12use crate::types::DefaultConfig;
13use crate::types::NoData;
14
15use core::{
16    error::Error,
17    fmt::{Debug, Display},
18    panic::Location,
19};
20
21#[cfg(feature = "backtrace")]
22use std::backtrace::Backtrace;
23
24use alloc::borrow::Cow;
25
26/// Error config.
27pub trait Config {
28    /// Error id.
29    ///
30    /// # Must be:
31    /// - Implementing `Display` & `Debug`.
32    /// - Implementing serde's Serialize and Deserialize for `serde` feature.
33    type Id;
34
35    /// Auto-generate error id.
36    ///
37    /// Defaults to `None`.
38    #[inline]
39    fn id() -> Option<Self::Id> {
40        None
41    }
42
43    /// Error code.
44    const CODE: Option<&'static str> = None;
45
46    /// Error tags.
47    const TAGS: Option<&'static [&'static str]> = None;
48
49    /// Display error message.
50    ///
51    /// It will be invoked in `Display` implementation.
52    ///
53    /// Defaults to `[<id>][<code>] <message>`.
54    #[inline]
55    fn display<C: Config, D>(gerr: &GErr<C, D>) -> String
56    where
57        C::Id: Display,
58        D: Debug,
59    {
60        match (gerr.id(), gerr.code()) {
61            (Some(id), Some(code)) => format!("[{id}][{code}] {}", gerr.message()),
62            (Some(id), None) => format!("[{id}][-] {}", gerr.message()),
63            (None, Some(code)) => format!("[-][{code}] {}", gerr.message()),
64            (None, None) => format!("[-][-] {}", gerr.message()),
65        }
66    }
67}
68
69/// Trait for upserting error data.
70///
71/// Self must implement `Default`.
72pub trait SetField<K, V> {
73    fn set_field(&mut self, key: K, value: V);
74}
75
76/// Alias for core's Result.
77pub type Result<T, C = DefaultConfig, D = NoData> = core::result::Result<T, GErr<C, D>>;
78
79/// GErr with default config(no id) and no data.
80pub type GErrDefault = GErr<DefaultConfig, NoData>;
81
82/// GErr's source
83#[derive(Debug)]
84pub enum Source {
85    /// General error.
86    Err(Box<dyn Error + Send + Sync + 'static>),
87
88    // It's boxed to reduce error size.
89    /// GErr error.
90    GErr(Box<GErrSource>),
91}
92
93/// GErr - Structured Error Type.
94///
95/// It's generically configurable with id, code, data, etc.
96///
97/// Error location is automatically generated.
98///
99/// It contains stack-trace if `backtrace` feature is enabled.
100///
101/// It provides 2 type parameters `C` and `D`:
102/// - `C` is for error config. It bounds with [`Config`] trait containing error type configuration.
103/// - `D` is for error data.
104///
105/// GErr contains error as having these attributes:
106/// - `id`: The error id. This is to identify the error. It can be autogenerated or from outside(e.g. request id).
107/// - `code`: The error code. Acts as label/category/group of errors.
108/// - `message`: The error message.
109/// - `sources`: The sources of current error. Errors can be linked/chained from multiple errors, serial or parallel.
110/// - `tags`: The error tags. Things related to the error. For easy querying.
111/// - `data`: The error data. This is additional data about the error. It can be error kinds(enum), user data triggering the errors, etc.
112/// - `help`: Hint about how to solve the error.
113/// - `location`: Where the error happen.
114/// - `backtrace`: The error stacktrace, feature-gated behind `backtrace`.
115///
116/// # Note
117/// Set aside size of error ID and Data:
118///
119/// - `GErr<DefaultConfig, NoData>` is approximately 160 bytes.
120/// - Enabling the `backtrace` feature increases this to approximately
121///   208 bytes (+48 bytes from `Backtrace`).
122///
123/// Clippy's `result_large_err` lint is intentionally allowed for this crate.
124/// Users requiring a smaller error representation may box `GErr` in their
125/// own APIs or using provided [`GErrBox`].
126pub struct GErr<C: Config = DefaultConfig, D = NoData> {
127    id: Option<C::Id>,
128
129    code: Option<Cow<'static, str>>,
130
131    message: Cow<'static, str>,
132
133    sources: Option<Vec<Source>>,
134
135    tags: Option<Vec<Cow<'static, str>>>,
136
137    data: Option<D>,
138
139    help: Option<Cow<'static, str>>,
140
141    location: ErrorLocation,
142
143    #[cfg(feature = "backtrace")]
144    backtrace: Backtrace,
145}
146
147/// Location where error happen.
148#[derive(Debug, PartialEq, Eq)]
149pub struct ErrorLocation {
150    /// Filename of where error happen.
151    pub file: Cow<'static, str>,
152    /// Line number of where error happen.
153    pub line: u32,
154    /// Column of where error happen.
155    pub column: u32,
156}
157
158impl<C: Config, D> GErr<C, D> {
159    /// Constructs new GErr with message and auto-generated id.
160    #[track_caller]
161    #[inline]
162    pub fn new<M>(message: M) -> Self
163    where
164        M: Into<Cow<'static, str>>,
165    {
166        Self::new_untracked(message, Location::caller())
167    }
168
169    #[inline]
170    pub(crate) fn new_untracked<M>(message: M, location: &'static Location<'static>) -> Self
171    where
172        M: Into<Cow<'static, str>>,
173    {
174        Self::new_with_id_untracked(C::id(), message.into(), location)
175    }
176
177    /// Constructs new GErr from any error implementing trait [`Error`]
178    ///
179    /// Id is auto-generated and only contains message, location and the error as source.
180    #[track_caller]
181    #[inline]
182    pub fn from_error<E>(err: E) -> Self
183    where
184        E: Error + Send + Sync + 'static,
185    {
186        Self::new_untracked(err.to_string(), Location::caller()).add_source(err)
187    }
188
189    /// Constructs new GErr with id and message.
190    ///
191    /// This is to set error id manually.
192    #[track_caller]
193    #[inline]
194    pub fn new_with_id<M>(id: C::Id, message: M) -> Self
195    where
196        M: Into<Cow<'static, str>>,
197    {
198        Self::new_with_id_untracked(Some(id), message, Location::caller())
199    }
200
201    #[inline]
202    pub(crate) fn new_with_id_untracked<M>(
203        id: Option<C::Id>,
204        message: M,
205        location: &'static Location<'static>,
206    ) -> Self
207    where
208        M: Into<Cow<'static, str>>,
209    {
210        Self {
211            id,
212
213            code: C::CODE.map(Cow::Borrowed),
214
215            message: message.into(),
216
217            sources: None,
218
219            tags: C::TAGS.map(|tags| {
220                tags.iter()
221                    .map(|tag| Cow::Borrowed(*tag))
222                    .collect::<Vec<_>>()
223            }),
224
225            data: None,
226
227            help: None,
228
229            location: location.into(),
230
231            #[cfg(feature = "backtrace")]
232            backtrace: Backtrace::capture(),
233        }
234    }
235
236    /// Constructs new GErr from any error implementing trait [`Error`]
237    ///
238    /// Id is manually-set and only contains message, location and the error as source.
239    #[track_caller]
240    #[inline]
241    pub fn from_error_with_id<E>(id: C::Id, err: E) -> Self
242    where
243        E: Error + Send + Sync + 'static,
244    {
245        Self::new_with_id_untracked(Some(id), err.to_string(), Location::caller()).add_source(err)
246    }
247
248    #[track_caller]
249    #[inline]
250    pub fn from_gerr<E>(gerr: E) -> Self
251    where
252        E: Into<GErrSource> + Send + Sync + 'static,
253    {
254        Self::from_gerr_untracked(gerr, Location::caller())
255    }
256
257    #[inline]
258    pub(crate) fn from_gerr_untracked<E>(gerr: E, location: &'static Location<'static>) -> Self
259    where
260        E: Into<GErrSource> + Send + Sync + 'static,
261    {
262        let gerr = gerr.into();
263        Self {
264            id: C::id(),
265            code: C::CODE.map(Cow::Borrowed),
266            message: gerr.message,
267            sources: gerr.sources,
268            tags: C::TAGS.map(|tags| {
269                tags.iter()
270                    .map(|tag| Cow::Borrowed(*tag))
271                    .collect::<Vec<_>>()
272            }),
273            data: None,
274            help: gerr.help,
275            location: location.into(),
276            #[cfg(feature = "backtrace")]
277            backtrace: Backtrace::capture(),
278        }
279    }
280}
281
282impl<C: Config, D> GErr<C, D> {
283    /// Set error id.
284    #[must_use]
285    #[inline]
286    pub fn set_id(mut self, id: C::Id) -> Self {
287        self.id = Some(id);
288        self
289    }
290
291    /// set error code.
292    #[must_use]
293    #[inline]
294    pub fn set_code<T>(mut self, code: T) -> Self
295    where
296        T: Into<Cow<'static, str>>,
297    {
298        self.code = Some(code.into());
299        self
300    }
301
302    /// Set list of sources.
303    #[must_use]
304    #[inline]
305    pub fn set_sources<I>(mut self, sources: I) -> Self
306    where
307        I: IntoIterator<Item = Source>,
308    {
309        self.sources = Some(sources.into_iter().collect());
310        self
311    }
312
313    /// Add general error(non-gerr) into list of sources.
314    #[must_use]
315    #[inline]
316    pub fn add_source<E>(mut self, source: E) -> Self
317    where
318        E: Error + Send + Sync + 'static,
319    {
320        self.sources
321            .get_or_insert_default()
322            .push(Source::Err(Box::new(source)));
323        self
324    }
325
326    /// Add GErr as error source.
327    ///
328    /// Any errors implementing `Into<GErrSource>` pass.
329    #[must_use]
330    #[inline]
331    pub fn add_source_gerr<E>(mut self, gerr: E) -> Self
332    where
333        E: Into<GErrSource> + Error + Send + Sync + 'static,
334    {
335        self.sources
336            .get_or_insert_default()
337            .push(Source::GErr(Box::new(gerr.into())));
338        self
339    }
340
341    /// Add tag to GErr.
342    #[must_use]
343    #[inline]
344    pub fn add_tag<T>(mut self, tag: T) -> Self
345    where
346        T: Into<Cow<'static, str>>,
347    {
348        self.tags.get_or_insert_default().push(tag.into());
349        self
350    }
351
352    /// Add multiple tags at once to GErr.
353    #[must_use]
354    #[inline]
355    pub fn add_tags<I, T>(mut self, tags: I) -> Self
356    where
357        I: IntoIterator<Item = T>,
358        T: Into<Cow<'static, str>>,
359    {
360        let mut tags = tags.into_iter().peekable();
361        if tags.peek().is_some() {
362            self.tags
363                .get_or_insert_default()
364                .extend(tags.map(Into::into));
365        }
366        self
367    }
368
369    /// Set error data.
370    #[must_use]
371    #[inline]
372    pub fn set_data(mut self, data: D) -> Self {
373        self.data = Some(data);
374        self
375    }
376
377    /// Set help.
378    #[must_use]
379    #[inline]
380    pub fn set_help<H>(mut self, help: H) -> Self
381    where
382        H: Into<Cow<'static, str>>,
383    {
384        self.help = Some(help.into());
385        self
386    }
387
388    /// Overwrite config type along with associated values.
389    #[must_use]
390    #[inline]
391    pub fn with_config<T: Config>(self) -> GErr<T, D> {
392        GErr {
393            id: T::id(),
394            code: T::CODE.map(Cow::Borrowed),
395            message: self.message,
396            sources: self.sources,
397            tags: T::TAGS.map(|tags| {
398                tags.iter()
399                    .map(|tag| Cow::Borrowed(*tag))
400                    .collect::<Vec<_>>()
401            }),
402            data: self.data,
403            help: self.help,
404            location: self.location,
405            #[cfg(feature = "backtrace")]
406            backtrace: self.backtrace,
407        }
408    }
409
410    /// Override Data(D) type.
411    #[must_use]
412    #[inline]
413    pub fn with_data_type<T>(self) -> GErr<C, T> {
414        GErr {
415            id: self.id,
416            code: self.code,
417            message: self.message,
418
419            sources: self.sources,
420
421            tags: self.tags,
422
423            data: None,
424
425            help: self.help,
426
427            location: self.location,
428
429            #[cfg(feature = "backtrace")]
430            backtrace: self.backtrace,
431        }
432    }
433
434    /// Override Data(D) value and type.
435    ///
436    /// This returns new GErr with different type of Data.
437    #[must_use]
438    #[inline]
439    pub fn with_data<T>(self, data: T) -> GErr<C, T> {
440        GErr {
441            id: self.id,
442            message: self.message,
443
444            code: self.code,
445            sources: self.sources,
446
447            tags: self.tags,
448
449            data: Some(data),
450
451            help: self.help,
452
453            location: self.location,
454
455            #[cfg(feature = "backtrace")]
456            backtrace: self.backtrace,
457        }
458    }
459
460    #[allow(dead_code)]
461    #[inline]
462    pub(crate) fn set_location(mut self, loc: ErrorLocation) -> Self {
463        self.location = loc;
464        self
465    }
466
467    // --- getter ---
468
469    /// Error id.
470    #[inline]
471    pub fn id(&self) -> Option<&C::Id> {
472        self.id.as_ref()
473    }
474
475    /// Error message.
476    #[inline]
477    pub fn message(&self) -> &str {
478        &self.message
479    }
480
481    /// Error code.
482    #[inline]
483    pub fn code(&self) -> Option<&str> {
484        self.code.as_deref().or(C::CODE)
485    }
486
487    /// Error tags.
488    #[inline]
489    pub fn tags(&self) -> Option<&[Cow<'static, str>]> {
490        self.tags.as_deref()
491    }
492
493    /// Error data.
494    #[inline]
495    pub fn data(&self) -> Option<&D> {
496        self.data.as_ref()
497    }
498
499    /// Error sources.
500    #[inline]
501    pub fn sources(&self) -> Option<&[Source]> {
502        self.sources.as_deref()
503    }
504
505    /// Error location.
506    #[inline]
507    pub fn location(&self) -> &ErrorLocation {
508        &self.location
509    }
510
511    /// Error help hint.
512    #[inline]
513    pub fn help(&self) -> Option<&str> {
514        self.help.as_deref()
515    }
516
517    /// Error stacktrace, if `backtrace` enabled.
518    #[cfg(feature = "backtrace")]
519    #[inline]
520    pub fn backtrace(&self) -> &std::backtrace::Backtrace {
521        &self.backtrace
522    }
523
524    /// Returns GErr as `Result<T, GErr<ID, P, D>>`.
525    #[inline]
526    pub fn result<T>(self) -> Result<T, C, D> {
527        Result::Err(self)
528    }
529
530    /// Box GErr
531    #[inline]
532    pub fn boxed(self) -> GErrBox<C, D> {
533        Box::new(self)
534    }
535}
536
537impl<C: Config, D> GErr<C, D> {
538    /// Update error data's fields provided that the data implement `Default` and [`SetField`].
539    ///
540    /// [`SetField`] trait implementation is user-defined.
541    #[must_use]
542    #[inline]
543    pub fn set_field<K, V>(mut self, key: K, value: V) -> Self
544    where
545        D: Default + SetField<K, V>,
546    {
547        let data = self.data.get_or_insert_with(Default::default);
548        data.set_field(key, value);
549        self
550    }
551}
552
553impl<C: Config, D> Display for GErr<C, D>
554where
555    C::Id: Display,
556    D: Debug,
557{
558    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
559        write!(f, "{}", C::display(self))
560    }
561}
562
563impl<C: Config, D: Debug> Debug for GErr<C, D>
564where
565    C::Id: Debug,
566{
567    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
568        let mut debug = f.debug_struct("GErr");
569
570        debug
571            .field("id", &self.id)
572            .field("code", &self.code())
573            .field("message", &self.message)
574            .field("sources", &self.sources)
575            .field("tags", &self.tags)
576            .field("data", &self.data)
577            .field("help", &self.help())
578            .field("location", &self.location);
579
580        #[cfg(feature = "backtrace")]
581        debug.field("backtrace", &self.backtrace);
582
583        debug.finish()
584    }
585}
586
587impl<C: Config, D> Error for GErr<C, D>
588where
589    C::Id: Debug + Display,
590    D: Debug,
591{
592    fn source(&self) -> Option<&(dyn Error + 'static)> {
593        if let Some(ref sources) = self.sources
594            && !sources.is_empty()
595        {
596            return sources.first().map(|s| match s {
597                Source::Err(e) => &**e as &(dyn Error + 'static),
598                Source::GErr(e) => &**e as &(dyn Error + 'static),
599            });
600        }
601        None
602    }
603}
604
605#[cfg(not(feature = "serde"))]
606impl<C: Config, D> From<GErr<C, D>> for GErrSource
607where
608    C::Id: IdSource + 'static,
609    D: DataSource + 'static,
610{
611    fn from(gerr: GErr<C, D>) -> Self {
612        GErrSource {
613            id: gerr.id.map(|id| Box::new(id) as Box<dyn IdSource>),
614            code: gerr.code,
615            message: gerr.message,
616            tags: gerr.tags,
617            data: gerr.data.map(|d| Box::new(d) as Box<dyn DataSource>),
618            help: gerr.help,
619            location: Some(gerr.location),
620            sources: gerr.sources,
621        }
622    }
623}
624
625#[cfg(not(feature = "serde"))]
626impl<C: Config, D> GErr<C, D>
627where
628    C::Id: IdSource + 'static,
629    D: DataSource + 'static,
630{
631    /// Converts GErr into [`GErrSource`].
632    #[inline]
633    pub fn into_gerr_source(self) -> GErrSource {
634        self.into()
635    }
636}
637
638#[cfg(feature = "serde")]
639impl<C: Config, D> From<GErr<C, D>> for GErrSource
640where
641    C::Id: ::serde::Serialize + IdSource + 'static,
642    D: ::serde::Serialize + DataSource + 'static,
643{
644    fn from(gerr: GErr<C, D>) -> Self {
645        GErrSource {
646            id_json: gerr
647                .id()
648                .map(|id| serde_json::to_value(id).unwrap_or_default()),
649            id: gerr.id.map(|id| Box::new(id) as Box<dyn IdSource>),
650            code: gerr.code,
651            message: gerr.message,
652            tags: gerr.tags,
653            data_json: gerr
654                .data
655                .as_ref()
656                .map(|d| serde_json::to_value(d).unwrap_or_default()),
657            data: gerr.data.map(|d| Box::new(d) as Box<dyn DataSource>),
658            help: gerr.help,
659            location: Some(gerr.location),
660            sources: gerr.sources,
661        }
662    }
663}
664
665#[cfg(feature = "serde")]
666impl<C: Config, D> GErr<C, D>
667where
668    C::Id: ::serde::Serialize + IdSource + 'static,
669    D: ::serde::Serialize + DataSource + 'static,
670{
671    /// Converts GErr into [`GErrSource`].
672    #[inline]
673    pub fn into_gerr_source(self) -> GErrSource {
674        self.into()
675    }
676}
677
678impl<T, C: Config, D> From<GErr<C, D>> for core::result::Result<T, GErr<C, D>> {
679    #[inline]
680    fn from(value: GErr<C, D>) -> Self {
681        core::result::Result::Err(value)
682    }
683}
684
685impl From<&'static core::panic::Location<'static>> for ErrorLocation {
686    #[inline]
687    fn from(location: &'static core::panic::Location<'static>) -> Self {
688        Self {
689            file: Cow::Borrowed(location.file()),
690            line: location.line(),
691            column: location.column(),
692        }
693    }
694}