Skip to main content

recon_core/
error.rs

1//! Error conventions.
2//!
3//! Each layer defines its own error type with `thiserror` and preserves the originating cause.
4//! Domain failures are never flattened into `std::io::Error`, and never reduced to a string —
5//! a decode failure must remain distinguishable from every other decode failure.
6
7use thiserror::Error;
8
9/// Failures at the wire boundary, where typed values become bytes and back.
10///
11/// This is the one place in the system that touches encoding: layers above pass typed values.
12#[derive(Debug, Error)]
13pub enum CodecError {
14    #[error("failed to encode {type_name}")]
15    Encode {
16        type_name: &'static str,
17        #[source]
18        source: Box<dyn core::error::Error + Send + Sync>,
19    },
20    #[error("failed to decode {type_name}")]
21    Decode {
22        type_name: &'static str,
23        #[source]
24        source: Box<dyn core::error::Error + Send + Sync>,
25    },
26    #[error("{type_name} did not survive an encode/decode round trip")]
27    RoundTripMismatch { type_name: &'static str },
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33    use core::error::Error as _;
34
35    #[derive(Debug, Error)]
36    #[error("the underlying detail")]
37    struct Underlying;
38
39    #[test]
40    fn every_variant_retains_its_cause() {
41        let e = CodecError::Decode { type_name: "PlMsg", source: Box::new(Underlying) };
42        // The message identifies decoding as the cause...
43        assert_eq!(e.to_string(), "failed to decode PlMsg");
44        // ...and the underlying detail survives, rather than being flattened to a string.
45        assert_eq!(e.source().expect("cause preserved").to_string(), "the underlying detail");
46    }
47
48    #[test]
49    fn encode_and_decode_are_distinguishable() {
50        let d = CodecError::Decode { type_name: "T", source: Box::new(Underlying) };
51        let e = CodecError::Encode { type_name: "T", source: Box::new(Underlying) };
52        assert_ne!(d.to_string(), e.to_string());
53        assert!(matches!(d, CodecError::Decode { .. }));
54        assert!(matches!(e, CodecError::Encode { .. }));
55    }
56
57    #[test]
58    fn round_trip_mismatch_names_the_type() {
59        let e = CodecError::RoundTripMismatch { type_name: "BebMsg" };
60        assert!(e.to_string().contains("BebMsg"));
61        assert!(e.source().is_none());
62    }
63}