#[cfg(all(aws_sdk_unstable, feature = "serde-deserialize"))]
mod de;
mod discriminated;
#[cfg(any(
all(aws_sdk_unstable, feature = "serde-deserialize"),
all(aws_sdk_unstable, feature = "serde-serialize")
))]
mod doc_error;
pub mod document_object;
mod error;
#[cfg(all(aws_sdk_unstable, feature = "serde-serialize"))]
mod ser;
mod settings;
#[cfg(all(aws_sdk_unstable, feature = "serde-deserialize"))]
pub use de::from_document;
pub use discriminated::DiscriminatedDocument;
#[cfg(any(
all(aws_sdk_unstable, feature = "serde-deserialize"),
all(aws_sdk_unstable, feature = "serde-serialize")
))]
pub use doc_error::DocError;
pub use document_object::DocumentObject;
pub use error::DocumentError;
#[cfg(all(aws_sdk_unstable, feature = "serde-serialize"))]
pub use ser::to_document;
pub use settings::DocumentSettings;
use crate::{BigDecimal, BigInteger, DateTime, Number};
use std::borrow::Cow;
use std::str::FromStr;
#[cfg(any(
all(aws_sdk_unstable, feature = "serde-deserialize"),
all(aws_sdk_unstable, feature = "serde-serialize")
))]
use serde;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-serialize"),
derive(serde::Serialize)
)]
#[cfg_attr(
all(aws_sdk_unstable, feature = "serde-deserialize"),
derive(serde::Deserialize)
)]
#[cfg_attr(
any(
all(aws_sdk_unstable, feature = "serde-deserialize"),
all(aws_sdk_unstable, feature = "serde-serialize")
),
serde(untagged)
)]
pub enum Document {
Object(DocumentObject),
Array(Vec<Document>),
Number(Number),
String(String),
Bool(bool),
Null,
Blob(Vec<u8>),
Timestamp(DateTime),
BigInteger(BigInteger),
BigDecimal(BigDecimal),
}
impl Document {
pub fn as_object(&self) -> Option<&DocumentObject> {
if let Self::Object(object) = self {
Some(object)
} else {
None
}
}
pub fn as_object_mut(&mut self) -> Option<&mut DocumentObject> {
if let Self::Object(object) = self {
Some(object)
} else {
None
}
}
pub fn as_array(&self) -> Option<&Vec<Document>> {
if let Self::Array(array) = self {
Some(array)
} else {
None
}
}
pub fn as_array_mut(&mut self) -> Option<&mut Vec<Document>> {
if let Self::Array(array) = self {
Some(array)
} else {
None
}
}
pub fn as_number(&self) -> Option<&Number> {
if let Self::Number(number) = self {
Some(number)
} else {
None
}
}
pub fn as_string(&self) -> Option<&str> {
if let Self::String(string) = self {
Some(string)
} else {
None
}
}
pub fn as_bool(&self) -> Option<bool> {
if let Self::Bool(boolean) = self {
Some(*boolean)
} else {
None
}
}
pub fn as_null(&self) -> Option<()> {
if let Self::Null = self {
Some(())
} else {
None
}
}
pub fn is_object(&self) -> bool {
matches!(self, Self::Object(_))
}
pub fn is_array(&self) -> bool {
matches!(self, Self::Array(_))
}
pub fn is_number(&self) -> bool {
matches!(self, Self::Number(_))
}
pub fn is_string(&self) -> bool {
matches!(self, Self::String(_))
}
pub fn is_bool(&self) -> bool {
matches!(self, Self::Bool(_))
}
pub fn is_null(&self) -> bool {
matches!(self, Self::Null)
}
pub fn as_blob(&self) -> Option<&[u8]> {
if let Self::Blob(b) = self {
Some(b.as_slice())
} else {
None
}
}
pub fn as_blob_mut(&mut self) -> Option<&mut Vec<u8>> {
if let Self::Blob(b) = self {
Some(b)
} else {
None
}
}
pub fn as_timestamp(&self) -> Option<DateTime> {
if let Self::Timestamp(t) = self {
Some(*t)
} else {
None
}
}
pub fn as_big_integer(&self) -> Option<&BigInteger> {
if let Self::BigInteger(bi) = self {
Some(bi)
} else {
None
}
}
pub fn as_big_integer_mut(&mut self) -> Option<&mut BigInteger> {
if let Self::BigInteger(bi) = self {
Some(bi)
} else {
None
}
}
pub fn as_big_decimal(&self) -> Option<&BigDecimal> {
if let Self::BigDecimal(bd) = self {
Some(bd)
} else {
None
}
}
pub fn as_big_decimal_mut(&mut self) -> Option<&mut BigDecimal> {
if let Self::BigDecimal(bd) = self {
Some(bd)
} else {
None
}
}
pub fn is_blob(&self) -> bool {
matches!(self, Self::Blob(_))
}
pub fn is_timestamp(&self) -> bool {
matches!(self, Self::Timestamp(_))
}
pub fn is_big_integer(&self) -> bool {
matches!(self, Self::BigInteger(_))
}
pub fn is_big_decimal(&self) -> bool {
matches!(self, Self::BigDecimal(_))
}
pub fn as_byte(&self) -> Result<i8, DocumentError> {
coerce_signed::<i8>(self, "byte")
}
pub fn as_short(&self) -> Result<i16, DocumentError> {
coerce_signed::<i16>(self, "short")
}
pub fn as_integer(&self) -> Result<i32, DocumentError> {
coerce_signed::<i32>(self, "integer")
}
pub fn as_long(&self) -> Result<i64, DocumentError> {
coerce_signed::<i64>(self, "long")
}
pub fn as_float(&self) -> Result<f32, DocumentError> {
Ok(self.as_double()? as f32)
}
pub fn as_double(&self) -> Result<f64, DocumentError> {
match self {
Self::Number(Number::PosInt(v)) => Ok(*v as f64),
Self::Number(Number::NegInt(v)) => Ok(*v as f64),
Self::Number(Number::Float(f)) => Ok(*f),
Self::BigInteger(bi) => bi
.as_ref()
.parse::<f64>()
.map_err(|e| invalid_input("double", bi.as_ref(), &e)),
Self::BigDecimal(bd) => bd
.as_ref()
.parse::<f64>()
.map_err(|e| invalid_input("double", bd.as_ref(), &e)),
other => Err(type_mismatch_for("double", other)),
}
}
pub fn coerce_big_integer(&self) -> Result<BigInteger, DocumentError> {
match self {
Self::BigInteger(bi) => Ok(bi.clone()),
Self::Number(Number::PosInt(v)) => BigInteger::from_str(&v.to_string())
.map_err(|e| invalid_input("bigInteger", &v.to_string(), &e)),
Self::Number(Number::NegInt(v)) => BigInteger::from_str(&v.to_string())
.map_err(|e| invalid_input("bigInteger", &v.to_string(), &e)),
Self::Number(Number::Float(_)) => Err(type_mismatch(
"cannot coerce float to bigInteger without explicit narrowing",
)),
Self::BigDecimal(bd) => {
let int_part = bd.to_integer_string().ok_or_else(|| {
DocumentError::custom(format!(
"cannot coerce bigDecimal {} to bigInteger: integer magnitude too large",
bd.as_ref()
))
})?;
BigInteger::from_str(&int_part)
.map_err(|e| invalid_input("bigInteger", bd.as_ref(), &e))
}
other => Err(type_mismatch_for("bigInteger", other)),
}
}
pub fn coerce_big_decimal(&self) -> Result<BigDecimal, DocumentError> {
match self {
Self::BigDecimal(bd) => Ok(bd.clone()),
Self::BigInteger(bi) => BigDecimal::from_str(bi.as_ref())
.map_err(|e| invalid_input("bigDecimal", bi.as_ref(), &e)),
Self::Number(Number::PosInt(v)) => BigDecimal::from_str(&v.to_string())
.map_err(|e| invalid_input("bigDecimal", &v.to_string(), &e)),
Self::Number(Number::NegInt(v)) => BigDecimal::from_str(&v.to_string())
.map_err(|e| invalid_input("bigDecimal", &v.to_string(), &e)),
Self::Number(Number::Float(f)) => {
if !f.is_finite() {
return Err(DocumentError::custom(format!(
"cannot coerce non-finite float {f} to bigDecimal"
)));
}
BigDecimal::from_str(&f.to_string())
.map_err(|e| invalid_input("bigDecimal", &f.to_string(), &e))
}
other => Err(type_mismatch_for("bigDecimal", other)),
}
}
}
impl Default for Document {
fn default() -> Self {
Self::Null
}
}
impl From<bool> for Document {
fn from(value: bool) -> Self {
Document::Bool(value)
}
}
impl<'a> From<&'a str> for Document {
fn from(value: &'a str) -> Self {
Document::String(value.to_string())
}
}
impl<'a> From<Cow<'a, str>> for Document {
fn from(value: Cow<'a, str>) -> Self {
Document::String(value.into_owned())
}
}
impl From<String> for Document {
fn from(value: String) -> Self {
Document::String(value)
}
}
impl From<Vec<Document>> for Document {
fn from(values: Vec<Document>) -> Self {
Document::Array(values)
}
}
impl From<DocumentObject> for Document {
fn from(object: DocumentObject) -> Self {
Document::Object(object)
}
}
impl From<std::collections::HashMap<String, Document>> for Document {
fn from(values: std::collections::HashMap<String, Document>) -> Self {
Document::Object(DocumentObject::from(values))
}
}
impl From<u64> for Document {
fn from(value: u64) -> Self {
Document::Number(Number::PosInt(value))
}
}
impl From<i64> for Document {
fn from(value: i64) -> Self {
Document::Number(Number::NegInt(value))
}
}
impl From<i32> for Document {
fn from(value: i32) -> Self {
Document::Number(Number::NegInt(value as i64))
}
}
impl From<f64> for Document {
fn from(value: f64) -> Self {
Document::Number(Number::Float(value))
}
}
impl From<Number> for Document {
fn from(value: Number) -> Self {
Document::Number(value)
}
}
impl<T> From<Option<T>> for Document
where
Document: From<T>,
{
fn from(value: Option<T>) -> Self {
match value {
Some(inner) => inner.into(),
None => Document::Null,
}
}
}
fn coerce_signed<T>(doc: &Document, name: &str) -> Result<T, DocumentError>
where
T: TryFrom<i64> + TryFrom<u64> + Bounded,
f64: NarrowAs<T>,
{
match doc {
Document::Number(Number::PosInt(v)) => {
T::try_from(*v).map_err(|_| overflow(name, format_args!("{v}")))
}
Document::Number(Number::NegInt(v)) => {
T::try_from(*v).map_err(|_| overflow(name, format_args!("{v}")))
}
Document::Number(Number::Float(_)) => Err(type_mismatch(format!(
"cannot coerce float to {name} without explicit narrowing"
))),
Document::BigInteger(bi) => {
let parsed = i64::from_str(bi.as_ref())
.map_err(|_| overflow(name, format_args!("{}", bi.as_ref())))?;
T::try_from(parsed).map_err(|_| overflow(name, format_args!("{parsed}")))
}
Document::BigDecimal(bd) => {
let f = bd
.as_ref()
.parse::<f64>()
.map_err(|e| invalid_input(name, bd.as_ref(), &e))?;
narrow_float::<T>(f, T::MIN_F64, T::MAX_F64, name)
}
other => Err(type_mismatch_for(name, other)),
}
}
trait Bounded {
const MIN_F64: f64;
const MAX_F64: f64;
}
impl Bounded for i8 {
const MIN_F64: f64 = i8::MIN as f64;
const MAX_F64: f64 = i8::MAX as f64;
}
impl Bounded for i16 {
const MIN_F64: f64 = i16::MIN as f64;
const MAX_F64: f64 = i16::MAX as f64;
}
impl Bounded for i32 {
const MIN_F64: f64 = i32::MIN as f64;
const MAX_F64: f64 = i32::MAX as f64;
}
impl Bounded for i64 {
const MIN_F64: f64 = i64::MIN as f64;
const MAX_F64: f64 = i64::MAX as f64;
}
trait NarrowAs<T> {
fn narrow(self) -> T;
}
impl NarrowAs<i8> for f64 {
fn narrow(self) -> i8 {
self as i8
}
}
impl NarrowAs<i16> for f64 {
fn narrow(self) -> i16 {
self as i16
}
}
impl NarrowAs<i32> for f64 {
fn narrow(self) -> i32 {
self as i32
}
}
impl NarrowAs<i64> for f64 {
fn narrow(self) -> i64 {
self as i64
}
}
fn narrow_float<T>(f: f64, min: f64, max: f64, name: &str) -> Result<T, DocumentError>
where
f64: NarrowAs<T>,
{
if !f.is_finite() {
return Err(DocumentError::custom(format!(
"cannot coerce non-finite float {f} to {name}"
)));
}
if !(min..=max).contains(&f) {
return Err(overflow(name, format_args!("{f}")));
}
Ok(<f64 as NarrowAs<T>>::narrow(f))
}
fn type_mismatch(message: impl Into<String>) -> DocumentError {
DocumentError::type_mismatch(message)
}
fn type_mismatch_for(expected: &str, found: &Document) -> DocumentError {
let found_name = match found {
Document::Null => "null",
Document::Bool(_) => "boolean",
Document::Number(_) => "number",
Document::BigInteger(_) => "bigInteger",
Document::BigDecimal(_) => "bigDecimal",
Document::String(_) => "string",
Document::Blob(_) => "blob",
Document::Timestamp(_) => "timestamp",
Document::Array(_) => "array",
Document::Object(_) => "object",
};
type_mismatch(format!("expected {expected}, found {found_name}"))
}
fn overflow(target: &str, value: std::fmt::Arguments<'_>) -> DocumentError {
DocumentError::numeric_coercion_overflow(target, value.to_string())
}
fn invalid_input(target: &str, value: &str, err: &dyn std::fmt::Display) -> DocumentError {
DocumentError::invalid_input(format!("cannot parse {value:?} as {target}: {err}"))
}
#[cfg(test)]
mod extended_variant_tests {
use super::{Document, DocumentError};
use crate::{BigDecimal, BigInteger, DateTime, Number};
use std::str::FromStr;
#[test]
fn blob_variant_round_trips_via_accessor() {
let d = Document::Blob(b"abcd".to_vec());
assert!(d.is_blob());
assert_eq!(d.as_blob(), Some(b"abcd".as_slice()));
}
#[test]
fn timestamp_variant_round_trips_via_accessor() {
let ts = DateTime::from_secs(0);
let d = Document::Timestamp(ts);
assert!(d.is_timestamp());
assert_eq!(d.as_timestamp(), Some(ts));
}
#[test]
fn big_integer_variant_round_trips_via_accessor() {
let bi = BigInteger::from_str("12345678901234567890").unwrap();
let d = Document::BigInteger(bi.clone());
assert!(d.is_big_integer());
assert_eq!(d.as_big_integer(), Some(&bi));
}
#[test]
fn big_decimal_variant_round_trips_via_accessor() {
let bd = BigDecimal::from_str("12345.678").unwrap();
let d = Document::BigDecimal(bd.clone());
assert!(d.is_big_decimal());
assert_eq!(d.as_big_decimal(), Some(&bd));
}
#[test]
fn type_check_accessors_do_not_coerce_across_variants() {
let s = Document::String("YWJjZA==".to_owned());
assert_eq!(s.as_blob(), None);
assert!(!s.is_blob());
assert_eq!(s.as_timestamp(), None);
assert_eq!(s.as_big_integer(), None);
assert_eq!(s.as_big_decimal(), None);
let blob = Document::Blob(b"hi".to_vec());
assert_eq!(blob.as_string(), None);
assert!(!blob.is_string());
}
#[test]
fn mut_accessors_let_callers_mutate_in_place() {
let mut d = Document::Blob(vec![1, 2, 3]);
if let Some(b) = d.as_blob_mut() {
b.push(4);
}
assert_eq!(d.as_blob(), Some([1, 2, 3, 4].as_slice()));
let mut d = Document::BigInteger(BigInteger::from_str("0").unwrap());
if let Some(bi) = d.as_big_integer_mut() {
*bi = BigInteger::from_str("42").unwrap();
}
assert_eq!(d.as_big_integer().unwrap().as_ref(), "42");
}
#[test]
fn as_byte_coerces_across_signed_integer_sources() {
assert_eq!(Document::Number(Number::PosInt(42)).as_byte().unwrap(), 42);
assert_eq!(
Document::Number(Number::NegInt(-42)).as_byte().unwrap(),
-42
);
let bi = BigInteger::from_str("100").unwrap();
assert_eq!(Document::BigInteger(bi).as_byte().unwrap(), 100);
let bd = BigDecimal::from_str("100.5").unwrap();
assert_eq!(Document::BigDecimal(bd).as_byte().unwrap(), 100);
}
#[test]
fn as_short_widens_from_smaller_sources() {
assert_eq!(
Document::Number(Number::PosInt(1234)).as_short().unwrap(),
1234
);
}
#[test]
fn as_integer_widens_from_smaller_sources_and_narrows_when_in_range() {
assert_eq!(
Document::Number(Number::PosInt(i32::MAX as u64))
.as_integer()
.unwrap(),
i32::MAX
);
assert_eq!(
Document::Number(Number::NegInt(i32::MIN as i64))
.as_integer()
.unwrap(),
i32::MIN
);
}
#[test]
fn as_long_handles_max_pos_int() {
assert_eq!(
Document::Number(Number::PosInt(i64::MAX as u64))
.as_long()
.unwrap(),
i64::MAX
);
}
#[test]
fn as_double_widens_from_integer_sources() {
assert_eq!(
Document::Number(Number::PosInt(42)).as_double().unwrap(),
42.0
);
assert_eq!(
Document::Number(Number::NegInt(-42)).as_double().unwrap(),
-42.0
);
assert_eq!(
Document::Number(Number::Float(2.5)).as_double().unwrap(),
2.5
);
}
#[test]
fn as_float_inherits_from_as_double() {
assert_eq!(
Document::Number(Number::Float(1.5)).as_float().unwrap(),
1.5_f32
);
}
#[test]
fn as_byte_overflow_emits_typed_variant() {
let err = Document::Number(Number::PosInt(200)).as_byte().unwrap_err();
match err {
DocumentError::NumericCoercionOverflow { target, value } => {
assert_eq!(target, "byte");
assert_eq!(value, "200");
}
other => panic!("expected NumericCoercionOverflow, got {other:?}"),
}
}
#[test]
fn as_byte_type_mismatch_for_non_numeric() {
let err = Document::String("not a number".to_owned())
.as_byte()
.unwrap_err();
assert!(matches!(err, DocumentError::TypeMismatch { .. }));
}
#[test]
fn as_integer_overflow_when_long_doesnt_fit() {
let too_negative = i64::from(i32::MIN) - 1;
let err = Document::Number(Number::NegInt(too_negative))
.as_integer()
.unwrap_err();
assert!(matches!(err, DocumentError::NumericCoercionOverflow { .. }));
}
#[test]
fn as_long_overflows_on_max_pos_int_above_i64_max() {
let err = Document::Number(Number::PosInt(u64::MAX))
.as_long()
.unwrap_err();
assert!(matches!(err, DocumentError::NumericCoercionOverflow { .. }));
}
#[test]
fn as_byte_rejects_float_source_even_with_zero_fractional() {
let err = Document::Number(Number::Float(42.0)).as_byte().unwrap_err();
assert!(matches!(err, DocumentError::TypeMismatch { .. }));
let err = Document::Number(Number::Float(42.7)).as_byte().unwrap_err();
assert!(matches!(err, DocumentError::TypeMismatch { .. }));
}
#[test]
fn as_short_as_integer_as_long_all_reject_float_source() {
let f = Document::Number(Number::Float(1.0));
assert!(matches!(
f.as_short().unwrap_err(),
DocumentError::TypeMismatch { .. }
));
assert!(matches!(
f.as_integer().unwrap_err(),
DocumentError::TypeMismatch { .. }
));
assert!(matches!(
f.as_long().unwrap_err(),
DocumentError::TypeMismatch { .. }
));
}
#[test]
fn coerce_big_integer_rejects_float_source() {
let err = Document::Number(Number::Float(42.0))
.coerce_big_integer()
.unwrap_err();
assert!(matches!(err, DocumentError::TypeMismatch { .. }));
}
#[test]
fn as_float_accepts_integer_source_losslessly() {
assert_eq!(
Document::Number(Number::PosInt(42)).as_float().unwrap(),
42.0_f32
);
assert_eq!(
Document::Number(Number::NegInt(-42)).as_double().unwrap(),
-42.0_f64
);
}
#[test]
fn coerce_big_integer_truncates_big_decimal_at_decimal_point() {
let bd = BigDecimal::from_str("12345678901234567890.123").unwrap();
let bi = Document::BigDecimal(bd).coerce_big_integer().unwrap();
assert_eq!(bi.as_ref(), "12345678901234567890");
}
#[test]
fn coerce_big_integer_expands_scientific_notation_big_decimal() {
let bd = BigDecimal::from_str("1.23e10").unwrap();
let bi = Document::BigDecimal(bd).coerce_big_integer().unwrap();
assert_eq!(bi.as_ref(), "12300000000");
let bd = BigDecimal::from_str("1e30").unwrap();
let bi = Document::BigDecimal(bd).coerce_big_integer().unwrap();
assert_eq!(bi.as_ref(), "1000000000000000000000000000000");
}
#[test]
fn coerce_big_integer_errors_on_unmaterializable_big_decimal() {
let bd = BigDecimal::from_str("1e1000000000").unwrap();
let err = Document::BigDecimal(bd).coerce_big_integer().unwrap_err();
assert!(
err.to_string().contains("too large"),
"expected a 'too large' error, got: {err}"
);
}
#[test]
fn coerce_big_decimal_widens_from_big_integer_lossless() {
let big_str = "12345678901234567890123456789";
let bi = BigInteger::from_str(big_str).unwrap();
let bd = Document::BigInteger(bi).coerce_big_decimal().unwrap();
assert_eq!(bd.as_ref(), big_str);
}
#[test]
fn as_double_on_big_integer_is_documented_lossy_path() {
let bi = BigInteger::from_str("12345678901234567890").unwrap();
let f = Document::BigInteger(bi).as_double().unwrap();
assert!(f.is_finite());
}
#[test]
fn special_floats_round_trip_via_as_double() {
assert!(Document::Number(Number::Float(f64::NAN))
.as_double()
.unwrap()
.is_nan());
assert_eq!(
Document::Number(Number::Float(f64::INFINITY))
.as_double()
.unwrap(),
f64::INFINITY
);
assert_eq!(
Document::Number(Number::Float(f64::NEG_INFINITY))
.as_double()
.unwrap(),
f64::NEG_INFINITY
);
}
#[test]
fn special_floats_in_coerce_big_integer_are_rejected() {
let err = Document::Number(Number::Float(f64::NAN))
.coerce_big_integer()
.unwrap_err();
assert!(matches!(err, DocumentError::TypeMismatch { .. }));
}
#[test]
fn special_floats_in_coerce_big_decimal_are_rejected() {
let err = Document::Number(Number::Float(f64::INFINITY))
.coerce_big_decimal()
.unwrap_err();
match err {
DocumentError::Custom { message } => {
assert!(message.contains("non-finite"));
}
other => panic!("expected Custom non-finite error, got {other:?}"),
}
}
}
#[cfg(test)]
#[cfg(all(
aws_sdk_unstable,
feature = "serde-serialize",
feature = "serde-deserialize"
))]
mod test {
use super::{from_document, to_document, Document};
use crate::document::DocumentObject;
use crate::Number;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
fn test_to_document_ok<T>(cases: &[(T, Document)])
where
T: Serialize + std::fmt::Debug,
{
for (value, expected) in cases {
let doc = to_document(value).unwrap();
assert_eq!(&doc, expected, "to_document({:?})", value);
}
}
fn test_roundtrip<T>(cases: &[T])
where
T: Serialize + for<'de> Deserialize<'de> + PartialEq + std::fmt::Debug + Clone,
{
for value in cases {
let doc = to_document(value).unwrap();
let roundtripped: T = from_document(doc).unwrap();
assert_eq!(&roundtripped, value, "roundtrip failed for {:?}", value);
}
}
#[test]
fn test_null() {
test_to_document_ok(&[((), Document::Null)]);
let v: () = from_document(Document::Null).unwrap();
assert_eq!(v, ());
}
#[test]
fn test_bool() {
test_to_document_ok(&[(true, Document::Bool(true)), (false, Document::Bool(false))]);
test_roundtrip(&[true, false]);
}
#[test]
fn test_u8() {
test_to_document_ok(&[
(0u8, Document::Number(Number::PosInt(0))),
(u8::MAX, Document::Number(Number::PosInt(u8::MAX as u64))),
]);
test_roundtrip(&[0u8, 1, 127, u8::MAX]);
}
#[test]
fn test_u16() {
test_to_document_ok(&[
(0u16, Document::Number(Number::PosInt(0))),
(u16::MAX, Document::Number(Number::PosInt(u16::MAX as u64))),
]);
test_roundtrip(&[0u16, 1, u16::MAX]);
}
#[test]
fn test_u32() {
test_to_document_ok(&[
(0u32, Document::Number(Number::PosInt(0))),
(u32::MAX, Document::Number(Number::PosInt(u32::MAX as u64))),
]);
test_roundtrip(&[0u32, 1, u32::MAX]);
}
#[test]
fn test_u64() {
test_to_document_ok(&[
(0u64, Document::Number(Number::PosInt(0))),
(u64::MAX, Document::Number(Number::PosInt(u64::MAX))),
]);
test_roundtrip(&[0u64, 1, u64::MAX]);
}
#[test]
fn test_i8() {
test_to_document_ok(&[
(0i8, Document::Number(Number::PosInt(0))),
(-1i8, Document::Number(Number::NegInt(-1))),
(i8::MIN, Document::Number(Number::NegInt(i8::MIN as i64))),
(i8::MAX, Document::Number(Number::PosInt(i8::MAX as u64))),
]);
test_roundtrip(&[0i8, -1, 1, i8::MIN, i8::MAX]);
}
#[test]
fn test_i16() {
test_to_document_ok(&[
(0i16, Document::Number(Number::PosInt(0))),
(i16::MIN, Document::Number(Number::NegInt(i16::MIN as i64))),
(i16::MAX, Document::Number(Number::PosInt(i16::MAX as u64))),
]);
test_roundtrip(&[0i16, -1, i16::MIN, i16::MAX]);
}
#[test]
fn test_i32() {
test_to_document_ok(&[
(0i32, Document::Number(Number::PosInt(0))),
(i32::MIN, Document::Number(Number::NegInt(i32::MIN as i64))),
(i32::MAX, Document::Number(Number::PosInt(i32::MAX as u64))),
]);
test_roundtrip(&[0i32, -1, i32::MIN, i32::MAX]);
}
#[test]
fn test_i64() {
test_to_document_ok(&[
(0i64, Document::Number(Number::PosInt(0))),
(-1i64, Document::Number(Number::NegInt(-1))),
(i64::MIN, Document::Number(Number::NegInt(i64::MIN))),
(i64::MAX, Document::Number(Number::PosInt(i64::MAX as u64))),
]);
test_roundtrip(&[0i64, -1, i64::MIN, i64::MAX]);
}
#[test]
fn test_f32() {
test_to_document_ok(&[
(0.0f32, Document::Number(Number::Float(0.0))),
(3.5f32, Document::Number(Number::Float(3.5))),
(-1.5f32, Document::Number(Number::Float(-1.5))),
]);
test_roundtrip(&[0.0f32, 3.5, -1.5, f32::MIN, f32::MAX]);
}
#[test]
fn test_f64() {
test_to_document_ok(&[
(0.0f64, Document::Number(Number::Float(0.0))),
(3.1f64, Document::Number(Number::Float(3.1))),
(-1.5f64, Document::Number(Number::Float(-1.5))),
(f64::MIN, Document::Number(Number::Float(f64::MIN))),
(f64::MAX, Document::Number(Number::Float(f64::MAX))),
(f64::EPSILON, Document::Number(Number::Float(f64::EPSILON))),
]);
test_roundtrip(&[0.0f64, 3.1, -1.5, 0.5, f64::MIN, f64::MAX]);
}
#[test]
fn test_nonfinite_floats() {
let doc = to_document(&f64::NAN).unwrap();
match doc {
Document::Number(Number::Float(v)) => assert!(v.is_nan()),
other => panic!("expected NaN float, got {:?}", other),
}
let doc = to_document(&f64::INFINITY).unwrap();
assert_eq!(doc, Document::Number(Number::Float(f64::INFINITY)));
let doc = to_document(&f64::NEG_INFINITY).unwrap();
assert_eq!(doc, Document::Number(Number::Float(f64::NEG_INFINITY)));
}
#[test]
fn test_string() {
test_to_document_ok(&[
(String::new(), Document::String(String::new())),
("hello".to_owned(), Document::String("hello".to_owned())),
(
"with\nnewline".to_owned(),
Document::String("with\nnewline".to_owned()),
),
(
"unicode: \u{1F600}".to_owned(),
Document::String("unicode: \u{1F600}".to_owned()),
),
]);
test_roundtrip(&[
String::new(),
"foo".to_owned(),
"bar\tbaz".to_owned(),
"\u{3A3}".to_owned(),
]);
}
#[test]
fn test_str_ref() {
let doc = to_document(&"borrowed str").unwrap();
assert_eq!(doc, Document::String("borrowed str".to_owned()));
}
#[test]
fn test_char() {
let doc = to_document(&'a').unwrap();
assert_eq!(doc, Document::String("a".to_owned()));
let doc = to_document(&'\u{1F600}').unwrap();
assert_eq!(doc, Document::String("\u{1F600}".to_owned()));
}
#[test]
fn test_option() {
test_to_document_ok(&[
(None::<String>, Document::Null),
(
Some("jodhpurs".to_owned()),
Document::String("jodhpurs".to_owned()),
),
]);
test_to_document_ok(&[
(None::<u32>, Document::Null),
(Some(42u32), Document::Number(Number::PosInt(42))),
]);
test_roundtrip(&[None::<u32>, Some(5), Some(0)]);
test_roundtrip(&[None::<String>, Some("x".to_owned())]);
}
#[test]
fn test_vec_empty() {
let doc = to_document(&Vec::<i32>::new()).unwrap();
assert_eq!(doc, Document::Array(vec![]));
let v: Vec<i32> = from_document(Document::Array(vec![])).unwrap();
assert_eq!(v, Vec::<i32>::new());
}
#[test]
fn test_vec_integers() {
test_to_document_ok(&[(
vec![1u64, 2, 3],
Document::Array(vec![
Document::Number(Number::PosInt(1)),
Document::Number(Number::PosInt(2)),
Document::Number(Number::PosInt(3)),
]),
)]);
test_roundtrip(&[vec![1i32, -2, 3], vec![], vec![0]]);
}
#[test]
fn test_vec_mixed_via_document() {
let mixed = vec![
Document::Bool(true),
Document::Null,
Document::String("foo".to_owned()),
Document::Number(Number::PosInt(42)),
];
let doc = Document::Array(mixed.clone());
let roundtripped: Vec<Document> = from_document(doc.clone()).unwrap();
assert_eq!(roundtripped, mixed);
}
#[test]
fn test_nested_vec() {
test_roundtrip(&[
vec![vec![1u32, 2], vec![], vec![3]],
vec![vec![], vec![], vec![]],
]);
}
#[test]
fn test_tuple() {
let doc = to_document(&(5u32,)).unwrap();
assert_eq!(
doc,
Document::Array(vec![Document::Number(Number::PosInt(5))])
);
let doc = to_document(&(1u32, "abc", true)).unwrap();
assert_eq!(
doc,
Document::Array(vec![
Document::Number(Number::PosInt(1)),
Document::String("abc".to_owned()),
Document::Bool(true),
])
);
test_roundtrip(&[(1u32, 2u32), (0, u32::MAX)]);
test_roundtrip(&[(1i32, "hello".to_owned(), true)]);
}
#[test]
fn test_map_empty() {
let map: HashMap<String, u32> = HashMap::new();
let doc = to_document(&map).unwrap();
assert_eq!(doc, Document::Object(DocumentObject::new()));
test_roundtrip(&[HashMap::<String, u32>::new()]);
}
#[test]
fn test_map_string_keys() {
let mut map = HashMap::new();
map.insert("a".to_owned(), 1u32);
map.insert("b".to_owned(), 2u32);
test_roundtrip(&[map]);
}
#[test]
fn test_map_integer_keys() {
let mut map = HashMap::new();
map.insert(1u32, "one".to_owned());
map.insert(2u32, "two".to_owned());
let doc = to_document(&map).unwrap();
assert!(doc.is_object());
let obj = doc.as_object().unwrap();
assert!(obj.contains_key("1") || obj.contains_key("2"));
}
#[test]
fn test_nested_map() {
let mut inner = HashMap::new();
inner.insert("x".to_owned(), 10u32);
let mut outer = HashMap::new();
outer.insert("inner".to_owned(), inner.clone());
test_roundtrip(&[outer]);
}
#[test]
fn test_struct() {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Inner {
a: (),
b: usize,
c: Vec<String>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Outer {
inner: Vec<Inner>,
}
let outer = Outer {
inner: vec![Inner {
a: (),
b: 2,
c: vec!["abc".to_owned(), "xyz".to_owned()],
}],
};
let doc = to_document(&outer).unwrap();
assert!(doc.is_object());
let roundtripped: Outer = from_document(doc).unwrap();
assert_eq!(outer, roundtripped);
test_roundtrip(&[Outer { inner: vec![] }]);
}
#[test]
fn test_newtype_struct() {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Wrapper(u32);
test_to_document_ok(&[(Wrapper(123), Document::Number(Number::PosInt(123)))]);
test_roundtrip(&[Wrapper(0), Wrapper(u32::MAX)]);
}
#[test]
fn test_unit_struct() {
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
struct Unit;
test_to_document_ok(&[(Unit, Document::Null)]);
let v: Unit = from_document(Document::Null).unwrap();
assert_eq!(v, Unit);
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
enum Animal {
Dog,
Frog(String, Vec<isize>),
Cat { age: usize, name: String },
AntHive(Vec<String>),
}
#[test]
fn test_enum_unit_variant() {
let doc = to_document(&Animal::Dog).unwrap();
assert_eq!(doc, Document::String("Dog".to_owned()));
test_roundtrip(&[Animal::Dog]);
}
#[test]
fn test_enum_tuple_variant() {
let frog = Animal::Frog("Henry".to_owned(), vec![349, 102]);
let doc = to_document(&frog).unwrap();
assert!(doc.is_object());
let obj = doc.as_object().unwrap();
assert!(obj.contains_key("Frog"));
test_roundtrip(&[
Animal::Frog("Henry".to_owned(), vec![]),
Animal::Frog("Henry".to_owned(), vec![349, 102]),
]);
}
#[test]
fn test_enum_struct_variant() {
let cat = Animal::Cat {
age: 5,
name: "Kate".to_owned(),
};
let doc = to_document(&cat).unwrap();
assert!(doc.is_object());
let obj = doc.as_object().unwrap();
assert!(obj.contains_key("Cat"));
test_roundtrip(&[cat]);
}
#[test]
fn test_enum_newtype_variant() {
let hive = Animal::AntHive(vec!["Bob".to_owned(), "Stuart".to_owned()]);
test_roundtrip(&[hive]);
}
#[test]
fn test_bytes() {
let data: &[u8] = &[1, 2, 3];
let doc = to_document(&data).unwrap();
assert_eq!(
doc,
Document::Array(vec![
Document::Number(Number::PosInt(1)),
Document::Number(Number::PosInt(2)),
Document::Number(Number::PosInt(3)),
])
);
let empty: &[u8] = &[];
let doc = to_document(&empty).unwrap();
assert_eq!(doc, Document::Array(vec![]));
}
#[test]
fn test_deserialize_wrong_type() {
let result = from_document::<bool>(Document::String("not a bool".to_owned()));
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
err.to_string().contains("invalid type"),
"unexpected error message: {}",
err
);
}
#[test]
fn test_deserialize_missing_field() {
#[derive(Debug, Deserialize)]
struct Required {
#[allow(dead_code)]
x: u32,
}
let doc = Document::Object(DocumentObject::new());
let result = from_document::<Required>(doc);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("missing field"));
}
#[test]
fn test_serialize_non_string_map_key_rejected() {
use std::collections::HashMap;
let mut map: HashMap<Option<u32>, u32> = HashMap::new();
map.insert(None, 1);
let result = to_document(&map);
assert!(result.is_err());
}
#[test]
fn test_serde_json_compatibility() {
let mut map: HashMap<String, Document> = HashMap::new();
map.insert("hello".into(), "world".to_string().into());
map.insert("pos_int".into(), Document::Number(Number::PosInt(1).into()));
map.insert(
"neg_int".into(),
Document::Number(Number::NegInt(-1).into()),
);
map.insert(
"float".into(),
Document::Number(Number::Float(0.1 + 0.2).into()),
);
map.insert("true".into(), true.into());
map.insert("false".into(), false.into());
map.insert(
"array".into(),
vec![
map.clone().into(),
"hello-world".to_string().into(),
true.into(),
false.into(),
]
.into(),
);
map.insert("map".into(), map.clone().into());
map.insert("null".into(), Document::Null);
let obj = Document::Object(map.into());
let target_file = include_str!("../../test_data/serialize_document.json");
let json: Result<serde_json::Value, _> = serde_json::from_str(target_file);
assert_eq!(serde_json::to_value(&obj).unwrap(), json.unwrap());
let doc: Result<Document, _> = serde_json::from_str(target_file);
assert_eq!(obj, doc.unwrap());
}
}