Skip to main content

ohno/
enrichable.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4use std::borrow::Cow;
5use std::error::Error as StdError;
6
7use crate::{EnrichmentEntry, OhnoCore};
8
9/// Base trait for adding error enrichment to error types.
10///
11/// This trait provides the fundamental error enrichment addition method and is dyn-compatible.
12/// It serves as the base for the more ergonomic [`EnrichableExt`] trait.
13pub trait Enrichable {
14    /// Adds enrichment information to the error.
15    ///
16    /// This is the core method that other error enrichment methods build upon.
17    ///
18    /// # Note
19    ///
20    /// This method is not intended to be used directly. Instead, use:
21    /// - The [`enrich_err!()`](crate::enrich_err) macro for convenient error enrichment
22    /// - Methods from [`EnrichableExt`] trait ([`enrich()`](EnrichableExt::enrich), [`enrich_with()`](EnrichableExt::enrich_with))
23    fn add_enrichment(&mut self, entry: EnrichmentEntry);
24}
25
26impl<T, E> Enrichable for Result<T, E>
27where
28    E: StdError + Enrichable,
29{
30    fn add_enrichment(&mut self, entry: EnrichmentEntry) {
31        if let Err(e) = self {
32            e.add_enrichment(entry);
33        }
34    }
35}
36
37impl Enrichable for OhnoCore {
38    fn add_enrichment(&mut self, entry: EnrichmentEntry) {
39        self.data.enrichment.push(entry);
40    }
41}
42
43/// Extension trait providing ergonomic error enrichment methods.
44pub trait EnrichableExt: Enrichable {
45    /// Adds enrichment information to the error.
46    ///
47    /// It uses [`Location::caller`](std::panic::Location::caller) to capture the file and line number
48    /// where this method is invoked.
49    #[must_use]
50    fn enrich(mut self, msg: impl Into<Cow<'static, str>>) -> Self
51    where
52        Self: Sized,
53    {
54        let location = std::panic::Location::caller();
55        self.add_enrichment(EnrichmentEntry::new(msg, location.file(), location.line()));
56        self
57    }
58
59    /// Adds lazily evaluated enrichment information to the error.
60    ///
61    /// It uses [`Location::caller`](std::panic::Location::caller) to capture the file and line number
62    /// where this method is invoked.
63    #[must_use]
64    fn enrich_with<F, R>(mut self, f: F) -> Self
65    where
66        F: FnOnce() -> R,
67        R: Into<Cow<'static, str>>,
68        Self: Sized,
69    {
70        let location = std::panic::Location::caller();
71        self.add_enrichment(EnrichmentEntry::new(f(), location.file(), location.line()));
72        self
73    }
74}
75
76// Blanket implementation: all types that implement Enrichable automatically get EnrichableExt
77impl<T: Enrichable> EnrichableExt for T {}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[derive(Default, ohno::Error)]
84    pub struct TestError {
85        pub data: OhnoCore,
86    }
87
88    #[test]
89    fn test_enrich() {
90        let mut error = TestError::default();
91        error.add_enrichment(EnrichmentEntry::new("Test enrichment", "test.rs", 5));
92        assert_eq!(error.data.data.enrichment.len(), 1);
93        assert_eq!(error.data.data.enrichment[0].message, "Test enrichment");
94        assert_eq!(error.data.data.enrichment[0].location.file, "test.rs");
95        assert_eq!(error.data.data.enrichment[0].location.line, 5);
96
97        error.add_enrichment(EnrichmentEntry::new("Test enrichment", "test.rs", 10));
98        assert_eq!(error.data.data.enrichment.len(), 2);
99        assert_eq!(error.data.data.enrichment[1].message, "Test enrichment");
100        let location = &error.data.data.enrichment[1].location;
101        assert_eq!(location.file, "test.rs");
102        assert_eq!(location.line, 10);
103    }
104
105    #[test]
106    fn test_enrichable_ext() {
107        let error = TestError::default();
108        let mut result: Result<(), _> = Err(error);
109
110        result.add_enrichment(EnrichmentEntry::new("Immediate enrichment", "test.rs", 15));
111
112        let err = result.unwrap_err();
113        assert_eq!(err.data.data.enrichment.len(), 1);
114        assert_eq!(err.data.data.enrichment[0].message, "Immediate enrichment");
115        assert_eq!(err.data.data.enrichment[0].location.file, "test.rs");
116        assert_eq!(err.data.data.enrichment[0].location.line, 15);
117
118        result = Err(err).enrich("Detailed enrichment");
119        let err = result.unwrap_err();
120
121        assert_eq!(err.data.data.enrichment.len(), 2);
122        assert_eq!(err.data.data.enrichment[1].message, "Detailed enrichment");
123        let location = &err.data.data.enrichment[1].location;
124        assert!(location.file.ends_with("enrichable.rs"));
125    }
126}