ez_err/
core.rs

1//! Core code.
2
3/// A custom [`std::result::Result<T, E>`] with the [`EzError`] type. This is used for
4/// passing down errors.
5pub type Result<T> = std::result::Result<T, EzError>;
6
7/// Throws an error and returns early.
8/// Shortcut for `Err(EzError::message("some error")).loc(flc!())?`
9#[macro_export]
10macro_rules! bail {
11    ($($args:tt)*) => {
12        Err(EzError::message(&::std::format_args!($($args)*).to_string())).loc(flc!())?
13    };
14}
15
16/// The flc (File-Line-Column) macro expands to a [`ConstLocation`], which describes
17/// a location in the source code.
18#[macro_export]
19macro_rules! flc {
20    () => {{
21        #[cfg(not(feature = "no_stacktrace"))]
22        const LOC: ConstLocation = ConstLocation::new(file!(), line!(), column!());
23        #[cfg(feature = "no_stacktrace")]
24        const LOC: ConstLocation = ConstLocation::new("", 0, 0);
25        &LOC
26    }};
27}
28
29/// Execute the provided function and catch any errors. This is
30/// useful for closures where no error type can be returned by default.
31pub fn handle<F, R>(func: F) -> Option<R>
32where
33    F: FnOnce() -> Result<R>,
34{
35    func().handle()
36}
37
38/// Stores information about the error and is used for proper error
39/// output to the Unity console.
40#[derive(Debug, PartialEq)]
41pub struct EzError {
42    inner: Box<EzErrorInner>,
43}
44
45#[derive(Debug, PartialEq)]
46struct EzErrorInner {
47    ty: ErrorType,
48    #[cfg(not(feature = "no_stacktrace"))]
49    frames: Vec<&'static ConstLocation>,
50}
51
52impl EzError {
53    /// Constructs a new `EzError` with the given error type.
54    pub fn new(ty: ErrorType) -> EzError {
55        #[cfg(not(feature = "no_stacktrace"))]
56        return EzError {
57            inner: Box::new(EzErrorInner {
58                ty,
59                frames: Vec::new(),
60            }),
61        };
62        #[cfg(feature = "no_stacktrace")]
63        return EzError {
64            inner: Box::new(EzErrorInner { ty }),
65        };
66    }
67
68    /// Constructs a new [`EzError`] with the type [`ErrorType::Message`]
69    /// using the specified message.
70    pub fn message(msg: &str) -> EzError {
71        EzError::new(ErrorType::Message(msg.to_owned()))
72    }
73
74    /// Constructs a new [`EzError`] with the type [`ErrorType::Custom`].
75    /// The code can be used to store arbitrary extra information.
76    pub fn custom(code: u32, name: String, message: String) -> EzError {
77        EzError::new(ErrorType::Custom {
78            code,
79            name,
80            message,
81        })
82    }
83
84    /// Adds a new frame to the `EzError` and sets `file_name`
85    /// to `file` and `line_number` to `line`.
86    pub fn add_frame(&mut self, loc: &'static ConstLocation) {
87        self.inner.frames.push(loc);
88    }
89
90    /// Merges the other error into this by adding the frames of it to this.
91    pub fn with(mut self, other: EzError) -> Self {
92        self.inner.frames.extend_from_slice(&other.inner.frames);
93        self
94    }
95
96    /// Returns the type of the error.
97    pub fn ty(&self) -> &ErrorType {
98        &self.inner.ty
99    }
100
101    /// Returns the stack frames of the error.
102    #[cfg(not(feature = "no_stacktrace"))]
103    pub fn frames(&self) -> &[&'static ConstLocation] {
104        &self.inner.frames
105    }
106}
107
108impl<E> From<E> for EzError
109where
110    E: std::fmt::Display,
111{
112    fn from(err: E) -> Self {
113        EzError::new(ErrorType::Internal(format!("{}", err)))
114    }
115}
116
117/// The different error types that can occur.
118#[derive(Debug, PartialEq)]
119pub enum ErrorType {
120    /// Wraps an internal error that is not compatible with the
121    /// custom error types by default.
122    Internal(String),
123
124    /// An error that occured where an `Option` was `None`.
125    NoneOption,
126
127    /// An error that occured when an array index was outside of the
128    /// valid range.
129    IndexOutOfBounds(usize, usize),
130
131    /// The range is larger than the array.
132    RangeOutOfBounds(usize, usize, usize),
133
134    /// The given range index is not valid (`end < start`).
135    InvalidRange,
136
137    /// A custom error with an attached message.
138    Message(String),
139
140    /// No error specified.
141    Custom {
142        /// A custom message code used for storing custom information.
143        code: u32,
144        /// The name of the error.
145        name: String,
146        /// The message of the error.
147        message: String,
148    },
149}
150
151impl ErrorType {
152    /// Formats the error type into a String for console output.
153    pub fn format(self) -> String {
154        match self {
155            ErrorType::Internal(msg) => msg,
156            ErrorType::NoneOption => format!("Option was none"),
157            ErrorType::IndexOutOfBounds(idx, len) => {
158                format!("Index {} was outside of the range 0..{}", idx, len)
159            }
160            ErrorType::RangeOutOfBounds(start, end, len) => {
161                format!(
162                    "Range {}..{} was larger than the array range 0..{}",
163                    start, end, len
164                )
165            }
166            ErrorType::InvalidRange => {
167                "The provided range was invalid (end < start or X..=usize::MAX)".into()
168            }
169            ErrorType::Message(msg) => msg,
170            ErrorType::Custom { message, .. } => message,
171        }
172    }
173
174    /// Returns the name of the `ErrorType` that should be included
175    /// in the stacktrace.
176    pub fn name(&self) -> &str {
177        match self {
178            ErrorType::Internal(_) => "WrappedInternal",
179            ErrorType::NoneOption => "NoneOption",
180            ErrorType::IndexOutOfBounds(_, _) => "IndexOutOfBounds",
181            ErrorType::RangeOutOfBounds(_, _, _) => "RangeOutOfBounds",
182            ErrorType::InvalidRange => "InvalidRange",
183            ErrorType::Message(_) => "Message",
184            ErrorType::Custom { name, .. } => &name,
185        }
186    }
187}
188
189/// Information about the location in a source file in a constant context.
190#[derive(Debug, PartialEq)]
191pub struct ConstLocation {
192    /// The file of the location.
193    pub file: &'static str,
194    /// The line of the location.
195    pub line: u32,
196    /// The column of the location.
197    pub column: u32,
198}
199
200impl ConstLocation {
201    /// Creates a new [`ConstLocation`] using the given file and line.
202    pub const fn new(file: &'static str, line: u32, column: u32) -> ConstLocation {
203        ConstLocation { file, line, column }
204    }
205}
206
207/// Extension for `Result<T>` to allow for custom error handling.
208pub trait LocData<T> {
209    /// The return type of `add_info`. This can be used to convert
210    /// between different error types.
211    type Result;
212
213    /// Adds a new frame info to the [`Result<T>`]. This only happens
214    /// when the [`Result<T>`] is [`Err(T)`]. Commonly used with the [`flc!`] macro.
215    fn loc(self, flc: &'static ConstLocation) -> Self::Result;
216}
217
218/// Extension for `Result<T>` to allow for custom error handling.
219pub trait Handle<T> {
220    /// Handles the result. If it contains an error a backtrace is
221    /// created and the error is printed to the console.
222    fn handle(self) -> Option<T>;
223
224    /// Handles the result or panics if it is [`Err`]. If it contains
225    /// an error a backtrace is created and the error is printed to the console.
226    fn handle_or_panic(self) -> T;
227}
228
229impl<T> LocData<T> for Result<T> {
230    type Result = Result<T>;
231
232    #[inline(always)]
233    fn loc(mut self, loc: &'static ConstLocation) -> Self::Result {
234        if let Err(err) = &mut self {
235            err.add_frame(loc);
236        }
237
238        self
239    }
240}
241
242impl<T> Handle<T> for Result<T> {
243    fn handle(self) -> Option<T> {
244        fn inner(e: EzError) {
245            let e = e.inner;
246
247            #[cfg(not(feature = "no_stacktrace"))]
248            let trace = {
249                let mut s = String::with_capacity(1024);
250                s.push_str("Stacktrace:\n");
251                for frame in e.frames {
252                    s.push_str(frame.file);
253                    s.push(':');
254                    s.push_str(&frame.line.to_string());
255                    s.push(':');
256                    s.push_str(&frame.column.to_string());
257                    s.push('\n');
258                }
259                s
260            };
261            #[cfg(feature = "no_stacktrace")]
262            let trace = "";
263
264            let name = e.ty.name().to_owned();
265            let message = e.ty.format();
266
267            #[cfg(feature = "log")]
268            log::error!("Error {}: {}\n\n{}", name, message, trace);
269            #[cfg(not(feature = "log"))]
270            println!("Error {}: {}\n\n{}", name, message, trace);
271        }
272
273        match self {
274            Ok(v) => Some(v),
275            Err(e) => {
276                inner(e);
277                None
278            }
279        }
280    }
281    fn handle_or_panic(self) -> T {
282        match self.handle() {
283            Some(v) => v,
284            None => panic!(),
285        }
286    }
287}
288
289impl<T, E> LocData<T> for std::result::Result<T, E>
290where
291    E: std::fmt::Display,
292{
293    type Result = Result<T>;
294
295    #[inline(always)]
296    fn loc(self, loc: &'static ConstLocation) -> Self::Result {
297        #[cfg(not(feature = "no_stacktrace"))]
298        match self {
299            Ok(v) => Ok(v),
300            Err(e) => Err({
301                let mut err: EzError = e.into();
302                err.add_frame(loc);
303                err
304            }),
305        }
306        #[cfg(feature = "no_stacktrace")]
307        self
308    }
309}
310
311impl<T> LocData<T> for Option<T> {
312    type Result = Result<T>;
313
314    #[inline(always)]
315    fn loc(self, loc: &'static ConstLocation) -> Self::Result {
316        #[cfg(not(feature = "no_stacktrace"))]
317        match self {
318            Some(v) => Ok(v),
319            None => Err({
320                let mut err = EzError::new(ErrorType::NoneOption);
321                err.add_frame(loc);
322                err
323            }),
324        }
325        #[cfg(feature = "no_stacktrace")]
326        self
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn correct_info() {
336        let err: Result<()> = Err(EzError::message("test")).loc(flc!());
337        let (file, line) = (file!(), line!());
338
339        let loc = err.err().unwrap().frames()[0];
340        assert_eq!(loc.file, file);
341        assert_eq!(loc.line, line - 1);
342        assert_eq!(loc.column, 65);
343    }
344
345    #[test]
346    fn correct_bail() {
347        let inner_line = line!() + 2;
348        fn inner() -> Result<()> {
349            bail!("bailed");
350
351            Ok(())
352        }
353
354        let err = inner().err().unwrap();
355        assert_eq!(&ErrorType::Message("bailed".into()), err.ty());
356        assert_eq!(inner_line, err.frames()[0].line);
357    }
358}