use std::str::FromStr;
use std::sync::Arc;
use aws_smithy_types::date_time::Format;
use aws_smithy_types::{
BigDecimal, BigInteger, Blob, DateTime, Document, DocumentSettings, Number,
};
use crate::serde::{capped_container_size, SerdeError, ShapeDeserializer};
use crate::Schema;
#[derive(Debug)]
pub struct DocumentShapeDeserializer<'a> {
cursor: &'a Document,
settings: Option<Arc<dyn DocumentSettings>>,
}
impl<'a> DocumentShapeDeserializer<'a> {
pub fn new(document: &'a Document) -> Self {
Self {
cursor: document,
settings: None,
}
}
pub fn new_with_settings(
document: &'a Document,
settings: Option<Arc<dyn DocumentSettings>>,
) -> Self {
Self {
cursor: document,
settings,
}
}
}
fn number_to_timestamp(n: &Number) -> Result<DateTime, SerdeError> {
match n {
Number::PosInt(v) => i64::try_from(*v)
.map(DateTime::from_secs)
.map_err(|_| SerdeError::custom(format!("epoch seconds {v} out of range"))),
Number::NegInt(v) => Ok(DateTime::from_secs(*v)),
Number::Float(v) => {
if !v.is_finite() {
return Err(SerdeError::custom(format!(
"epoch seconds {v} is not finite"
)));
}
Ok(DateTime::from_secs_f64(*v))
}
}
}
fn type_mismatch(expected: &str, found: &Document) -> SerdeError {
SerdeError::type_mismatch(format!(
"expected {expected} document, got {}",
kind_name(found)
))
}
fn kind_name(d: &Document) -> &'static str {
match d {
Document::Null => "null",
Document::Bool(_) => "boolean",
Document::Number(_) => "number",
Document::String(_) => "string",
Document::Array(_) => "list",
Document::Object(_) => "map",
}
}
fn resolve_member<'s>(schema: &'s Schema<'s>, wire_name: &str) -> Option<&'s Schema<'s>> {
let idx = schema
.members()
.iter()
.position(|m| m.member_name() == Some(wire_name))?;
schema.member_schema_by_index(idx)
}
fn coerce_signed<T>(doc: &Document, name: &str) -> Result<T, SerdeError>
where
T: TryFrom<i64> + TryFrom<u64>,
{
match doc {
Document::Number(Number::PosInt(v)) => {
T::try_from(*v).map_err(|_| SerdeError::numeric_coercion_overflow(name, v.to_string()))
}
Document::Number(Number::NegInt(v)) => {
T::try_from(*v).map_err(|_| SerdeError::numeric_coercion_overflow(name, v.to_string()))
}
Document::Number(Number::Float(v)) => {
if !is_integral(*v) {
return Err(SerdeError::numeric_coercion_lossy(name, v.to_string()));
}
float_as_i64(*v)
.and_then(|i| T::try_from(i).ok())
.ok_or_else(|| SerdeError::numeric_coercion_overflow(name, v.to_string()))
}
other => Err(type_mismatch(name, other)),
}
}
fn is_integral(v: f64) -> bool {
v.is_finite() && v.fract() == 0.0
}
pub(crate) fn float_as_i64(v: f64) -> Option<i64> {
const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
(is_integral(v) && (-TWO_POW_63..TWO_POW_63).contains(&v)).then_some(v as i64)
}
fn coerce_double(doc: &Document) -> Result<f64, SerdeError> {
match doc {
Document::Number(Number::PosInt(v)) => Ok(*v as f64),
Document::Number(Number::NegInt(v)) => Ok(*v as f64),
Document::Number(Number::Float(f)) => Ok(*f),
other => Err(type_mismatch("double", other)),
}
}
fn coerce_float(doc: &Document) -> Result<f32, SerdeError> {
Ok(coerce_double(doc)? as f32)
}
fn coerce_big_integer(doc: &Document) -> Result<BigInteger, SerdeError> {
match doc {
Document::Number(Number::PosInt(v)) => parse_big_integer(&v.to_string()),
Document::Number(Number::NegInt(v)) => parse_big_integer(&v.to_string()),
Document::Number(Number::Float(v)) => {
if !is_integral(*v) {
return Err(SerdeError::numeric_coercion_lossy(
"bigInteger",
v.to_string(),
));
}
parse_big_integer(&format!("{:.0}", v + 0.0))
}
Document::String(s) => {
if let Ok(bi) = BigInteger::from_str(s) {
return Ok(bi);
}
let bd = BigDecimal::from_str(s).map_err(|e| invalid_input("bigInteger", s, &e))?;
if !is_unambiguous_decimal(bd.as_ref()) {
return Err(SerdeError::invalid_input(format!(
"cannot coerce {:?} to bigInteger: not a decimal number",
bd.as_ref()
)));
}
let int_part = big_decimal_to_integer_string(&bd).ok_or_else(|| {
SerdeError::custom(format!(
"cannot coerce bigDecimal {} to bigInteger: integer magnitude too large",
bd.as_ref()
))
})?;
parse_big_integer(&int_part)
}
other => Err(type_mismatch("bigInteger", other)),
}
}
fn coerce_big_decimal(doc: &Document) -> Result<BigDecimal, SerdeError> {
match doc {
Document::String(s) => parse_big_decimal(s),
Document::Number(Number::PosInt(v)) => parse_big_decimal(&v.to_string()),
Document::Number(Number::NegInt(v)) => parse_big_decimal(&v.to_string()),
Document::Number(Number::Float(f)) => {
if !f.is_finite() {
return Err(SerdeError::custom(format!(
"cannot coerce non-finite float {f} to bigDecimal"
)));
}
parse_big_decimal(&f.to_string())
}
other => Err(type_mismatch("bigDecimal", other)),
}
}
fn parse_big_integer(s: &str) -> Result<BigInteger, SerdeError> {
BigInteger::from_str(s).map_err(|e| invalid_input("bigInteger", s, &e))
}
fn parse_big_decimal(s: &str) -> Result<BigDecimal, SerdeError> {
BigDecimal::from_str(s).map_err(|e| invalid_input("bigDecimal", s, &e))
}
fn invalid_input(target: &str, value: &str, err: &dyn std::fmt::Display) -> SerdeError {
SerdeError::invalid_input(format!("cannot parse {value:?} as {target}: {err}"))
}
fn is_ascii_digits(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
fn is_unambiguous_decimal(text: &str) -> bool {
let rest = text.strip_prefix(['-', '+']).unwrap_or(text);
let (mantissa, exponent) = match rest.split_once(['e', 'E']) {
Some((mantissa, exponent)) => (mantissa, Some(exponent)),
None => (rest, None),
};
let mantissa_ok = match mantissa.split_once('.') {
Some((int_part, frac_part)) => {
(!int_part.is_empty() || !frac_part.is_empty())
&& int_part.bytes().all(|b| b.is_ascii_digit())
&& frac_part.bytes().all(|b| b.is_ascii_digit())
}
None => is_ascii_digits(mantissa),
};
let exponent_ok = match exponent {
None => true,
Some(exponent) => is_ascii_digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent)),
};
mantissa_ok && exponent_ok
}
const MAX_INTEGER_DIGITS: usize = 1 << 20;
fn big_decimal_to_integer_string(bd: &BigDecimal) -> Option<String> {
debug_assert!(
is_unambiguous_decimal(bd.as_ref()),
"big_decimal_to_integer_string requires decimal text, got {:?}",
bd.as_ref()
);
let text = bd.as_ref();
let (negative, rest) = match text.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, text.strip_prefix('+').unwrap_or(text)),
};
let (mantissa, exp) = match rest.split_once(['e', 'E']) {
Some((mantissa, exp_str)) => match exp_str.parse::<i64>() {
Ok(exp) => (mantissa, exp),
Err(_) => {
return if exp_str.starts_with('-') {
Some("0".to_string())
} else {
None
}
}
},
None => (rest, 0),
};
let (int_digits, frac_digits) = match mantissa.split_once('.') {
Some((int_digits, frac_digits)) => (int_digits, frac_digits),
None => (mantissa, ""),
};
let point = (int_digits.len() as i64).saturating_add(exp);
let int_part = if point <= 0 {
"0".to_string()
} else {
let point = point as usize;
let mut digits = String::with_capacity(int_digits.len() + frac_digits.len());
digits.push_str(int_digits);
digits.push_str(frac_digits);
if point >= digits.len() {
if point > MAX_INTEGER_DIGITS {
return None;
}
digits.push_str(&"0".repeat(point - digits.len()));
digits
} else {
digits.truncate(point);
digits
}
};
let normalized = match int_part.trim_start_matches('0') {
"" => "0",
trimmed => trimmed,
};
Some(if negative && normalized != "0" {
format!("-{normalized}")
} else {
normalized.to_string()
})
}
impl<'a> ShapeDeserializer for DocumentShapeDeserializer<'a> {
fn read_struct(
&mut self,
schema: &Schema<'_>,
consumer: &mut dyn FnMut(&Schema<'_>, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
let map = self
.cursor
.as_object()
.ok_or_else(|| type_mismatch("struct (map)", self.cursor))?;
for (key, value) in map {
let Some(member_schema) = resolve_member(schema, key) else {
continue;
};
let mut sub = Self::new_with_settings(value, self.settings.clone());
consumer(member_schema, &mut sub)?;
}
Ok(())
}
fn read_list(
&mut self,
_schema: &Schema<'_>,
consumer: &mut dyn FnMut(&mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
let items = self
.cursor
.as_array()
.ok_or_else(|| type_mismatch("list", self.cursor))?;
for item in items {
let mut sub = Self::new_with_settings(item, self.settings.clone());
consumer(&mut sub)?;
}
Ok(())
}
fn read_map(
&mut self,
_schema: &Schema<'_>,
consumer: &mut dyn FnMut(String, &mut dyn ShapeDeserializer) -> Result<(), SerdeError>,
) -> Result<(), SerdeError> {
let entries = self
.cursor
.as_object()
.ok_or_else(|| type_mismatch("map", self.cursor))?;
for (key, value) in entries {
let mut sub = Self::new_with_settings(value, self.settings.clone());
consumer(key.clone(), &mut sub)?;
}
Ok(())
}
fn read_boolean(&mut self, _schema: &Schema<'_>) -> Result<bool, SerdeError> {
self.cursor
.as_bool()
.ok_or_else(|| type_mismatch("boolean", self.cursor))
}
fn read_byte(&mut self, _schema: &Schema<'_>) -> Result<i8, SerdeError> {
coerce_signed::<i8>(self.cursor, "byte")
}
fn read_short(&mut self, _schema: &Schema<'_>) -> Result<i16, SerdeError> {
coerce_signed::<i16>(self.cursor, "short")
}
fn read_integer(&mut self, _schema: &Schema<'_>) -> Result<i32, SerdeError> {
coerce_signed::<i32>(self.cursor, "integer")
}
fn read_long(&mut self, _schema: &Schema<'_>) -> Result<i64, SerdeError> {
coerce_signed::<i64>(self.cursor, "long")
}
fn read_float(&mut self, _schema: &Schema<'_>) -> Result<f32, SerdeError> {
coerce_float(self.cursor)
}
fn read_double(&mut self, _schema: &Schema<'_>) -> Result<f64, SerdeError> {
coerce_double(self.cursor)
}
fn read_big_integer(&mut self, _schema: &Schema<'_>) -> Result<BigInteger, SerdeError> {
coerce_big_integer(self.cursor)
}
fn read_big_decimal(&mut self, _schema: &Schema<'_>) -> Result<BigDecimal, SerdeError> {
coerce_big_decimal(self.cursor)
}
fn read_string(&mut self, _schema: &Schema<'_>) -> Result<String, SerdeError> {
match self.cursor {
Document::String(s) => Ok(s.clone()),
other => Err(type_mismatch("string", other)),
}
}
fn read_blob(&mut self, _schema: &Schema<'_>) -> Result<Blob, SerdeError> {
match (self.cursor, &self.settings) {
(Document::String(s), Some(settings)) => settings
.coerce_string_to_blob(s)
.map(Blob::new)
.map_err(SerdeError::from),
(Document::String(s), None) => aws_smithy_types::base64::decode(s)
.map(Blob::new)
.map_err(|e| {
SerdeError::custom(format!("cannot base64-decode string as blob: {e}"))
}),
(other, _) => Err(type_mismatch("blob", other)),
}
}
fn read_timestamp(&mut self, _schema: &Schema<'_>) -> Result<DateTime, SerdeError> {
match (self.cursor, &self.settings) {
(Document::Number(n), Some(settings)) => settings
.coerce_number_to_timestamp(n)
.map_err(SerdeError::from),
(Document::Number(n), None) => number_to_timestamp(n),
(Document::String(s), Some(settings)) => settings
.coerce_string_to_timestamp(s)
.map_err(SerdeError::from),
(Document::String(s), None) => DateTime::from_str(s, Format::DateTime).map_err(|e| {
SerdeError::custom(format!("cannot parse string as a date-time timestamp: {e}"))
}),
(other, _) => Err(type_mismatch("timestamp", other)),
}
}
fn read_document(&mut self, _schema: &Schema<'_>) -> Result<Document, SerdeError> {
Ok(self.cursor.clone())
}
fn is_null(&self) -> bool {
matches!(self.cursor, Document::Null)
}
fn skip_value(&mut self) -> Result<(), SerdeError> {
Ok(())
}
fn container_size(&self) -> Option<usize> {
let raw = match self.cursor {
Document::Array(items) => items.len(),
Document::Object(entries) => entries.len(),
_ => return None,
};
Some(capped_container_size(raw))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use super::*;
use crate::document::DocumentShapeSerializer;
use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer, ShapeSerializer};
use crate::{prelude, shape_id, Schema, ShapeId, ShapeType};
use aws_smithy_types::Number;
const PERSON_ID: ShapeId<'static> = shape_id!("smithy.example", "Person");
const PERSON_NAME_ID: ShapeId<'static> = shape_id!("smithy.example", "Person", "name");
const PERSON_AGE_ID: ShapeId<'static> = shape_id!("smithy.example", "Person", "age");
static PERSON_NAME_MEMBER: Schema<'static> =
Schema::new_member(PERSON_NAME_ID, ShapeType::String, "name", 0);
static PERSON_AGE_MEMBER: Schema<'static> =
Schema::new_member(PERSON_AGE_ID, ShapeType::Integer, "age", 1);
static PERSON_SCHEMA: Schema<'static> = Schema::new_struct(
PERSON_ID,
ShapeType::Structure,
&[&PERSON_NAME_MEMBER, &PERSON_AGE_MEMBER],
);
#[derive(Debug, Default, PartialEq)]
struct Person {
name: Option<String>,
age: Option<i32>,
}
impl SerializableStruct for Person {
fn schema(&self) -> &Schema<'_> {
&PERSON_SCHEMA
}
fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
if let Some(n) = &self.name {
ser.write_string(&PERSON_NAME_MEMBER, n)?;
}
if let Some(a) = self.age {
ser.write_integer(&PERSON_AGE_MEMBER, a)?;
}
Ok(())
}
}
fn deserialize_person(deser: &mut dyn ShapeDeserializer) -> Result<Person, SerdeError> {
let mut out = Person::default();
deser.read_struct(&PERSON_SCHEMA, &mut |member, sub| {
match member.member_index() {
Some(0) => out.name = Some(sub.read_string(member)?),
Some(1) => out.age = Some(sub.read_integer(member)?),
_ => {}
}
Ok(())
})?;
Ok(out)
}
#[test]
fn read_string_returns_value() {
let doc = Document::String("hello".to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(deser.read_string(&prelude::STRING).unwrap(), "hello");
}
#[test]
fn read_string_on_non_string_errors() {
let doc = Document::Number(Number::PosInt(1));
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deser.read_string(&prelude::STRING).unwrap_err();
assert!(matches!(err, SerdeError::TypeMismatch { .. }));
}
#[test]
fn read_integer_with_coercion() {
let doc = Document::Number(Number::PosInt(42));
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(deser.read_integer(&prelude::INTEGER).unwrap(), 42);
}
#[test]
fn read_integer_overflow_errors() {
let doc = Document::Number(Number::PosInt(u64::MAX));
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deser.read_integer(&prelude::INTEGER).unwrap_err();
assert!(matches!(err, SerdeError::NumericCoercionOverflow { .. }));
}
#[test]
fn read_boolean_returns_value() {
let doc = Document::Bool(true);
let mut deser = DocumentShapeDeserializer::new(&doc);
assert!(deser.read_boolean(&prelude::BOOLEAN).unwrap());
}
#[test]
fn read_blob_decodes_base64_string_without_settings() {
let doc = Document::String("YWJjZA==".to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
let blob = deser.read_blob(&prelude::BLOB).unwrap();
assert_eq!(blob.as_ref(), b"abcd");
}
#[test]
fn read_blob_on_malformed_base64_errors() {
let doc = Document::String("not base64!!!".to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deser.read_blob(&prelude::BLOB).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("base64"), "unexpected error message: {msg}");
}
#[test]
fn read_blob_on_non_string_errors() {
let doc = Document::Number(Number::PosInt(1));
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deser.read_blob(&prelude::BLOB).unwrap_err();
assert!(matches!(err, SerdeError::TypeMismatch { .. }));
}
#[test]
fn read_timestamp_defaults_to_epoch_seconds_without_settings() {
let doc = Document::Number(Number::PosInt(1234));
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
DateTime::from_secs(1234)
);
let doc = Document::Number(Number::NegInt(-9));
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
DateTime::from_secs(-9)
);
}
#[test]
fn read_timestamp_retains_fractional_seconds() {
let doc = Document::Number(Number::Float(1234.5));
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
DateTime::from_secs_f64(1234.5)
);
}
#[test]
fn read_timestamp_parses_date_time_string_without_settings() {
let doc = Document::String("1970-01-01T00:00:00Z".to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser.read_timestamp(&prelude::TIMESTAMP).unwrap(),
DateTime::from_secs(0)
);
}
#[test]
fn read_timestamp_on_non_coercible_variant_errors() {
let doc = Document::Bool(true);
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deser.read_timestamp(&prelude::TIMESTAMP).unwrap_err();
assert!(matches!(err, SerdeError::TypeMismatch { .. }));
}
#[test]
fn read_big_integer_and_decimal_reverse_the_string_representation() {
let big = "12345678901234567890123456789";
let doc = Document::String(big.to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap()
.as_ref(),
big
);
let dec = "12345678901234567890123456789.25";
let doc = Document::String(dec.to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(
deser
.read_big_decimal(&prelude::BIG_DECIMAL)
.unwrap()
.as_ref(),
dec
);
}
#[test]
fn is_null_for_null_document() {
let doc = Document::Null;
let deser = DocumentShapeDeserializer::new(&doc);
assert!(deser.is_null());
}
#[test]
fn is_null_false_for_non_null() {
let doc = Document::String("not-null".to_string());
let deser = DocumentShapeDeserializer::new(&doc);
assert!(!deser.is_null());
}
#[test]
fn read_list_iterates_elements() {
let doc = Document::Array(vec![
Document::String("a".to_string()),
Document::String("b".to_string()),
Document::String("c".to_string()),
]);
let mut deser = DocumentShapeDeserializer::new(&doc);
let mut collected = Vec::new();
deser
.read_list(&prelude::DOCUMENT, &mut |sub| {
collected.push(sub.read_string(&prelude::STRING)?);
Ok(())
})
.unwrap();
assert_eq!(collected, ["a", "b", "c"]);
}
#[test]
fn read_map_iterates_entries() {
let doc = Document::Object(HashMap::from([
("k1".to_string(), Document::String("v1".to_string())),
("k2".to_string(), Document::String("v2".to_string())),
]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let mut collected = HashMap::new();
deser
.read_map(&prelude::DOCUMENT, &mut |key, sub| {
collected.insert(key, sub.read_string(&prelude::STRING)?);
Ok(())
})
.unwrap();
assert_eq!(collected.get("k1").map(String::as_str), Some("v1"));
assert_eq!(collected.get("k2").map(String::as_str), Some("v2"));
}
#[test]
fn container_size_on_list() {
let doc = Document::Array(vec![Document::Null; 5]);
let deser = DocumentShapeDeserializer::new(&doc);
assert_eq!(deser.container_size(), Some(5));
}
#[test]
fn container_size_on_scalar_is_none() {
let doc = Document::String("foo".to_string());
let deser = DocumentShapeDeserializer::new(&doc);
assert!(deser.container_size().is_none());
}
#[test]
fn read_struct_with_consumer_dispatch() {
let doc = Document::Object(HashMap::from([
("name".to_string(), Document::String("Alex".to_string())),
("age".to_string(), Document::Number(Number::PosInt(30))),
]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let person = deserialize_person(&mut deser).unwrap();
assert_eq!(
person,
Person {
name: Some("Alex".into()),
age: Some(30),
}
);
}
#[test]
fn read_struct_with_missing_optional_member() {
let doc = Document::Object(HashMap::from([(
"name".to_string(),
Document::String("Sam".to_string()),
)]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let person = deserialize_person(&mut deser).unwrap();
assert_eq!(
person,
Person {
name: Some("Sam".into()),
age: None,
}
);
}
#[test]
fn read_struct_ignores_unknown_members() {
let doc = Document::Object(HashMap::from([
("name".to_string(), Document::String("Joe".to_string())),
(
"unknown_field".to_string(),
Document::String("ignored".to_string()),
),
]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let person = deserialize_person(&mut deser).unwrap();
assert_eq!(person.name.as_deref(), Some("Joe"));
}
#[test]
fn read_struct_on_non_map_errors() {
let doc = Document::String("not-a-struct".to_string());
let mut deser = DocumentShapeDeserializer::new(&doc);
let err = deserialize_person(&mut deser).unwrap_err();
assert!(matches!(err, SerdeError::TypeMismatch { .. }));
}
#[test]
fn round_trip_through_document() {
let original = Person {
name: Some("Iago".into()),
age: Some(7),
};
let mut ser = DocumentShapeSerializer::new();
ser.write_struct(&PERSON_SCHEMA, &original).unwrap();
let doc = ser.finish().unwrap();
let mut deser = DocumentShapeDeserializer::new(doc.document());
let restored = deserialize_person(&mut deser).unwrap();
assert_eq!(restored, original);
assert_eq!(doc.discriminator(), Some("smithy.example#Person"));
}
fn num(n: Number) -> Document {
Document::Number(n)
}
#[test]
fn read_signed_integers_coerce_across_bounded_integer_sources() {
let doc = num(Number::PosInt(42));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_byte(&prelude::BYTE)
.unwrap(),
42
);
let doc = num(Number::NegInt(-42));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_byte(&prelude::BYTE)
.unwrap(),
-42
);
let doc = num(Number::PosInt(1234));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_short(&prelude::SHORT)
.unwrap(),
1234
);
let doc = num(Number::PosInt(i32::MAX as u64));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_integer(&prelude::INTEGER)
.unwrap(),
i32::MAX
);
let doc = num(Number::NegInt(i32::MIN as i64));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_integer(&prelude::INTEGER)
.unwrap(),
i32::MIN
);
let doc = num(Number::PosInt(i64::MAX as u64));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_long(&prelude::LONG)
.unwrap(),
i64::MAX
);
}
#[test]
fn read_byte_overflow_populates_target_and_value() {
let doc = num(Number::PosInt(200));
let err = DocumentShapeDeserializer::new(&doc)
.read_byte(&prelude::BYTE)
.unwrap_err();
match err {
SerdeError::NumericCoercionOverflow { target, value } => {
assert_eq!(target, "byte");
assert_eq!(value, "200");
}
other => panic!("expected NumericCoercionOverflow, got {other:?}"),
}
}
#[test]
fn read_signed_integers_overflow_at_the_edges() {
let doc = num(Number::NegInt(i64::from(i32::MIN) - 1));
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_integer(&prelude::INTEGER)
.unwrap_err(),
SerdeError::NumericCoercionOverflow { .. }
));
let doc = num(Number::PosInt(u64::MAX));
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_long(&prelude::LONG)
.unwrap_err(),
SerdeError::NumericCoercionOverflow { .. }
));
}
#[test]
fn read_byte_on_non_numeric_is_type_mismatch() {
let doc = Document::Bool(true);
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_byte(&prelude::BYTE)
.unwrap_err(),
SerdeError::TypeMismatch { .. }
));
}
#[test]
fn integer_reads_accept_integral_floats_and_reject_fractional_ones() {
let read_all = |value: f64| {
let doc = num(Number::Float(value));
let de = || DocumentShapeDeserializer::new(&doc);
(
de().read_byte(&prelude::BYTE),
de().read_short(&prelude::SHORT),
de().read_integer(&prelude::INTEGER),
de().read_long(&prelude::LONG),
de().read_big_integer(&prelude::BIG_INTEGER),
)
};
let (b, sh, i, l, bi) = read_all(42.0);
assert_eq!(
(b.unwrap(), sh.unwrap(), i.unwrap(), l.unwrap()),
(42, 42, 42, 42)
);
assert_eq!(bi.unwrap().as_ref(), "42");
assert_eq!(read_all(-0.0).2.unwrap(), 0);
assert_eq!(read_all(-0.0).4.unwrap().as_ref(), "0");
for value in [42.7, -0.5, f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
let (b, sh, i, l, bi) = read_all(value);
for err in [b.err(), sh.err(), i.err(), l.err(), bi.err()] {
assert!(
matches!(err, Some(SerdeError::NumericCoercionLossy { .. })),
"{value}: {err:?}"
);
}
}
}
#[test]
fn integral_float_range_checks_are_exact() {
let long = |v: f64| {
DocumentShapeDeserializer::new(&num(Number::Float(v))).read_long(&prelude::LONG)
};
let int = |v: f64| {
DocumentShapeDeserializer::new(&num(Number::Float(v))).read_integer(&prelude::INTEGER)
};
const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
assert_eq!(long(-TWO_POW_63).unwrap(), i64::MIN);
assert!(matches!(
long(TWO_POW_63),
Err(SerdeError::NumericCoercionOverflow { .. })
));
assert_eq!(int(2_147_483_647.0).unwrap(), i32::MAX);
assert_eq!(int(-2_147_483_648.0).unwrap(), i32::MIN);
assert!(matches!(
int(2_147_483_648.0),
Err(SerdeError::NumericCoercionOverflow { .. })
));
let big = DocumentShapeDeserializer::new(&num(Number::Float(1e20)))
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap();
assert_eq!(big.as_ref(), "100000000000000000000");
let big = DocumentShapeDeserializer::new(&num(Number::Float(TWO_POW_63)))
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap();
assert_eq!(big.as_ref(), "9223372036854775808");
}
#[test]
fn read_double_and_float_widen_from_integer_sources() {
let doc = num(Number::PosInt(42));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap(),
42.0
);
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_float(&prelude::FLOAT)
.unwrap(),
42.0_f32
);
let doc = num(Number::NegInt(-42));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap(),
-42.0
);
let doc = num(Number::Float(1.5));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_float(&prelude::FLOAT)
.unwrap(),
1.5_f32
);
}
#[test]
fn read_double_carries_non_finite_floats_intact() {
let doc = num(Number::Float(f64::NAN));
assert!(DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap()
.is_nan());
let doc = num(Number::Float(f64::INFINITY));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap(),
f64::INFINITY
);
let doc = num(Number::Float(f64::NEG_INFINITY));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap(),
f64::NEG_INFINITY
);
}
#[test]
fn read_big_integer_truncates_a_decimal_string_as_text() {
let doc = Document::String("12345678901234567890.123".to_owned());
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap()
.as_ref(),
"12345678901234567890"
);
}
#[test]
fn read_big_integer_expands_scientific_notation() {
let doc = Document::String("1.23e10".to_owned());
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap()
.as_ref(),
"12300000000"
);
let doc = Document::String("1e30".to_owned());
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap()
.as_ref(),
"1000000000000000000000000000000"
);
}
#[test]
fn read_big_integer_errors_on_unmaterializable_exponent() {
let doc = Document::String("1e1000000000".to_owned());
let err = DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap_err();
assert!(
err.to_string().contains("too large"),
"expected a 'too large' error, got: {err}"
);
}
#[test]
fn read_big_integer_rejects_non_numeric_text() {
let doc = Document::String("not a number".to_owned());
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap_err(),
SerdeError::InvalidInput { .. }
));
}
#[test]
fn read_big_numbers_coerce_from_integer_sources() {
let doc = num(Number::PosInt(7));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap()
.as_ref(),
"7"
);
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_decimal(&prelude::BIG_DECIMAL)
.unwrap()
.as_ref(),
"7"
);
let doc = num(Number::NegInt(-7));
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_decimal(&prelude::BIG_DECIMAL)
.unwrap()
.as_ref(),
"-7"
);
}
#[test]
fn read_big_decimal_rejects_non_finite_floats() {
let doc = num(Number::Float(f64::INFINITY));
let err = DocumentShapeDeserializer::new(&doc)
.read_big_decimal(&prelude::BIG_DECIMAL)
.unwrap_err();
match err {
SerdeError::Custom { message } => assert!(message.contains("non-finite")),
other => panic!("expected Custom non-finite error, got {other:?}"),
}
}
#[test]
fn big_number_reads_reject_structural_variants() {
let doc = Document::Array(vec![]);
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap_err(),
SerdeError::TypeMismatch { .. }
));
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_big_decimal(&prelude::BIG_DECIMAL)
.unwrap_err(),
SerdeError::TypeMismatch { .. }
));
assert!(matches!(
DocumentShapeDeserializer::new(&doc)
.read_double(&prelude::DOUBLE)
.unwrap_err(),
SerdeError::TypeMismatch { .. }
));
}
#[test]
fn big_integer_legacy_string_round_trips() {
let bi = BigInteger::from_str("98765432109876543210").unwrap();
let doc = Document::String(bi.as_ref().to_owned());
assert_eq!(
DocumentShapeDeserializer::new(&doc)
.read_big_integer(&prelude::BIG_INTEGER)
.unwrap(),
bi
);
}
#[test]
fn big_decimal_to_integer_string_truncates_and_expands() {
let cases = [
("0", "0"),
("123", "123"),
("123.99", "123"), ("-123.99", "-123"),
("0.5", "0"),
("-0.5", "0"), ("1.23e10", "12300000000"), ("1.23e1", "12"), ("1.5e1", "15"),
("1e3", "1000"),
("5e-3", "0"), ("-1.23e10", "-12300000000"),
("10.0", "10"),
("+123", "123"), (".5", "0"), ("-.5", "0"), ("1.", "1"), ("+1.23e1", "12"), ("00123", "123"), ("1.23E10", "12300000000"), ];
for (input, expected) in cases {
let bd = BigDecimal::from_str(input).unwrap();
assert_eq!(
big_decimal_to_integer_string(&bd).as_deref(),
Some(expected),
"big_decimal_to_integer_string({input:?})"
);
}
}
#[test]
fn big_decimal_to_integer_string_guards_pathological_exponent() {
assert_eq!(
big_decimal_to_integer_string(&BigDecimal::from_str("1e1000000000").unwrap()),
None
);
assert_eq!(
big_decimal_to_integer_string(
&BigDecimal::from_str("1.0e9223372036854775808").unwrap()
),
None
);
}
}
#[cfg(test)]
mod skip_value_contract {
use super::*;
use crate::serde::ShapeDeserializer;
use crate::{shape_id, Schema, ShapeType};
use std::collections::HashMap;
static NAME: Schema<'static> = Schema::new_member(
shape_id!("smithy.example", "Person", "name"),
ShapeType::String,
"name",
0,
);
static AGE: Schema<'static> = Schema::new_member(
shape_id!("smithy.example", "Person", "age"),
ShapeType::Integer,
"age",
1,
);
static PERSON: Schema<'static> = Schema::new_struct(
shape_id!("smithy.example", "Person"),
ShapeType::Structure,
&[&NAME, &AGE],
);
#[test]
fn skipping_one_member_leaves_the_others_readable() {
let doc = Document::Object(HashMap::from([
("name".to_string(), Document::String("Alex".into())),
(
"age".to_string(),
Document::Number(aws_smithy_types::Number::PosInt(30)),
),
]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let mut age = None;
deser
.read_struct(&PERSON, &mut |member, d| {
match member.member_index() {
Some(0) => d.skip_value()?,
Some(1) => age = Some(d.read_integer(member)?),
_ => {}
}
Ok(())
})
.unwrap();
assert_eq!(age, Some(30));
}
#[test]
fn skipping_an_aggregate_member_is_a_no_op() {
let doc = Document::Object(HashMap::from([
(
"name".to_string(),
Document::Array(vec![
Document::String("nested".into()),
Document::Object(HashMap::from([(
"age".to_string(),
Document::Number(aws_smithy_types::Number::PosInt(999)),
)])),
]),
),
(
"age".to_string(),
Document::Number(aws_smithy_types::Number::PosInt(30)),
),
]));
let mut deser = DocumentShapeDeserializer::new(&doc);
let mut age = None;
deser
.read_struct(&PERSON, &mut |member, d| {
match member.member_index() {
Some(0) => d.skip_value()?,
Some(1) => age = Some(d.read_integer(member)?),
_ => {}
}
Ok(())
})
.unwrap();
assert_eq!(age, Some(30), "the decoy nested `age` must not be read");
}
#[test]
fn skip_value_does_not_consume_the_cursor() {
let doc = Document::String("still here".into());
let mut deser = DocumentShapeDeserializer::new(&doc);
let dynamic: &mut dyn ShapeDeserializer = &mut deser;
dynamic.skip_value().unwrap();
assert_eq!(deser.read_string(&NAME).unwrap(), "still here");
}
}