Skip to main content

anyhow_auto_context/
lib.rs

1#![doc = include_str!("../README.md")]
2
3/// Adds automatic `anyhow` context based on scope and location.
4///
5/// Additional context passed to the macro is formatted lazily and evaluated
6/// only on the error path.
7///
8/// ```
9/// # use anyhow_auto_context::auto_context;
10///
11/// let expected_some: Option<()> = None;
12/// let result = auto_context!(expected_some);
13/// assert!(result.unwrap_err().to_string().starts_with("expected_some in"));
14///
15/// let expected_ok: anyhow::Result<()> = Err(anyhow::anyhow!("foo_err"));
16/// let result = auto_context!(expected_ok);
17/// assert!(result.unwrap_err().to_string().starts_with("expected_ok in"));
18///
19/// let user = "Alice";
20/// let expected_ok: anyhow::Result<()> = Err(anyhow::anyhow!("foo_err"));
21/// let result = auto_context!(expected_ok, "for user {user}");
22/// let err = result.unwrap_err().to_string();
23/// assert!(err.starts_with("expected_ok in"));
24/// assert!(err.ends_with("with for user Alice"));
25/// ```
26#[macro_export]
27macro_rules! auto_context {
28    (@location $result:expr_2021) => {{
29        const fn f() {}
30        fn type_name<T>(_: T) -> &'static str {
31            ::std::any::type_name::<T>()
32        }
33        let scope = type_name(f)
34            .strip_suffix("::f")
35            .unwrap_or_default()
36            .trim_end_matches("::{{closure}}");
37        format!(
38            "{} in {} at {}:{}:{}",
39            stringify!($result),
40            scope,
41            file!(),
42            line!(),
43            column!(),
44        )
45    }};
46    ($result:expr_2021 $(,)?) => {
47        anyhow::Context::with_context($result, || $crate::auto_context!(@location $result))
48    };
49    ($result:expr_2021, $($context:tt)+) => {
50        anyhow::Context::with_context($result, || {
51            let location = $crate::auto_context!(@location $result);
52            format!("{location} with {}", format!($($context)+))
53        })
54    };
55}
56
57#[cfg(test)]
58mod tests {
59    fn ensure_42(n: i32) -> anyhow::Result<()> {
60        anyhow::ensure!(n == 42, "Expected 42");
61        Ok(())
62    }
63
64    #[test]
65    fn ok() {
66        assert!(auto_context!(ensure_42(42)).is_ok());
67    }
68
69    #[test]
70    fn err() {
71        let err_str = auto_context!(ensure_42(0)).unwrap_err().to_string();
72        assert!(err_str.starts_with("ensure_42(0) in"), "{err_str}");
73    }
74
75    #[test]
76    fn some() {
77        assert_eq!(auto_context!(Some(42)).unwrap(), 42);
78    }
79
80    #[test]
81    fn none() {
82        let expected_some: Option<i32> = None;
83
84        let err_str = auto_context!(expected_some).unwrap_err().to_string();
85        assert!(err_str.starts_with("expected_some in"), "{err_str}");
86    }
87
88    #[test]
89    fn err_with_custom_context() {
90        let user = "Alice";
91
92        let err_str = auto_context!(ensure_42(0), "user {user}")
93            .unwrap_err()
94            .to_string();
95        assert!(err_str.starts_with("ensure_42(0) in"), "{err_str}");
96        assert!(err_str.ends_with("with user Alice"), "{err_str}");
97    }
98
99    #[test]
100    fn none_with_custom_context() {
101        let user = "Alice";
102        let expected_some: Option<i32> = None;
103
104        let err_str = auto_context!(expected_some, "user {user}")
105            .unwrap_err()
106            .to_string();
107        assert!(err_str.starts_with("expected_some in"), "{err_str}");
108        assert!(err_str.ends_with("with user Alice"), "{err_str}");
109    }
110}