Skip to content

Commit a72431e

Browse files
authored
RUST-1874 Add optional integration with serde_path_to_error (#488)
1 parent d0f5d23 commit a72431e

File tree

10 files changed

+217
-29
lines changed

10 files changed

+217
-29
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ chrono-0_4 = ["chrono"]
4343
uuid-1 = []
4444
# if enabled, include API for interfacing with time 0.3
4545
time-0_3 = []
46+
serde_path_to_error = ["dep:serde_path_to_error"]
4647
# if enabled, include serde_with interop.
4748
# should be used in conjunction with chrono-0_4 or uuid-0_8.
4849
# it's commented out here because Cargo implicitly adds a feature flag for
@@ -69,6 +70,7 @@ serde_with = { version = "1.3.1", optional = true }
6970
serde_with-3 = { package = "serde_with", version = "3.1.0", optional = true }
7071
time = { version = "0.3.9", features = ["formatting", "parsing", "macros", "large-dates"] }
7172
bitvec = "1.0.1"
73+
serde_path_to_error = { version = "0.1.16", optional = true }
7274
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
7375
js-sys = "0.3"
7476

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,11 @@ Note that if you are using `bson` through the `mongodb` crate, you do not need t
5050
| `chrono-0_4` | Enable support for v0.4 of the [`chrono`](https://docs.rs/chrono/0.4) crate in the public API. | n/a | no |
5151
| `uuid-0_8` | Enable support for v0.8 of the [`uuid`](https://docs.rs/uuid/0.8) crate in the public API. | n/a | no |
5252
| `uuid-1` | Enable support for v1.x of the [`uuid`](https://docs.rs/uuid/1.0) crate in the public API. | n/a | no |
53+
| `time-0_3` | Enable support for v0.3 of the [`time`](https://docs.rs/time/0.3) crate in the public API. | n/a | no |
5354
| `serde_with` | Enable [`serde_with`](https://docs.rs/serde_with/1.x) 1.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
5455
| `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for `bson::DateTime` and `bson::Uuid`.| serde_with | no |
56+
| `serde_path_to_error` | Enable support for error paths via integration with [`serde_path_to_error`](https://docs.rs/serde_path_to_err/latest). This is an unstable feature and any breaking changes to `serde_path_to_error` may affect usage of it via this feature. | serde_path_to_error | no |
57+
5558
## Overview of the BSON Format
5659

5760
BSON, short for Binary JSON, is a binary-encoded serialization of JSON-like documents.
@@ -208,6 +211,14 @@ separate the "business logic" that operates over the data from the (de)serializa
208211
translates the data to/from its serialized form. This can lead to more clear and concise code
209212
that is also less error prone.
210213

214+
When serializing values that cannot be represented in BSON, or deserialzing from BSON that does
215+
not match the format expected by the type, the default error will only report the specific field
216+
that failed. To aid debugging, enabling the [`serde_path_to_error`](#feature-flags) feature will
217+
[augment errors](https://docs.rs/bson/latest/bson/de/enum.Error.html#variant.WithPath) with the
218+
full field path from root object to failing field. This feature does incur a small CPU and memory
219+
overhead during (de)serialization and should be enabled with care in performance-sensitive
220+
environments.
221+
211222
### Working with datetimes
212223

213224
The BSON format includes a datetime type, which is modeled in this crate by the

src/de/error.rs

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,17 @@ pub enum Error {
3636
/// A message describing the error.
3737
message: String,
3838
},
39+
40+
#[cfg(feature = "serde_path_to_error")]
41+
#[cfg_attr(docsrs, doc(cfg(feature = "serde_path_to_error")))]
42+
#[non_exhaustive]
43+
WithPath {
44+
/// The path to the error.
45+
path: serde_path_to_error::Path,
46+
47+
/// The original error.
48+
source: Box<Error>,
49+
},
3950
}
4051

4152
impl Error {
@@ -44,6 +55,13 @@ impl Error {
4455
message: msg.to_string(),
4556
}
4657
}
58+
59+
#[cfg(feature = "serde_path_to_error")]
60+
pub(crate) fn with_path(err: serde_path_to_error::Error<Error>) -> Self {
61+
let path = err.path().clone();
62+
let source = Box::new(err.into_inner());
63+
Self::WithPath { path, source }
64+
}
4765
}
4866

4967
impl From<io::Error> for Error {
@@ -66,19 +84,18 @@ impl From<crate::raw::Error> for Error {
6684

6785
impl fmt::Display for Error {
6886
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
69-
match *self {
70-
Error::Io(ref inner) => inner.fmt(fmt),
71-
Error::InvalidUtf8String(ref inner) => inner.fmt(fmt),
72-
Error::UnrecognizedDocumentElementType {
73-
ref key,
74-
element_type,
75-
} => write!(
87+
match self {
88+
Error::Io(inner) => inner.fmt(fmt),
89+
Error::InvalidUtf8String(inner) => inner.fmt(fmt),
90+
Error::UnrecognizedDocumentElementType { key, element_type } => write!(
7691
fmt,
7792
"unrecognized element type for key \"{}\": `{:#x}`",
7893
key, element_type
7994
),
8095
Error::EndOfStream => fmt.write_str("end of stream"),
81-
Error::DeserializationError { ref message } => message.fmt(fmt),
96+
Error::DeserializationError { message } => message.fmt(fmt),
97+
#[cfg(feature = "serde_path_to_error")]
98+
Error::WithPath { path, source } => write!(fmt, "error at {}: {}", path, source),
8299
}
83100
}
84101
}

src/de/mod.rs

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,14 @@ where
8989
T: DeserializeOwned,
9090
{
9191
let de = Deserializer::new(bson);
92-
Deserialize::deserialize(de)
92+
#[cfg(feature = "serde_path_to_error")]
93+
{
94+
return serde_path_to_error::deserialize(de).map_err(Error::with_path);
95+
}
96+
#[cfg(not(feature = "serde_path_to_error"))]
97+
{
98+
Deserialize::deserialize(de)
99+
}
93100
}
94101

95102
/// Deserialize a `T` from the provided [`Bson`] value, configuring the underlying
@@ -201,8 +208,7 @@ pub fn from_slice<'de, T>(bytes: &'de [u8]) -> Result<T>
201208
where
202209
T: Deserialize<'de>,
203210
{
204-
let deserializer = raw::Deserializer::new(bytes, false)?;
205-
T::deserialize(deserializer)
211+
from_raw(raw::Deserializer::new(bytes, false)?)
206212
}
207213

208214
/// Deserialize an instance of type `T` from a slice of BSON bytes, replacing any invalid UTF-8
@@ -215,6 +221,18 @@ pub fn from_slice_utf8_lossy<'de, T>(bytes: &'de [u8]) -> Result<T>
215221
where
216222
T: Deserialize<'de>,
217223
{
218-
let deserializer = raw::Deserializer::new(bytes, true)?;
219-
T::deserialize(deserializer)
224+
from_raw(raw::Deserializer::new(bytes, true)?)
225+
}
226+
227+
pub(crate) fn from_raw<'de, T: Deserialize<'de>>(
228+
deserializer: raw::Deserializer<'de>,
229+
) -> Result<T> {
230+
#[cfg(feature = "serde_path_to_error")]
231+
{
232+
serde_path_to_error::deserialize(deserializer).map_err(Error::with_path)
233+
}
234+
#[cfg(not(feature = "serde_path_to_error"))]
235+
{
236+
T::deserialize(deserializer)
237+
}
220238
}

src/document.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ use std::{
99

1010
use ahash::RandomState;
1111
use indexmap::IndexMap;
12-
use serde::Deserialize;
1312

1413
use crate::{
1514
bson::{Array, Bson, Timestamp},
@@ -547,8 +546,7 @@ impl Document {
547546

548547
fn decode<R: Read + ?Sized>(reader: &mut R, utf_lossy: bool) -> crate::de::Result<Document> {
549548
let buf = crate::de::reader_to_vec(reader)?;
550-
let deserializer = crate::de::RawDeserializer::new(&buf, utf_lossy)?;
551-
Document::deserialize(deserializer)
549+
crate::de::from_raw(crate::de::RawDeserializer::new(&buf, utf_lossy)?)
552550
}
553551

554552
/// Attempts to deserialize a [`Document`] from a byte stream.

src/lib.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,9 @@
6565
//! | `uuid-0_8` | Enable support for v0.8 of the [`uuid`](https://docs.rs/uuid/0.8) crate in the public API. | no |
6666
//! | `uuid-1` | Enable support for v1.x of the [`uuid`](https://docs.rs/uuid/1.x) crate in the public API. | no |
6767
//! | `time-0_3` | Enable support for v0.3 of the [`time`](https://docs.rs/time/0.3) crate in the public API. | no |
68-
//! | `serde_with` | Enable [`serde_with`](https://docs.rs/serde_with/latest) integrations for [`DateTime`] and [`Uuid`]. | no |
68+
//! | `serde_with` | Enable [`serde_with`](https://docs.rs/serde_with/1.x) 1.x integrations for [`DateTime`] and [`Uuid`]. | no |
69+
//! | `serde_with-3` | Enable [`serde_with`](https://docs.rs/serde_with/3.x) 3.x integrations for [`DateTime`] and [`Uuid`]. | no |
70+
//! | `serde_path_to_error` | Enable support for error paths via integration with [`serde_path_to_error`](https://docs.rs/serde_path_to_err/latest). This is an unstable feature and any breaking changes to `serde_path_to_error` may affect usage of it via this feature. | no |
6971
//!
7072
//! ## BSON values
7173
//!
@@ -213,6 +215,13 @@
213215
//! data from the (de)serialization logic that translates the data to/from its serialized form. This
214216
//! can lead to more clear and concise code that is also less error prone.
215217
//!
218+
//! When serializing values that cannot be represented in BSON, or deserialzing from BSON that does
219+
//! not match the format expected by the type, the default error will only report the specific field
220+
//! that failed. To aid debugging, enabling the `serde_path_to_error` feature will
221+
//! [augment errors](crate::de::Error::WithPath) with the full field path from root object to
222+
//! failing field. This feature does incur a small CPU and memory overhead during (de)serialization
223+
//! and should be enabled with care in performance-sensitive environments.
224+
//!
216225
//! ## Working with datetimes
217226
//!
218227
//! The BSON format includes a datetime type, which is modeled in this crate by the

src/ser/error.rs

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,39 @@ pub enum Error {
2727

2828
/// An unsigned integer type could not fit into a signed integer type.
2929
UnsignedIntegerExceededRange(u64),
30+
31+
#[cfg(feature = "serde_path_to_error")]
32+
#[cfg_attr(docsrs, doc(cfg(feature = "serde_path_to_error")))]
33+
#[non_exhaustive]
34+
WithPath {
35+
/// The path to the error.
36+
path: serde_path_to_error::Path,
37+
38+
/// The original error.
39+
source: Box<Error>,
40+
},
41+
}
42+
43+
impl Error {
44+
#[cfg(feature = "serde_path_to_error")]
45+
pub(crate) fn with_path(err: serde_path_to_error::Error<Error>) -> Self {
46+
let path = err.path().clone();
47+
let source = Box::new(err.into_inner());
48+
Self::WithPath { path, source }
49+
}
50+
51+
#[cfg(test)]
52+
pub(crate) fn strip_path(self) -> Self {
53+
#[cfg(feature = "serde_path_to_error")]
54+
match self {
55+
Self::WithPath { path: _, source } => *source,
56+
_ => self,
57+
}
58+
#[cfg(not(feature = "serde_path_to_error"))]
59+
{
60+
self
61+
}
62+
}
3063
}
3164

3265
impl From<io::Error> for Error {
@@ -37,20 +70,22 @@ impl From<io::Error> for Error {
3770

3871
impl fmt::Display for Error {
3972
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
40-
match *self {
41-
Error::Io(ref inner) => inner.fmt(fmt),
42-
Error::InvalidDocumentKey(ref key) => write!(fmt, "Invalid map key type: {}", key),
73+
match self {
74+
Error::Io(inner) => inner.fmt(fmt),
75+
Error::InvalidDocumentKey(key) => write!(fmt, "Invalid map key type: {}", key),
4376
Error::InvalidCString(ref string) => {
4477
write!(fmt, "cstrings cannot contain null bytes: {:?}", string)
4578
}
46-
Error::SerializationError { ref message } => message.fmt(fmt),
79+
Error::SerializationError { message } => message.fmt(fmt),
4780
Error::UnsignedIntegerExceededRange(value) => write!(
4881
fmt,
4982
"BSON does not support unsigned integers.
5083
An attempt to serialize the value: {} in a signed type failed due to the value's \
5184
size.",
5285
value
5386
),
87+
#[cfg(feature = "serde_path_to_error")]
88+
Error::WithPath { path, source } => write!(fmt, "error at {}: {}", path, source),
5489
}
5590
}
5691
}

src/ser/mod.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ where
117117
T: Serialize + ?Sized,
118118
{
119119
let ser = Serializer::new();
120+
#[cfg(feature = "serde_path_to_error")]
121+
{
122+
serde_path_to_error::serialize(value, ser).map_err(Error::with_path)
123+
}
124+
#[cfg(not(feature = "serde_path_to_error"))]
120125
value.serialize(ser)
121126
}
122127

@@ -197,7 +202,14 @@ where
197202
T: Serialize,
198203
{
199204
let mut serializer = raw::Serializer::new();
200-
value.serialize(&mut serializer)?;
205+
#[cfg(feature = "serde_path_to_error")]
206+
{
207+
serde_path_to_error::serialize(value, &mut serializer).map_err(Error::with_path)?;
208+
}
209+
#[cfg(not(feature = "serde_path_to_error"))]
210+
{
211+
value.serialize(&mut serializer)?;
212+
}
201213
Ok(serializer.into_vec())
202214
}
203215

src/tests/modules/ser.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,11 +109,8 @@ fn uint64_u2i() {
109109
let deser_min: u64 = from_bson(obj_min).unwrap();
110110
assert_eq!(deser_min, u64::MIN);
111111

112-
let obj_max: ser::Result<Bson> = to_bson(&u64::MAX);
113-
assert_matches!(
114-
obj_max,
115-
Err(ser::Error::UnsignedIntegerExceededRange(u64::MAX))
116-
);
112+
let err: ser::Error = to_bson(&u64::MAX).unwrap_err().strip_path();
113+
assert_matches!(err, ser::Error::UnsignedIntegerExceededRange(u64::MAX));
117114
}
118115

119116
#[test]
@@ -161,11 +158,11 @@ fn cstring_null_bytes_error() {
161158
fn verify_doc(doc: Document) {
162159
let mut vec = Vec::new();
163160
assert!(matches!(
164-
doc.to_writer(&mut vec).unwrap_err(),
161+
doc.to_writer(&mut vec).unwrap_err().strip_path(),
165162
ser::Error::InvalidCString(_)
166163
));
167164
assert!(matches!(
168-
to_vec(&doc).unwrap_err(),
165+
to_vec(&doc).unwrap_err().strip_path(),
169166
ser::Error::InvalidCString(_)
170167
));
171168
}

0 commit comments

Comments
 (0)