use super::{Exn, RawException, RawExceptionRef};
use crate::call::RawCallError;
use crate::xdr::ReadXDROwned;
pub trait MatchExn {
fn as_exn(&self) -> Option<RawExceptionRef<'_>>;
fn try_as<E: Exn + ReadXDROwned>(&self) -> Option<E> {
self.as_exn().and_then(|e| e.try_as())
}
fn try_decode<E: Exn + ReadXDROwned>(
&self,
) -> Option<Result<E, crate::xdr::ReadError>> {
self.as_exn().and_then(|e| e.try_decode())
}
}
impl MatchExn for RawException {
fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
Some(self.as_ref_exn())
}
}
impl MatchExn for RawExceptionRef<'_> {
fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
Some(*self)
}
}
impl<X: MatchExn> MatchExn for RawCallError<X> {
fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
match self {
RawCallError::Exception(x) => x.as_exn(),
_ => None,
}
}
}
impl<T: MatchExn + ?Sized> MatchExn for &T {
fn as_exn(&self) -> Option<RawExceptionRef<'_>> {
(**self).as_exn()
}
}
#[doc(hidden)]
pub fn __try_as<T: Exn + ReadXDROwned>(
exn: Option<RawExceptionRef<'_>>,
) -> Option<T> {
exn.and_then(|e| e.try_as())
}
#[macro_export]
macro_rules! match_exn {
($e:expr, { $($arms:tt)* }) => {{
#[allow(
unreachable_code,
unused_labels,
clippy::diverging_sub_expression
)]
let __match_exn_result = '__match_exn: {
let __match_exn_scrutinee = &$e;
let __match_exn_raw =
$crate::exn::MatchExn::as_exn(__match_exn_scrutinee);
$crate::__match_exn_arms!(
'__match_exn, __match_exn_raw, $($arms)*
)
};
__match_exn_result
}};
}
#[doc(hidden)]
#[macro_export]
macro_rules! __match_exn_arms {
($lbl:lifetime, $exn:ident, _ => $body:block $(,)?) => {
break $lbl ($body)
};
($lbl:lifetime, $exn:ident, _ => $body:expr $(,)?) => {
break $lbl ({ $body })
};
($lbl:lifetime, $exn:ident, _ => $($rest:tt)*) => {
::core::compile_error!(
"the `_` arm of `match_exn!` must come last; arms after it \
could never be reached"
)
};
($lbl:lifetime, $exn:ident,
$pat:pat if $guard:expr => $body:block , $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
&& $guard
{
break $lbl ($body);
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident,
$pat:pat if $guard:expr => $body:block $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
&& $guard
{
break $lbl ($body);
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident,
$pat:pat if $guard:expr => $body:expr , $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
&& $guard
{
break $lbl ({ $body });
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident, $pat:pat => $body:block , $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
{
break $lbl ($body);
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident, $pat:pat => $body:block $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
{
break $lbl ($body);
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident, $pat:pat => $body:expr , $($rest:tt)*) => {{
if let ::core::option::Option::Some($pat) = $crate::exn::__try_as($exn)
{
break $lbl ({ $body });
}
$crate::__match_exn_arms!($lbl, $exn, $($rest)*)
}};
($lbl:lifetime, $exn:ident $(,)?) => {
::core::compile_error!(
"`match_exn!` requires a final `_ => ...` arm: its arms are \
tried in order rather than checked for exhaustiveness, so an \
exception matching none of them must be handled explicitly \
(`_ => e.re_throw()` propagates it)"
)
};
}
#[cfg(all(test, feature = "alloc"))]
mod tests {
use super::*;
use crate::exn::{FaultInfo, LibraryExn};
use crate::internal::encode_xdr;
use crate::xdr::*;
#[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
#[repr(i32)]
enum Alpha {
NotFound = 1,
Big { n: u64 } = 2,
}
impl Exn for Alpha {
const SELECTOR: u64 = 0xA1FA_0000_0000_0001;
}
#[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
#[repr(i32)]
enum Beta {
Rejected = 1,
}
impl Exn for Beta {
const SELECTOR: u64 = 0xBE7A_0000_0000_0001;
}
fn caught<E: Exn + WriteXDR>(e: E) -> RawException {
RawException::new(E::SELECTOR, &encode_xdr(&e).unwrap())
}
fn err<E: Exn + WriteXDR>(e: E) -> RawCallError {
RawCallError::Exception(caught(e))
}
fn classify(e: RawCallError) -> alloc::string::String {
use alloc::string::ToString;
match_exn!(e, {
Alpha::Big { n } if n > 100 => "alpha-big-huge".to_string(),
Alpha::Big { n } => alloc::format!("alpha-big-{n}"),
Alpha::NotFound => "alpha-not-found".to_string(),
Beta::Rejected => "beta-rejected".to_string(),
LibraryExn::NonNegativeIntOverflow => "lib-overflow".to_string(),
_ => alloc::format!("other:{e:?}"),
})
}
#[test]
fn matches_each_type_by_selector() {
assert_eq!(classify(err(Alpha::NotFound)), "alpha-not-found");
assert_eq!(classify(err(Beta::Rejected)), "beta-rejected");
assert_eq!(
classify(err(LibraryExn::NonNegativeIntOverflow)),
"lib-overflow"
);
}
#[test]
fn guard_failure_falls_through_to_a_later_arm_not_to_wildcard() {
assert_eq!(classify(err(Alpha::Big { n: 500 })), "alpha-big-huge");
assert_eq!(classify(err(Alpha::Big { n: 7 })), "alpha-big-7");
}
#[test]
fn non_exception_failures_reach_the_wildcard() {
assert!(classify(RawCallError::Invalid).starts_with("other:"));
assert!(
classify(RawCallError::Fault(FaultInfo::empty()))
.starts_with("other:")
);
}
#[test]
fn unrecognised_selector_reaches_the_wildcard() {
let raw = RawException::new(!0u64, b"whatever");
assert!(classify(RawCallError::Exception(raw)).starts_with("other:"));
}
#[test]
fn try_decode_separates_wrong_selector_from_bad_payload() {
let other = err(Beta::Rejected);
assert!(other.try_decode::<Alpha>().is_none());
assert_eq!(other.try_as::<Alpha>(), None);
let good = err(Alpha::NotFound);
assert!(matches!(
good.try_decode::<Alpha>(),
Some(Ok(Alpha::NotFound))
));
let bad = RawCallError::Exception(RawException::new(
Alpha::SELECTOR,
&encode_xdr(&9_999_999i32).unwrap(),
));
assert_eq!(bad.try_as::<Alpha>(), None);
assert!(matches!(bad.try_decode::<Alpha>(), Some(Err(_))));
}
#[test]
fn try_decode_reports_an_unknown_variant_under_a_shared_selector() {
#[derive(Debug, Clone, PartialEq, ReadXDR, WriteXDR)]
#[repr(i32)]
enum AlphaPlus {
NotFound = 1,
Big { n: u64 } = 2,
AddedLater = 3,
}
impl Exn for AlphaPlus {
const SELECTOR: u64 = Alpha::SELECTOR;
}
let newer = RawCallError::Exception(RawException::new(
AlphaPlus::SELECTOR,
&encode_xdr(&AlphaPlus::AddedLater).unwrap(),
));
assert_eq!(newer.try_as::<Alpha>(), None);
assert!(matches!(newer.try_decode::<Alpha>(), Some(Err(_))));
let landed = match_exn!(newer, {
Alpha::NotFound => "not-found",
Alpha::Big { .. } => "big",
_ => "wildcard",
});
assert_eq!(landed, "wildcard");
}
#[test]
fn try_decode_accepts_a_payload_with_trailing_bytes() {
let mut payload = encode_xdr(&Alpha::NotFound).unwrap();
payload.extend_from_slice(b"appended-later");
let padded = RawCallError::Exception(RawException::new(
Alpha::SELECTOR,
&payload,
));
assert!(matches!(
padded.try_decode::<Alpha>(),
Some(Ok(Alpha::NotFound))
));
assert_eq!(padded.try_as::<Alpha>(), Some(Alpha::NotFound));
}
#[test]
fn try_decode_is_none_when_no_exception_is_carried() {
let fault: RawCallError = RawCallError::Fault(FaultInfo::empty());
assert!(fault.try_decode::<Alpha>().is_none());
let invalid: RawCallError = RawCallError::Invalid;
assert!(invalid.try_decode::<Alpha>().is_none());
}
#[test]
fn undecodable_payload_of_a_matching_selector_reaches_the_wildcard() {
let raw = RawException::new(Alpha::SELECTOR, &[]);
assert!(classify(RawCallError::Exception(raw)).starts_with("other:"));
}
#[test]
fn matches_an_owned_raw_exception_directly() {
let raw = caught(Beta::Rejected);
let got = match_exn!(raw, {
Beta::Rejected => 1u32,
_ => 0,
});
assert_eq!(got, 1);
}
#[test]
fn alternates_within_one_type_and_at_binding() {
let got = match_exn!(err(Alpha::Big { n: 3 }), {
Alpha::NotFound | Alpha::Big { .. } => 1u32,
_ => 0,
});
assert_eq!(got, 1);
let bound = match_exn!(err(Alpha::NotFound), {
v @ Alpha::NotFound => alloc::format!("{v:?}"),
_ => alloc::string::String::new(),
});
assert_eq!(bound, "NotFound");
}
#[test]
fn arms_are_tried_in_order_first_match_wins() {
let got = match_exn!(err(Alpha::NotFound), {
Alpha::NotFound => 1u32,
Alpha::NotFound => 2,
_ => 0,
});
assert_eq!(got, 1);
}
#[test]
#[should_panic(expected = "diverging arm")]
fn diverging_arm_bodies_do_not_warn() {
let _: u32 = match_exn!(err(Beta::Rejected), {
Alpha::NotFound => 1,
Beta::Rejected => panic!("diverging arm"),
_ => 0,
});
}
#[test]
fn diverging_wildcard_body_does_not_warn() {
let got = match_exn!(err(Beta::Rejected), {
Beta::Rejected => 1u32,
_ => panic!("unreachable in this case"),
});
assert_eq!(got, 1);
}
#[test]
fn wildcard_arm_moves_the_named_scrutinee() {
fn consume(e: RawCallError) -> u32 {
match e {
RawCallError::Exception(_) => 7,
_ => 0,
}
}
let e = err(Alpha::NotFound);
let got = match_exn!(e, {
Beta::Rejected => 1u32,
_ => consume(e),
});
assert_eq!(got, 7);
}
#[test]
fn wildcard_only_is_legal() {
let got = match_exn!(err(Alpha::NotFound), { _ => 5u32 });
assert_eq!(got, 5);
}
#[test]
fn block_bodies_need_no_trailing_comma() {
let got = match_exn!(err(Beta::Rejected), {
Alpha::NotFound => { 1u32 }
Beta::Rejected => { 2 }
_ => { 0 }
});
assert_eq!(got, 2);
}
#[test]
fn block_body_then_comma_plain_arm() {
let got = match_exn!(err(Beta::Rejected), {
Alpha::NotFound => { 1u32 },
Beta::Rejected => { 2 },
_ => 0,
});
assert_eq!(got, 2);
}
#[test]
fn block_body_then_comma_guarded_arm() {
let classify = |e: RawCallError| {
match_exn!(e, {
Alpha::Big { n } if n > 100 => { 1u32 },
Alpha::Big { n } if n > 10 => { 2 },
Alpha::Big { .. } => { 3 },
_ => 0,
})
};
assert_eq!(classify(err(Alpha::Big { n: 500 })), 1);
assert_eq!(classify(err(Alpha::Big { n: 50 })), 2);
assert_eq!(classify(err(Alpha::Big { n: 5 })), 3);
assert_eq!(classify(RawCallError::Invalid), 0);
}
#[test]
fn body_shapes_and_commas_mix_freely() {
let got = match_exn!(err(Beta::Rejected), {
Alpha::NotFound => 1u32,
Alpha::Big { .. } => { 2 },
Beta::Rejected => { 3 }
_ => 0,
});
assert_eq!(got, 3);
let no_exn: RawCallError = RawCallError::Invalid;
let fell_through = match_exn!(no_exn, {
Alpha::NotFound => 1u32,
Beta::Rejected => { 2 },
_ => { 0 }
});
assert_eq!(fell_through, 0);
}
#[test]
fn block_body_then_comma_before_wildcard() {
let got = match_exn!(err(Alpha::NotFound), {
Alpha::NotFound => { 7u32 },
_ => 0,
});
assert_eq!(got, 7);
}
}