This is the basis for substantial changes to make it usable for the `gix-*` crates. The wonderful aspect about this is not only a great reduction in dependencies, but it also removes most proc-macro invocations.
Unified · Split
| @@ -0,0 +1,37 @@ | ||
| 1 | +# Copyright 2025 FastLabs Developers | |
| 2 | +# | |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +# you may not use this file except in compliance with the License. | |
| 5 | +# You may obtain a copy of the License at | |
| 6 | +# | |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +# | |
| 9 | +# Unless required by applicable law or agreed to in writing, software | |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +# See the License for the specific language governing permissions and | |
| 13 | +# limitations under the License. | |
| 14 | + | |
| 15 | +[package] | |
| 16 | +name = "exn" | |
| 17 | +version = "0.2.1" | |
| 18 | + | |
| 19 | +description = "A context-aware concrete Error type built on `std::error::Error`." | |
| 20 | + | |
| 21 | +edition.workspace = true | |
| 22 | +homepage.workspace = true | |
| 23 | +license.workspace = true | |
| 24 | +readme.workspace = true | |
| 25 | +repository.workspace = true | |
| 26 | + | |
| 27 | +[package.metadata.docs.rs] | |
| 28 | +all-features = true | |
| 29 | +rustdoc-args = ["--cfg", "docsrs"] | |
| 30 | + | |
| 31 | +[dependencies] | |
| 32 | + | |
| 33 | +[dev-dependencies] | |
| 34 | +insta = { workspace = true } | |
| 35 | + | |
| 36 | +[lints] | |
| 37 | +workspace = true | |
| @@ -0,0 +1,108 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use std::fmt; | |
| 16 | +use std::fmt::Formatter; | |
| 17 | + | |
| 18 | +use crate::Error; | |
| 19 | +use crate::Exn; | |
| 20 | +use crate::Frame; | |
| 21 | + | |
| 22 | +impl<E: Error> fmt::Debug for Exn<E> { | |
| 23 | + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { | |
| 24 | + write_exn(f, self.as_frame(), 0, "") | |
| 25 | + } | |
| 26 | +} | |
| 27 | + | |
| 28 | +impl fmt::Debug for Frame { | |
| 29 | + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { | |
| 30 | + write_exn(f, self, 0, "") | |
| 31 | + } | |
| 32 | +} | |
| 33 | + | |
| 34 | +fn write_exn(f: &mut Formatter<'_>, frame: &Frame, level: usize, prefix: &str) -> fmt::Result { | |
| 35 | + write!(f, "{}", frame.as_error())?; | |
| 36 | + write_location(f, frame)?; | |
| 37 | + | |
| 38 | + let children = frame.children(); | |
| 39 | + let children_len = children.len(); | |
| 40 | + | |
| 41 | + for (i, child) in children.iter().enumerate() { | |
| 42 | + write!(f, "\n{}|", prefix)?; | |
| 43 | + write!(f, "\n{}|-> ", prefix)?; | |
| 44 | + | |
| 45 | + let child_child_len = child.children().len(); | |
| 46 | + if level == 0 && children_len == 1 && child_child_len == 1 { | |
| 47 | + write_exn(f, child, 0, prefix)?; | |
| 48 | + } else if i < children_len - 1 { | |
| 49 | + write_exn(f, child, level + 1, &format!("{}| ", prefix))?; | |
| 50 | + } else { | |
| 51 | + write_exn(f, child, level + 1, &format!("{} ", prefix))?; | |
| 52 | + } | |
| 53 | + } | |
| 54 | + | |
| 55 | + Ok(()) | |
| 56 | +} | |
| 57 | + | |
| 58 | +#[cfg(not(windows_test))] | |
| 59 | +fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result { | |
| 60 | + let location = exn.location(); | |
| 61 | + write!( | |
| 62 | + f, | |
| 63 | + ", at {}:{}:{}", | |
| 64 | + location.file(), | |
| 65 | + location.line(), | |
| 66 | + location.column() | |
| 67 | + ) | |
| 68 | +} | |
| 69 | + | |
| 70 | +#[cfg(windows_test)] | |
| 71 | +fn write_location(f: &mut Formatter<'_>, exn: &Frame) -> fmt::Result { | |
| 72 | + let location = exn.location(); | |
| 73 | + use std::os::windows::ffi::OsStrExt; | |
| 74 | + use std::path::Component; | |
| 75 | + use std::path::MAIN_SEPARATOR; | |
| 76 | + use std::path::Path; | |
| 77 | + | |
| 78 | + let file = location.file(); | |
| 79 | + let path = Path::new(file); | |
| 80 | + | |
| 81 | + let mut resolved = String::new(); | |
| 82 | + | |
| 83 | + for c in path.components() { | |
| 84 | + match c { | |
| 85 | + Component::RootDir => {} | |
| 86 | + Component::CurDir => resolved.push('.'), | |
| 87 | + Component::ParentDir => resolved.push_str(".."), | |
| 88 | + Component::Prefix(prefix) => { | |
| 89 | + resolved.push_str(&prefix.as_os_str().to_string_lossy()); | |
| 90 | + continue; | |
| 91 | + } | |
| 92 | + Component::Normal(s) => resolved.push_str(&s.to_string_lossy()), | |
| 93 | + } | |
| 94 | + resolved.push('/'); | |
| 95 | + } | |
| 96 | + | |
| 97 | + if path.as_os_str().encode_wide().last() != Some(MAIN_SEPARATOR as u16) | |
| 98 | + && resolved != "/" | |
| 99 | + && resolved.ends_with('/') | |
| 100 | + { | |
| 101 | + resolved.pop(); // Pop last '/' | |
| 102 | + } | |
| 103 | + | |
| 104 | + let line = location.line(); | |
| 105 | + let column = location.column(); | |
| 106 | + | |
| 107 | + write!(f, ", at {resolved}:{line}:{column}") | |
| 108 | +} | |
| @@ -0,0 +1,24 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use std::fmt; | |
| 16 | + | |
| 17 | +use crate::Error; | |
| 18 | +use crate::Exn; | |
| 19 | + | |
| 20 | +impl<E: Error> fmt::Display for Exn<E> { | |
| 21 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 22 | + write!(f, "{}", self.as_error()) | |
| 23 | + } | |
| 24 | +} | |
| @@ -0,0 +1,157 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use std::fmt; | |
| 16 | +use std::marker::PhantomData; | |
| 17 | +use std::panic::Location; | |
| 18 | + | |
| 19 | +use crate::Error; | |
| 20 | + | |
| 21 | +/// An exception type that can hold an error tree and additional context. | |
| 22 | +pub struct Exn<E: Error> { | |
| 23 | + // trade one more indirection for less stack size | |
| 24 | + frame: Box<Frame>, | |
| 25 | + phantom: PhantomData<E>, | |
| 26 | +} | |
| 27 | + | |
| 28 | +impl<E: Error> From<E> for Exn<E> { | |
| 29 | + #[track_caller] | |
| 30 | + fn from(error: E) -> Self { | |
| 31 | + Exn::new(error) | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 35 | +impl<E: Error> Exn<E> { | |
| 36 | + /// Create a new exception with the given error. | |
| 37 | + /// | |
| 38 | + /// This will automatically walk the [source chain of the error] and add them as children | |
| 39 | + /// frames. | |
| 40 | + /// | |
| 41 | + /// See also [`Error::raise`] for a fluent way to convert an error into an `Exn` instance. | |
| 42 | + /// | |
| 43 | + /// [source chain of the error]: std::error::Error::source | |
| 44 | + #[track_caller] | |
| 45 | + pub fn new(error: E) -> Self { | |
| 46 | + struct SourceError(String); | |
| 47 | + | |
| 48 | + impl fmt::Debug for SourceError { | |
| 49 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 50 | + fmt::Debug::fmt(&self.0, f) | |
| 51 | + } | |
| 52 | + } | |
| 53 | + | |
| 54 | + impl fmt::Display for SourceError { | |
| 55 | + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 56 | + fmt::Display::fmt(&self.0, f) | |
| 57 | + } | |
| 58 | + } | |
| 59 | + | |
| 60 | + impl std::error::Error for SourceError {} | |
| 61 | + | |
| 62 | + fn walk(error: &dyn std::error::Error, location: &'static Location<'static>) -> Vec<Frame> { | |
| 63 | + if let Some(source) = error.source() { | |
| 64 | + let children = vec![Frame { | |
| 65 | + error: Box::new(SourceError(source.to_string())), | |
| 66 | + location, | |
| 67 | + children: walk(source, location), | |
| 68 | + }]; | |
| 69 | + children | |
| 70 | + } else { | |
| 71 | + vec![] | |
| 72 | + } | |
| 73 | + } | |
| 74 | + | |
| 75 | + let location = Location::caller(); | |
| 76 | + let children = walk(&error, location); | |
| 77 | + let frame = Frame { | |
| 78 | + error: Box::new(error), | |
| 79 | + location, | |
| 80 | + children, | |
| 81 | + }; | |
| 82 | + | |
| 83 | + Self { | |
| 84 | + frame: Box::new(frame), | |
| 85 | + phantom: PhantomData, | |
| 86 | + } | |
| 87 | + } | |
| 88 | + | |
| 89 | + /// Create a new exception with the given error and children. | |
| 90 | + #[track_caller] | |
| 91 | + pub fn from_iter<T, I>(children: I, err: E) -> Self | |
| 92 | + where | |
| 93 | + T: Error, | |
| 94 | + I: IntoIterator, | |
| 95 | + I::Item: Into<Exn<T>>, | |
| 96 | + { | |
| 97 | + let mut new_exn = Exn::new(err); | |
| 98 | + for exn in children { | |
| 99 | + let exn = exn.into(); | |
| 100 | + new_exn.frame.children.push(*exn.frame); | |
| 101 | + } | |
| 102 | + new_exn | |
| 103 | + } | |
| 104 | + | |
| 105 | + /// Raise a new exception; this will make the current exception a child of the new one. | |
| 106 | + #[track_caller] | |
| 107 | + pub fn raise<T: Error>(self, err: T) -> Exn<T> { | |
| 108 | + let mut new_exn = Exn::new(err); | |
| 109 | + new_exn.frame.children.push(*self.frame); | |
| 110 | + new_exn | |
| 111 | + } | |
| 112 | + | |
| 113 | + /// Return the current exception. | |
| 114 | + pub fn as_error(&self) -> &E { | |
| 115 | + self.frame | |
| 116 | + .as_any() | |
| 117 | + .downcast_ref() | |
| 118 | + .expect("error type must match") | |
| 119 | + } | |
| 120 | + | |
| 121 | + /// Return the underlying exception frame. | |
| 122 | + pub fn as_frame(&self) -> &Frame { | |
| 123 | + &self.frame | |
| 124 | + } | |
| 125 | +} | |
| 126 | + | |
| 127 | +/// A frame in the exception tree. | |
| 128 | +pub struct Frame { | |
| 129 | + /// The error that occurred at this frame. | |
| 130 | + error: Box<dyn Error>, | |
| 131 | + /// The source code location where this exception frame was created. | |
| 132 | + location: &'static Location<'static>, | |
| 133 | + /// Child exception frames that provide additional context or source errors. | |
| 134 | + children: Vec<Frame>, | |
| 135 | +} | |
| 136 | + | |
| 137 | +impl Frame { | |
| 138 | + /// Return the error as a reference to [`std::any::Any`]. | |
| 139 | + pub fn as_any(&self) -> &dyn std::any::Any { | |
| 140 | + &*self.error | |
| 141 | + } | |
| 142 | + | |
| 143 | + /// Return the error as a reference to [`std::error::Error`]. | |
| 144 | + pub fn as_error(&self) -> &dyn std::error::Error { | |
| 145 | + &*self.error | |
| 146 | + } | |
| 147 | + | |
| 148 | + /// Return the source code location where this exception frame was created. | |
| 149 | + pub fn location(&self) -> &'static Location<'static> { | |
| 150 | + self.location | |
| 151 | + } | |
| 152 | + | |
| 153 | + /// Return a slice of the children of the exception. | |
| 154 | + pub fn children(&self) -> &[Frame] { | |
| 155 | + &self.children | |
| 156 | + } | |
| 157 | +} | |
| @@ -0,0 +1,124 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +//! A context-aware concrete Error type built on `std::error::Error` | |
| 16 | +//! | |
| 17 | +//! # Examples | |
| 18 | +//! | |
| 19 | +//! ``` | |
| 20 | +//! use exn::Result; | |
| 21 | +//! use exn::ResultExt; | |
| 22 | +//! use exn::bail; | |
| 23 | +//! | |
| 24 | +//! // It's recommended to define errors as structs. Exn will maintain the error tree automatically. | |
| 25 | +//! #[derive(Debug)] | |
| 26 | +//! struct LogicError(String); | |
| 27 | +//! | |
| 28 | +//! impl std::fmt::Display for LogicError { | |
| 29 | +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 30 | +//! write!(f, "logic error: {}", self.0) | |
| 31 | +//! } | |
| 32 | +//! } | |
| 33 | +//! | |
| 34 | +//! impl std::error::Error for LogicError {} | |
| 35 | +//! | |
| 36 | +//! fn do_logic() -> Result<(), LogicError> { | |
| 37 | +//! bail!(LogicError("0 == 1".to_string())); | |
| 38 | +//! } | |
| 39 | +//! | |
| 40 | +//! // Errors can be enum but notably don't need to chain source error. | |
| 41 | +//! #[derive(Debug)] | |
| 42 | +//! enum AppError { | |
| 43 | +//! Fatal { consequences: &'static str }, | |
| 44 | +//! Trivial, | |
| 45 | +//! } | |
| 46 | +//! | |
| 47 | +//! impl std::fmt::Display for AppError { | |
| 48 | +//! fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 49 | +//! match self { | |
| 50 | +//! AppError::Fatal { consequences } => write!(f, "fatal error: {consequences}"), | |
| 51 | +//! AppError::Trivial => write!(f, "trivial error"), | |
| 52 | +//! } | |
| 53 | +//! } | |
| 54 | +//! } | |
| 55 | +//! | |
| 56 | +//! impl std::error::Error for AppError {} | |
| 57 | +//! | |
| 58 | +//! fn main() { | |
| 59 | +//! if let Err(err) = do_logic().or_raise(|| AppError::Fatal { | |
| 60 | +//! consequences: "math no longer works", | |
| 61 | +//! }) { | |
| 62 | +//! eprintln!("{err:?}"); | |
| 63 | +//! } | |
| 64 | +//! } | |
| 65 | +//! ``` | |
| 66 | +//! | |
| 67 | +//! The above program will print an error message like: | |
| 68 | +//! | |
| 69 | +//! ```text | |
| 70 | +//! fatal error: math no longer works, at exn/src/lib.rs:44:16 | |
| 71 | +//! | | |
| 72 | +//! |-> logic error: 0 == 1, at exn/src/lib.rs:40:5 | |
| 73 | +//! ``` | |
| 74 | + | |
| 75 | +#![cfg_attr(docsrs, feature(doc_cfg))] | |
| 76 | +#![deny(missing_docs)] | |
| 77 | + | |
| 78 | +mod debug; | |
| 79 | +mod display; | |
| 80 | +mod impls; | |
| 81 | +mod macros; | |
| 82 | +mod option; | |
| 83 | +mod result; | |
| 84 | + | |
| 85 | +pub use self::impls::Exn; | |
| 86 | +pub use self::impls::Frame; | |
| 87 | +pub use self::option::OptionExt; | |
| 88 | +pub use self::result::Result; | |
| 89 | +pub use self::result::ResultExt; | |
| 90 | + | |
| 91 | +/// A trait bound of the supported error type of [`Exn`]. | |
| 92 | +pub trait Error: std::error::Error + std::any::Any + Send + Sync + 'static { | |
| 93 | + /// Raise this error as a new exception. | |
| 94 | + #[track_caller] | |
| 95 | + fn raise(self) -> Exn<Self> | |
| 96 | + where | |
| 97 | + Self: Sized, | |
| 98 | + { | |
| 99 | + Exn::new(self) | |
| 100 | + } | |
| 101 | +} | |
| 102 | + | |
| 103 | +impl<T> Error for T where T: std::error::Error + std::any::Any + Send + Sync + 'static {} | |
| 104 | + | |
| 105 | +/// Equivalent to `Ok::<_, Exn<E>>(value)`. | |
| 106 | +/// | |
| 107 | +/// This simplifies creation of an `exn::Result` in places where type inference cannot deduce the | |
| 108 | +/// `E` type of the result — without needing to write `Ok::<_, Exn<E>>(value)`. | |
| 109 | +/// | |
| 110 | +/// One might think that `exn::Result::Ok(value)` would work in such cases, but it does not. | |
| 111 | +/// | |
| 112 | +/// ```console | |
| 113 | +/// error[E0282]: type annotations needed for `std::result::Result<i32, E>` | |
| 114 | +/// --> src/main.rs:11:13 | |
| 115 | +/// | | |
| 116 | +/// 11 | let _ = exn::Result::Ok(1); | |
| 117 | +/// | - ^^^^^^^^^^^^^^^ cannot infer type for type parameter `E` declared on the enum `Result` | |
| 118 | +/// | | | |
| 119 | +/// | consider giving this pattern the explicit type `std::result::Result<i32, E>`, where the type parameter `E` is specified | |
| 120 | +/// ``` | |
| 121 | +#[expect(non_snake_case)] | |
| 122 | +pub fn Ok<T, E: Error>(value: T) -> Result<T, E> { | |
| 123 | + Result::Ok(value) | |
| 124 | +} | |
| @@ -0,0 +1,90 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +/// Creates an [`Exn`] and returns it as [`Result`]. | |
| 16 | +/// | |
| 17 | +/// Shorthand for `return Err(Exn::from(err))`. | |
| 18 | +/// | |
| 19 | +/// # Examples | |
| 20 | +/// | |
| 21 | +/// Create an [`Exn`] from [`Error`]: | |
| 22 | +/// | |
| 23 | +/// [`Exn`]: crate::Exn | |
| 24 | +/// [`Error`]: std::error::Error | |
| 25 | +/// | |
| 26 | +/// ``` | |
| 27 | +/// use std::fs; | |
| 28 | +/// | |
| 29 | +/// use exn::bail; | |
| 30 | +/// # fn wrapper() -> exn::Result<(), impl exn::Error> { | |
| 31 | +/// match fs::read_to_string("/path/to/file") { | |
| 32 | +/// Ok(content) => println!("file contents: {content}"), | |
| 33 | +/// Err(err) => bail!(err), | |
| 34 | +/// } | |
| 35 | +/// # Ok(()) } | |
| 36 | +/// ``` | |
| 37 | +#[macro_export] | |
| 38 | +macro_rules! bail { | |
| 39 | + ($err:expr) => {{ | |
| 40 | + return ::std::result::Result::Err($crate::Exn::from($err)); | |
| 41 | + }}; | |
| 42 | +} | |
| 43 | + | |
| 44 | +/// Ensures `$cond` is met; otherwise return an error. | |
| 45 | +/// | |
| 46 | +/// Shorthand for `if !$cond { bail!(...); }`. | |
| 47 | +/// | |
| 48 | +/// # Examples | |
| 49 | +/// | |
| 50 | +/// Create an [`Exn`] from an [`Error`]: | |
| 51 | +/// | |
| 52 | +/// [`Exn`]: crate::Exn | |
| 53 | +/// [`Error`]: std::error::Error | |
| 54 | +/// | |
| 55 | +/// ``` | |
| 56 | +/// # fn has_permission(_: &u32, _: &u32) -> bool { true } | |
| 57 | +/// # type User = u32; | |
| 58 | +/// # let user = 0; | |
| 59 | +/// # type Resource = u32; | |
| 60 | +/// # let resource = 0; | |
| 61 | +/// use std::error::Error; | |
| 62 | +/// use std::fmt; | |
| 63 | +/// | |
| 64 | +/// use exn::ensure; | |
| 65 | +/// | |
| 66 | +/// #[derive(Debug)] | |
| 67 | +/// struct PermissionDenied(User, Resource); | |
| 68 | +/// | |
| 69 | +/// impl fmt::Display for PermissionDenied { | |
| 70 | +/// fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { | |
| 71 | +/// write!(fmt, "permission denied") | |
| 72 | +/// } | |
| 73 | +/// } | |
| 74 | +/// | |
| 75 | +/// impl Error for PermissionDenied {} | |
| 76 | +/// | |
| 77 | +/// ensure!( | |
| 78 | +/// has_permission(&user, &resource), | |
| 79 | +/// PermissionDenied(user, resource), | |
| 80 | +/// ); | |
| 81 | +/// # Ok(()) | |
| 82 | +/// ``` | |
| 83 | +#[macro_export] | |
| 84 | +macro_rules! ensure { | |
| 85 | + ($cond:expr, $err:expr $(,)?) => {{ | |
| 86 | + if !bool::from($cond) { | |
| 87 | + $crate::bail!($err) | |
| 88 | + } | |
| 89 | + }}; | |
| 90 | +} | |
| @@ -0,0 +1,45 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use crate::Error; | |
| 16 | +use crate::Exn; | |
| 17 | +use crate::Result; | |
| 18 | + | |
| 19 | +/// An extension trait for [`Option`] to provide raising new exceptions on `None`. | |
| 20 | +pub trait OptionExt { | |
| 21 | + /// The `Some` type. | |
| 22 | + type Some; | |
| 23 | + | |
| 24 | + /// Construct a new [`Exn`] on the `None` variant. | |
| 25 | + fn ok_or_raise<A, F>(self, err: F) -> Result<Self::Some, A> | |
| 26 | + where | |
| 27 | + A: Error, | |
| 28 | + F: FnOnce() -> A; | |
| 29 | +} | |
| 30 | + | |
| 31 | +impl<T> OptionExt for Option<T> { | |
| 32 | + type Some = T; | |
| 33 | + | |
| 34 | + #[track_caller] | |
| 35 | + fn ok_or_raise<A, F>(self, err: F) -> Result<T, A> | |
| 36 | + where | |
| 37 | + A: Error, | |
| 38 | + F: FnOnce() -> A, | |
| 39 | + { | |
| 40 | + match self { | |
| 41 | + Some(v) => Ok(v), | |
| 42 | + None => Err(Exn::new(err())), | |
| 43 | + } | |
| 44 | + } | |
| 45 | +} | |
| @@ -0,0 +1,76 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use crate::Error; | |
| 16 | +use crate::Exn; | |
| 17 | + | |
| 18 | +/// A reasonable return type to use throughout an application. | |
| 19 | +pub type Result<T, E> = std::result::Result<T, Exn<E>>; | |
| 20 | + | |
| 21 | +/// An extension trait for [`Result`] to provide context information on [`Exn`]s. | |
| 22 | +pub trait ResultExt { | |
| 23 | + /// The `Ok` type. | |
| 24 | + type Success; | |
| 25 | + | |
| 26 | + /// The `Err` type that would be wrapped in an [`Exn`]. | |
| 27 | + type Error: Error; | |
| 28 | + | |
| 29 | + /// Raise a new exception on the [`Exn`] inside the [`Result`]. | |
| 30 | + /// | |
| 31 | + /// Apply [`Exn::raise`] on the `Err` variant, refer to it for more information. | |
| 32 | + fn or_raise<A, F>(self, err: F) -> Result<Self::Success, A> | |
| 33 | + where | |
| 34 | + A: Error, | |
| 35 | + F: FnOnce() -> A; | |
| 36 | +} | |
| 37 | + | |
| 38 | +impl<T, E> ResultExt for std::result::Result<T, E> | |
| 39 | +where | |
| 40 | + E: Error, | |
| 41 | +{ | |
| 42 | + type Success = T; | |
| 43 | + type Error = E; | |
| 44 | + | |
| 45 | + #[track_caller] | |
| 46 | + fn or_raise<A, F>(self, err: F) -> Result<Self::Success, A> | |
| 47 | + where | |
| 48 | + A: Error, | |
| 49 | + F: FnOnce() -> A, | |
| 50 | + { | |
| 51 | + match self { | |
| 52 | + Ok(v) => Ok(v), | |
| 53 | + Err(e) => Err(Exn::new(e).raise(err())), | |
| 54 | + } | |
| 55 | + } | |
| 56 | +} | |
| 57 | + | |
| 58 | +impl<T, E> ResultExt for std::result::Result<T, Exn<E>> | |
| 59 | +where | |
| 60 | + E: Error, | |
| 61 | +{ | |
| 62 | + type Success = T; | |
| 63 | + type Error = E; | |
| 64 | + | |
| 65 | + #[track_caller] | |
| 66 | + fn or_raise<A, F>(self, err: F) -> Result<Self::Success, A> | |
| 67 | + where | |
| 68 | + A: Error, | |
| 69 | + F: FnOnce() -> A, | |
| 70 | + { | |
| 71 | + match self { | |
| 72 | + Ok(v) => Ok(v), | |
| 73 | + Err(e) => Err(e.raise(err())), | |
| 74 | + } | |
| 75 | + } | |
| 76 | +} | |
| @@ -0,0 +1,118 @@ | ||
| 1 | +// Copyright 2025 FastLabs Developers | |
| 2 | +// | |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); | |
| 4 | +// you may not use this file except in compliance with the License. | |
| 5 | +// You may obtain a copy of the License at | |
| 6 | +// | |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 | |
| 8 | +// | |
| 9 | +// Unless required by applicable law or agreed to in writing, software | |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, | |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| 12 | +// See the License for the specific language governing permissions and | |
| 13 | +// limitations under the License. | |
| 14 | + | |
| 15 | +use exn::Error; | |
| 16 | +use exn::Exn; | |
| 17 | +use exn::OptionExt; | |
| 18 | +use exn::ResultExt; | |
| 19 | + | |
| 20 | +#[derive(Debug)] | |
| 21 | +struct SimpleError(&'static str); | |
| 22 | + | |
| 23 | +impl std::fmt::Display for SimpleError { | |
| 24 | + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| 25 | + write!(f, "{}", self.0) | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +impl std::error::Error for SimpleError {} | |
| 30 | + | |
| 31 | +#[test] | |
| 32 | +fn test_error_straightforward() { | |
| 33 | + let e1 = SimpleError("E1").raise(); | |
| 34 | + let e2 = e1.raise(SimpleError("E2")); | |
| 35 | + let e3 = e2.raise(SimpleError("E3")); | |
| 36 | + let e4 = e3.raise(SimpleError("E4")); | |
| 37 | + let e5 = e4.raise(SimpleError("E5")); | |
| 38 | + insta::assert_debug_snapshot!(e5); | |
| 39 | +} | |
| 40 | + | |
| 41 | +#[test] | |
| 42 | +fn test_error_tree() { | |
| 43 | + let e1 = SimpleError("E1").raise(); | |
| 44 | + let e3 = e1.raise(SimpleError("E3")); | |
| 45 | + | |
| 46 | + let e9 = SimpleError("E9").raise(); | |
| 47 | + let e10 = e9.raise(SimpleError("E10")); | |
| 48 | + | |
| 49 | + let e11 = SimpleError("E11").raise(); | |
| 50 | + let e12 = e11.raise(SimpleError("E12")); | |
| 51 | + | |
| 52 | + let e5 = Exn::from_iter([e3, e10, e12], SimpleError("E5")); | |
| 53 | + | |
| 54 | + let e2 = SimpleError("E2").raise(); | |
| 55 | + let e4 = e2.raise(SimpleError("E4")); | |
| 56 | + | |
| 57 | + let e7 = SimpleError("E7").raise(); | |
| 58 | + let e8 = e7.raise(SimpleError("E8")); | |
| 59 | + | |
| 60 | + let e6 = Exn::from_iter([e5, e4, e8], SimpleError("E6")); | |
| 61 | + insta::assert_debug_snapshot!(e6); | |
| 62 | +} | |
| 63 | + | |
| 64 | +#[test] | |
| 65 | +fn test_result_ext() { | |
| 66 | + let result: Result<(), SimpleError> = Err(SimpleError("An error")); | |
| 67 | + let result = result.or_raise(|| SimpleError("Another error")); | |
| 68 | + insta::assert_debug_snapshot!(result.unwrap_err()); | |
| 69 | +} | |
| 70 | + | |
| 71 | +#[test] | |
| 72 | +fn test_option_ext() { | |
| 73 | + let result: Option<()> = None; | |
| 74 | + let result = result.ok_or_raise(|| SimpleError("An error")); | |
| 75 | + insta::assert_debug_snapshot!(result.unwrap_err()); | |
| 76 | +} | |
| 77 | + | |
| 78 | +#[test] | |
| 79 | +fn test_from_error() { | |
| 80 | + fn foo() -> exn::Result<(), SimpleError> { | |
| 81 | + Err(SimpleError("An error"))?; | |
| 82 | + Ok(()) | |
| 83 | + } | |
| 84 | + | |
| 85 | + let result = foo(); | |
| 86 | + insta::assert_debug_snapshot!(result.unwrap_err()); | |
| 87 | +} | |
| 88 | + | |
| 89 | +#[test] | |
| 90 | +fn test_bail() { | |
| 91 | + fn foo() -> exn::Result<(), SimpleError> { | |
| 92 | + exn::bail!(SimpleError("An error")); | |
| 93 | + } | |
| 94 | + | |
| 95 | + let result = foo(); | |
| 96 | + insta::assert_debug_snapshot!(result.unwrap_err()); | |
| 97 | +} | |
| 98 | + | |
| 99 | +#[test] | |
| 100 | +fn test_ensure_ok() { | |
| 101 | + fn foo() -> exn::Result<(), SimpleError> { | |
| 102 | + exn::ensure!(true, SimpleError("An error")); | |
| 103 | + Ok(()) | |
| 104 | + } | |
| 105 | + | |
| 106 | + foo().unwrap(); | |
| 107 | +} | |
| 108 | + | |
| 109 | +#[test] | |
| 110 | +fn test_ensure_fail() { | |
| 111 | + fn foo() -> exn::Result<(), SimpleError> { | |
| 112 | + exn::ensure!(false, SimpleError("An error")); | |
| 113 | + Ok(()) | |
| 114 | + } | |
| 115 | + | |
| 116 | + let result = foo(); | |
| 117 | + insta::assert_debug_snapshot!(result.unwrap_err()); | |
| 118 | +} | |
| @@ -0,0 +1,5 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: result.unwrap_err() | |
| 4 | +--- | |
| 5 | +An error, at exn/tests/simple.rs:92:9 | |
| @@ -0,0 +1,5 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: result.unwrap_err() | |
| 4 | +--- | |
| 5 | +An error, at exn/tests/simple.rs:112:9 | |
| @@ -0,0 +1,13 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: e5 | |
| 4 | +--- | |
| 5 | +E5, at exn/tests/simple.rs:37:17 | |
| 6 | +| | |
| 7 | +|-> E4, at exn/tests/simple.rs:36:17 | |
| 8 | +| | |
| 9 | +|-> E3, at exn/tests/simple.rs:35:17 | |
| 10 | +| | |
| 11 | +|-> E2, at exn/tests/simple.rs:34:17 | |
| 12 | +| | |
| 13 | +|-> E1, at exn/tests/simple.rs:33:32 | |
| @@ -0,0 +1,27 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: e6 | |
| 4 | +--- | |
| 5 | +E6, at exn/tests/simple.rs:60:14 | |
| 6 | +| | |
| 7 | +|-> E5, at exn/tests/simple.rs:52:14 | |
| 8 | +| | | |
| 9 | +| |-> E3, at exn/tests/simple.rs:44:17 | |
| 10 | +| | | | |
| 11 | +| | |-> E1, at exn/tests/simple.rs:43:32 | |
| 12 | +| | | |
| 13 | +| |-> E10, at exn/tests/simple.rs:47:18 | |
| 14 | +| | | | |
| 15 | +| | |-> E9, at exn/tests/simple.rs:46:32 | |
| 16 | +| | | |
| 17 | +| |-> E12, at exn/tests/simple.rs:50:19 | |
| 18 | +| | | |
| 19 | +| |-> E11, at exn/tests/simple.rs:49:34 | |
| 20 | +| | |
| 21 | +|-> E4, at exn/tests/simple.rs:55:17 | |
| 22 | +| | | |
| 23 | +| |-> E2, at exn/tests/simple.rs:54:32 | |
| 24 | +| | |
| 25 | +|-> E8, at exn/tests/simple.rs:58:17 | |
| 26 | + | | |
| 27 | + |-> E7, at exn/tests/simple.rs:57:32 | |
| @@ -0,0 +1,5 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: result.unwrap_err() | |
| 4 | +--- | |
| 5 | +An error, at exn/tests/simple.rs:81:9 | |
| @@ -0,0 +1,5 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: result.unwrap_err() | |
| 4 | +--- | |
| 5 | +An error, at exn/tests/simple.rs:74:25 | |
| @@ -0,0 +1,7 @@ | ||
| 1 | +--- | |
| 2 | +source: exn/tests/simple.rs | |
| 3 | +expression: result.unwrap_err() | |
| 4 | +--- | |
| 5 | +Another error, at exn/tests/simple.rs:67:25 | |
| 6 | +| | |
| 7 | +|-> An error, at exn/tests/simple.rs:67:25 | |