use aws_smithy_types::{DiscriminatedDocument, Number};
use super::{DocumentShapeDeserializer, DocumentShapeSerializer};
use crate::serde::{SerdeError, SerializableStruct, ShapeDeserializer};
use crate::{Schema, ShapeType};
pub trait DiscriminatedDocumentExt {
fn from_struct(
schema: &Schema<'_>,
value: &dyn SerializableStruct,
) -> Result<DiscriminatedDocument, SerdeError>;
fn as_shape<T, F>(&self, deserialize: F) -> Result<T, SerdeError>
where
F: FnOnce(&mut dyn ShapeDeserializer) -> Result<T, SerdeError>;
fn shape_type(&self) -> ShapeType;
}
impl DiscriminatedDocumentExt for DiscriminatedDocument {
fn from_struct(
schema: &Schema<'_>,
value: &dyn SerializableStruct,
) -> Result<DiscriminatedDocument, SerdeError> {
let mut ser = DocumentShapeSerializer::default();
ser.write_struct(schema, value)?;
ser.finish()
}
fn as_shape<T, F>(&self, deserialize: F) -> Result<T, SerdeError>
where
F: FnOnce(&mut dyn ShapeDeserializer) -> Result<T, SerdeError>,
{
let mut deser =
DocumentShapeDeserializer::new_with_settings(self.document(), self.settings().cloned());
deserialize(&mut deser)
}
fn shape_type(&self) -> ShapeType {
use aws_smithy_types::Document as Doc;
match self.document() {
Doc::Null => ShapeType::Document,
Doc::Bool(_) => ShapeType::Boolean,
Doc::Number(n) => number_shape_type(n),
Doc::String(_) => ShapeType::String,
Doc::Array(_) => ShapeType::List,
Doc::Object(_) => ShapeType::Map,
}
}
}
fn number_shape_type(n: &Number) -> ShapeType {
match n {
Number::PosInt(v) => {
if *v <= i32::MAX as u64 {
ShapeType::Integer
} else if *v <= i64::MAX as u64 {
ShapeType::Long
} else {
ShapeType::BigInteger
}
}
Number::NegInt(v) => {
if *v >= i32::MIN as i64 && *v <= i32::MAX as i64 {
ShapeType::Integer
} else {
ShapeType::Long
}
}
Number::Float(v) => {
match crate::schema::document::deserializer::float_as_i64(*v) {
Some(i) if i32::try_from(i).is_ok() => ShapeType::Integer,
Some(_) => ShapeType::Long,
None => ShapeType::Double,
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::serde::{SerdeError, ShapeSerializer};
use crate::{prelude, shape_id, Schema, ShapeId, ShapeType};
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 from_struct_attaches_fqn_discriminator() {
let p = Person {
name: Some("Iago".into()),
age: Some(7),
};
let doc = DiscriminatedDocument::from_struct(&PERSON_SCHEMA, &p).unwrap();
assert_eq!(doc.discriminator(), Some("smithy.example#Person"));
assert!(doc.settings().is_none());
let map = doc.document().as_object().unwrap();
assert_eq!(map.len(), 2);
assert!(map.contains_key("name"));
assert!(map.contains_key("age"));
}
#[test]
fn from_struct_with_only_optional_members_set() {
let p = Person {
name: Some("Sam".into()),
age: None,
};
let doc = DiscriminatedDocument::from_struct(&PERSON_SCHEMA, &p).unwrap();
let map = doc.document().as_object().unwrap();
assert_eq!(map.len(), 1);
assert!(map.contains_key("name"));
}
#[test]
fn as_shape_reverses_from_struct() {
let original = Person {
name: Some("Alex".into()),
age: Some(30),
};
let doc = DiscriminatedDocument::from_struct(&PERSON_SCHEMA, &original).unwrap();
let restored: Person = doc.as_shape(|deser| deserialize_person(deser)).unwrap();
assert_eq!(restored, original);
}
#[test]
fn as_shape_works_on_directly_constructed_document() {
let mut map = std::collections::HashMap::new();
map.insert(
"name".to_string(),
aws_smithy_types::Document::String("Joe".into()),
);
map.insert(
"age".to_string(),
aws_smithy_types::Document::Number(aws_smithy_types::Number::PosInt(42)),
);
let doc = DiscriminatedDocument::new(aws_smithy_types::Document::Object(map));
let person: Person = doc.as_shape(|deser| deserialize_person(deser)).unwrap();
assert_eq!(
person,
Person {
name: Some("Joe".into()),
age: Some(42),
}
);
}
#[test]
fn shape_type_reports_each_base_variant() {
use aws_smithy_types::{Document, Number};
let cases = [
(Document::Null, ShapeType::Document),
(Document::Bool(true), ShapeType::Boolean),
(Document::String("x".into()), ShapeType::String),
(Document::Number(Number::PosInt(0)), ShapeType::Integer),
(
Document::Number(Number::PosInt(u64::MAX)),
ShapeType::BigInteger,
),
(Document::Array(vec![]), ShapeType::List),
(Document::Object(Default::default()), ShapeType::Map),
];
for (doc, expected) in cases {
let wrapped = DiscriminatedDocument::new(doc);
assert_eq!(wrapped.shape_type(), expected);
}
}
#[test]
fn reported_shape_type_can_be_read_back() {
use crate::prelude::{DOUBLE, INTEGER, LONG};
use aws_smithy_types::{Document, Number};
const TWO_POW_63: f64 = 9_223_372_036_854_775_808.0;
for (value, expected) in [
(42.0, ShapeType::Integer),
(-1.0, ShapeType::Integer),
(2_147_483_647.0, ShapeType::Integer),
(-2_147_483_648.0, ShapeType::Integer),
(2_147_483_648.0, ShapeType::Long),
(-TWO_POW_63, ShapeType::Long),
(TWO_POW_63, ShapeType::Double),
(1e20, ShapeType::Double),
(1.5, ShapeType::Double),
(f64::NAN, ShapeType::Double),
] {
let doc = DiscriminatedDocument::new(Document::Number(Number::Float(value)));
let reported = doc.shape_type();
assert_eq!(reported, expected, "{value}");
let read = match reported {
ShapeType::Integer => doc.as_shape(|de| de.read_integer(&INTEGER)).map(|_| ()),
ShapeType::Long => doc.as_shape(|de| de.read_long(&LONG)).map(|_| ()),
ShapeType::Double => doc.as_shape(|de| de.read_double(&DOUBLE)).map(|_| ()),
other => panic!("unexpected report {other:?} for {value:?}"),
};
assert!(
read.is_ok(),
"reported {reported:?} for {value:?}, read failed: {read:?}"
);
}
}
#[test]
fn from_struct_with_blob_member_round_trips() {
const BLOBBY_ID: ShapeId<'static> = shape_id!("smithy.example", "Blobby");
const BLOBBY_DATA_ID: ShapeId<'static> = shape_id!("smithy.example", "Blobby", "data");
static BLOBBY_DATA_MEMBER: Schema<'static> =
Schema::new_member(BLOBBY_DATA_ID, ShapeType::Blob, "data", 0);
static BLOBBY_SCHEMA: Schema<'static> =
Schema::new_struct(BLOBBY_ID, ShapeType::Structure, &[&BLOBBY_DATA_MEMBER]);
struct Blobby {
data: Vec<u8>,
}
impl SerializableStruct for Blobby {
fn schema(&self) -> &Schema<'_> {
&BLOBBY_SCHEMA
}
fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
ser.write_blob(
&BLOBBY_DATA_MEMBER,
aws_smithy_types::Blob::new(self.data.clone()),
)
}
}
let doc = DiscriminatedDocument::from_struct(
&BLOBBY_SCHEMA,
&Blobby {
data: b"raw".to_vec(),
},
)
.expect("from_struct should succeed for blob members");
assert_eq!(doc.discriminator(), Some("smithy.example#Blobby"));
let map = doc.document().as_object().unwrap();
match map.get("data").unwrap() {
aws_smithy_types::Document::String(s) => assert_eq!(s, "cmF3"),
other => panic!("expected a base64 String in 'data' field, got {other:?}"),
}
let member = DiscriminatedDocument::new(map.get("data").unwrap().clone());
assert_eq!(member.as_blob().unwrap().as_ref(), b"raw");
}
#[test]
fn as_shape_on_top_level_blob_document() {
let doc = DiscriminatedDocument::new(aws_smithy_types::Document::String("eA==".to_owned()));
let blob = doc
.as_shape(|deser| deser.read_blob(&prelude::BLOB))
.expect("as_shape on a Blob document should succeed");
assert_eq!(blob.as_ref(), b"x");
}
#[test]
fn from_struct_then_as_shape_preserves_data_but_ignores_discriminator() {
let original = Person {
name: Some("Lee".into()),
age: Some(25),
};
let doc = DiscriminatedDocument::from_struct(&PERSON_SCHEMA, &original).unwrap();
assert_eq!(doc.discriminator(), Some("smithy.example#Person"));
let restored: Person = doc.as_shape(|d| deserialize_person(d)).unwrap();
assert_eq!(restored, original);
}
}