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