Skip to main content

geop_core_math/
geop_error.rs

1use std::backtrace::Backtrace;
2
3/// A renderable diagnostic artefact that can be attached to any
4/// frame of a `GeopError`.
5pub trait DebugContext: Send + Sync + core::fmt::Debug {
6    /// One-line label shown in the error chain display.
7    fn label(&self) -> &str;
8}
9
10pub enum GeopError {
11    Root {
12        message: String,
13        backtrace: Backtrace,
14    },
15    Context {
16        inner: Box<GeopError>,
17        context: Option<Box<dyn DebugContext>>,
18    },
19}
20
21impl GeopError {
22    /// Create a root error. Captures a backtrace at the call site.
23    pub fn new(message: impl Into<String>) -> Self {
24        let backtrace = Backtrace::capture();
25        GeopError::Root {
26            message: message.into(),
27            backtrace,
28        }
29    }
30
31    /// Wrap this error in a Context frame with a string message.
32    pub fn with_context(self, message: impl Into<String>) -> Self {
33        let label = message.into();
34        GeopError::Context {
35            inner: Box::new(self),
36            context: Some(Box::new(StringContext(label))),
37        }
38    }
39
40    /// Wrap this error in a Context frame with a debug scene.
41    pub fn with_scene(self, scene: impl DebugContext + 'static) -> Self {
42        GeopError::Context {
43            inner: Box::new(self),
44            context: Some(Box::new(scene)),
45        }
46    }
47
48    /// Walk the chain and collect labels of all attached scenes, root-first.
49    pub fn scene_labels(&self) -> Vec<&str> {
50        match self {
51            GeopError::Root { .. } => vec![],
52            GeopError::Context { inner, context } => {
53                let mut labels = inner.scene_labels();
54                if let Some(ctx) = context {
55                    labels.push(ctx.label());
56                }
57                labels
58            }
59        }
60    }
61}
62
63impl std::fmt::Display for GeopError {
64    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
65        match self {
66            GeopError::Root { message, backtrace } => {
67                writeln!(f, "GeopError")?;
68                writeln!(f, "Backtrace: {}", backtrace)?;
69                writeln!(f, "RootError: {}", message)
70            }
71            GeopError::Context { inner, context } => {
72                write!(f, "{}", inner)?;
73                match context {
74                    Some(ctx) => writeln!(f, "Context:\n{}", ctx.label()),
75                    None => writeln!(f, "Context: (no details)"),
76                }
77            }
78        }
79    }
80}
81
82impl std::fmt::Debug for GeopError {
83    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
84        write!(f, "{}", self)
85    }
86}
87
88impl std::error::Error for GeopError {}
89
90impl From<&str> for GeopError {
91    fn from(message: &str) -> Self {
92        GeopError::new(message)
93    }
94}
95
96impl From<String> for GeopError {
97    fn from(message: String) -> Self {
98        GeopError::new(message)
99    }
100}
101
102pub type GeopResult<T> = Result<T, GeopError>;
103
104/// Something that can turn one `GeopError` into another, more-annotated one
105/// — either a plain message (wrapped via `GeopError::with_context`) or a
106/// callback that builds the wrapped error itself (e.g. to embed live state
107/// captured at the call site). Lets [`WithContext::with_context`] accept
108/// either `.with_context("a message")` or `.with_context(&|e| ...)`.
109pub trait ContextSource {
110    fn apply(&self, err: GeopError) -> GeopError;
111}
112
113impl ContextSource for str {
114    fn apply(&self, err: GeopError) -> GeopError {
115        err.with_context(self)
116    }
117}
118
119impl ContextSource for String {
120    fn apply(&self, err: GeopError) -> GeopError {
121        err.with_context(self.as_str())
122    }
123}
124
125impl<F: Fn(GeopError) -> GeopError + ?Sized> ContextSource for F {
126    fn apply(&self, err: GeopError) -> GeopError {
127        self(err)
128    }
129}
130
131pub trait WithContext<T> {
132    fn with_context(self, ctx: &(impl ContextSource + ?Sized)) -> GeopResult<T>;
133}
134
135impl<T> WithContext<T> for GeopResult<T> {
136    fn with_context(self, ctx: &(impl ContextSource + ?Sized)) -> GeopResult<T> {
137        match self {
138            Ok(v) => Ok(v),
139            Err(err) => Err(ctx.apply(err)),
140        }
141    }
142}
143
144/// Like [`format!`], but for [`WithContext::with_context`]: builds a
145/// `ContextSource` that formats its message lazily, only if the result is
146/// actually an `Err` — `.with_context(with_context!("failed at i={i}"))`
147/// costs nothing on the success path, unlike `.with_context(&format!(...))`
148/// (eagerly formats every time, even when nothing is wrong).
149#[macro_export]
150macro_rules! with_context {
151    ($($arg:tt)*) => {
152        &|e: $crate::geop_error::GeopError| e.with_context(format!($($arg)*))
153    };
154}
155
156// ── internal helper ──────────────────────────────────────────────────────────
157
158#[derive(Debug)]
159struct StringContext(String);
160
161impl DebugContext for StringContext {
162    fn label(&self) -> &str {
163        &self.0
164    }
165}