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::Blob(_) => ShapeType::Blob,
Doc::Timestamp(_) => ShapeType::Timestamp,
Doc::BigInteger(_) => ShapeType::BigInteger,
Doc::BigDecimal(_) => ShapeType::BigDecimal,
Doc::String(_) => ShapeType::String,
Doc::Array(_) => ShapeType::List,
Doc::Object(_) => ShapeType::Map,
_ => ShapeType::Document,
}
}
}
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) => {
if v.is_finite() && v.fract() == 0.0 {
if (i32::MIN as f64..=i32::MAX as f64).contains(v) {
ShapeType::Integer
} else if (i64::MIN as f64..=i64::MAX as f64).contains(v) {
ShapeType::Long
} else {
ShapeType::Double
}
} else {
ShapeType::Double
}
}
}
}
#[cfg(test)]
mod tests {
use aws_smithy_types::DateTime;
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 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() {
use aws_smithy_types::document::DocumentObject;
let mut map = DocumentObject::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 shape_type_reports_each_extended_variant() {
use aws_smithy_types::{BigDecimal, BigInteger, Document};
use std::str::FromStr;
let cases: [(Document, ShapeType); 4] = [
(Document::Blob(vec![1, 2, 3]), ShapeType::Blob),
(
Document::Timestamp(DateTime::from_secs(0)),
ShapeType::Timestamp,
),
(
Document::BigInteger(BigInteger::from_str("1").unwrap()),
ShapeType::BigInteger,
),
(
Document::BigDecimal(BigDecimal::from_str("1.0").unwrap()),
ShapeType::BigDecimal,
),
];
for (doc, expected) in cases {
let wrapped = DiscriminatedDocument::new(doc);
assert_eq!(wrapped.shape_type(), expected);
}
}
#[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 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::Blob(b) => assert_eq!(b.as_slice(), b"raw"),
other => panic!("expected Blob in 'data' field, got {other:?}"),
}
}
#[test]
fn as_shape_on_top_level_blob_document() {
let doc = DiscriminatedDocument::new(aws_smithy_types::Document::Blob(b"x".to_vec()));
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);
}
}