diff --git a/CHANGELOG.md b/CHANGELOG.md index 43d6f08..8389928 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,9 @@ All significant changes to this project will be documented in this file. * `Exn::from_iter` has been renamed to `Exn::raise_all` * `exn::Error` trait bound has been removed in favor of inlined `StdError + Send + Sync + 'static` bounds. * `err.raise()` has been moved to the `exn::ErrorExt` extension trait. +* `Exn::error(&self)` has been replaced with `impl Deref for Exn`. ### New Features +* `Exn` now implements `Deref`, allowing for more ergonomic access to the inner error. * This crate is now `no_std` compatible, while the `alloc` crate is still required for heap allocations. It is worth noting that `no_std` support is a nice-to-have feature, and can be dropped if it blocks other important features in the future. Before 1.0, once `exn` APIs settle down, the decision on whether to keep `no_std` as a promise will be finalized. diff --git a/exn/src/display.rs b/exn/src/display.rs index d0c6380..928f09e 100644 --- a/exn/src/display.rs +++ b/exn/src/display.rs @@ -14,13 +14,14 @@ use core::error::Error; use core::fmt; +use core::ops::Deref; use crate::Exn; use crate::Frame; impl fmt::Display for Exn { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.error()) + write!(f, "{}", self.deref()) } } diff --git a/exn/src/impls.rs b/exn/src/impls.rs index 389fe74..da7ee63 100644 --- a/exn/src/impls.rs +++ b/exn/src/impls.rs @@ -20,6 +20,7 @@ use alloc::vec::Vec; use core::error::Error; use core::fmt; use core::marker::PhantomData; +use core::ops::Deref; use core::panic::Location; /// An exception type that can hold an error tree and additional context. @@ -118,18 +119,24 @@ impl Exn { new_exn } - /// Return the current exception. - pub fn error(&self) -> &E { + /// Return the underlying exception frame. + pub fn frame(&self) -> &Frame { + &self.frame + } +} + +impl Deref for Exn +where + E: Error + Send + Sync + 'static, +{ + type Target = E; + + fn deref(&self) -> &Self::Target { self.frame .error() .downcast_ref() .expect("error type must match") } - - /// Return the underlying exception frame. - pub fn frame(&self) -> &Frame { - &self.frame - } } /// A frame in the exception tree. diff --git a/exn/tests/common.rs b/exn/tests/common.rs new file mode 100644 index 0000000..9b83206 --- /dev/null +++ b/exn/tests/common.rs @@ -0,0 +1,71 @@ +// Copyright 2025 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use exn::ErrorExt; +use exn::Exn; + +pub fn new_tree_error() -> Exn { + let e1 = Error("E1").raise(); + let e3 = e1.raise(Error("E3")); + + let e9 = Error("E9").raise(); + let e10 = e9.raise(Error("E10")); + + let e11 = Error("E11").raise(); + let e12 = e11.raise(Error("E12")); + + let e5 = Exn::raise_all(Error("E5"), [e3, e10, e12]); + + let e2 = Error("E2").raise(); + let e4 = e2.raise(Error("E4")); + + let e7 = Error("E7").raise(); + let e8 = e7.raise(Error("E8")); + + Exn::raise_all(Error("E6"), [e5, e4, e8]) +} + +pub fn new_linear_error() -> Exn { + let e1 = Error("E1").raise(); + let e2 = e1.raise(Error("E2")); + let e3 = e2.raise(Error("E3")); + let e4 = e3.raise(Error("E4")); + e4.raise(Error("E5")) +} + +#[derive(Debug)] +pub struct Error(pub &'static str); + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for Error {} + +#[derive(Debug)] +pub struct ErrorWithSource(pub &'static str, pub Error); + +impl std::fmt::Display for ErrorWithSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::error::Error for ErrorWithSource { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(&self.1) + } +} diff --git a/exn/tests/main.rs b/exn/tests/main.rs new file mode 100644 index 0000000..321e7ef --- /dev/null +++ b/exn/tests/main.rs @@ -0,0 +1,97 @@ +// Copyright 2025 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use exn::Exn; +use exn::OptionExt; +use exn::ResultExt; + +mod common; +use common::Error; +use common::ErrorWithSource; + +#[test] +fn linear_error() { + let e = common::new_linear_error().raise(Error("topmost")); + assert_eq!(e.to_string(), "topmost"); + insta::assert_debug_snapshot!(e); +} + +#[test] +fn tree_error() { + let e = common::new_tree_error().raise(Error("topmost")); + assert_eq!(e.to_string(), "topmost"); + insta::assert_debug_snapshot!(e); +} + +#[test] +fn new_with_source() { + let e = Exn::new(ErrorWithSource("top", Error("source"))); + insta::assert_debug_snapshot!(e); +} + +#[test] +fn result_ext() { + let result: Result<(), Error> = Err(Error("An error")); + let result = result.or_raise(|| Error("Another error")); + insta::assert_debug_snapshot!(result.unwrap_err()); +} + +#[test] +fn option_ext() { + let result: Option<()> = None; + let result = result.ok_or_raise(|| Error("An error")); + insta::assert_debug_snapshot!(result.unwrap_err()); +} + +#[test] +fn from_error() { + fn foo() -> exn::Result<(), Error> { + Err(Error("An error"))?; + Ok(()) + } + + let result = foo(); + insta::assert_debug_snapshot!(result.unwrap_err()); +} + +#[test] +fn bail() { + fn foo() -> exn::Result<(), Error> { + exn::bail!(Error("An error")); + } + + let result = foo(); + insta::assert_debug_snapshot!(result.unwrap_err()); +} + +#[test] +fn ensure_ok() { + fn foo() -> exn::Result<(), Error> { + exn::ensure!(true, Error("An error")); + Ok(()) + } + + foo().unwrap(); +} + +#[test] +fn ensure_fail() { + fn foo() -> exn::Result<(), Error> { + exn::ensure!(false, Error("An error")); + Ok(()) + } + + let result = foo(); + insta::assert_debug_snapshot!(result.unwrap_err()); +} diff --git a/exn/tests/simple.rs b/exn/tests/simple.rs deleted file mode 100644 index 0c91dae..0000000 --- a/exn/tests/simple.rs +++ /dev/null @@ -1,118 +0,0 @@ -// Copyright 2025 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use exn::ErrorExt; -use exn::Exn; -use exn::OptionExt; -use exn::ResultExt; - -#[derive(Debug)] -struct SimpleError(&'static str); - -impl std::fmt::Display for SimpleError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.0) - } -} - -impl std::error::Error for SimpleError {} - -#[test] -fn test_error_straightforward() { - let e1 = SimpleError("E1").raise(); - let e2 = e1.raise(SimpleError("E2")); - let e3 = e2.raise(SimpleError("E3")); - let e4 = e3.raise(SimpleError("E4")); - let e5 = e4.raise(SimpleError("E5")); - insta::assert_debug_snapshot!(e5); -} - -#[test] -fn test_error_tree() { - let e1 = SimpleError("E1").raise(); - let e3 = e1.raise(SimpleError("E3")); - - let e9 = SimpleError("E9").raise(); - let e10 = e9.raise(SimpleError("E10")); - - let e11 = SimpleError("E11").raise(); - let e12 = e11.raise(SimpleError("E12")); - - let e5 = Exn::raise_all(SimpleError("E5"), [e3, e10, e12]); - - let e2 = SimpleError("E2").raise(); - let e4 = e2.raise(SimpleError("E4")); - - let e7 = SimpleError("E7").raise(); - let e8 = e7.raise(SimpleError("E8")); - - let e6 = Exn::raise_all(SimpleError("E6"), [e5, e4, e8]); - insta::assert_debug_snapshot!(e6); -} - -#[test] -fn test_result_ext() { - let result: Result<(), SimpleError> = Err(SimpleError("An error")); - let result = result.or_raise(|| SimpleError("Another error")); - insta::assert_debug_snapshot!(result.unwrap_err()); -} - -#[test] -fn test_option_ext() { - let result: Option<()> = None; - let result = result.ok_or_raise(|| SimpleError("An error")); - insta::assert_debug_snapshot!(result.unwrap_err()); -} - -#[test] -fn test_from_error() { - fn foo() -> exn::Result<(), SimpleError> { - Err(SimpleError("An error"))?; - Ok(()) - } - - let result = foo(); - insta::assert_debug_snapshot!(result.unwrap_err()); -} - -#[test] -fn test_bail() { - fn foo() -> exn::Result<(), SimpleError> { - exn::bail!(SimpleError("An error")); - } - - let result = foo(); - insta::assert_debug_snapshot!(result.unwrap_err()); -} - -#[test] -fn test_ensure_ok() { - fn foo() -> exn::Result<(), SimpleError> { - exn::ensure!(true, SimpleError("An error")); - Ok(()) - } - - foo().unwrap(); -} - -#[test] -fn test_ensure_fail() { - fn foo() -> exn::Result<(), SimpleError> { - exn::ensure!(false, SimpleError("An error")); - Ok(()) - } - - let result = foo(); - insta::assert_debug_snapshot!(result.unwrap_err()); -} diff --git a/exn/tests/snapshots/main__bail.snap b/exn/tests/snapshots/main__bail.snap new file mode 100644 index 0000000..4e21f38 --- /dev/null +++ b/exn/tests/snapshots/main__bail.snap @@ -0,0 +1,5 @@ +--- +source: exn/tests/main.rs +expression: result.unwrap_err() +--- +An error, at exn/tests/main.rs:71:9 diff --git a/exn/tests/snapshots/main__ensure_fail.snap b/exn/tests/snapshots/main__ensure_fail.snap new file mode 100644 index 0000000..fe44aff --- /dev/null +++ b/exn/tests/snapshots/main__ensure_fail.snap @@ -0,0 +1,5 @@ +--- +source: exn/tests/main.rs +expression: result.unwrap_err() +--- +An error, at exn/tests/main.rs:91:9 diff --git a/exn/tests/snapshots/main__from_error.snap b/exn/tests/snapshots/main__from_error.snap new file mode 100644 index 0000000..bf0bf47 --- /dev/null +++ b/exn/tests/snapshots/main__from_error.snap @@ -0,0 +1,5 @@ +--- +source: exn/tests/main.rs +expression: result.unwrap_err() +--- +An error, at exn/tests/main.rs:60:9 diff --git a/exn/tests/snapshots/main__linear_error.snap b/exn/tests/snapshots/main__linear_error.snap new file mode 100644 index 0000000..c89d4a5 --- /dev/null +++ b/exn/tests/snapshots/main__linear_error.snap @@ -0,0 +1,15 @@ +--- +source: exn/tests/main.rs +expression: e +--- +topmost, at exn/tests/main.rs:25:40 +| +|-> E5, at exn/tests/common.rs:44:8 +| +|-> E4, at exn/tests/common.rs:43:17 +| +|-> E3, at exn/tests/common.rs:42:17 +| +|-> E2, at exn/tests/common.rs:41:17 +| +|-> E1, at exn/tests/common.rs:40:26 diff --git a/exn/tests/snapshots/main__new_with_source.snap b/exn/tests/snapshots/main__new_with_source.snap new file mode 100644 index 0000000..ca4e46f --- /dev/null +++ b/exn/tests/snapshots/main__new_with_source.snap @@ -0,0 +1,7 @@ +--- +source: exn/tests/main.rs +expression: e +--- +top, at exn/tests/main.rs:39:13 +| +|-> source, at exn/tests/main.rs:39:13 diff --git a/exn/tests/snapshots/main__option_ext.snap b/exn/tests/snapshots/main__option_ext.snap new file mode 100644 index 0000000..efea6e7 --- /dev/null +++ b/exn/tests/snapshots/main__option_ext.snap @@ -0,0 +1,5 @@ +--- +source: exn/tests/main.rs +expression: result.unwrap_err() +--- +An error, at exn/tests/main.rs:53:25 diff --git a/exn/tests/snapshots/main__result_ext.snap b/exn/tests/snapshots/main__result_ext.snap new file mode 100644 index 0000000..4d57554 --- /dev/null +++ b/exn/tests/snapshots/main__result_ext.snap @@ -0,0 +1,7 @@ +--- +source: exn/tests/main.rs +expression: result.unwrap_err() +--- +Another error, at exn/tests/main.rs:46:25 +| +|-> An error, at exn/tests/main.rs:46:25 diff --git a/exn/tests/snapshots/main__tree_error.snap b/exn/tests/snapshots/main__tree_error.snap new file mode 100644 index 0000000..7ab4ae4 --- /dev/null +++ b/exn/tests/snapshots/main__tree_error.snap @@ -0,0 +1,29 @@ +--- +source: exn/tests/main.rs +expression: e +--- +topmost, at exn/tests/main.rs:32:38 +| +|-> E6, at exn/tests/common.rs:36:5 + | + |-> E5, at exn/tests/common.rs:28:14 + | | + | |-> E3, at exn/tests/common.rs:20:17 + | | | + | | |-> E1, at exn/tests/common.rs:19:26 + | | + | |-> E10, at exn/tests/common.rs:23:18 + | | | + | | |-> E9, at exn/tests/common.rs:22:26 + | | + | |-> E12, at exn/tests/common.rs:26:19 + | | + | |-> E11, at exn/tests/common.rs:25:28 + | + |-> E4, at exn/tests/common.rs:31:17 + | | + | |-> E2, at exn/tests/common.rs:30:26 + | + |-> E8, at exn/tests/common.rs:34:17 + | + |-> E7, at exn/tests/common.rs:33:26 diff --git a/exn/tests/snapshots/simple__bail.snap b/exn/tests/snapshots/simple__bail.snap deleted file mode 100644 index c34b677..0000000 --- a/exn/tests/snapshots/simple__bail.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: result.unwrap_err() ---- -An error, at exn/tests/simple.rs:92:9 diff --git a/exn/tests/snapshots/simple__ensure_fail.snap b/exn/tests/snapshots/simple__ensure_fail.snap deleted file mode 100644 index e159214..0000000 --- a/exn/tests/snapshots/simple__ensure_fail.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: result.unwrap_err() ---- -An error, at exn/tests/simple.rs:112:9 diff --git a/exn/tests/snapshots/simple__error_straightforward.snap b/exn/tests/snapshots/simple__error_straightforward.snap deleted file mode 100644 index e772fed..0000000 --- a/exn/tests/snapshots/simple__error_straightforward.snap +++ /dev/null @@ -1,13 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: e5 ---- -E5, at exn/tests/simple.rs:37:17 -| -|-> E4, at exn/tests/simple.rs:36:17 -| -|-> E3, at exn/tests/simple.rs:35:17 -| -|-> E2, at exn/tests/simple.rs:34:17 -| -|-> E1, at exn/tests/simple.rs:33:32 diff --git a/exn/tests/snapshots/simple__error_tree.snap b/exn/tests/snapshots/simple__error_tree.snap deleted file mode 100644 index 943f4ce..0000000 --- a/exn/tests/snapshots/simple__error_tree.snap +++ /dev/null @@ -1,27 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: e6 ---- -E6, at exn/tests/simple.rs:60:14 -| -|-> E5, at exn/tests/simple.rs:52:14 -| | -| |-> E3, at exn/tests/simple.rs:44:17 -| | | -| | |-> E1, at exn/tests/simple.rs:43:32 -| | -| |-> E10, at exn/tests/simple.rs:47:18 -| | | -| | |-> E9, at exn/tests/simple.rs:46:32 -| | -| |-> E12, at exn/tests/simple.rs:50:19 -| | -| |-> E11, at exn/tests/simple.rs:49:34 -| -|-> E4, at exn/tests/simple.rs:55:17 -| | -| |-> E2, at exn/tests/simple.rs:54:32 -| -|-> E8, at exn/tests/simple.rs:58:17 - | - |-> E7, at exn/tests/simple.rs:57:32 diff --git a/exn/tests/snapshots/simple__from_error.snap b/exn/tests/snapshots/simple__from_error.snap deleted file mode 100644 index cb43ac5..0000000 --- a/exn/tests/snapshots/simple__from_error.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: result.unwrap_err() ---- -An error, at exn/tests/simple.rs:81:9 diff --git a/exn/tests/snapshots/simple__option_ext.snap b/exn/tests/snapshots/simple__option_ext.snap deleted file mode 100644 index d5d3390..0000000 --- a/exn/tests/snapshots/simple__option_ext.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: result.unwrap_err() ---- -An error, at exn/tests/simple.rs:74:25 diff --git a/exn/tests/snapshots/simple__result_ext.snap b/exn/tests/snapshots/simple__result_ext.snap deleted file mode 100644 index 013566e..0000000 --- a/exn/tests/snapshots/simple__result_ext.snap +++ /dev/null @@ -1,7 +0,0 @@ ---- -source: exn/tests/simple.rs -expression: result.unwrap_err() ---- -Another error, at exn/tests/simple.rs:67:25 -| -|-> An error, at exn/tests/simple.rs:67:25