Skip to main content

geop_core_math/
geop_error.rs

1use std::{backtrace::Backtrace, sync::Arc};
2
3/// An error: what went wrong at the root, and the context it was reported
4/// through. A copy of one is the same error — a step's failure kept with
5/// what it built, and reported again with more context — so the backtrace
6/// is shared, not captured again.
7#[derive(Clone)]
8pub enum GeopError {
9    Root {
10        message: String,
11        backtrace: Arc<Backtrace>,
12    },
13    /// `inner`, reported through a frame that says what was being done:
14    /// one line of the chain that [`GeopError::with_context`] builds.
15    Context {
16        inner: Box<GeopError>,
17        message: String,
18    },
19    /// `inner`, caused by the argument `key` of the step it was raised in:
20    /// what an editor shows the error beside — the field of that key (see
21    /// [`GeopError::concerning`]).
22    Argument { inner: Box<GeopError>, key: String },
23}
24
25impl GeopError {
26    /// Create a root error. Captures a backtrace at the call site.
27    pub fn new(message: impl Into<String>) -> Self {
28        let backtrace = Arc::new(Backtrace::capture());
29        GeopError::Root {
30            message: message.into(),
31            backtrace,
32        }
33    }
34
35    /// Wrap this error in a Context frame with a string message.
36    pub fn with_context(self, message: impl Into<String>) -> Self {
37        GeopError::Context {
38            inner: Box::new(self),
39            message: message.into(),
40        }
41    }
42
43    /// Caused by the argument `key` of the step it is raised in — its
44    /// sketch, its distance — by a value that argument holds or an element
45    /// it refers to: an editor shows it beside that argument's field.
46    pub fn concerning(self, key: impl Into<String>) -> Self {
47        GeopError::Argument {
48            inner: Box::new(self),
49            key: key.into(),
50        }
51    }
52
53    /// The argument it was caused by (see [`GeopError::concerning`]): the
54    /// outermost one said, which the step that failed said — an argument of
55    /// a step it read from is that step's.
56    pub fn argument(&self) -> Option<&str> {
57        match self {
58            GeopError::Root { .. } => None,
59            GeopError::Context { inner, .. } => inner.argument(),
60            GeopError::Argument { key, .. } => Some(key),
61        }
62    }
63
64    /// What went wrong at the root, without the context it was reported
65    /// through: what a user is shown.
66    pub fn root_message(&self) -> &str {
67        match self {
68            GeopError::Root { message, .. } => message,
69            GeopError::Context { inner, .. } | GeopError::Argument { inner, .. } => {
70                inner.root_message()
71            }
72        }
73    }
74}
75
76impl std::fmt::Display for GeopError {
77    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
78        match self {
79            GeopError::Root { message, backtrace } => {
80                writeln!(f, "GeopError")?;
81                writeln!(f, "Backtrace: {}", backtrace)?;
82                writeln!(f, "RootError: {}", message)
83            }
84            GeopError::Context { inner, message } => {
85                write!(f, "{}", inner)?;
86                writeln!(f, "Context:\n{}", message)
87            }
88            GeopError::Argument { inner, key } => {
89                write!(f, "{}", inner)?;
90                writeln!(f, "Argument: {}", key)
91            }
92        }
93    }
94}
95
96/// Two errors are the same when they say the same: their messages, root
97/// and context — not where they were captured.
98impl PartialEq for GeopError {
99    fn eq(&self, other: &Self) -> bool {
100        match (self, other) {
101            (GeopError::Root { message: a, .. }, GeopError::Root { message: b, .. }) => a == b,
102            (
103                GeopError::Context {
104                    inner: a,
105                    message: m,
106                },
107                GeopError::Context {
108                    inner: b,
109                    message: n,
110                },
111            ) => m == n && a == b,
112            _ => false,
113        }
114    }
115}
116
117impl std::fmt::Debug for GeopError {
118    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
119        write!(f, "{}", self)
120    }
121}
122
123impl std::error::Error for GeopError {}
124
125impl From<&str> for GeopError {
126    fn from(message: &str) -> Self {
127        GeopError::new(message)
128    }
129}
130
131impl From<String> for GeopError {
132    fn from(message: String) -> Self {
133        GeopError::new(message)
134    }
135}
136
137pub type GeopResult<T> = Result<T, GeopError>;
138
139/// Something that can turn one `GeopError` into another, more-annotated one
140/// — either a plain message (wrapped via `GeopError::with_context`) or a
141/// callback that builds the wrapped error itself (e.g. to embed live state
142/// captured at the call site). Lets [`WithContext::with_context`] accept
143/// either `.with_context("a message")` or `.with_context(&|e| ...)`.
144pub trait ContextSource {
145    fn apply(&self, err: GeopError) -> GeopError;
146}
147
148impl ContextSource for str {
149    fn apply(&self, err: GeopError) -> GeopError {
150        err.with_context(self)
151    }
152}
153
154impl ContextSource for String {
155    fn apply(&self, err: GeopError) -> GeopError {
156        err.with_context(self.as_str())
157    }
158}
159
160impl<F: Fn(GeopError) -> GeopError + ?Sized> ContextSource for F {
161    fn apply(&self, err: GeopError) -> GeopError {
162        self(err)
163    }
164}
165
166pub trait WithContext<T> {
167    fn with_context(self, ctx: &(impl ContextSource + ?Sized)) -> GeopResult<T>;
168
169    /// Its error caused by the step's argument `key` (see
170    /// [`GeopError::concerning`]).
171    fn concerning(self, key: &str) -> GeopResult<T>;
172}
173
174impl<T> WithContext<T> for GeopResult<T> {
175    fn with_context(self, ctx: &(impl ContextSource + ?Sized)) -> GeopResult<T> {
176        match self {
177            Ok(v) => Ok(v),
178            Err(err) => Err(ctx.apply(err)),
179        }
180    }
181
182    fn concerning(self, key: &str) -> GeopResult<T> {
183        self.map_err(|e| e.concerning(key))
184    }
185}
186
187/// Like [`format!`], but for [`WithContext::with_context`]: builds a
188/// `ContextSource` that formats its message lazily, only if the result is
189/// actually an `Err` — `.with_context(with_context!("failed at i={i}"))`
190/// costs nothing on the success path, unlike `.with_context(&format!(...))`
191/// (eagerly formats every time, even when nothing is wrong).
192#[macro_export]
193macro_rules! with_context {
194    ($($arg:tt)*) => {
195        &|e: $crate::geop_error::GeopError| e.with_context(format!($($arg)*))
196    };
197}