From 6380979800e4427061f8682253d463612a6be86e Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Thu, 12 Jun 2025 10:36:03 -0600 Subject: [PATCH 01/12] datetime and oid --- src/datetime.rs | 49 +++++-------------------- src/datetime/builder.rs | 16 +++++---- src/error.rs | 35 ++++++++++++++---- src/extjson/de.rs | 10 ++---- src/extjson/models.rs | 3 +- src/oid.rs | 80 +++++++++-------------------------------- src/raw.rs | 22 ++++++------ src/raw/document.rs | 10 +++--- src/raw/document_buf.rs | 2 +- src/raw/iter.rs | 18 +++++----- 10 files changed, 93 insertions(+), 152 deletions(-) diff --git a/src/datetime.rs b/src/datetime.rs index b1394c32..c37349dc 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -1,18 +1,13 @@ //! Module containing functionality related to BSON DateTimes. //! For more information, see the documentation for the [`DateTime`] type. +pub(crate) mod builder; use std::{ convert::TryInto, - error, fmt::{self, Display}, - result, time::{Duration, SystemTime}, }; -pub(crate) mod builder; -pub use crate::datetime::builder::DateTimeBuilder; -use time::format_description::well_known::Rfc3339; - #[cfg(feature = "chrono-0_4")] use chrono::{LocalResult, TimeZone, Utc}; #[cfg(all( @@ -20,6 +15,10 @@ use chrono::{LocalResult, TimeZone, Utc}; any(feature = "chrono-0_4", feature = "time-0_3") ))] use serde::{Deserialize, Deserializer, Serialize}; +use time::format_description::well_known::Rfc3339; + +pub use crate::datetime::builder::DateTimeBuilder; +use crate::error::{Error, Result}; /// Struct representing a BSON datetime. /// Note: BSON datetimes have millisecond precision. @@ -390,19 +389,14 @@ impl crate::DateTime { pub fn try_to_rfc3339_string(self) -> Result { self.to_time_0_3() .format(&Rfc3339) - .map_err(|e| Error::CannotFormat { - message: e.to_string(), - }) + .map_err(Error::bad_conversion) } /// Convert the given RFC 3339 formatted string to a [`DateTime`], truncating it to millisecond /// precision. pub fn parse_rfc3339_str(s: impl AsRef) -> Result { - let odt = time::OffsetDateTime::parse(s.as_ref(), &Rfc3339).map_err(|e| { - Error::InvalidTimestamp { - message: e.to_string(), - } - })?; + let odt = + time::OffsetDateTime::parse(s.as_ref(), &Rfc3339).map_err(Error::invalid_value)?; Ok(Self::from_time_0_3(odt)) } @@ -547,30 +541,3 @@ impl serde_with::SerializeAs for crate::DateTime { dt.serialize(serializer) } } - -/// Errors that can occur during [`DateTime`] construction and generation. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Error { - /// Error returned when an invalid datetime format is provided to a conversion method. - #[non_exhaustive] - InvalidTimestamp { message: String }, - /// Error returned when a [`DateTime`] cannot be represented in a particular format. - #[non_exhaustive] - CannotFormat { message: String }, -} - -/// Alias for `Result` -pub type Result = result::Result; - -impl fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::InvalidTimestamp { message } | Error::CannotFormat { message } => { - write!(fmt, "{}", message) - } - } - } -} - -impl error::Error for Error {} diff --git a/src/datetime/builder.rs b/src/datetime/builder.rs index c16e22e2..8c38ddf8 100644 --- a/src/datetime/builder.rs +++ b/src/datetime/builder.rs @@ -1,7 +1,12 @@ -use super::*; use std::convert::TryFrom; + use time::Date; +use crate::{ + datetime::DateTime, + error::{Error, Result}, +}; + /// Builder for constructing a BSON [`DateTime`] pub struct DateTimeBuilder { pub(crate) year: Y, @@ -169,19 +174,16 @@ impl DateTimeBuilder { /// /// Note: You cannot call `build()` before setting at least the year, month and day. pub fn build(self) -> Result { - let err = |e: time::error::ComponentRange| Error::InvalidTimestamp { - message: e.to_string(), - }; - let month = time::Month::try_from(self.month.0).map_err(err)?; + let month = time::Month::try_from(self.month.0).map_err(Error::invalid_value)?; let dt = Date::from_calendar_date(self.year.0, month, self.day.0) - .map_err(err)? + .map_err(Error::invalid_value)? .with_hms_milli( self.hour.unwrap_or(0), self.minute.unwrap_or(0), self.second.unwrap_or(0), self.millisecond.unwrap_or(0), ) - .map_err(err)?; + .map_err(Error::invalid_value)?; Ok(DateTime::from_time_private(dt.assume_utc())) } } diff --git a/src/error.rs b/src/error.rs index d1497760..32e2ef38 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,7 +5,7 @@ use crate::spec::ElementType; pub type Result = std::result::Result; /// An error that can occur in the `bson` crate. -#[derive(Debug, Error)] +#[derive(Clone, Debug, Error)] #[non_exhaustive] pub struct Error { /// The kind of error that occurred. @@ -31,13 +31,22 @@ impl std::fmt::Display for Error { } /// The types of errors that can occur in the `bson` crate. -#[derive(Debug, Error)] +#[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum ErrorKind { + /// An error occurred when converting a BSON type to an external format. + #[error("Bad conversion: {message}")] + BadConversion { message: String }, + + /// An error occurred when attempting to parse a value from an external format to BSON. + #[error("Invalid value: {message}")] + #[non_exhaustive] + InvalidValue { message: String }, + /// Malformed BSON bytes were encountered. #[error("Malformed BSON: {message}")] #[non_exhaustive] - MalformedValue { message: String }, + MalformedBytes { message: String }, /// Invalid UTF-8 bytes were encountered. #[error("Invalid UTF-8")] @@ -78,7 +87,7 @@ impl From for Error { } /// The types of errors that can occur when attempting to access a value in a document. -#[derive(Debug, Error)] +#[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum ValueAccessErrorKind { /// No value for the specified key was present in the document. @@ -134,8 +143,22 @@ impl Error { .into() } - pub(crate) fn malformed_value(message: impl ToString) -> Self { - ErrorKind::MalformedValue { + pub(crate) fn malformed_bytes(message: impl ToString) -> Self { + ErrorKind::MalformedBytes { + message: message.to_string(), + } + .into() + } + + pub(crate) fn invalid_value(message: impl ToString) -> Self { + ErrorKind::InvalidValue { + message: message.to_string(), + } + .into() + } + + pub(crate) fn bad_conversion(message: impl ToString) -> Self { + ErrorKind::BadConversion { message: message.to_string(), } .into() diff --git a/src/extjson/de.rs b/src/extjson/de.rs index 82b8fcde..f147ad43 100644 --- a/src/extjson/de.rs +++ b/src/extjson/de.rs @@ -25,14 +25,14 @@ use std::convert::{TryFrom, TryInto}; use serde::de::{Error as _, Unexpected}; -use crate::{extjson::models, oid, Bson, Document}; +use crate::{extjson::models, Bson, Document}; #[derive(Clone, Debug)] #[non_exhaustive] /// Error cases that can occur during deserialization from [extended JSON](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). pub enum Error { /// Errors that can occur during OID construction and generation from the input data. - InvalidObjectId(oid::Error), + InvalidObjectId(crate::error::Error), /// A general error encountered during deserialization. /// See: @@ -69,12 +69,6 @@ impl From for Error { } } -impl From for Error { - fn from(err: oid::Error) -> Self { - Self::InvalidObjectId(err) - } -} - pub type Result = std::result::Result; /// This converts from the input JSON object as if it were [MongoDB Extended JSON v2](https://www.mongodb.com/docs/manual/reference/mongodb-extended-json/). diff --git a/src/extjson/models.rs b/src/extjson/models.rs index 297b47d6..caccd12f 100644 --- a/src/extjson/models.rs +++ b/src/extjson/models.rs @@ -100,7 +100,8 @@ pub(crate) struct ObjectId { impl ObjectId { pub(crate) fn parse(self) -> extjson::de::Result { - let oid = oid::ObjectId::parse_str(self.oid.as_str())?; + let oid = oid::ObjectId::parse_str(self.oid.as_str()) + .map_err(extjson::de::Error::InvalidObjectId)?; Ok(oid) } } diff --git a/src/oid.rs b/src/oid.rs index a6bb7dbd..a15278d5 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -1,21 +1,20 @@ //! Module containing functionality related to BSON ObjectIds. //! For more information, see the documentation for the [`ObjectId`] type. +#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] +use std::{convert::TryInto, time::SystemTime}; use std::{ - error, fmt, - result, str::FromStr, sync::atomic::{AtomicUsize, Ordering}, }; -#[cfg(not(all(target_arch = "wasm32", target_os = "unknown")))] -use std::{convert::TryInto, time::SystemTime}; - use hex::{self, FromHexError}; use once_cell::sync::Lazy; use rand::{random, rng, Rng}; +use crate::error::{Error, Result}; + const TIMESTAMP_SIZE: usize = 4; const PROCESS_ID_SIZE: usize = 5; const COUNTER_SIZE: usize = 3; @@ -29,49 +28,6 @@ const MAX_U24: usize = 0xFF_FFFF; static OID_COUNTER: Lazy = Lazy::new(|| AtomicUsize::new(rng().random_range(0..=MAX_U24))); -/// Errors that can occur during [`ObjectId`] construction and generation. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Error { - /// An invalid character was found in the provided hex string. Valid characters are: `0...9`, - /// `a...f`, or `A...F`. - #[non_exhaustive] - InvalidHexStringCharacter { c: char, index: usize, hex: String }, - - /// An [`ObjectId`]'s hex string representation must be an exactly 12-byte (24-char) - /// hexadecimal string. - #[non_exhaustive] - InvalidHexStringLength { length: usize, hex: String }, -} - -/// Alias for Result. -pub type Result = result::Result; - -impl fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::InvalidHexStringCharacter { c, index, hex } => { - write!( - fmt, - "invalid character '{}' was found at index {} in the provided hex string: \ - \"{}\"", - c, index, hex - ) - } - Error::InvalidHexStringLength { length, hex } => { - write!( - fmt, - "provided hex string representation must be exactly 12 bytes, instead got: \ - \"{}\", length {}", - hex, length - ) - } - } - } -} - -impl error::Error for Error {} - /// A wrapper around a raw 12-byte ObjectId. /// /// ## `serde` integration @@ -198,24 +154,22 @@ impl ObjectId { pub fn parse_str(s: impl AsRef) -> Result { let s = s.as_ref(); - let bytes: Vec = hex::decode(s.as_bytes()).map_err(|e| match e { - FromHexError::InvalidHexCharacter { c, index } => Error::InvalidHexStringCharacter { - c, - index, - hex: s.to_string(), - }, - FromHexError::InvalidStringLength | FromHexError::OddLength => { - Error::InvalidHexStringLength { - length: s.len(), - hex: s.to_string(), + let bytes: Vec = hex::decode(s.as_bytes()).map_err(|e| { + let message = match e { + FromHexError::InvalidHexCharacter { c, index } => { + format!("invalid hex character {c} encountered at index {index}") + } + FromHexError::InvalidStringLength | FromHexError::OddLength => { + format!("invalid hex string length {}", s.len()) } - } + }; + Error::invalid_value(message) })?; if bytes.len() != 12 { - Err(Error::InvalidHexStringLength { - length: s.len(), - hex: s.to_string(), - }) + Err(Error::invalid_value(format!( + "invalid object ID byte vector length {}", + bytes.len() + ))) } else { let mut byte_array: [u8; 12] = [0; 12]; byte_array[..].copy_from_slice(&bytes[..]); diff --git a/src/raw.rs b/src/raw.rs index 252832c2..697362c8 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -164,7 +164,7 @@ fn f64_from_slice(val: &[u8]) -> Result { .get(0..8) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - Error::malformed_value(format!( + Error::malformed_bytes(format!( "expected 8 bytes to read double, instead got {}", val.len() )) @@ -179,7 +179,7 @@ fn i32_from_slice(val: &[u8]) -> Result { .get(0..4) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - Error::malformed_value(format!( + Error::malformed_bytes(format!( "expected 4 bytes to read i32, instead got {}", val.len() )) @@ -194,7 +194,7 @@ fn i64_from_slice(val: &[u8]) -> Result { .get(0..8) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - Error::malformed_value(format!( + Error::malformed_bytes(format!( "expected 8 bytes to read i64, instead got {}", val.len() )) @@ -207,7 +207,7 @@ fn u8_from_slice(val: &[u8]) -> Result { .get(0..1) .and_then(|s| s.try_into().ok()) .ok_or_else(|| { - Error::malformed_value(format!( + Error::malformed_bytes(format!( "expected 1 byte to read u8, instead got {}", val.len() )) @@ -218,7 +218,7 @@ fn u8_from_slice(val: &[u8]) -> Result { pub(crate) fn bool_from_slice(val: &[u8]) -> Result { let val = u8_from_slice(val)?; if val > 1 { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "boolean must be stored as 0 or 1, got {}", val ))); @@ -229,7 +229,7 @@ pub(crate) fn bool_from_slice(val: &[u8]) -> Result { fn read_len(buf: &[u8]) -> Result { if buf.len() < 4 { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "expected buffer with string to contain at least 4 bytes, but it only has {}", buf.len() ))); @@ -239,14 +239,14 @@ fn read_len(buf: &[u8]) -> Result { let end = checked_add(usize_try_from_i32(length)?, 4)?; if end < MIN_BSON_STRING_SIZE as usize { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "BSON length encoded string needs to be at least {} bytes, instead got {}", MIN_BSON_STRING_SIZE, end ))); } if buf.len() < end { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "expected buffer to contain at least {} bytes, but it only has {}", end, buf.len() @@ -254,7 +254,7 @@ fn read_len(buf: &[u8]) -> Result { } if buf[end - 1] != 0 { - return Err(Error::malformed_value( + return Err(Error::malformed_bytes( "expected string to be null-terminated", )); } @@ -278,10 +278,10 @@ fn try_to_str(data: &[u8]) -> Result<&str> { } fn usize_try_from_i32(i: i32) -> Result { - usize::try_from(i).map_err(Error::malformed_value) + usize::try_from(i).map_err(Error::malformed_bytes) } fn checked_add(lhs: usize, rhs: usize) -> Result { lhs.checked_add(rhs) - .ok_or_else(|| Error::malformed_value("attempted to add with overflow")) + .ok_or_else(|| Error::malformed_bytes("attempted to add with overflow")) } diff --git a/src/raw/document.rs b/src/raw/document.rs index e62eb62e..a1978cb7 100644 --- a/src/raw/document.rs +++ b/src/raw/document.rs @@ -99,17 +99,17 @@ impl RawDocument { let data = data.as_ref(); if data.len() < 5 { - return Err(Error::malformed_value("document too short")); + return Err(Error::malformed_bytes("document too short")); } let length = i32_from_slice(data)?; if data.len() as i32 != length { - return Err(Error::malformed_value("document length incorrect")); + return Err(Error::malformed_bytes("document length incorrect")); } if data[data.len() - 1] != 0 { - return Err(Error::malformed_value("document not null-terminated")); + return Err(Error::malformed_bytes("document not null-terminated")); } Ok(RawDocument::new_unchecked(data)) @@ -500,11 +500,11 @@ impl RawDocument { let mut splits = buf.splitn(2, |x| *x == 0); let value = splits .next() - .ok_or_else(|| RawError::malformed_value("no value"))?; + .ok_or_else(|| RawError::malformed_bytes("no value"))?; if splits.next().is_some() { Ok(value) } else { - Err(RawError::malformed_value("expected null terminator")) + Err(RawError::malformed_bytes("expected null terminator")) } } diff --git a/src/raw/document_buf.rs b/src/raw/document_buf.rs index e89ef704..2744703a 100644 --- a/src/raw/document_buf.rs +++ b/src/raw/document_buf.rs @@ -114,7 +114,7 @@ impl RawDocumentBuf { /// ``` pub fn from_document(doc: &Document) -> Result { let mut data = Vec::new(); - doc.to_writer(&mut data).map_err(Error::malformed_value)?; + doc.to_writer(&mut data).map_err(Error::malformed_bytes)?; Ok(Self { data }) } diff --git a/src/raw/iter.rs b/src/raw/iter.rs index 62fdc8e6..342c6eab 100644 --- a/src/raw/iter.rs +++ b/src/raw/iter.rs @@ -81,7 +81,7 @@ impl<'a> RawIter<'a> { fn verify_enough_bytes(&self, start: usize, num_bytes: usize) -> Result<()> { let end = checked_add(start, num_bytes)?; if self.doc.as_bytes().get(start..end).is_none() { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "length exceeds remaining length of buffer: {} vs {}", num_bytes, self.doc.as_bytes().len() - start @@ -95,7 +95,7 @@ impl<'a> RawIter<'a> { let size = i32_from_slice(&self.doc.as_bytes()[starting_at..])? as usize; if size < MIN_BSON_DOCUMENT_SIZE as usize { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "document too small: {} bytes", size ))); @@ -104,7 +104,7 @@ impl<'a> RawIter<'a> { self.verify_enough_bytes(starting_at, size)?; if self.doc.as_bytes()[starting_at + size - 1] != 0 { - return Err(Error::malformed_value("not null terminated")); + return Err(Error::malformed_bytes("not null terminated")); } Ok(size) } @@ -314,7 +314,7 @@ impl<'a> RawElement<'a> { } fn malformed_error(&self, e: impl ToString) -> Error { - Error::malformed_value(e).with_key(self.key) + Error::malformed_bytes(e).with_key(self.key) } pub(crate) fn slice(&self) -> &'a [u8] { @@ -341,7 +341,7 @@ impl<'a> RawElement<'a> { Ok(ObjectId::from_bytes( self.doc.as_bytes()[start_at..(start_at + 12)] .try_into() - .map_err(|e| Error::malformed_value(e).with_key(self.key))?, + .map_err(|e| Error::malformed_bytes(e).with_key(self.key))?, )) } } @@ -350,7 +350,7 @@ impl RawIter<'_> { fn get_next_length_at(&self, start_at: usize) -> Result { let len = i32_from_slice(&self.doc.as_bytes()[start_at..])?; if len < 0 { - Err(Error::malformed_value("lengths can't be negative")) + Err(Error::malformed_bytes("lengths can't be negative")) } else { Ok(len as usize) } @@ -360,7 +360,7 @@ impl RawIter<'_> { let element_type = match ElementType::from(self.doc.as_bytes()[self.offset]) { Some(et) => et, None => { - return Err(Error::malformed_value(format!( + return Err(Error::malformed_bytes(format!( "invalid tag: {}", self.doc.as_bytes()[self.offset] ))); @@ -414,11 +414,11 @@ impl<'a> Iterator for RawIter<'a> { return None; } else { self.valid = false; - return Some(Err(Error::malformed_value("document not null terminated"))); + return Some(Err(Error::malformed_bytes("document not null terminated"))); } } else if self.offset >= self.doc.as_bytes().len() { self.valid = false; - return Some(Err(Error::malformed_value("iteration overflowed document"))); + return Some(Err(Error::malformed_bytes("iteration overflowed document"))); } let key = match self.doc.read_cstring_at(self.offset + 1) { From a10b6806a2ef4710abc50c3e471391f79848f83d Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Thu, 12 Jun 2025 10:42:22 -0600 Subject: [PATCH 02/12] binary --- src/binary.rs | 39 ++++++++----------------------- src/binary/vector.rs | 55 +++++++++++++++++++------------------------- 2 files changed, 33 insertions(+), 61 deletions(-) diff --git a/src/binary.rs b/src/binary.rs index f977088b..3b66ebc7 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -1,14 +1,19 @@ #! Module containing functionality related to BSON binary values. - mod vector; -use crate::{base64, spec::BinarySubtype, Document, RawBinaryRef}; use std::{ convert::TryFrom, - error, fmt::{self, Display}, }; +use crate::{ + base64, + error::{Error, Result}, + spec::BinarySubtype, + Document, + RawBinaryRef, +}; + pub use vector::{PackedBitVector, Vector}; /// Represents a BSON binary value. @@ -51,9 +56,7 @@ impl Binary { input: impl AsRef, subtype: impl Into>, ) -> Result { - let bytes = base64::decode(input.as_ref()).map_err(|e| Error::DecodingError { - message: e.to_string(), - })?; + let bytes = base64::decode(input.as_ref()).map_err(Error::invalid_value)?; let subtype = match subtype.into() { Some(s) => s, None => BinarySubtype::Generic, @@ -97,27 +100,3 @@ impl Binary { } } } - -/// Possible errors that can arise during [`Binary`] construction. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Error { - /// While trying to decode from base64, an error was returned. - DecodingError { message: String }, - - /// A [`Vector`]-related error occurred. - Vector { message: String }, -} - -impl error::Error for Error {} - -impl std::fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::DecodingError { message } => fmt.write_str(message), - Error::Vector { message } => fmt.write_str(message), - } - } -} - -pub type Result = std::result::Result; diff --git a/src/binary/vector.rs b/src/binary/vector.rs index 7226e867..b48500f9 100644 --- a/src/binary/vector.rs +++ b/src/binary/vector.rs @@ -93,17 +93,14 @@ impl PackedBitVector { pub fn new(vector: Vec, padding: impl Into>) -> Result { let padding = padding.into().unwrap_or(0); if !(0..8).contains(&padding) { - return Err(Error::Vector { - message: format!("padding must be within 0-7 inclusive, got {}", padding), - }); + return Err(Error::invalid_value(format!( + "padding must be within 0-7 inclusive, got {padding}" + ))); } if padding != 0 && vector.is_empty() { - return Err(Error::Vector { - message: format!( - "cannot specify non-zero padding if the provided vector is empty, got {}", - padding - ), - }); + return Err(Error::invalid_value(format!( + "cannot specify non-zero padding if the provided vector is empty, got {padding}", + ))); } Ok(Self { vector, padding }) } @@ -117,24 +114,19 @@ impl Vector { let bytes = bytes.as_ref(); if bytes.len() < 2 { - return Err(Error::Vector { - message: format!( - "the provided bytes must have a length of at least 2, got {}", - bytes.len() - ), - }); + return Err(Error::invalid_value(format!( + "the provided bytes must have a length of at least 2, got {}", + bytes.len() + ))); } let d_type = bytes[0]; let padding = bytes[1]; if d_type != PACKED_BIT && padding != 0 { - return Err(Error::Vector { - message: format!( - "padding can only be specified for a packed bit vector (data type {}), got \ - type {}", - PACKED_BIT, d_type - ), - }); + return Err(Error::invalid_value(format!( + "padding can only be specified for a packed bit vector (data type {}), got type {}", + PACKED_BIT, d_type + ))); } let number_bytes = &bytes[2..]; @@ -151,11 +143,11 @@ impl Vector { let mut vector = Vec::new(); for chunk in number_bytes.chunks(F32_BYTES) { - let bytes: [u8; F32_BYTES] = chunk.try_into().map_err(|_| Error::Vector { - message: format!( + let bytes: [u8; F32_BYTES] = chunk.try_into().map_err(|_| { + Error::invalid_value(format!( "f32 vector values must be {} bytes, got {:?}", F32_BYTES, chunk, - ), + )) })?; vector.push(f32::from_le_bytes(bytes)); } @@ -165,9 +157,9 @@ impl Vector { let packed_bit_vector = PackedBitVector::new(number_bytes.to_vec(), padding)?; Ok(Self::PackedBit(packed_bit_vector)) } - other => Err(Error::Vector { - message: format!("unsupported vector data type: {}", other), - }), + other => Err(Error::invalid_value(format!( + "unsupported vector data type: {other}" + ))), } } @@ -230,9 +222,10 @@ impl TryFrom<&Binary> for Vector { fn try_from(binary: &Binary) -> Result { if binary.subtype != BinarySubtype::Vector { - return Err(Error::Vector { - message: format!("expected vector binary subtype, got {:?}", binary.subtype), - }); + return Err(Error::invalid_value(format!( + "expected vector binary subtype, got {:?}", + binary.subtype + ))); } Self::from_bytes(&binary.bytes) } From 33a6a6fd60abe0f4eb921bad9aaf2aa680a45188 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Thu, 12 Jun 2025 15:29:49 -0600 Subject: [PATCH 03/12] maintain uuid structure --- src/error.rs | 11 ++++++ src/error/uuid.rs | 79 +++++++++++++++++++++++++++++++++++++ src/uuid.rs | 99 +++++++++-------------------------------------- 3 files changed, 109 insertions(+), 80 deletions(-) create mode 100644 src/error/uuid.rs diff --git a/src/error.rs b/src/error.rs index 32e2ef38..1677dff4 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,11 @@ +mod uuid; + use thiserror::Error; use crate::spec::ElementType; +pub use uuid::UuidErrorKind; + pub type Result = std::result::Result; /// An error that can occur in the `bson` crate. @@ -52,6 +56,13 @@ pub enum ErrorKind { #[error("Invalid UTF-8")] Utf8Encoding, + /// An error related to the [`Uuid`](crate::uuid::Uuid) type occurred. + #[error("A UUID-related error occurred: {kind}")] + Uuid { + /// The kind of error that occurred. + kind: UuidErrorKind, + }, + /// An error occurred when attempting to access a value in a document. #[error("An error occurred when attempting to access a document value: {kind}")] #[non_exhaustive] diff --git a/src/error/uuid.rs b/src/error/uuid.rs new file mode 100644 index 00000000..28c5fa78 --- /dev/null +++ b/src/error/uuid.rs @@ -0,0 +1,79 @@ +use thiserror::Error as ThisError; + +use crate::{ + error::{Error, ErrorKind}, + spec::BinarySubtype, + UuidRepresentation, +}; + +/// The kinds of errors that can occur when working with the [`Uuid`](crate::uuid::Uuid) type. +#[derive(Clone, Debug, ThisError)] +#[non_exhaustive] +pub enum UuidErrorKind { + /// An invalid string was used to construct a UUID. + #[error("Invalid UUID string: {message}")] + #[non_exhaustive] + InvalidString { + /// A message describing the error. + message: String, + }, + + /// The requested [`UuidRepresentation`] does not match the binary subtype of a [`Binary`] + /// value. + #[error( + "UUID representation mismatch: expected binary subtype {expected_binary_subtype:?} for \ + representation {requested_representation:?}, got {actual_binary_subtype:?}" + )] + #[non_exhaustive] + RepresentationMismatch { + /// The subtype that was expected given the requested representation. + expected_binary_subtype: BinarySubtype, + + /// The actual subtype of the binary value. + actual_binary_subtype: BinarySubtype, + + /// The requested representation. + requested_representation: UuidRepresentation, + }, + + /// An invalid length of bytes was used to construct a UUID value. + #[error("Invalid UUID length: expected 16 bytes, got {length}")] + #[non_exhaustive] + InvalidLength { + /// The actual length of the data. + length: usize, + }, +} + +impl Error { + pub(crate) fn invalid_uuid_string(message: impl ToString) -> Self { + ErrorKind::Uuid { + kind: UuidErrorKind::InvalidString { + message: message.to_string(), + }, + } + .into() + } + + pub(crate) fn uuid_representation_mismatch( + requested_representation: UuidRepresentation, + actual_binary_subtype: BinarySubtype, + expected_binary_subtype: BinarySubtype, + ) -> Self { + ErrorKind::Uuid { + kind: UuidErrorKind::RepresentationMismatch { + expected_binary_subtype, + actual_binary_subtype, + requested_representation, + }, + } + .into() + } + + pub(crate) fn invalid_uuid_length(length: usize) -> Self { + ErrorKind::Uuid { + kind: UuidErrorKind::InvalidLength { length }, + } + .into() + } +} diff --git a/src/uuid.rs b/src/uuid.rs index c6bf3aef..8d8c877f 100644 --- a/src/uuid.rs +++ b/src/uuid.rs @@ -142,7 +142,13 @@ use std::{ use serde::{Deserialize, Serialize}; -use crate::{de::BsonVisitor, spec::BinarySubtype, Binary, Bson}; +use crate::{ + de::BsonVisitor, + error::{Error, Result}, + spec::BinarySubtype, + Binary, + Bson, +}; /// Special type name used in the [`Uuid`] serialization implementation to indicate a BSON /// UUID is being serialized or deserialized. The BSON serializers/deserializers will handle this @@ -189,9 +195,7 @@ impl Uuid { /// Creates a [`Uuid`] from the provided hex string. pub fn parse_str(input: impl AsRef) -> Result { - let uuid = uuid::Uuid::parse_str(input.as_ref()).map_err(|e| Error::InvalidUuidString { - message: e.to_string(), - })?; + let uuid = uuid::Uuid::parse_str(input.as_ref()).map_err(Error::invalid_uuid_string)?; Ok(Self::from_external_uuid(uuid)) } @@ -394,25 +398,23 @@ impl Binary { pub fn to_uuid_with_representation(&self, rep: UuidRepresentation) -> Result { // If representation is non-standard, then its subtype must be UuidOld if rep != UuidRepresentation::Standard && self.subtype != BinarySubtype::UuidOld { - return Err(Error::RepresentationMismatch { - requested_representation: rep, - actual_binary_subtype: self.subtype, - expected_binary_subtype: BinarySubtype::UuidOld, - }); + return Err(Error::uuid_representation_mismatch( + rep, + self.subtype, + BinarySubtype::UuidOld, + )); } // If representation is standard, then its subtype must be Uuid if rep == UuidRepresentation::Standard && self.subtype != BinarySubtype::Uuid { - return Err(Error::RepresentationMismatch { - requested_representation: rep, - actual_binary_subtype: self.subtype, - expected_binary_subtype: BinarySubtype::Uuid, - }); + return Err(Error::uuid_representation_mismatch( + rep, + self.subtype, + BinarySubtype::UuidOld, + )); } // Must be 16 bytes long if self.bytes.len() != 16 { - return Err(Error::InvalidLength { - length: self.bytes.len(), - }); + return Err(Error::invalid_uuid_length(self.bytes.len())); } let mut buf = [0u8; 16]; buf.copy_from_slice(&self.bytes); @@ -479,66 +481,3 @@ macro_rules! trait_impls { }; } trait_impls!(feature = "uuid-1", uuid::Uuid); - -/// Errors that can occur during [`Uuid`] construction and generation. -#[derive(Clone, Debug)] -#[non_exhaustive] -pub enum Error { - /// Error returned when an invalid string is provided to [`Uuid::parse_str`]. - #[non_exhaustive] - InvalidUuidString { message: String }, - - /// Error returned when the representation specified does not match the underlying - /// [`crate::Binary`] value in [`crate::Binary::to_uuid_with_representation`]. - #[non_exhaustive] - RepresentationMismatch { - /// The subtype that was expected given the requested representation. - expected_binary_subtype: BinarySubtype, - - /// The actual subtype of the binary value. - actual_binary_subtype: BinarySubtype, - - /// The requested representation. - requested_representation: UuidRepresentation, - }, - - /// Error returned from [`crate::Binary::to_uuid`] if the underling data is not 16 bytes long. - #[non_exhaustive] - InvalidLength { - /// The actual length of the data. - length: usize, - }, -} - -/// Alias for `Result`. -pub type Result = std::result::Result; - -impl fmt::Display for Error { - fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - match self { - Error::InvalidUuidString { message } => { - write!(fmt, "{}", message) - } - Error::RepresentationMismatch { - expected_binary_subtype, - actual_binary_subtype, - requested_representation, - } => { - write!( - fmt, - "expected {:?} when converting to UUID with {:?}, isntead got {:?}", - expected_binary_subtype, requested_representation, actual_binary_subtype - ) - } - Error::InvalidLength { length } => { - write!( - fmt, - "expected UUID to contain 16 bytes, instead got {}", - length - ) - } - } - } -} - -impl std::error::Error for Error {} From 950965ccddcfd837842856899346b989924d0c2f Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Thu, 12 Jun 2025 15:46:31 -0600 Subject: [PATCH 04/12] maintain datetime structure --- src/datetime.rs | 6 +++--- src/datetime/builder.rs | 6 +++--- src/error.rs | 30 ++++++++++++++++------------- src/error/datetime.rs | 42 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 19 deletions(-) create mode 100644 src/error/datetime.rs diff --git a/src/datetime.rs b/src/datetime.rs index c37349dc..853782b8 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -389,14 +389,14 @@ impl crate::DateTime { pub fn try_to_rfc3339_string(self) -> Result { self.to_time_0_3() .format(&Rfc3339) - .map_err(Error::bad_conversion) + .map_err(Error::cannot_format_datetime) } /// Convert the given RFC 3339 formatted string to a [`DateTime`], truncating it to millisecond /// precision. pub fn parse_rfc3339_str(s: impl AsRef) -> Result { - let odt = - time::OffsetDateTime::parse(s.as_ref(), &Rfc3339).map_err(Error::invalid_value)?; + let odt = time::OffsetDateTime::parse(s.as_ref(), &Rfc3339) + .map_err(Error::invalid_datetime_value)?; Ok(Self::from_time_0_3(odt)) } diff --git a/src/datetime/builder.rs b/src/datetime/builder.rs index 8c38ddf8..bbf58e3c 100644 --- a/src/datetime/builder.rs +++ b/src/datetime/builder.rs @@ -174,16 +174,16 @@ impl DateTimeBuilder { /// /// Note: You cannot call `build()` before setting at least the year, month and day. pub fn build(self) -> Result { - let month = time::Month::try_from(self.month.0).map_err(Error::invalid_value)?; + let month = time::Month::try_from(self.month.0).map_err(Error::invalid_datetime_value)?; let dt = Date::from_calendar_date(self.year.0, month, self.day.0) - .map_err(Error::invalid_value)? + .map_err(Error::invalid_datetime_value)? .with_hms_milli( self.hour.unwrap_or(0), self.minute.unwrap_or(0), self.second.unwrap_or(0), self.millisecond.unwrap_or(0), ) - .map_err(Error::invalid_value)?; + .map_err(Error::invalid_datetime_value)?; Ok(DateTime::from_time_private(dt.assume_utc())) } } diff --git a/src/error.rs b/src/error.rs index 1677dff4..e110fc37 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,11 @@ +mod datetime; mod uuid; use thiserror::Error; use crate::spec::ElementType; +pub use datetime::DateTimeErrorKind; pub use uuid::UuidErrorKind; pub type Result = std::result::Result; @@ -38,19 +40,28 @@ impl std::fmt::Display for Error { #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum ErrorKind { - /// An error occurred when converting a BSON type to an external format. - #[error("Bad conversion: {message}")] - BadConversion { message: String }, + /// An error related to the [`DateTime`](crate::DateTime) type occurred. + #[error("A DateTime-related error occurred: {kind}")] + DateTime { + /// The kind of error that occurred. + kind: DateTimeErrorKind, + }, /// An error occurred when attempting to parse a value from an external format to BSON. #[error("Invalid value: {message}")] #[non_exhaustive] - InvalidValue { message: String }, + InvalidValue { + /// A message describing the error. + message: String, + }, /// Malformed BSON bytes were encountered. - #[error("Malformed BSON: {message}")] + #[error("Malformed BSON bytes: {message}")] #[non_exhaustive] - MalformedBytes { message: String }, + MalformedBytes { + /// A message describing the error. + message: String, + }, /// Invalid UTF-8 bytes were encountered. #[error("Invalid UTF-8")] @@ -168,13 +179,6 @@ impl Error { .into() } - pub(crate) fn bad_conversion(message: impl ToString) -> Self { - ErrorKind::BadConversion { - message: message.to_string(), - } - .into() - } - #[cfg(test)] pub(crate) fn is_value_access_not_present(&self) -> bool { matches!( diff --git a/src/error/datetime.rs b/src/error/datetime.rs new file mode 100644 index 00000000..b0099e3f --- /dev/null +++ b/src/error/datetime.rs @@ -0,0 +1,42 @@ +use thiserror::Error as ThisError; + +use crate::error::{Error, ErrorKind}; + +#[derive(Clone, Debug, ThisError)] +pub enum DateTimeErrorKind { + /// The `DateTime` could not be formatted. + #[error("{message}")] + #[non_exhaustive] + CannotFormat { + /// A message describing the error. + message: String, + }, + + /// An invalid value was provided. + #[error("{message}")] + #[non_exhaustive] + InvalidValue { + /// A message describing the error. + message: String, + }, +} + +impl Error { + pub(crate) fn invalid_datetime_value(message: impl ToString) -> Self { + ErrorKind::DateTime { + kind: DateTimeErrorKind::InvalidValue { + message: message.to_string(), + }, + } + .into() + } + + pub(crate) fn cannot_format_datetime(message: impl ToString) -> Self { + ErrorKind::DateTime { + kind: DateTimeErrorKind::CannotFormat { + message: message.to_string(), + }, + } + .into() + } +} From d7e05d4c1523e491b3190923893175072f1bde9a Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Fri, 13 Jun 2025 14:11:22 -0600 Subject: [PATCH 05/12] restructure, maintain oid structure --- src/error.rs | 81 ++++++--------------------------------- src/error/datetime.rs | 1 + src/error/oid.rs | 51 ++++++++++++++++++++++++ src/error/value_access.rs | 76 ++++++++++++++++++++++++++++++++++++ src/oid.rs | 19 ++------- 5 files changed, 142 insertions(+), 86 deletions(-) create mode 100644 src/error/oid.rs create mode 100644 src/error/value_access.rs diff --git a/src/error.rs b/src/error.rs index e110fc37..afc539ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,12 +1,14 @@ mod datetime; +mod oid; mod uuid; +mod value_access; use thiserror::Error; -use crate::spec::ElementType; - pub use datetime::DateTimeErrorKind; +pub use oid::ObjectIdErrorKind; pub use uuid::UuidErrorKind; +pub use value_access::ValueAccessErrorKind; pub type Result = std::result::Result; @@ -63,6 +65,13 @@ pub enum ErrorKind { message: String, }, + /// An error related to the [`ObjectId`](crate::oid::ObjectId) type occurred. + #[error("An ObjectId-related error occurred: {kind}")] + ObjectId { + /// The kind of error that occurred. + kind: ObjectIdErrorKind, + }, + /// Invalid UTF-8 bytes were encountered. #[error("Invalid UTF-8")] Utf8Encoding, @@ -108,31 +117,6 @@ impl From for Error { } } -/// The types of errors that can occur when attempting to access a value in a document. -#[derive(Clone, Debug, Error)] -#[non_exhaustive] -pub enum ValueAccessErrorKind { - /// No value for the specified key was present in the document. - #[error("The key was not present in the document")] - NotPresent, - - /// The type of the value in the document did not match the requested type. - #[error("Expected type {expected:?}, got type {actual:?}")] - #[non_exhaustive] - UnexpectedType { - /// The actual type of the value. - actual: ElementType, - - /// The expected type of the value. - expected: ElementType, - }, - - /// An error occurred when attempting to parse the document's BSON bytes. - #[error("{message}")] - #[non_exhaustive] - InvalidBson { message: String }, -} - impl Error { pub(crate) fn with_key(mut self, key: impl Into) -> Self { self.key = Some(key.into()); @@ -144,27 +128,6 @@ impl Error { self } - pub(crate) fn value_access_not_present() -> Self { - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::NotPresent, - } - .into() - } - - pub(crate) fn value_access_unexpected_type(actual: ElementType, expected: ElementType) -> Self { - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::UnexpectedType { actual, expected }, - } - .into() - } - - pub(crate) fn value_access_invalid_bson(message: String) -> Self { - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::InvalidBson { message }, - } - .into() - } - pub(crate) fn malformed_bytes(message: impl ToString) -> Self { ErrorKind::MalformedBytes { message: message.to_string(), @@ -178,26 +141,4 @@ impl Error { } .into() } - - #[cfg(test)] - pub(crate) fn is_value_access_not_present(&self) -> bool { - matches!( - self.kind, - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::NotPresent, - .. - } - ) - } - - #[cfg(test)] - pub(crate) fn is_value_access_unexpected_type(&self) -> bool { - matches!( - self.kind, - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::UnexpectedType { .. }, - .. - } - ) - } } diff --git a/src/error/datetime.rs b/src/error/datetime.rs index b0099e3f..82f7916a 100644 --- a/src/error/datetime.rs +++ b/src/error/datetime.rs @@ -2,6 +2,7 @@ use thiserror::Error as ThisError; use crate::error::{Error, ErrorKind}; +/// The kinds of errors that can occur when working with the [`DateTime`](crate::DateTime) type. #[derive(Clone, Debug, ThisError)] pub enum DateTimeErrorKind { /// The `DateTime` could not be formatted. diff --git a/src/error/oid.rs b/src/error/oid.rs new file mode 100644 index 00000000..1e80d4fa --- /dev/null +++ b/src/error/oid.rs @@ -0,0 +1,51 @@ +use hex::FromHexError; +use thiserror::Error as ThisError; + +use crate::error::{Error, ErrorKind}; + +/// The kinds of errors that can occur when working with the [`ObjectId`](crate::oid::ObjectId) +/// type. +#[derive(Clone, Debug, ThisError)] +pub enum ObjectIdErrorKind { + /// An invalid character was found in the provided hex string. Valid characters are: `0...9`, + /// `a...f`, or `A...F`. + #[error("invalid character '{c}' encountered at index {index}")] + #[non_exhaustive] + InvalidHexStringCharacter { + /// The invalid character. + c: char, + + /// The index at which the invalid character was encountered. + index: usize, + }, + + /// An `ObjectId` with an invalid length was encountered. + #[error("invalid hex string length {length}")] + #[non_exhaustive] + InvalidHexStringLength { + /// The length of the invalid hex string. + length: usize, + }, +} + +impl Error { + // This method is not a From implementation so that it is not part of the public API. + pub(crate) fn from_hex_error(error: FromHexError, length: usize) -> Self { + let kind = match error { + FromHexError::InvalidHexCharacter { c, index } => { + ObjectIdErrorKind::InvalidHexStringCharacter { c, index } + } + FromHexError::InvalidStringLength | FromHexError::OddLength => { + ObjectIdErrorKind::InvalidHexStringLength { length } + } + }; + ErrorKind::ObjectId { kind }.into() + } + + pub(crate) fn oid_invalid_length(length: usize) -> Self { + ErrorKind::ObjectId { + kind: ObjectIdErrorKind::InvalidHexStringLength { length }, + } + .into() + } +} diff --git a/src/error/value_access.rs b/src/error/value_access.rs new file mode 100644 index 00000000..0d810050 --- /dev/null +++ b/src/error/value_access.rs @@ -0,0 +1,76 @@ +use thiserror::Error as ThisError; + +use crate::{ + error::{Error, ErrorKind}, + spec::ElementType, +}; + +/// The types of errors that can occur when attempting to access a value in a document. +#[derive(Clone, Debug, ThisError)] +#[non_exhaustive] +pub enum ValueAccessErrorKind { + /// No value for the specified key was present in the document. + #[error("the key was not present in the document")] + NotPresent, + + /// The type of the value in the document did not match the requested type. + #[error("expected type {expected:?}, got type {actual:?}")] + #[non_exhaustive] + UnexpectedType { + /// The actual type of the value. + actual: ElementType, + + /// The expected type of the value. + expected: ElementType, + }, + + /// An error occurred when attempting to parse the document's BSON bytes. + #[error("{message}")] + #[non_exhaustive] + InvalidBson { message: String }, +} + +impl Error { + pub(crate) fn value_access_not_present() -> Self { + ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::NotPresent, + } + .into() + } + + pub(crate) fn value_access_unexpected_type(actual: ElementType, expected: ElementType) -> Self { + ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::UnexpectedType { actual, expected }, + } + .into() + } + + pub(crate) fn value_access_invalid_bson(message: String) -> Self { + ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::InvalidBson { message }, + } + .into() + } + + #[cfg(test)] + pub(crate) fn is_value_access_not_present(&self) -> bool { + matches!( + self.kind, + ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::NotPresent, + .. + } + ) + } + + #[cfg(test)] + pub(crate) fn is_value_access_unexpected_type(&self) -> bool { + matches!( + self.kind, + ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::UnexpectedType { .. }, + .. + } + ) + } +} diff --git a/src/oid.rs b/src/oid.rs index a15278d5..d771cd17 100644 --- a/src/oid.rs +++ b/src/oid.rs @@ -9,7 +9,6 @@ use std::{ sync::atomic::{AtomicUsize, Ordering}, }; -use hex::{self, FromHexError}; use once_cell::sync::Lazy; use rand::{random, rng, Rng}; @@ -154,22 +153,10 @@ impl ObjectId { pub fn parse_str(s: impl AsRef) -> Result { let s = s.as_ref(); - let bytes: Vec = hex::decode(s.as_bytes()).map_err(|e| { - let message = match e { - FromHexError::InvalidHexCharacter { c, index } => { - format!("invalid hex character {c} encountered at index {index}") - } - FromHexError::InvalidStringLength | FromHexError::OddLength => { - format!("invalid hex string length {}", s.len()) - } - }; - Error::invalid_value(message) - })?; + let bytes: Vec = + hex::decode(s.as_bytes()).map_err(|e| Error::from_hex_error(e, s.len()))?; if bytes.len() != 12 { - Err(Error::invalid_value(format!( - "invalid object ID byte vector length {}", - bytes.len() - ))) + Err(Error::oid_invalid_length(bytes.len())) } else { let mut byte_array: [u8; 12] = [0; 12]; byte_array[..].copy_from_slice(&bytes[..]); From d368d4c6f7556a4c1321dc482b54c95bc2049eb6 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Fri, 13 Jun 2025 14:20:23 -0600 Subject: [PATCH 06/12] maintain binary structure --- src/binary.rs | 2 +- src/binary/vector.rs | 18 +++++++++--------- src/error.rs | 15 +++++++++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/binary.rs b/src/binary.rs index 3b66ebc7..c2023bdf 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -56,7 +56,7 @@ impl Binary { input: impl AsRef, subtype: impl Into>, ) -> Result { - let bytes = base64::decode(input.as_ref()).map_err(Error::invalid_value)?; + let bytes = base64::decode(input.as_ref()).map_err(Error::binary)?; let subtype = match subtype.into() { Some(s) => s, None => BinarySubtype::Generic, diff --git a/src/binary/vector.rs b/src/binary/vector.rs index b48500f9..e6f367ed 100644 --- a/src/binary/vector.rs +++ b/src/binary/vector.rs @@ -93,12 +93,12 @@ impl PackedBitVector { pub fn new(vector: Vec, padding: impl Into>) -> Result { let padding = padding.into().unwrap_or(0); if !(0..8).contains(&padding) { - return Err(Error::invalid_value(format!( - "padding must be within 0-7 inclusive, got {padding}" + return Err(Error::binary(format!( + "vector padding must be within 0-7 inclusive, got {padding}" ))); } if padding != 0 && vector.is_empty() { - return Err(Error::invalid_value(format!( + return Err(Error::binary(format!( "cannot specify non-zero padding if the provided vector is empty, got {padding}", ))); } @@ -114,8 +114,8 @@ impl Vector { let bytes = bytes.as_ref(); if bytes.len() < 2 { - return Err(Error::invalid_value(format!( - "the provided bytes must have a length of at least 2, got {}", + return Err(Error::binary(format!( + "the provided vector bytes must have a length of at least 2, got {}", bytes.len() ))); } @@ -123,7 +123,7 @@ impl Vector { let d_type = bytes[0]; let padding = bytes[1]; if d_type != PACKED_BIT && padding != 0 { - return Err(Error::invalid_value(format!( + return Err(Error::binary(format!( "padding can only be specified for a packed bit vector (data type {}), got type {}", PACKED_BIT, d_type ))); @@ -144,7 +144,7 @@ impl Vector { let mut vector = Vec::new(); for chunk in number_bytes.chunks(F32_BYTES) { let bytes: [u8; F32_BYTES] = chunk.try_into().map_err(|_| { - Error::invalid_value(format!( + Error::binary(format!( "f32 vector values must be {} bytes, got {:?}", F32_BYTES, chunk, )) @@ -157,7 +157,7 @@ impl Vector { let packed_bit_vector = PackedBitVector::new(number_bytes.to_vec(), padding)?; Ok(Self::PackedBit(packed_bit_vector)) } - other => Err(Error::invalid_value(format!( + other => Err(Error::binary(format!( "unsupported vector data type: {other}" ))), } @@ -222,7 +222,7 @@ impl TryFrom<&Binary> for Vector { fn try_from(binary: &Binary) -> Result { if binary.subtype != BinarySubtype::Vector { - return Err(Error::invalid_value(format!( + return Err(Error::binary(format!( "expected vector binary subtype, got {:?}", binary.subtype ))); diff --git a/src/error.rs b/src/error.rs index afc539ca..3ebfbe5c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -42,6 +42,13 @@ impl std::fmt::Display for Error { #[derive(Clone, Debug, Error)] #[non_exhaustive] pub enum ErrorKind { + /// An error related to the [`Binary`](crate::Binary) type occurred. + #[error("A Binary-related error occurred: {message}")] + Binary { + /// A message describing the error. + message: String, + }, + /// An error related to the [`DateTime`](crate::DateTime) type occurred. #[error("A DateTime-related error occurred: {kind}")] DateTime { @@ -128,15 +135,15 @@ impl Error { self } - pub(crate) fn malformed_bytes(message: impl ToString) -> Self { - ErrorKind::MalformedBytes { + pub(crate) fn binary(message: impl ToString) -> Self { + ErrorKind::Binary { message: message.to_string(), } .into() } - pub(crate) fn invalid_value(message: impl ToString) -> Self { - ErrorKind::InvalidValue { + pub(crate) fn malformed_bytes(message: impl ToString) -> Self { + ErrorKind::MalformedBytes { message: message.to_string(), } .into() From ec549478c57a4c776a2d491c51ed8619290da9da Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Fri, 13 Jun 2025 14:24:17 -0600 Subject: [PATCH 07/12] api cleanup --- src/error/datetime.rs | 1 + src/error/oid.rs | 1 + src/error/uuid.rs | 8 ++++---- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/error/datetime.rs b/src/error/datetime.rs index 82f7916a..4174cab5 100644 --- a/src/error/datetime.rs +++ b/src/error/datetime.rs @@ -4,6 +4,7 @@ use crate::error::{Error, ErrorKind}; /// The kinds of errors that can occur when working with the [`DateTime`](crate::DateTime) type. #[derive(Clone, Debug, ThisError)] +#[non_exhaustive] pub enum DateTimeErrorKind { /// The `DateTime` could not be formatted. #[error("{message}")] diff --git a/src/error/oid.rs b/src/error/oid.rs index 1e80d4fa..6c770d88 100644 --- a/src/error/oid.rs +++ b/src/error/oid.rs @@ -6,6 +6,7 @@ use crate::error::{Error, ErrorKind}; /// The kinds of errors that can occur when working with the [`ObjectId`](crate::oid::ObjectId) /// type. #[derive(Clone, Debug, ThisError)] +#[non_exhaustive] pub enum ObjectIdErrorKind { /// An invalid character was found in the provided hex string. Valid characters are: `0...9`, /// `a...f`, or `A...F`. diff --git a/src/error/uuid.rs b/src/error/uuid.rs index 28c5fa78..cd018c3e 100644 --- a/src/error/uuid.rs +++ b/src/error/uuid.rs @@ -11,7 +11,7 @@ use crate::{ #[non_exhaustive] pub enum UuidErrorKind { /// An invalid string was used to construct a UUID. - #[error("Invalid UUID string: {message}")] + #[error("{message}")] #[non_exhaustive] InvalidString { /// A message describing the error. @@ -21,8 +21,8 @@ pub enum UuidErrorKind { /// The requested [`UuidRepresentation`] does not match the binary subtype of a [`Binary`] /// value. #[error( - "UUID representation mismatch: expected binary subtype {expected_binary_subtype:?} for \ - representation {requested_representation:?}, got {actual_binary_subtype:?}" + "expected binary subtype {expected_binary_subtype:?} for representation \ + {requested_representation:?}, got {actual_binary_subtype:?}" )] #[non_exhaustive] RepresentationMismatch { @@ -37,7 +37,7 @@ pub enum UuidErrorKind { }, /// An invalid length of bytes was used to construct a UUID value. - #[error("Invalid UUID length: expected 16 bytes, got {length}")] + #[error("expected length of 16 bytes, got {length}")] #[non_exhaustive] InvalidLength { /// The actual length of the data. From a74e72ea3d4ed6824863fae4d85a1d55e6b221f8 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Fri, 13 Jun 2025 14:30:00 -0600 Subject: [PATCH 08/12] remove invalid value --- src/error.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/error.rs b/src/error.rs index 3ebfbe5c..1ab381e0 100644 --- a/src/error.rs +++ b/src/error.rs @@ -56,14 +56,6 @@ pub enum ErrorKind { kind: DateTimeErrorKind, }, - /// An error occurred when attempting to parse a value from an external format to BSON. - #[error("Invalid value: {message}")] - #[non_exhaustive] - InvalidValue { - /// A message describing the error. - message: String, - }, - /// Malformed BSON bytes were encountered. #[error("Malformed BSON bytes: {message}")] #[non_exhaustive] From 3d8e8871bc6096e2a96d7d0a4fb8742926f725e5 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Fri, 13 Jun 2025 14:37:14 -0600 Subject: [PATCH 09/12] docs --- src/binary.rs | 2 +- src/binary/vector.rs | 4 ++-- src/error/uuid.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/binary.rs b/src/binary.rs index c2023bdf..a869ecee 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -43,7 +43,7 @@ impl Binary { /// [`BinarySubtype::Generic`]. /// /// ```rust - /// # use bson::{Binary, binary::Result}; + /// # use bson::{Binary, error::Result}; /// # fn example() -> Result<()> { /// let input = base64::encode("hello"); /// let binary = Binary::from_base64(input, None)?; diff --git a/src/binary/vector.rs b/src/binary/vector.rs index e6f367ed..af09a774 100644 --- a/src/binary/vector.rs +++ b/src/binary/vector.rs @@ -25,7 +25,7 @@ const PACKED_BIT: u8 = 0x10; /// /// ```rust /// # use serde::{Serialize, Deserialize}; -/// # use bson::{binary::{Result, Vector}, spec::ElementType}; +/// # use bson::{binary::Vector, error::Result, spec::ElementType}; /// #[derive(Serialize, Deserialize)] /// struct Data { /// vector: Vector, @@ -66,7 +66,7 @@ impl PackedBitVector { /// single-bit elements in little-endian format. For example, the following vector: /// /// ```rust - /// # use bson::binary::{Result, PackedBitVector}; + /// # use bson::{binary::PackedBitVector, error::Result}; /// # fn main() -> Result<()> { /// let packed_bits = vec![238, 224]; /// let vector = PackedBitVector::new(packed_bits, 0)?; diff --git a/src/error/uuid.rs b/src/error/uuid.rs index cd018c3e..003c33b5 100644 --- a/src/error/uuid.rs +++ b/src/error/uuid.rs @@ -18,7 +18,7 @@ pub enum UuidErrorKind { message: String, }, - /// The requested [`UuidRepresentation`] does not match the binary subtype of a [`Binary`] + /// The requested `UuidRepresentation` does not match the binary subtype of a `Binary` /// value. #[error( "expected binary subtype {expected_binary_subtype:?} for representation \ From 54c7dbb6f110b7cac8ad9ca10bcd3b8aa94cfe82 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Mon, 16 Jun 2025 10:24:53 -0600 Subject: [PATCH 10/12] collapse datetime --- src/datetime.rs | 7 ++----- src/datetime/builder.rs | 6 +++--- src/error.rs | 15 +++++++++----- src/error/datetime.rs | 44 ----------------------------------------- 4 files changed, 15 insertions(+), 57 deletions(-) delete mode 100644 src/error/datetime.rs diff --git a/src/datetime.rs b/src/datetime.rs index 853782b8..553c9175 100644 --- a/src/datetime.rs +++ b/src/datetime.rs @@ -387,16 +387,13 @@ impl crate::DateTime { /// Convert this [`DateTime`] to an RFC 3339 formatted string. pub fn try_to_rfc3339_string(self) -> Result { - self.to_time_0_3() - .format(&Rfc3339) - .map_err(Error::cannot_format_datetime) + self.to_time_0_3().format(&Rfc3339).map_err(Error::datetime) } /// Convert the given RFC 3339 formatted string to a [`DateTime`], truncating it to millisecond /// precision. pub fn parse_rfc3339_str(s: impl AsRef) -> Result { - let odt = time::OffsetDateTime::parse(s.as_ref(), &Rfc3339) - .map_err(Error::invalid_datetime_value)?; + let odt = time::OffsetDateTime::parse(s.as_ref(), &Rfc3339).map_err(Error::datetime)?; Ok(Self::from_time_0_3(odt)) } diff --git a/src/datetime/builder.rs b/src/datetime/builder.rs index bbf58e3c..d8cd8ff8 100644 --- a/src/datetime/builder.rs +++ b/src/datetime/builder.rs @@ -174,16 +174,16 @@ impl DateTimeBuilder { /// /// Note: You cannot call `build()` before setting at least the year, month and day. pub fn build(self) -> Result { - let month = time::Month::try_from(self.month.0).map_err(Error::invalid_datetime_value)?; + let month = time::Month::try_from(self.month.0).map_err(Error::datetime)?; let dt = Date::from_calendar_date(self.year.0, month, self.day.0) - .map_err(Error::invalid_datetime_value)? + .map_err(Error::datetime)? .with_hms_milli( self.hour.unwrap_or(0), self.minute.unwrap_or(0), self.second.unwrap_or(0), self.millisecond.unwrap_or(0), ) - .map_err(Error::invalid_datetime_value)?; + .map_err(Error::datetime)?; Ok(DateTime::from_time_private(dt.assume_utc())) } } diff --git a/src/error.rs b/src/error.rs index 1ab381e0..8fda0b72 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,11 +1,9 @@ -mod datetime; mod oid; mod uuid; mod value_access; use thiserror::Error; -pub use datetime::DateTimeErrorKind; pub use oid::ObjectIdErrorKind; pub use uuid::UuidErrorKind; pub use value_access::ValueAccessErrorKind; @@ -50,10 +48,10 @@ pub enum ErrorKind { }, /// An error related to the [`DateTime`](crate::DateTime) type occurred. - #[error("A DateTime-related error occurred: {kind}")] + #[error("A DateTime-related error occurred: {message}")] DateTime { - /// The kind of error that occurred. - kind: DateTimeErrorKind, + /// A message describing the error. + message: String, }, /// Malformed BSON bytes were encountered. @@ -134,6 +132,13 @@ impl Error { .into() } + pub(crate) fn datetime(message: impl ToString) -> Self { + ErrorKind::DateTime { + message: message.to_string(), + } + .into() + } + pub(crate) fn malformed_bytes(message: impl ToString) -> Self { ErrorKind::MalformedBytes { message: message.to_string(), diff --git a/src/error/datetime.rs b/src/error/datetime.rs deleted file mode 100644 index 4174cab5..00000000 --- a/src/error/datetime.rs +++ /dev/null @@ -1,44 +0,0 @@ -use thiserror::Error as ThisError; - -use crate::error::{Error, ErrorKind}; - -/// The kinds of errors that can occur when working with the [`DateTime`](crate::DateTime) type. -#[derive(Clone, Debug, ThisError)] -#[non_exhaustive] -pub enum DateTimeErrorKind { - /// The `DateTime` could not be formatted. - #[error("{message}")] - #[non_exhaustive] - CannotFormat { - /// A message describing the error. - message: String, - }, - - /// An invalid value was provided. - #[error("{message}")] - #[non_exhaustive] - InvalidValue { - /// A message describing the error. - message: String, - }, -} - -impl Error { - pub(crate) fn invalid_datetime_value(message: impl ToString) -> Self { - ErrorKind::DateTime { - kind: DateTimeErrorKind::InvalidValue { - message: message.to_string(), - }, - } - .into() - } - - pub(crate) fn cannot_format_datetime(message: impl ToString) -> Self { - ErrorKind::DateTime { - kind: DateTimeErrorKind::CannotFormat { - message: message.to_string(), - }, - } - .into() - } -} From 2176ad697d90fde39ac2e2f35a98a810714d732e Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Mon, 16 Jun 2025 10:46:55 -0600 Subject: [PATCH 11/12] decimal128 --- src/decimal128.rs | 114 +++++++++++++++------------------------- src/error.rs | 9 ++++ src/error/decimal128.rs | 60 +++++++++++++++++++++ src/raw/test.rs | 8 ++- 4 files changed, 115 insertions(+), 76 deletions(-) create mode 100644 src/error/decimal128.rs diff --git a/src/decimal128.rs b/src/decimal128.rs index 546b9619..017109b8 100644 --- a/src/decimal128.rs +++ b/src/decimal128.rs @@ -1,9 +1,11 @@ //! [BSON Decimal128](https://github.com/mongodb/specifications/blob/master/source/bson-decimal128/decimal128.rst) data type representation -use std::{convert::TryInto, fmt}; +use std::{convert::TryInto, fmt, num::ParseIntError}; use bitvec::prelude::*; +use crate::error::{Decimal128ErrorKind, Error, Result}; + /// Struct representing a BSON Decimal128 type. /// /// This type supports conversion to and from human-readable strings via the [std::fmt::Display] and @@ -59,9 +61,9 @@ impl fmt::Display for Decimal128 { } impl std::str::FromStr for Decimal128 { - type Err = ParseError; + type Err = Error; - fn from_str(s: &str) -> Result { + fn from_str(s: &str) -> Result { Ok(s.parse::()?.pack()) } } @@ -137,10 +139,7 @@ impl Coefficient { /// The maximum allowable value of a coefficient. const MAX_VALUE: u128 = 9_999_999_999_999_999_999_999_999_999_999_999; - fn from_bits( - src_prefix: &BitSlice, - src_suffix: &BitSlice, - ) -> Result { + fn from_bits(src_prefix: &BitSlice, src_suffix: &BitSlice) -> Result { let mut bytes = [0u8; 16]; let bits = &mut bytes.view_bits_mut::()[Self::UNUSED_BITS..]; let prefix_len = src_prefix.len(); @@ -148,7 +147,7 @@ impl Coefficient { bits[prefix_len..].copy_from_bitslice(src_suffix); let out = Self(bytes); if out.value() > Self::MAX_VALUE { - Err(ParseError::Overflow) + Err(Error::decimal128(Decimal128ErrorKind::Overflow)) } else { Ok(out) } @@ -324,46 +323,10 @@ impl fmt::Display for ParsedDecimal128 { } } -#[derive(Debug)] -#[non_exhaustive] -pub enum ParseError { - EmptyExponent, - InvalidExponent(std::num::ParseIntError), - InvalidCoefficient(std::num::ParseIntError), - Overflow, - Underflow, - InexactRounding, - Unparseable, -} - -impl fmt::Display for ParseError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - ParseError::EmptyExponent => write!(f, "empty exponent"), - ParseError::InvalidExponent(e) => write!(f, "invalid exponent: {}", e), - ParseError::InvalidCoefficient(e) => write!(f, "invalid coefficient: {}", e), - ParseError::Overflow => write!(f, "overflow"), - ParseError::Underflow => write!(f, "underflow"), - ParseError::InexactRounding => write!(f, "inexact rounding"), - ParseError::Unparseable => write!(f, "unparseable"), - } - } -} - -impl std::error::Error for ParseError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - ParseError::InvalidExponent(e) => Some(e), - ParseError::InvalidCoefficient(e) => Some(e), - _ => None, - } - } -} - impl std::str::FromStr for ParsedDecimal128 { - type Err = ParseError; + type Err = Error; - fn from_str(mut s: &str) -> Result { + fn from_str(mut s: &str) -> Result { let sign; if let Some(rest) = s.strip_prefix(&['-', '+'][..]) { sign = s.starts_with('-'); @@ -384,21 +347,30 @@ impl std::str::FromStr for ParsedDecimal128 { decimal_str = finite_str; exp_str = "0"; } - Some((_, "")) => return Err(ParseError::EmptyExponent), + Some((_, "")) => { + return Err(Error::decimal128(Decimal128ErrorKind::EmptyExponent)) + } Some((pre, post)) => { decimal_str = pre; exp_str = post; } } - let mut exp = exp_str - .parse::() - .map_err(ParseError::InvalidExponent)?; + let mut exp = exp_str.parse::().map_err(|e| { + Error::decimal128(Decimal128ErrorKind::InvalidExponent { + message: e.to_string(), + }) + })?; // Remove decimal point and adjust exponent let joined_str; if let Some((pre, post)) = decimal_str.split_once('.') { - let exp_adj = post.len().try_into().map_err(|_| ParseError::Underflow)?; - exp = exp.checked_sub(exp_adj).ok_or(ParseError::Underflow)?; + let exp_adj = post + .len() + .try_into() + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Underflow))?; + exp = exp + .checked_sub(exp_adj) + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Underflow))?; joined_str = format!("{}{}", pre, post); decimal_str = &joined_str; } @@ -414,8 +386,10 @@ impl std::str::FromStr for ParsedDecimal128 { decimal_str = round_decimal_str(decimal_str, Coefficient::MAX_DIGITS)?; let exp_adj = (len - decimal_str.len()) .try_into() - .map_err(|_| ParseError::Overflow)?; - exp = exp.checked_add(exp_adj).ok_or(ParseError::Overflow)?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; + exp = exp + .checked_add(exp_adj) + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))?; } } @@ -424,11 +398,11 @@ impl std::str::FromStr for ParsedDecimal128 { if decimal_str != "0" { let delta = (Exponent::TINY - exp) .try_into() - .map_err(|_| ParseError::Overflow)?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; let new_precision = decimal_str .len() .checked_sub(delta) - .ok_or(ParseError::Underflow)?; + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))?; decimal_str = round_decimal_str(decimal_str, new_precision)?; } exp = Exponent::TINY; @@ -438,14 +412,14 @@ impl std::str::FromStr for ParsedDecimal128 { if decimal_str != "0" { let delta = (exp - Exponent::MAX) .try_into() - .map_err(|_| ParseError::Overflow)?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; if decimal_str .len() .checked_add(delta) - .ok_or(ParseError::Overflow)? + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))? > Coefficient::MAX_DIGITS { - return Err(ParseError::Overflow); + return Err(Error::decimal128(Decimal128ErrorKind::Overflow)); } padded_str = format!("{}{}", decimal_str, "0".repeat(delta)); decimal_str = &padded_str; @@ -455,9 +429,11 @@ impl std::str::FromStr for ParsedDecimal128 { // Assemble the final value let exponent = Exponent::from_native(exp); - let coeff: u128 = decimal_str - .parse() - .map_err(ParseError::InvalidCoefficient)?; + let coeff: u128 = decimal_str.parse().map_err(|e: ParseIntError| { + Error::decimal128(Decimal128ErrorKind::InvalidCoefficient { + message: e.to_string(), + }) + })?; let coefficient = Coefficient::from_native(coeff); Decimal128Kind::Finite { exponent, @@ -470,17 +446,13 @@ impl std::str::FromStr for ParsedDecimal128 { } } -fn round_decimal_str(s: &str, precision: usize) -> Result<&str, ParseError> { - // TODO: In 1.80+ there's split_at_checked to make sure the split doesn't - // panic if the index doesn't falls at a codepoint boundary, until then - // we can check it with s.is_char_boundary(precision) - if !s.is_char_boundary(precision) { - return Err(ParseError::Unparseable); - } - let (pre, post) = s.split_at(precision); +fn round_decimal_str(s: &str, precision: usize) -> Result<&str> { + let (pre, post) = s + .split_at_checked(precision) + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Unparseable))?; // Any nonzero trimmed digits mean it would be an imprecise round. if post.chars().any(|c| c != '0') { - return Err(ParseError::InexactRounding); + return Err(Error::decimal128(Decimal128ErrorKind::InexactRounding)); } Ok(pre) } diff --git a/src/error.rs b/src/error.rs index 8fda0b72..a0bb7bbe 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,9 +1,11 @@ +mod decimal128; mod oid; mod uuid; mod value_access; use thiserror::Error; +pub use decimal128::Decimal128ErrorKind; pub use oid::ObjectIdErrorKind; pub use uuid::UuidErrorKind; pub use value_access::ValueAccessErrorKind; @@ -54,6 +56,13 @@ pub enum ErrorKind { message: String, }, + /// An error related to the [`Decimal128`](crate::Decimal128) type occurred. + #[error("A Decimal128-related error occurred: {kind}")] + Decimal128 { + /// The kind of error that occurred. + kind: Decimal128ErrorKind, + }, + /// Malformed BSON bytes were encountered. #[error("Malformed BSON bytes: {message}")] #[non_exhaustive] diff --git a/src/error/decimal128.rs b/src/error/decimal128.rs new file mode 100644 index 00000000..473b8be0 --- /dev/null +++ b/src/error/decimal128.rs @@ -0,0 +1,60 @@ +use thiserror::Error as ThisError; + +use crate::error::{Error, ErrorKind}; + +/// The kinds of errors that can occur when working with the [`Decimal128`](crate::Decimal128) type. +#[derive(Clone, Debug, ThisError)] +#[non_exhaustive] +pub enum Decimal128ErrorKind { + /// Empty exponent. + #[error("empty exponent")] + EmptyExponent, + + /// Invalid exponent. + #[error("invalid exponent: {message}")] + #[non_exhaustive] + InvalidExponent { + /// A message describing the error. + message: String, + }, + + /// Invalid coefficient. + #[error("invalid coefficient: {message}")] + #[non_exhaustive] + InvalidCoefficient { + /// A message describing the error. + message: String, + }, + + /// Overflow. + #[error("overflow")] + Overflow, + + /// Underflow. + #[error("underflow")] + Underflow, + + /// Inexact rounding. + #[error("inexact rounding")] + InexactRounding, + + /// Unparseable. + #[error("unparseable")] + Unparseable, +} + +impl Error { + pub(crate) fn decimal128(kind: Decimal128ErrorKind) -> Self { + ErrorKind::Decimal128 { kind }.into() + } + + #[cfg(test)] + pub(crate) fn is_decimal128_unparseable(&self) -> bool { + matches!( + self.kind, + ErrorKind::Decimal128 { + kind: Decimal128ErrorKind::Unparseable, + } + ) + } +} diff --git a/src/raw/test.rs b/src/raw/test.rs index caf1fc99..dfa2e66f 100644 --- a/src/raw/test.rs +++ b/src/raw/test.rs @@ -16,13 +16,11 @@ use crate::{ #[test] fn test_decimal128_doesnt_panic_on_bad_codepoint_boundary() { - use crate::decimal128::ParseError; use std::str::FromStr; // idx 34 (Coefficient::MAX_DIGITS) on this string isn't a valid codepoint boundary - assert!(matches!( - Decimal128::from_str("111111111111111111111111111111111❤"), - Err(ParseError::Unparseable) - )) + assert!(Decimal128::from_str("111111111111111111111111111111111❤") + .unwrap_err() + .is_decimal128_unparseable()); } #[test] From 5b0057c1570714962bb0c1e9eca8a797f4d768d2 Mon Sep 17 00:00:00 2001 From: Isabel Atkinson Date: Wed, 18 Jun 2025 11:34:51 -0600 Subject: [PATCH 12/12] refactor message --- src/decimal128.rs | 34 +++++++++----------- src/error.rs | 66 +++++++++++++++++++++------------------ src/error/decimal128.rs | 31 +++++++++--------- src/error/uuid.rs | 17 ++++------ src/error/value_access.rs | 19 +++++------ src/raw.rs | 2 +- 6 files changed, 82 insertions(+), 87 deletions(-) diff --git a/src/decimal128.rs b/src/decimal128.rs index 017109b8..5851531c 100644 --- a/src/decimal128.rs +++ b/src/decimal128.rs @@ -147,7 +147,7 @@ impl Coefficient { bits[prefix_len..].copy_from_bitslice(src_suffix); let out = Self(bytes); if out.value() > Self::MAX_VALUE { - Err(Error::decimal128(Decimal128ErrorKind::Overflow)) + Err(Error::decimal128(Decimal128ErrorKind::Overflow {})) } else { Ok(out) } @@ -348,7 +348,7 @@ impl std::str::FromStr for ParsedDecimal128 { exp_str = "0"; } Some((_, "")) => { - return Err(Error::decimal128(Decimal128ErrorKind::EmptyExponent)) + return Err(Error::decimal128(Decimal128ErrorKind::EmptyExponent {})) } Some((pre, post)) => { decimal_str = pre; @@ -356,9 +356,7 @@ impl std::str::FromStr for ParsedDecimal128 { } } let mut exp = exp_str.parse::().map_err(|e| { - Error::decimal128(Decimal128ErrorKind::InvalidExponent { - message: e.to_string(), - }) + Error::decimal128(Decimal128ErrorKind::InvalidExponent {}).with_message(e) })?; // Remove decimal point and adjust exponent @@ -367,10 +365,10 @@ impl std::str::FromStr for ParsedDecimal128 { let exp_adj = post .len() .try_into() - .map_err(|_| Error::decimal128(Decimal128ErrorKind::Underflow))?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Underflow {}))?; exp = exp .checked_sub(exp_adj) - .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Underflow))?; + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Underflow {}))?; joined_str = format!("{}{}", pre, post); decimal_str = &joined_str; } @@ -386,10 +384,10 @@ impl std::str::FromStr for ParsedDecimal128 { decimal_str = round_decimal_str(decimal_str, Coefficient::MAX_DIGITS)?; let exp_adj = (len - decimal_str.len()) .try_into() - .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?; exp = exp .checked_add(exp_adj) - .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))?; + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow {}))?; } } @@ -398,11 +396,11 @@ impl std::str::FromStr for ParsedDecimal128 { if decimal_str != "0" { let delta = (Exponent::TINY - exp) .try_into() - .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?; let new_precision = decimal_str .len() .checked_sub(delta) - .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))?; + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow {}))?; decimal_str = round_decimal_str(decimal_str, new_precision)?; } exp = Exponent::TINY; @@ -412,14 +410,14 @@ impl std::str::FromStr for ParsedDecimal128 { if decimal_str != "0" { let delta = (exp - Exponent::MAX) .try_into() - .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow))?; + .map_err(|_| Error::decimal128(Decimal128ErrorKind::Overflow {}))?; if decimal_str .len() .checked_add(delta) - .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow))? + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Overflow {}))? > Coefficient::MAX_DIGITS { - return Err(Error::decimal128(Decimal128ErrorKind::Overflow)); + return Err(Error::decimal128(Decimal128ErrorKind::Overflow {})); } padded_str = format!("{}{}", decimal_str, "0".repeat(delta)); decimal_str = &padded_str; @@ -430,9 +428,7 @@ impl std::str::FromStr for ParsedDecimal128 { // Assemble the final value let exponent = Exponent::from_native(exp); let coeff: u128 = decimal_str.parse().map_err(|e: ParseIntError| { - Error::decimal128(Decimal128ErrorKind::InvalidCoefficient { - message: e.to_string(), - }) + Error::decimal128(Decimal128ErrorKind::InvalidCoefficient {}).with_message(e) })?; let coefficient = Coefficient::from_native(coeff); Decimal128Kind::Finite { @@ -449,10 +445,10 @@ impl std::str::FromStr for ParsedDecimal128 { fn round_decimal_str(s: &str, precision: usize) -> Result<&str> { let (pre, post) = s .split_at_checked(precision) - .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Unparseable))?; + .ok_or_else(|| Error::decimal128(Decimal128ErrorKind::Unparseable {}))?; // Any nonzero trimmed digits mean it would be an imprecise round. if post.chars().any(|c| c != '0') { - return Err(Error::decimal128(Decimal128ErrorKind::InexactRounding)); + return Err(Error::decimal128(Decimal128ErrorKind::InexactRounding {})); } Ok(pre) } diff --git a/src/error.rs b/src/error.rs index a0bb7bbe..c7ef23fd 100644 --- a/src/error.rs +++ b/src/error.rs @@ -19,6 +19,9 @@ pub struct Error { /// The kind of error that occurred. pub kind: ErrorKind, + /// An optional message describing the error. + pub message: Option, + /// The document key associated with the error, if any. pub key: Option, @@ -28,13 +31,19 @@ pub struct Error { impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "BSON error")?; + if let Some(key) = self.key.as_deref() { - write!(f, "Error at key \"{key}\": ")?; + write!(f, " at key \"{key}\"")?; } else if let Some(index) = self.index { - write!(f, "Error at array index {index}: ")?; + write!(f, " at array index {index}")?; } - write!(f, "{}", self.kind) + write!(f, ". Kind: {}", self.kind)?; + if let Some(ref message) = self.message { + write!(f, ". Message: {}", message)?; + } + write!(f, ".") } } @@ -43,36 +52,31 @@ impl std::fmt::Display for Error { #[non_exhaustive] pub enum ErrorKind { /// An error related to the [`Binary`](crate::Binary) type occurred. - #[error("A Binary-related error occurred: {message}")] - Binary { - /// A message describing the error. - message: String, - }, + #[error("A Binary-related error occurred")] + #[non_exhaustive] + Binary {}, /// An error related to the [`DateTime`](crate::DateTime) type occurred. - #[error("A DateTime-related error occurred: {message}")] - DateTime { - /// A message describing the error. - message: String, - }, + #[error("A DateTime-related error occurred")] + #[non_exhaustive] + DateTime {}, /// An error related to the [`Decimal128`](crate::Decimal128) type occurred. #[error("A Decimal128-related error occurred: {kind}")] + #[non_exhaustive] Decimal128 { /// The kind of error that occurred. kind: Decimal128ErrorKind, }, /// Malformed BSON bytes were encountered. - #[error("Malformed BSON bytes: {message}")] + #[error("Malformed BSON bytes")] #[non_exhaustive] - MalformedBytes { - /// A message describing the error. - message: String, - }, + MalformedBytes {}, /// An error related to the [`ObjectId`](crate::oid::ObjectId) type occurred. #[error("An ObjectId-related error occurred: {kind}")] + #[non_exhaustive] ObjectId { /// The kind of error that occurred. kind: ObjectIdErrorKind, @@ -80,10 +84,12 @@ pub enum ErrorKind { /// Invalid UTF-8 bytes were encountered. #[error("Invalid UTF-8")] - Utf8Encoding, + #[non_exhaustive] + Utf8Encoding {}, /// An error related to the [`Uuid`](crate::uuid::Uuid) type occurred. #[error("A UUID-related error occurred: {kind}")] + #[non_exhaustive] Uuid { /// The kind of error that occurred. kind: UuidErrorKind, @@ -109,6 +115,7 @@ impl From for Error { kind, key: None, index: None, + message: None, } } } @@ -119,6 +126,7 @@ impl From for Error { kind: ErrorKind::DeError(value), key: None, index: None, + message: None, } } } @@ -134,24 +142,20 @@ impl Error { self } + pub(crate) fn with_message(mut self, message: impl ToString) -> Self { + self.message = Some(message.to_string()); + self + } + pub(crate) fn binary(message: impl ToString) -> Self { - ErrorKind::Binary { - message: message.to_string(), - } - .into() + Self::from(ErrorKind::Binary {}).with_message(message) } pub(crate) fn datetime(message: impl ToString) -> Self { - ErrorKind::DateTime { - message: message.to_string(), - } - .into() + Self::from(ErrorKind::DateTime {}).with_message(message) } pub(crate) fn malformed_bytes(message: impl ToString) -> Self { - ErrorKind::MalformedBytes { - message: message.to_string(), - } - .into() + Self::from(ErrorKind::MalformedBytes {}).with_message(message) } } diff --git a/src/error/decimal128.rs b/src/error/decimal128.rs index 473b8be0..ab40eed1 100644 --- a/src/error/decimal128.rs +++ b/src/error/decimal128.rs @@ -8,39 +8,38 @@ use crate::error::{Error, ErrorKind}; pub enum Decimal128ErrorKind { /// Empty exponent. #[error("empty exponent")] - EmptyExponent, + #[non_exhaustive] + EmptyExponent {}, /// Invalid exponent. - #[error("invalid exponent: {message}")] + #[error("invalid exponent")] #[non_exhaustive] - InvalidExponent { - /// A message describing the error. - message: String, - }, + InvalidExponent {}, /// Invalid coefficient. - #[error("invalid coefficient: {message}")] + #[error("invalid coefficient")] #[non_exhaustive] - InvalidCoefficient { - /// A message describing the error. - message: String, - }, + InvalidCoefficient {}, /// Overflow. #[error("overflow")] - Overflow, + #[non_exhaustive] + Overflow {}, /// Underflow. #[error("underflow")] - Underflow, + #[non_exhaustive] + Underflow {}, /// Inexact rounding. #[error("inexact rounding")] - InexactRounding, + #[non_exhaustive] + InexactRounding {}, /// Unparseable. #[error("unparseable")] - Unparseable, + #[non_exhaustive] + Unparseable {}, } impl Error { @@ -53,7 +52,7 @@ impl Error { matches!( self.kind, ErrorKind::Decimal128 { - kind: Decimal128ErrorKind::Unparseable, + kind: Decimal128ErrorKind::Unparseable {}, } ) } diff --git a/src/error/uuid.rs b/src/error/uuid.rs index 003c33b5..323dbd18 100644 --- a/src/error/uuid.rs +++ b/src/error/uuid.rs @@ -11,12 +11,9 @@ use crate::{ #[non_exhaustive] pub enum UuidErrorKind { /// An invalid string was used to construct a UUID. - #[error("{message}")] + #[error("invalid UUID string")] #[non_exhaustive] - InvalidString { - /// A message describing the error. - message: String, - }, + InvalidString {}, /// The requested `UuidRepresentation` does not match the binary subtype of a `Binary` /// value. @@ -47,12 +44,10 @@ pub enum UuidErrorKind { impl Error { pub(crate) fn invalid_uuid_string(message: impl ToString) -> Self { - ErrorKind::Uuid { - kind: UuidErrorKind::InvalidString { - message: message.to_string(), - }, - } - .into() + Self::from(ErrorKind::Uuid { + kind: UuidErrorKind::InvalidString {}, + }) + .with_message(message) } pub(crate) fn uuid_representation_mismatch( diff --git a/src/error/value_access.rs b/src/error/value_access.rs index 0d810050..ffc82f4d 100644 --- a/src/error/value_access.rs +++ b/src/error/value_access.rs @@ -11,7 +11,8 @@ use crate::{ pub enum ValueAccessErrorKind { /// No value for the specified key was present in the document. #[error("the key was not present in the document")] - NotPresent, + #[non_exhaustive] + NotPresent {}, /// The type of the value in the document did not match the requested type. #[error("expected type {expected:?}, got type {actual:?}")] @@ -25,15 +26,15 @@ pub enum ValueAccessErrorKind { }, /// An error occurred when attempting to parse the document's BSON bytes. - #[error("{message}")] + #[error("invalid BSON bytes")] #[non_exhaustive] - InvalidBson { message: String }, + InvalidBson {}, } impl Error { pub(crate) fn value_access_not_present() -> Self { ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::NotPresent, + kind: ValueAccessErrorKind::NotPresent {}, } .into() } @@ -46,10 +47,10 @@ impl Error { } pub(crate) fn value_access_invalid_bson(message: String) -> Self { - ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::InvalidBson { message }, - } - .into() + Self::from(ErrorKind::ValueAccess { + kind: ValueAccessErrorKind::InvalidBson {}, + }) + .with_message(message) } #[cfg(test)] @@ -57,7 +58,7 @@ impl Error { matches!( self.kind, ErrorKind::ValueAccess { - kind: ValueAccessErrorKind::NotPresent, + kind: ValueAccessErrorKind::NotPresent {}, .. } ) diff --git a/src/raw.rs b/src/raw.rs index 697362c8..dfe8ccd4 100644 --- a/src/raw.rs +++ b/src/raw.rs @@ -274,7 +274,7 @@ fn read_lenencode(buf: &[u8]) -> Result<&str> { } fn try_to_str(data: &[u8]) -> Result<&str> { - simdutf8::basic::from_utf8(data).map_err(|_| ErrorKind::Utf8Encoding.into()) + simdutf8::basic::from_utf8(data).map_err(|_| ErrorKind::Utf8Encoding {}.into()) } fn usize_try_from_i32(i: i32) -> Result {