dynamodb_facade/item/mod.rs
1mod key;
2mod key_id;
3
4use core::{fmt, marker::PhantomData, ops::Deref};
5use std::collections::HashMap;
6
7use serde::{Serialize, de::DeserializeOwned};
8
9use super::{
10 AttributeDefinition, AttributeList, AttributeValue, AttributeValueRef, CompositeKey,
11 CompositeKeySchema, HasTableKeyAttributes, KeySchema, KeySchemaKind, Result, SimpleKey,
12 SimpleKeySchema, TableDefinition,
13};
14
15pub use key::{Key, KeyBuilder};
16pub use key_id::*;
17
18/// Core trait for types stored in a DynamoDB table.
19///
20/// Implementing this trait connects a Rust type to a specific [`TableDefinition`],
21/// enabling typed CRUD operations, expression building, and (de)serialization. Most of
22/// the time you implement it via the [`dynamodb_item!`](crate::dynamodb_item) macro
23/// rather than by hand.
24///
25/// # Why `TD` is a generic parameter
26///
27/// `TD` is a **type parameter** rather than an associated type so that a single
28/// type can implement this trait for multiple tables. This supports scenarios
29/// where the same domain type must live in more than one table — for example,
30/// shared types across a primary and an archive table, or migration logic that
31/// reads from one table and writes to another with different key mappings.
32///
33/// # Methods
34///
35/// The three associated methods cover the full round-trip between a Rust value and
36/// a DynamoDB item:
37///
38/// - [`to_item`](DynamoDBItem::to_item) — serialize `self` into an [`Item<TD>`]
39/// - [`try_from_item`](DynamoDBItem::try_from_item) — fallibly deserialize an [`Item<TD>`]
40/// - [`from_item`](DynamoDBItem::from_item) — infallibly deserialize (panics on mismatch)
41///
42/// # Associated Types
43///
44/// `AdditionalAttributes` lists the non-key attributes that are written by
45/// [`Item::minimal_from`] (e.g. type discriminators such as `_TYPE`). Usualy,
46/// additional attributes are there to ensure the resulting DynamoDB item is included
47/// in some Local or Global Secondary Indexes.
48///
49/// Note that the table Key attributes are always included automatically regardless of
50/// the content of `AdditionalAttributes`.
51///
52/// # Blanket Implementations
53///
54/// Implementing [`DynamoDBItem<TD>`] is the **gateway to the entire typed operation
55/// API**. Three additional traits are automatically provided via blanket
56/// implementations:
57///
58/// - [`DynamoDBItemOp<TD>`](crate::DynamoDBItemOp) — single-item CRUD and collection operations:
59/// `get`, `put`, `delete`, `update`, `query`, `scan`
60/// - [`DynamoDBItemBatchOp<TD>`](crate::DynamoDBItemBatchOp) — batch write requests:
61/// `batch_put`, `batch_delete`
62/// - [`DynamoDBItemTransactOp<TD>`](crate::DynamoDBItemTransactOp) — transactional requests:
63/// `transact_put`, `transact_delete`, `transact_update`, `transact_condition`
64///
65/// # Examples
66///
67/// Serializing a user to a DynamoDB item and back:
68///
69/// ```
70/// # use dynamodb_facade::test_fixtures::*;
71/// use serde::{Deserialize, Serialize};
72/// use dynamodb_facade::{dynamodb_item, DynamoDBItem, DynamoDBItemOp, KeyId};
73///
74/// #[derive(Serialize, Deserialize)]
75/// pub struct User {
76/// pub id: String,
77/// pub name: String,
78/// pub email: String,
79/// pub role: String,
80/// }
81///
82/// dynamodb_item! {
83/// #[table = PlatformTable]
84/// User {
85/// #[partition_key]
86/// PK {
87/// fn attribute_id(&self) -> &'id str { &self.id }
88/// fn attribute_value(id) -> String { format!("USER#{id}") }
89/// }
90/// #[sort_key]
91/// SK { const VALUE: &'static str = "USER"; }
92/// }
93/// }
94///
95/// fn _assert_dynamodb_item<DBI: DynamoDBItem<PlatformTable> + DynamoDBItemOp<PlatformTable>>() {}
96/// _assert_dynamodb_item::<User>();
97///
98/// # async fn example(cclient: aws_sdk_dynamodb::Client) -> dynamodb_facade::Result<()> {
99/// let user = User {
100/// id: "user-1".to_owned(),
101/// name: "Alice".to_owned(),
102/// email: "alice@example.com".to_owned(),
103/// role: "student".to_owned(),
104/// };
105///
106/// # let client = cclient.clone();
107/// // Put the User in DynamoDB if it does not already exist
108/// user.put(client).not_exists().await?;
109///
110/// # let client = cclient.clone();
111/// // Retrieve an existing user
112/// let existing /* : Option<User> */ = User::get(client, KeyId::pk("user-2")).await?;
113///
114/// # let client = cclient.clone();
115/// // Delete the user, if it exist
116/// if let Some(u) = existing {
117/// u.delete(client).exists().await?;
118/// }
119/// # Ok(())
120/// # }
121/// ```
122pub trait DynamoDBItem<TD: TableDefinition>:
123 Sized + HasTableKeyAttributes<TD> + KeyBuilder<TD>
124{
125 type AdditionalAttributes: AttributeList<TD, Self>;
126
127 /// Serializes `self` into an [`Item<TD>`].
128 ///
129 /// Combines the key attributes and
130 /// [`AdditionalAttributes`](DynamoDBItem::AdditionalAttributes) produced by
131 /// [`Item::minimal_from`] with the full `serde_dynamo` representation of
132 /// `self`.
133 ///
134 /// # Panics
135 ///
136 /// Panics if [`serde_dynamo::to_item`] fails to serialize `self` — e.g.
137 /// non-string map keys, or a [`Serialize`] impl that returns an error.
138 /// Users are responsible for providing a [`Serialize`] implementation
139 /// compatible with DynamoDB's attribute-value model; plain
140 /// `#[derive(Serialize)]` on structs with string / number / bool / Vec /
141 /// Option / nested-struct fields is always fine.
142 fn to_item(&self) -> Item<TD>
143 where
144 Self: Serialize,
145 {
146 let minimal_item = Item::minimal_from(self);
147 let item: HashMap<_, _> = serde_dynamo::to_item(self).expect("valid serialization");
148 minimal_item.with_attributes(item)
149 }
150
151 /// Fallibly deserializes an [`Item<TD>`] into `Self`.
152 ///
153 /// # Errors
154 ///
155 /// Returns [`Err`] if [`serde_dynamo::from_item`] fails — e.g. a required
156 /// attribute is missing, has an unexpected type, or cannot be decoded into
157 /// the target field.
158 fn try_from_item(item: Item<TD>) -> Result<Self>
159 where
160 Self: DeserializeOwned,
161 {
162 Ok(serde_dynamo::from_item(item.into_inner())?)
163 }
164
165 /// Infallibly deserializes an [`Item<TD>`] into `Self`.
166 ///
167 /// # Panics
168 ///
169 /// Panics if [`try_from_item`](DynamoDBItem::try_from_item) returns an
170 /// error. Use [`try_from_item`](DynamoDBItem::try_from_item) when the
171 /// item may not match the schema.
172 fn from_item(item: Item<TD>) -> Self
173 where
174 Self: DeserializeOwned,
175 {
176 Self::try_from_item(item).expect("valid schema")
177 }
178}
179/// A type-safe wrapper around a DynamoDB item for a specific table.
180///
181/// `Item<TD>` is a [`HashMap<String, AttributeValue>`] branded with the
182/// [`TableDefinition`] `TD`. The branding enforces at compile time that items
183/// from different tables are never mixed up, and it unlocks typed attribute
184/// accessors such as [`pk`](Item::pk), [`sk`](Item::sk), and
185/// [`attribute`](Item::attribute).
186///
187/// An `Item<TD>` is guaranteed to contain the table's key attributes (PK, and SK
188/// for composite-key tables). This invariant is upheld by all constructors.
189///
190/// `Item<TD>` implements [`Deref`] to `HashMap<String, AttributeValue>`, so you
191/// can call `.get("field")` and other map methods directly.
192///
193/// # Examples
194///
195/// Building an item and inspecting its attributes:
196///
197/// ```
198/// # use dynamodb_facade::test_fixtures::*;
199/// use dynamodb_facade::DynamoDBItem;
200///
201/// let user /* : User */ = sample_user();
202/// let item /* : Item<PlatformTable> */ = user.to_item();
203///
204/// // Typed key accessors — always present.
205/// assert_eq!(item.pk(), "USER#user-1");
206/// assert_eq!(item.sk(), "USER");
207///
208/// // Optional typed attribute access (present for the User type).
209/// let item_type: Option<&str> = item.attribute::<ItemType>();
210/// assert_eq!(item_type, Some("USER"));
211/// ```
212///
213/// Consuming the item into its raw map:
214///
215/// ```
216/// # use dynamodb_facade::test_fixtures::*;
217/// use dynamodb_facade::DynamoDBItem;
218///
219/// let item /* : Item<PlatformTable> */ = sample_user().to_item();
220/// let raw /* : HashMap<String, AttributeValue> */ = item.into_inner();
221/// assert!(raw.contains_key("PK"));
222/// ```
223///
224/// # Equality and hashing
225///
226/// `Item<TD>` intentionally does not implement [`PartialEq`], [`Eq`], or
227/// [`Hash`](core::hash::Hash). The backing type is a
228/// [`HashMap<String, AttributeValue>`], and while attribute-value byte-equality
229/// could be derived, it does not match DynamoDB's semantic equality (number
230/// string normalization, set element ordering, etc.). Compare items by
231/// deserializing to `T` first.
232#[derive(Clone)]
233pub struct Item<TD: TableDefinition>(HashMap<String, AttributeValue>, PhantomData<TD>);
234impl<TD: TableDefinition> fmt::Debug for Item<TD> {
235 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236 fmt::Debug::fmt(&self.0, f)
237 }
238}
239
240/// Shorthand for the partition key [`AttributeDefinition`] of table `TD`.
241pub(crate) type PartitionKeyDefinition<TD> =
242 <<TD as TableDefinition>::KeySchema as KeySchema>::PartitionKey;
243/// The [`AttributeType`](crate::AttributeType) of the partition key for table `TD`.
244type PartitionKeyType<TD> = <PartitionKeyDefinition<TD> as AttributeDefinition>::Type;
245/// Shorthand for the sort key [`AttributeDefinition`] of composite-key table `TD`.
246type SortKeyDefinition<TD> = <<TD as TableDefinition>::KeySchema as CompositeKeySchema>::SortKey;
247/// The [`AttributeType`](crate::AttributeType) of the sort key for table `TD`.
248type SortKeyType<TD> = <SortKeyDefinition<TD> as AttributeDefinition>::Type;
249
250impl<TD: TableDefinition> Item<TD> {
251 /// Returns the partition key value of this item.
252 ///
253 /// The return type is a typed reference determined by the table's partition
254 /// key attribute definition (e.g. `&str` for a `StringAttribute` PK).
255 ///
256 /// # Panics
257 ///
258 /// Panics if the partition key attribute is absent. This should never happen
259 /// for items produced by this crate's constructors, which always include the
260 /// key attributes.
261 ///
262 /// # Examples
263 ///
264 /// ```
265 /// # use dynamodb_facade::test_fixtures::*;
266 /// use dynamodb_facade::DynamoDBItem;
267 ///
268 /// let item /* : Item<PlatformTable> */ = sample_user().to_item();
269 /// assert_eq!(item.pk(), "USER#user-1");
270 /// ```
271 pub fn pk(&self) -> <PartitionKeyType<TD> as AttributeValueRef>::Ref<'_> {
272 self.attribute::<PartitionKeyDefinition<TD>>()
273 .expect("PK is always present")
274 }
275
276 /// Returns a typed reference to the named attribute, or `None` if absent.
277 ///
278 /// The attribute is identified by the type parameter `A`, which must
279 /// implement [`AttributeDefinition`]. The return type is a typed reference
280 /// whose concrete type is determined by `A::Type` (e.g. `&str` for
281 /// `StringAttribute`, `&str` for `NumberAttribute`).
282 ///
283 /// # Examples
284 ///
285 /// ```
286 /// # use dynamodb_facade::test_fixtures::*;
287 /// use dynamodb_facade::DynamoDBItem;
288 ///
289 /// let item /* : Item<PlatformTable> */ = sample_user().to_item();
290 ///
291 /// // Present attribute:
292 /// let item_type: Option<&str> = item.attribute::<ItemType>();
293 /// assert_eq!(item_type, Some("USER"));
294 ///
295 /// // Absent attribute returns None:
296 /// let expiration: Option<&str> = item.attribute::<Expiration>();
297 /// assert!(expiration.is_none());
298 /// ```
299 pub fn attribute<A: AttributeDefinition>(
300 &self,
301 ) -> Option<<A::Type as AttributeValueRef>::Ref<'_>> {
302 self.0
303 .get(A::NAME)
304 .map(<A::Type as AttributeValueRef>::attribute_value_ref)
305 }
306
307 /// Consumes the item and returns the underlying raw attribute map.
308 ///
309 /// Use this when you need to pass the item to code that works with the raw
310 /// `aws-sdk-dynamodb` types.
311 ///
312 /// Beware that you will not be able to re-construct the original [`Item<TD>`].
313 /// See [`Item::extract_key`] and [`Item::from_key_and_attributes`] if you want
314 /// to manipulate the underlying `HashMap` and then re-create it afterward.
315 ///
316 /// # Examples
317 ///
318 /// ```
319 /// # use dynamodb_facade::test_fixtures::*;
320 /// use dynamodb_facade::DynamoDBItem;
321 ///
322 /// let raw /* : HashMap<String, AttributeValue> */ = sample_user().to_item().into_inner();
323 /// assert!(raw.contains_key("PK"));
324 /// assert!(raw.contains_key("SK"));
325 /// ```
326 pub fn into_inner(self) -> HashMap<String, AttributeValue> {
327 self.0
328 }
329
330 /// Wraps a raw attribute map returned by the SDK into a typed `Item<TD>`.
331 pub(crate) fn from_dynamodb_response(item: HashMap<String, AttributeValue>) -> Self {
332 Self(item, PhantomData)
333 }
334
335 /// Creates a minimal [`Item<TD>`] from a [`DynamoDBItem`] value.
336 ///
337 /// The resulting item contains only the key attributes (PK and SK for
338 /// composite-key tables) and the type's
339 /// [`AdditionalAttributes`](DynamoDBItem::AdditionalAttributes) (e.g. type
340 /// discriminators). It does **not** include the serialized underlying type.
341 ///
342 /// This is mainly used when you need to implement [`DynamoDBItem::to_item`]
343 /// manually.
344 ///
345 /// # Examples
346 ///
347 /// ```
348 /// # use dynamodb_facade::test_fixtures::*;
349 /// use dynamodb_facade::{DynamoDBItem, Item};
350 ///
351 /// let user = sample_user();
352 /// let minimal = Item::minimal_from(&user);
353 ///
354 /// // Key attributes are present.
355 /// assert_eq!(minimal.pk(), "USER#user-1");
356 /// assert_eq!(minimal.sk(), "USER");
357 ///
358 /// // The type discriminator (AdditionalAttributes) is also present.
359 /// assert_eq!(minimal.attribute::<ItemType>(), Some("USER"));
360 ///
361 /// // But non-key payload fields are absent.
362 /// assert!(!minimal.contains_key("name"));
363 /// ```
364 pub fn minimal_from<DBI: DynamoDBItem<TD>>(dynamodb_item: &DBI) -> Self {
365 let key = dynamodb_item.get_key();
366 let additional_attributes = DBI::AdditionalAttributes::get_attributes(dynamodb_item);
367 Item::from_key_and_attributes(key, additional_attributes)
368 }
369
370 /// Merges additional attributes into this item.
371 ///
372 /// In case of an attribute name conflict, attributes already present on
373 /// the item take precedence. In other words, this method cannot overwrite
374 /// existing item attributes.
375 ///
376 /// This is the mechanism used by [`DynamoDBItem::to_item`]'s default
377 /// implementation to combine the full serde payload with the typed key
378 /// attributes.
379 ///
380 /// # Examples
381 ///
382 /// ```
383 /// # use dynamodb_facade::test_fixtures::*;
384 /// use dynamodb_facade::{DynamoDBItem, Item, IntoAttributeValue};
385 ///
386 /// let user = sample_user();
387 /// let minimal = Item::minimal_from(&user);
388 ///
389 /// // Merge in extra attributes.
390 /// let enriched = minimal.with_attributes([
391 /// ("extra".to_owned(), "hello".into_attribute_value()),
392 /// ]);
393 ///
394 /// assert_eq!(enriched.pk(), "USER#user-1");
395 /// assert!(enriched.contains_key("extra"));
396 /// ```
397 pub fn with_attributes(self, attributes: impl Into<HashMap<String, AttributeValue>>) -> Self {
398 let mut item = attributes.into();
399 item.extend(self.0);
400 Self(item, PhantomData)
401 }
402
403 /// Constructs an item from a [`Key<TD>`] and an additional attribute map.
404 ///
405 /// The key attributes always take precedence: if `attributes` contains an
406 /// entry with the same name as a key attribute, the key attribute wins.
407 ///
408 /// # Examples
409 ///
410 /// ```
411 /// # use dynamodb_facade::test_fixtures::*;
412 /// use dynamodb_facade::{DynamoDBItem, KeyBuilder, Item, IntoAttributeValue};
413 ///
414 /// let key = sample_user().get_key();
415 /// let item: Item<PlatformTable> = Item::from_key_and_attributes(key, [
416 /// ("role".to_owned(), "instructor".into_attribute_value()),
417 /// ]);
418 ///
419 /// assert_eq!(item.pk(), "USER#user-1");
420 /// assert!(item.contains_key("role"));
421 /// ```
422 pub fn from_key_and_attributes(
423 key: Key<TD>,
424 attributes: impl Into<HashMap<String, AttributeValue>>,
425 ) -> Self {
426 let mut item = attributes.into();
427 item.extend(key.into_inner());
428 Self(item, PhantomData)
429 }
430}
431impl<TD: TableDefinition> Item<TD>
432where
433 TD::KeySchema: CompositeKeySchema,
434{
435 /// Returns the sort key value of this item.
436 ///
437 /// Only available when the table uses a [`CompositeKeySchema`] (PK + SK).
438 /// The return type is a typed reference determined by the table's sort key
439 /// attribute definition (e.g. `&str` for a `StringAttribute` SK).
440 ///
441 /// # Panics
442 ///
443 /// Panics if the sort key attribute is absent. This should never happen for
444 /// items produced by this crate's constructors, which always include the key
445 /// attributes.
446 ///
447 /// # Examples
448 ///
449 /// ```
450 /// # use dynamodb_facade::test_fixtures::*;
451 /// use dynamodb_facade::DynamoDBItem;
452 ///
453 /// let item = sample_user().to_item();
454 /// assert_eq!(item.sk(), "USER");
455 ///
456 /// let enrollment_item = sample_enrollment().to_item();
457 /// assert_eq!(enrollment_item.sk(), "ENROLL#course-42");
458 /// ```
459 pub fn sk(&self) -> <SortKeyType<TD> as AttributeValueRef>::Ref<'_> {
460 self.attribute::<SortKeyDefinition<TD>>()
461 .expect("SK is always present")
462 }
463}
464
465impl<TD: TableDefinition> Deref for Item<TD> {
466 type Target = HashMap<String, AttributeValue>;
467
468 fn deref(&self) -> &Self::Target {
469 &self.0
470 }
471}
472
473impl<TD: TableDefinition> IntoIterator for Item<TD> {
474 type Item = <HashMap<String, AttributeValue> as IntoIterator>::Item;
475 type IntoIter = <HashMap<String, AttributeValue> as IntoIterator>::IntoIter;
476
477 fn into_iter(self) -> Self::IntoIter {
478 self.0.into_iter()
479 }
480}
481
482#[cfg(test)]
483mod tests {
484 use std::collections::HashMap;
485
486 use aws_sdk_dynamodb::types::AttributeValue;
487
488 use super::super::test_fixtures::*;
489 use super::*;
490
491 // ---------------------------------------------------------------------------
492 // test_item_minimal_from_user
493 // ---------------------------------------------------------------------------
494
495 #[test]
496 fn test_item_minimal_from_user() {
497 let user = sample_user();
498 let item = Item::<PlatformTable>::minimal_from(&user);
499
500 // Key attributes are present with correct values.
501 assert_eq!(
502 item.get("PK"),
503 Some(&AttributeValue::S("USER#user-1".to_owned()))
504 );
505 assert_eq!(item.get("SK"), Some(&AttributeValue::S("USER".to_owned())));
506 // ItemType is in AdditionalAttributes (not marker_only).
507 assert_eq!(
508 item.get("_TYPE"),
509 Some(&AttributeValue::S("USER".to_owned()))
510 );
511 // Email is #[marker_only] — NOT in AdditionalAttributes, so absent from minimal_from.
512 assert!(!item.contains_key("email"));
513 // Exactly PK + SK + _TYPE = 3 attributes.
514 assert_eq!(item.len(), 3);
515 }
516
517 // ---------------------------------------------------------------------------
518 // test_item_with_attributes_key_takes_precedence
519 // ---------------------------------------------------------------------------
520
521 #[test]
522 fn test_item_with_attributes_key_takes_precedence() {
523 let item = sample_user().to_item();
524 let extra = HashMap::from([
525 (
526 "PK".to_owned(),
527 AttributeValue::S("SHOULD_NOT_WIN".to_owned()),
528 ),
529 ("custom".to_owned(), AttributeValue::S("added".to_owned())),
530 ]);
531
532 let enriched = item.with_attributes(extra);
533
534 // Key attribute wins over the conflicting extra value.
535 assert_eq!(
536 enriched.get("PK"),
537 Some(&AttributeValue::S("USER#user-1".to_owned()))
538 );
539 // New attribute is present.
540 assert_eq!(
541 enriched.get("custom"),
542 Some(&AttributeValue::S("added".to_owned()))
543 );
544 // Existing non-conflicting attributes are unchanged.
545 assert_eq!(
546 enriched.get("SK"),
547 Some(&AttributeValue::S("USER".to_owned()))
548 );
549 assert_eq!(
550 enriched.get("_TYPE"),
551 Some(&AttributeValue::S("USER".to_owned()))
552 );
553 }
554
555 // ---------------------------------------------------------------------------
556 // test_item_from_key_and_attributes_key_takes_precedence
557 // ---------------------------------------------------------------------------
558
559 #[test]
560 fn test_item_from_key_and_attributes_key_takes_precedence() {
561 let key: Key<PlatformTable> = sample_user().get_key();
562 let extra = HashMap::from([
563 ("PK".to_owned(), AttributeValue::S("WRONG".to_owned())),
564 ("other".to_owned(), AttributeValue::S("kept".to_owned())),
565 ]);
566
567 let item = Item::from_key_and_attributes(key, extra);
568
569 // Key attribute wins over the conflicting extra value.
570 assert_eq!(
571 item.get("PK"),
572 Some(&AttributeValue::S("USER#user-1".to_owned()))
573 );
574 // SK from key is present.
575 assert_eq!(item.get("SK"), Some(&AttributeValue::S("USER".to_owned())));
576 // Non-conflicting extra attribute is preserved.
577 assert_eq!(
578 item.get("other"),
579 Some(&AttributeValue::S("kept".to_owned()))
580 );
581 }
582
583 // ---------------------------------------------------------------------------
584 // test_item_extract_key_composite
585 // ---------------------------------------------------------------------------
586
587 #[test]
588 fn test_item_extract_key_composite() {
589 let item = sample_enrollment().to_item();
590 let (key, rest) = item.extract_key();
591
592 let raw_key = key.into_inner();
593
594 // Key contains PK and SK with correct values.
595 assert_eq!(
596 raw_key.get("PK"),
597 Some(&AttributeValue::S("USER#user-1".to_owned()))
598 );
599 assert_eq!(
600 raw_key.get("SK"),
601 Some(&AttributeValue::S("ENROLL#course-42".to_owned()))
602 );
603 // Key contains exactly PK + SK.
604 assert_eq!(raw_key.len(), 2);
605
606 // Remaining map does NOT contain key attributes.
607 assert!(!rest.contains_key("PK"));
608 assert!(!rest.contains_key("SK"));
609
610 // Remaining map contains the payload fields.
611 assert!(rest.contains_key("user_id"));
612 assert!(rest.contains_key("course_id"));
613 assert!(rest.contains_key("enrolled_at"));
614 assert!(rest.contains_key("progress"));
615 // _TYPE is in AdditionalAttributes for Enrollment.
616 assert!(rest.contains_key("_TYPE"));
617 }
618
619 // ---------------------------------------------------------------------------
620 // test_item_extract_key_simple — local simple-key table
621 // ---------------------------------------------------------------------------
622
623 crate::attribute_definitions! {
624 SimplePK { "SPK": crate::StringAttribute }
625 }
626 crate::table_definitions! {
627 SimpleTable {
628 type PartitionKey = SimplePK;
629 fn table_name() -> String { "simple".to_owned() }
630 }
631 }
632 #[derive(serde::Deserialize, serde::Serialize)]
633 struct SimpleItem {
634 id: String,
635 value: String,
636 }
637 crate::dynamodb_item! {
638 #[table = SimpleTable]
639 SimpleItem {
640 #[partition_key]
641 SimplePK {
642 fn attribute_id(&self) -> &'id str { &self.id }
643 fn attribute_value(id) -> String { format!("ID#{id}") }
644 }
645 }
646 }
647
648 #[test]
649 fn test_item_extract_key_simple() {
650 let si = SimpleItem {
651 id: "x".to_owned(),
652 value: "v".to_owned(),
653 };
654 let item = si.to_item();
655 let (key, rest) = item.extract_key();
656
657 let raw_key = key.into_inner();
658
659 // Key contains only SPK.
660 assert_eq!(raw_key.len(), 1);
661 assert_eq!(
662 raw_key.get("SPK"),
663 Some(&AttributeValue::S("ID#x".to_owned()))
664 );
665
666 // Remaining map does NOT contain the key attribute.
667 assert!(!rest.contains_key("SPK"));
668 // Payload field is present.
669 assert!(rest.contains_key("value"));
670 }
671
672 // ---------------------------------------------------------------------------
673 // test_dynamodb_item_to_item_and_try_from_item_roundtrip
674 // ---------------------------------------------------------------------------
675
676 #[test]
677 fn test_dynamodb_item_to_item_and_try_from_item_roundtrip() {
678 let original = sample_user();
679 let item = original.to_item();
680 let restored = User::try_from_item(item).unwrap();
681
682 assert_eq!(restored.id, original.id);
683 assert_eq!(restored.name, original.name);
684 assert_eq!(restored.email, original.email);
685 assert_eq!(restored.role, original.role);
686 }
687}