dynamodb_facade/schema/attribute_list.rs
1use std::collections::HashMap;
2
3use super::{AttributeDefinition, HasAttribute, HasTableKeyAttributes, TableDefinition};
4use crate::{AttributeValue, IntoAttributeValue};
5
6mod sealed_traits {
7 use super::*;
8 /// Seals [`AttributeList`] and provides the recursive attribute-collection machinery.
9 pub trait AttributeListSeal<TD: TableDefinition, T: HasTableKeyAttributes<TD>> {
10 const ATTRIBUTE_LIST_LEN: usize;
11 fn enrich(source: &T, item: &mut HashMap<String, AttributeValue>);
12 }
13}
14
15/// A sealed, recursive tuple-list of additional DynamoDB attributes for an item.
16///
17/// This trait is sealed and automatically implemented for right-nested tuples of
18/// [`AttributeDefinition`] types — for example `(ItemType, (Expiration, ()))`.
19/// It is the type-level representation of
20/// [`DynamoDBItem::AdditionalAttributes`](crate::DynamoDBItem::AdditionalAttributes):
21/// the set of attributes that are written to DynamoDB in addition to the
22/// primary key attributes and the serialization of the underlying type.
23///
24/// The [`attr_list!`](crate::attr_list) macro builds these nested tuple types
25/// conveniently: `attr_list![ItemType, Expiration]` expands to
26/// `(ItemType, (Expiration, ()))`.
27///
28/// # Examples
29///
30/// ```
31/// # use dynamodb_facade::test_fixtures::*;
32/// use dynamodb_facade::{attr_list, AttributeList, DynamoDBItem};
33///
34/// // The AdditionalAttributes type for PlatformConfig is (ItemType, ()).
35/// type ConfigAttrs = <PlatformConfig as DynamoDBItem<PlatformTable>>::AdditionalAttributes;
36///
37/// // AttributeList::get_attributes collects all additional attributes into a HashMap.
38/// let config = sample_config();
39/// let attrs = <ConfigAttrs as AttributeList<PlatformTable, PlatformConfig>>::get_attributes(&config);
40/// // Only 1 additional attribute in this case
41/// assert_eq!(attrs.len(), 1);
42/// // It is "_TYPE" and its value for PlatformConfig is "PLATFORM_CONFIG"
43/// assert!(attrs.get("_TYPE").is_some_and(|v| v.as_s().unwrap() == "PLATFORM_CONFIG"));
44/// ```
45pub trait AttributeList<TD: TableDefinition, T: HasTableKeyAttributes<TD>>:
46 sealed_traits::AttributeListSeal<TD, T>
47{
48 /// Collects all attributes in this list from `source` into a
49 /// `HashMap<String, AttributeValue>`.
50 fn get_attributes(source: &T) -> HashMap<String, AttributeValue>;
51}
52
53// Base case: empty list
54impl<TD: TableDefinition, T: HasTableKeyAttributes<TD>> sealed_traits::AttributeListSeal<TD, T>
55 for ()
56{
57 const ATTRIBUTE_LIST_LEN: usize = 0;
58 fn enrich(_source: &T, _item: &mut HashMap<String, AttributeValue>) {}
59}
60
61// Recursive case: head + tail
62impl<TD, A, Rest, T> sealed_traits::AttributeListSeal<TD, T> for (A, Rest)
63where
64 TD: TableDefinition,
65 A: AttributeDefinition,
66 T: HasTableKeyAttributes<TD> + HasAttribute<A>,
67 Rest: sealed_traits::AttributeListSeal<TD, T>,
68{
69 const ATTRIBUTE_LIST_LEN: usize = Rest::ATTRIBUTE_LIST_LEN + 1;
70
71 fn enrich(source: &T, item: &mut HashMap<String, AttributeValue>) {
72 item.insert(
73 A::NAME.to_owned(),
74 <T as HasAttribute<A>>::attribute(source).into_attribute_value(),
75 );
76 Rest::enrich(source, item);
77 }
78}
79impl<TD, T, AL> AttributeList<TD, T> for AL
80where
81 TD: TableDefinition,
82 T: HasTableKeyAttributes<TD>,
83 AL: sealed_traits::AttributeListSeal<TD, T>,
84{
85 fn get_attributes(source: &T) -> HashMap<String, AttributeValue> {
86 let mut attributes = HashMap::with_capacity(AL::ATTRIBUTE_LIST_LEN);
87 Self::enrich(source, &mut attributes);
88 attributes
89 }
90}