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 | ||