Skip to content

Commit 5624fe1

Browse files
authored
serde: record errors as a struct with fields for sources (#91)
Depends on #89 This PR changes `valuable-serde`'s recording of `dyn Error` values to record the error as a `serde` struct with `message` and `source` fields. This way, we can serialize errors with source chains more nicely. When the backtrace support for `std::error::Error` is stable, we could also record backtraces as a field. We could even consider adding a build script to detect the nightly compiler and conditionally enable a `cfg` for backtrace support, but that seems better left to a follow-up.
1 parent 461c3d7 commit 5624fe1

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

valuable-serde/src/lib.rs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ where
264264
#[cfg(feature = "std")]
265265
Value::Path(p) => Serialize::serialize(p, serializer),
266266
#[cfg(feature = "std")]
267-
Value::Error(e) => serializer.collect_str(e),
267+
Value::Error(e) => SerializeError(e).serialize(serializer),
268268

269269
v => unimplemented!("{:?}", v),
270270
}
@@ -668,3 +668,19 @@ impl<S: Serializer> Visit for VisitDynamic<'_, S> {
668668
}
669669
}
670670
}
671+
672+
struct SerializeError<'a>(&'a dyn std::error::Error);
673+
impl Serialize for SerializeError<'_> {
674+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
675+
struct CollectStr<'a>(&'a dyn std::error::Error);
676+
impl Serialize for CollectStr<'_> {
677+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
678+
serializer.collect_str(&self.0)
679+
}
680+
}
681+
let mut s = serializer.serialize_struct("Error", 2)?;
682+
s.serialize_field("message", &CollectStr(self.0))?;
683+
s.serialize_field("source", &self.0.source().map(SerializeError))?;
684+
s.end()
685+
}
686+
}

valuable-serde/tests/test.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,3 +518,75 @@ fn test_dyn_enum() {
518518
],
519519
);
520520
}
521+
522+
#[test]
523+
fn test_errors() {
524+
use std::{error::Error, fmt};
525+
526+
#[derive(Debug)]
527+
struct TestError {
528+
message: &'static str,
529+
source: Option<Box<dyn Error + 'static>>,
530+
}
531+
532+
impl fmt::Display for TestError {
533+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
534+
f.write_str(self.message)
535+
}
536+
}
537+
538+
impl Error for TestError {
539+
fn source(&self) -> Option<&(dyn Error + 'static)> {
540+
self.source.as_deref()
541+
}
542+
}
543+
544+
let no_source = TestError {
545+
message: "an error occurred",
546+
source: None,
547+
};
548+
549+
assert_ser_eq!(
550+
&Serializable::new(&no_source as &(dyn Error + 'static)),
551+
&[
552+
Token::Struct {
553+
name: "Error",
554+
len: 2
555+
},
556+
Token::Str("message"),
557+
Token::Str("an error occurred"),
558+
Token::Str("source"),
559+
Token::None,
560+
Token::StructEnd
561+
]
562+
);
563+
564+
let with_source = TestError {
565+
message: "the error caused another error",
566+
source: Some(Box::new(no_source)),
567+
};
568+
569+
assert_ser_eq!(
570+
&Serializable::new(&with_source as &(dyn Error + 'static)),
571+
&[
572+
Token::Struct {
573+
name: "Error",
574+
len: 2
575+
},
576+
Token::Str("message"),
577+
Token::Str("the error caused another error"),
578+
Token::Str("source"),
579+
Token::Some,
580+
Token::Struct {
581+
name: "Error",
582+
len: 2
583+
},
584+
Token::Str("message"),
585+
Token::Str("an error occurred"),
586+
Token::Str("source"),
587+
Token::None,
588+
Token::StructEnd,
589+
Token::StructEnd
590+
]
591+
);
592+
}

0 commit comments

Comments
 (0)