mod key;
mod key_id;
use core::{fmt, marker::PhantomData, ops::Deref};
use std::collections::HashMap;
use serde::{Serialize, de::DeserializeOwned};
use super::{
AttributeDefinition, AttributeList, AttributeValue, AttributeValueRef, CompositeKey,
CompositeKeySchema, HasTableKeyAttributes, KeySchema, KeySchemaKind, Result, SimpleKey,
SimpleKeySchema, TableDefinition,
};
pub use key::{Key, KeyBuilder};
pub use key_id::*;
pub trait DynamoDBItem<TD: TableDefinition>:
Sized + HasTableKeyAttributes<TD> + KeyBuilder<TD>
{
type AdditionalAttributes: AttributeList<TD, Self>;
fn to_item(&self) -> Item<TD>
where
Self: Serialize,
{
let minimal_item = Item::minimal_from(self);
let item: HashMap<_, _> = serde_dynamo::to_item(self).expect("valid serialization");
minimal_item.with_attributes(item)
}
fn try_from_item(item: Item<TD>) -> Result<Self>
where
Self: DeserializeOwned,
{
Ok(serde_dynamo::from_item(item.into_inner())?)
}
fn from_item(item: Item<TD>) -> Self
where
Self: DeserializeOwned,
{
Self::try_from_item(item).expect("valid schema")
}
}
#[derive(Clone)]
pub struct Item<TD: TableDefinition>(HashMap<String, AttributeValue>, PhantomData<TD>);
impl<TD: TableDefinition> fmt::Debug for Item<TD> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
pub(crate) type PartitionKeyDefinition<TD> =
<<TD as TableDefinition>::KeySchema as KeySchema>::PartitionKey;
type PartitionKeyType<TD> = <PartitionKeyDefinition<TD> as AttributeDefinition>::Type;
type SortKeyDefinition<TD> = <<TD as TableDefinition>::KeySchema as CompositeKeySchema>::SortKey;
type SortKeyType<TD> = <SortKeyDefinition<TD> as AttributeDefinition>::Type;
impl<TD: TableDefinition> Item<TD> {
pub fn pk(&self) -> <PartitionKeyType<TD> as AttributeValueRef>::Ref<'_> {
self.attribute::<PartitionKeyDefinition<TD>>()
.expect("PK is always present")
}
pub fn attribute<A: AttributeDefinition>(
&self,
) -> Option<<A::Type as AttributeValueRef>::Ref<'_>> {
self.0
.get(A::NAME)
.map(<A::Type as AttributeValueRef>::attribute_value_ref)
}
pub fn into_inner(self) -> HashMap<String, AttributeValue> {
self.0
}
pub(crate) fn from_dynamodb_response(item: HashMap<String, AttributeValue>) -> Self {
Self(item, PhantomData)
}
pub fn minimal_from<DBI: DynamoDBItem<TD>>(dynamodb_item: &DBI) -> Self {
let key = dynamodb_item.get_key();
let additional_attributes = DBI::AdditionalAttributes::get_attributes(dynamodb_item);
Item::from_key_and_attributes(key, additional_attributes)
}
pub fn with_attributes(self, attributes: impl Into<HashMap<String, AttributeValue>>) -> Self {
let mut item = attributes.into();
item.extend(self.0);
Self(item, PhantomData)
}
pub fn from_key_and_attributes(
key: Key<TD>,
attributes: impl Into<HashMap<String, AttributeValue>>,
) -> Self {
let mut item = attributes.into();
item.extend(key.into_inner());
Self(item, PhantomData)
}
}
impl<TD: TableDefinition> Item<TD>
where
TD::KeySchema: CompositeKeySchema,
{
pub fn sk(&self) -> <SortKeyType<TD> as AttributeValueRef>::Ref<'_> {
self.attribute::<SortKeyDefinition<TD>>()
.expect("SK is always present")
}
}
impl<TD: TableDefinition> Deref for Item<TD> {
type Target = HashMap<String, AttributeValue>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<TD: TableDefinition> IntoIterator for Item<TD> {
type Item = <HashMap<String, AttributeValue> as IntoIterator>::Item;
type IntoIter = <HashMap<String, AttributeValue> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use aws_sdk_dynamodb::types::AttributeValue;
use super::super::test_fixtures::*;
use super::*;
#[test]
fn test_item_minimal_from_user() {
let user = sample_user();
let item = Item::<PlatformTable>::minimal_from(&user);
assert_eq!(
item.get("PK"),
Some(&AttributeValue::S("USER#user-1".to_owned()))
);
assert_eq!(item.get("SK"), Some(&AttributeValue::S("USER".to_owned())));
assert_eq!(
item.get("_TYPE"),
Some(&AttributeValue::S("USER".to_owned()))
);
assert!(!item.contains_key("email"));
assert_eq!(item.len(), 3);
}
#[test]
fn test_item_with_attributes_key_takes_precedence() {
let item = sample_user().to_item();
let extra = HashMap::from([
(
"PK".to_owned(),
AttributeValue::S("SHOULD_NOT_WIN".to_owned()),
),
("custom".to_owned(), AttributeValue::S("added".to_owned())),
]);
let enriched = item.with_attributes(extra);
assert_eq!(
enriched.get("PK"),
Some(&AttributeValue::S("USER#user-1".to_owned()))
);
assert_eq!(
enriched.get("custom"),
Some(&AttributeValue::S("added".to_owned()))
);
assert_eq!(
enriched.get("SK"),
Some(&AttributeValue::S("USER".to_owned()))
);
assert_eq!(
enriched.get("_TYPE"),
Some(&AttributeValue::S("USER".to_owned()))
);
}
#[test]
fn test_item_from_key_and_attributes_key_takes_precedence() {
let key: Key<PlatformTable> = sample_user().get_key();
let extra = HashMap::from([
("PK".to_owned(), AttributeValue::S("WRONG".to_owned())),
("other".to_owned(), AttributeValue::S("kept".to_owned())),
]);
let item = Item::from_key_and_attributes(key, extra);
assert_eq!(
item.get("PK"),
Some(&AttributeValue::S("USER#user-1".to_owned()))
);
assert_eq!(item.get("SK"), Some(&AttributeValue::S("USER".to_owned())));
assert_eq!(
item.get("other"),
Some(&AttributeValue::S("kept".to_owned()))
);
}
#[test]
fn test_item_extract_key_composite() {
let item = sample_enrollment().to_item();
let (key, rest) = item.extract_key();
let raw_key = key.into_inner();
assert_eq!(
raw_key.get("PK"),
Some(&AttributeValue::S("USER#user-1".to_owned()))
);
assert_eq!(
raw_key.get("SK"),
Some(&AttributeValue::S("ENROLL#course-42".to_owned()))
);
assert_eq!(raw_key.len(), 2);
assert!(!rest.contains_key("PK"));
assert!(!rest.contains_key("SK"));
assert!(rest.contains_key("user_id"));
assert!(rest.contains_key("course_id"));
assert!(rest.contains_key("enrolled_at"));
assert!(rest.contains_key("progress"));
assert!(rest.contains_key("_TYPE"));
}
crate::attribute_definitions! {
SimplePK { "SPK": crate::StringAttribute }
}
crate::table_definitions! {
SimpleTable {
type PartitionKey = SimplePK;
fn table_name() -> String { "simple".to_owned() }
}
}
#[derive(serde::Deserialize, serde::Serialize)]
struct SimpleItem {
id: String,
value: String,
}
crate::dynamodb_item! {
#[table = SimpleTable]
SimpleItem {
#[partition_key]
SimplePK {
fn attribute_id(&self) -> &'id str { &self.id }
fn attribute_value(id) -> String { format!("ID#{id}") }
}
}
}
#[test]
fn test_item_extract_key_simple() {
let si = SimpleItem {
id: "x".to_owned(),
value: "v".to_owned(),
};
let item = si.to_item();
let (key, rest) = item.extract_key();
let raw_key = key.into_inner();
assert_eq!(raw_key.len(), 1);
assert_eq!(
raw_key.get("SPK"),
Some(&AttributeValue::S("ID#x".to_owned()))
);
assert!(!rest.contains_key("SPK"));
assert!(rest.contains_key("value"));
}
#[test]
fn test_dynamodb_item_to_item_and_try_from_item_roundtrip() {
let original = sample_user();
let item = original.to_item();
let restored = User::try_from_item(item).unwrap();
assert_eq!(restored.id, original.id);
assert_eq!(restored.name, original.name);
assert_eq!(restored.email, original.email);
assert_eq!(restored.role, original.role);
}
}