dynamodb_facade/macros.rs
1/// Declares one or more DynamoDB attribute definitions as zero-sized types.
2///
3/// Each invocation generates a `pub struct` that implements
4/// [`AttributeDefinition`](crate::AttributeDefinition), providing a
5/// compile-time `const NAME: &str` and an associated `type Type` (one of
6/// [`StringAttribute`](crate::StringAttribute),
7/// [`NumberAttribute`](crate::NumberAttribute), or
8/// [`BinaryAttribute`](crate::BinaryAttribute)).
9///
10/// These zero-sized types serve as type-level identifiers throughout the library: they
11/// are used as generic parameters in [`HasAttribute`](crate::HasAttribute),
12/// [`HasConstAttribute`](crate::HasConstAttribute),
13/// [`KeySchema`](crate::KeySchema), and [`IndexDefinition`](crate::IndexDefinition).
14///
15/// # Syntax
16///
17/// ```text
18/// attribute_definitions! {
19/// [doc comments and attributes]
20/// TypeName { "dynamo_attribute_name": AttributeTypeMarker }
21/// ...
22/// }
23/// ```
24///
25/// Multiple definitions can appear in a single invocation.
26///
27/// # Examples
28///
29/// ```
30/// use dynamodb_facade::{attribute_definitions, StringAttribute, NumberAttribute};
31///
32/// attribute_definitions! {
33/// /// Partition key for the platform mono-table.
34/// PK { "PK": StringAttribute }
35///
36/// /// Sort key for the platform mono-table.
37/// SK { "SK": StringAttribute }
38///
39/// /// Item type discriminator (single-table design).
40/// ItemType { "_TYPE": StringAttribute }
41///
42/// /// TTL attribute for expiring items.
43/// Expiration { "expiration_timestamp": NumberAttribute }
44///
45/// /// Email attribute, used as a GSI partition key.
46/// Email { "email": StringAttribute }
47/// }
48///
49/// use dynamodb_facade::AttributeDefinition;
50/// // Each generated type exposes its DynamoDB attribute name as a constant.
51/// assert_eq!(PK::NAME, "PK");
52/// assert_eq!(SK::NAME, "SK");
53/// assert_eq!(ItemType::NAME, "_TYPE");
54/// assert_eq!(Expiration::NAME, "expiration_timestamp");
55/// assert_eq!(Email::NAME, "email");
56/// ```
57#[macro_export]
58macro_rules! attribute_definitions {
59 {
60 $(
61 $(#[$meta:meta])*
62 $tname:ident {
63 $name:literal: $t:ty
64 }
65 )+
66 } => {
67 $(
68 $(#[$meta])*
69 pub struct $tname;
70 impl $crate::AttributeDefinition for $tname {
71 const NAME: &'static str = $name;
72 type Type = $t;
73 }
74 )+
75 };
76 // === diagnostic arm: catch-all for malformed input ===
77 ($($tt:tt)*) => {
78 ::core::compile_error!(concat!(
79 "`attribute_definitions!` expected:\n",
80 " TypeName { \"dynamo_attribute_name\": StringAttribute|NumberAttribute|BinaryAttribute }\n",
81 " ... (one or more)"
82 ));
83 };
84}
85
86/// Builds the nested tuple type used to represent a list of
87/// [`AttributeDefinition`](crate::AttributeDefinition) types.
88///
89/// `attr_list![A, B, C]` expands to the right-nested tuple
90/// `(A, (B, (C, ())))`, which is the representation expected by
91/// [`AttributeList`](crate::AttributeList) and the
92/// [`DynamoDBItem::AdditionalAttributes`](crate::DynamoDBItem::AdditionalAttributes)
93/// associated type.
94///
95/// You rarely need to invoke this macro directly — [`dynamodb_item!`](crate::dynamodb_item) calls it
96/// internally. It is exposed for use in manual [`DynamoDBItem`](crate::DynamoDBItem)
97/// implementations where you need to spell out the `AdditionalAttributes` type
98/// explicitly.
99///
100/// # Syntax
101///
102/// ```text
103/// attr_list![AttrType1, AttrType2, ...]
104/// ```
105///
106/// An empty list `attr_list![]` expands to `()`.
107///
108/// # Examples
109///
110/// ```
111/// # use dynamodb_facade::test_fixtures::*;
112/// use dynamodb_facade::attr_list;
113///
114/// // Equivalent to (ItemType, (Expiration, ()))
115/// type MyAttrs = attr_list![ItemType, Expiration];
116/// ```
117#[macro_export]
118macro_rules! attr_list {
119 // Expands to the nested tuple type
120 [$($attr:ty),*] => {
121 // just the type
122 $crate::attr_list!(@nest $($attr),*)
123 };
124 (@nest) => { () };
125 (@nest $head:ty $(, $tail:ty)*) => {
126 ($head, $crate::attr_list!(@nest $($tail),*))
127 };
128}
129
130/// Defines a key schema struct implementing [`KeySchema`](crate::KeySchema).
131///
132/// This is a lower-level macro used internally by [`table_definitions!`](crate::table_definitions) and
133/// [`index_definitions!`](crate::index_definitions). You can use it directly when you need a named key
134/// schema type outside of a table or index definition.
135///
136/// Generates a `pub struct` that implements:
137/// - [`KeySchema`](crate::KeySchema) — always
138/// - [`SimpleKeySchema`](crate::SimpleKeySchema) — when only `PartitionKey` is given
139/// - [`CompositeKeySchema`](crate::CompositeKeySchema) — when both `PartitionKey` and `SortKey` are given
140///
141/// # Syntax
142///
143/// Simple key (partition key only):
144/// ```text
145/// key_schema! {
146/// MySchema {
147/// type PartitionKey = MyPkAttr;
148/// }
149/// }
150/// ```
151///
152/// Composite key (partition + sort key):
153/// ```text
154/// key_schema! {
155/// MySchema {
156/// type PartitionKey = MyPkAttr;
157/// type SortKey = MySkAttr;
158/// }
159/// }
160/// ```
161///
162/// The `PartitionKey` and `SortKey` fields may appear in either order.
163///
164/// The given types must implement [`AttributeDefinition`](crate::AttributeDefinition) and will
165/// typically have been created using [`attribute_definitions!`]
166///
167/// # Examples
168///
169/// ```
170/// # use dynamodb_facade::test_fixtures::*;
171/// use dynamodb_facade::{key_schema, CompositeKeySchema, KeySchema, SimpleKeySchema};
172///
173/// key_schema! {
174/// UserSchema {
175/// type PartitionKey = PK;
176/// type SortKey = SK;
177/// }
178/// }
179///
180/// key_schema! {
181/// ConfigSchema {
182/// type PartitionKey = PK;
183/// }
184/// }
185///
186/// fn _assert_composite<KS: CompositeKeySchema>() {}
187/// fn _assert_simple<KS: SimpleKeySchema>() {}
188///
189/// _assert_composite::<UserSchema>();
190/// _assert_simple::<ConfigSchema>();
191/// ```
192#[macro_export]
193macro_rules! key_schema {
194 // Syntaxic QoL
195 {
196 $(#[$meta:meta])*
197 $ksname:ident {
198 type SortKey = $skty:ty;
199 type PartitionKey = $pkty:ty;
200 }
201 } => {
202 $crate::key_schema!{
203 $(#[$meta])*
204 $ksname {
205 type PartitionKey = $pkty;
206 type SortKey = $skty;
207 }
208 }
209 };
210 // Processing
211 {
212 $(#[$meta:meta])*
213 $ksname:ident {
214 type PartitionKey = $pkty:ty;
215 type SortKey = $skty:ty;
216 }
217 } => {
218 $(#[$meta])*
219 pub struct $ksname;
220 impl $crate::KeySchema for $ksname {
221 type Kind = $crate::CompositeKey;
222 type PartitionKey = $pkty;
223 }
224 impl $crate::CompositeKeySchema for $ksname {
225 type SortKey = $skty;
226 }
227 };
228 {
229 $(#[$meta:meta])*
230 $ksname:ident {
231 type PartitionKey = $pkty:ty;
232 }
233 } => {
234 $(#[$meta])*
235 pub struct $ksname;
236 impl $crate::KeySchema for $ksname {
237 type Kind = $crate::SimpleKey;
238 type PartitionKey = $pkty;
239 }
240 impl $crate::SimpleKeySchema for $ksname {}
241 };
242 // === diagnostic arm: catch-all for malformed input ===
243 ($($tt:tt)*) => {
244 ::core::compile_error!(concat!(
245 "`key_schema!` expected:\n",
246 " SchemaName {\n",
247 " type PartitionKey = PkAttr;\n",
248 " [type SortKey = SkAttr;]\n",
249 " }"
250 ));
251 };
252}
253
254/// Manually implements [`HasAttribute`](crate::HasAttribute) or
255/// [`HasConstAttribute`](crate::HasConstAttribute) for a type.
256///
257/// Use this macro when you need to implement the attribute traits without going
258/// through [`dynamodb_item!`](crate::dynamodb_item) — for example, when writing a manual
259/// [`DynamoDBItem`](crate::DynamoDBItem) implementation or when adding
260/// attribute bindings to a type that is already wired to a table.
261///
262/// # Syntax
263///
264/// Each attribute block uses one of two forms:
265///
266/// **Constant attribute** — implements [`HasConstAttribute`](crate::HasConstAttribute):
267/// ```text
268/// has_attributes! {
269/// MyType {
270/// MyAttr { const VALUE: AttrValueType = expr; }
271/// }
272/// }
273/// ```
274///
275/// **Dynamic attribute** — implements [`HasAttribute`](crate::HasAttribute):
276/// ```text
277/// has_attributes! {
278/// MyType {
279/// MyAttr {
280/// fn attribute_id(&self) -> IdType { ... }
281/// fn attribute_value(id) -> ValueType { ... }
282/// }
283/// }
284/// }
285/// ```
286///
287/// The `attribute_id` and `attribute_value` functions may appear in either
288/// order. If `attribute_id` is omitted, it defaults to returning
289/// [`NoId`](crate::NoId).
290///
291/// Multiple attribute blocks can appear in a single invocation.
292///
293/// # The `attribute_id` → `attribute_value` pipeline
294///
295/// For dynamic attributes, the return type of `attribute_id(&self)` is
296/// **always** the input type of `attribute_value(id)`. The two functions
297/// form a pipeline: `attribute_id` extracts a lightweight identifier from
298/// `&self`, and `attribute_value` transforms it into the final DynamoDB
299/// value. This separation allows for independant usages of the methods,
300/// in particular it powers the "_by_id" variants of the get/update/delete
301/// operations.
302///
303/// # The `'id` lifetime
304///
305/// When `attribute_id` returns a reference — typically `&str` — you must
306/// annotate it with the **`'id`** lifetime: `&'id str`. This lifetime
307/// comes from the [`Id<'id>`](crate::HasAttribute::Id) associated type on
308/// [`HasAttribute`](crate::HasAttribute) and must be used exactly as-is.
309///
310/// The typical use-case is when the identifier is a `String` field on the
311/// struct and the final attribute value is a formatted composition of that
312/// field (e.g. `format!("USER#{id}")`). Returning `&'id str` lets
313/// `attribute_id` borrow the field without cloning it, and
314/// `attribute_value` can then use the reference to produce an owned
315/// `String`.
316///
317/// If the attribute does not need data from `&self` (e.g. the value is
318/// always a constant), you can omit `attribute_id` entirely — the macro
319/// defaults it to returning [`NoId`](crate::NoId).
320///
321/// # Examples
322///
323/// ```
324/// # use dynamodb_facade::test_fixtures::*;
325/// use dynamodb_facade::has_attributes;
326///
327/// struct CourseStatus(String);
328///
329/// has_attributes! {
330/// CourseStatus {
331/// // Constant attribute: always the same value
332/// ItemType { const VALUE: &'static str = "COURSE_STATUS"; }
333///
334/// // Dynamic attribute: attribute_id borrows &self.0 as &'id str,
335/// // then attribute_value receives that same &str to format the
336/// // DynamoDB value — no .clone() needed.
337/// SK {
338/// fn attribute_id(&self) -> &'id str { &self.0 }
339/// fn attribute_value(id) -> String { format!("STATUS#{id}") }
340/// }
341/// }
342/// }
343///
344/// use dynamodb_facade::HasConstAttribute;
345/// assert_eq!(<CourseStatus as HasConstAttribute<ItemType>>::VALUE, "COURSE_STATUS");
346///
347/// use dynamodb_facade::HasAttribute;
348/// let status = CourseStatus("draft".to_owned());
349/// assert_eq!(
350/// <CourseStatus as HasAttribute<SK>>::attribute(&status),
351/// "STATUS#draft".to_owned()
352/// );
353/// ```
354#[macro_export]
355macro_rules! has_attributes {
356 {
357 $item:ty {
358 $(
359 $attr:path {$($blk:tt)+}
360 )+
361 }
362 } => {
363 $(
364 $crate::has_attributes! {
365 @inner $attr ; $item {$($blk)+}
366 }
367 )+
368 };
369 // Syntaxic QoL
370 // Re-order functions
371 {
372 @inner $attr:path ; $item:ty {
373 fn attribute_value ($id:ident) -> $outty:ty $produce:block
374 fn attribute_id ($(&)?$self:ident) -> $idty:ty $extract:block
375 }
376 } => {
377 $crate::has_attributes! {
378 @inner $attr ; $item {
379 fn attribute_id ($self) -> $idty $extract
380 fn attribute_value ($id) -> $outty $produce
381 }
382 }
383 };
384 // Default attribute_id
385 {
386 @inner $attr:path ; $item:ty {
387 fn attribute_value ($id:ident) -> $outty:ty $produce:block
388 }
389 } => {
390 $crate::has_attributes! {
391 @inner $attr ; $item {
392 fn attribute_id(&self) -> $crate::NoId {
393 $crate::NoId
394 }
395 fn attribute_value ($id) -> $outty $produce
396 }
397 }
398 };
399 // Process
400 // HasAttribute
401 {
402 @inner $attr:path ; $item:ty {
403 fn $id_fct:ident ($(&)?$self:ident) -> $idty:ty $extract:block
404 fn $value_fct:ident ($id:ident) -> $outty:ty $produce:block
405 }
406 } => {
407 impl $crate::HasAttribute<$attr> for $item {
408 type Id<'id> = $idty;
409 type Value = $outty;
410 fn $id_fct(& $self) -> Self::Id<'_> $extract
411 fn $value_fct($id: Self::Id<'_>) -> Self::Value $produce
412 }
413 };
414 // HasConstAttribute
415 {
416 @inner $attr:path ; $item:ty {
417 const VALUE: $t:ty = $v:expr;
418 }
419 } => {
420 impl $crate::HasConstAttribute<$attr> for $item {
421 type Value = $t;
422 const VALUE: Self::Value = $v;
423 }
424 };
425 // === diagnostic arm: catch-all for malformed input ===
426 ($($tt:tt)*) => {
427 ::core::compile_error!(concat!(
428 "`has_attributes!` expected:\n",
429 " ItemType {\n",
430 " AttrType { const VALUE: T = expr; }\n",
431 " AttrType {\n",
432 " fn attribute_id(&self) -> &'id str { ... }\n",
433 " fn attribute_value(id) -> T { ... }\n",
434 " }\n",
435 " ... (one or more attribute blocks)\n",
436 " }"
437 ));
438 };
439}
440
441/// Wires a Rust struct to a DynamoDB table by implementing
442/// [`DynamoDBItem`](crate::DynamoDBItem) and the attribute traits.
443///
444/// This is the primary macro for defining how a Rust type maps to a DynamoDB
445/// item. It generates:
446///
447/// - [`DynamoDBItem<TD>`](crate::DynamoDBItem) — with the correct
448/// `AdditionalAttributes` type derived from the non-key, non-`#[marker_only]`
449/// attribute blocks.
450/// - [`HasAttribute`](crate::HasAttribute) or
451/// [`HasConstAttribute`](crate::HasConstAttribute) for every attribute block,
452/// including the partition key and sort key.
453///
454/// # Syntax
455///
456/// ```text
457/// dynamodb_item! {
458/// #[table = TableType]
459/// StructType {
460/// #[partition_key]
461/// PkAttr { ... }
462///
463/// #[sort_key] // optional
464/// SkAttr { ... }
465///
466/// #[marker_only] // optional; implements HasAttribute but excluded from AdditionalAttributes
467/// OtherAttr { ... }
468///
469/// AdditionalAttr { ... }
470/// ...
471/// }
472/// }
473/// ```
474///
475/// Each attribute block uses the same syntax as [`has_attributes!`]:
476/// either `const VALUE: T = expr;` for constant attributes, or
477/// `fn attribute_id(&self) -> T { ... }` + `fn attribute_value(id) -> T { ... }`
478/// for dynamic attributes. The return type of `attribute_id` is always the
479/// input type of `attribute_value` — the two form a pipeline.
480///
481/// When `attribute_id` returns a reference (typically borrowing a `String`
482/// field to avoid cloning), annotate it with the **`'id`** lifetime:
483/// `&'id str`. This lifetime is dictated by the
484/// [`Id<'id>`](crate::HasAttribute::Id) associated type.
485/// See [`has_attributes!`] for a detailed explanation.
486///
487/// ## Attribute modifiers
488///
489/// - `#[partition_key]` — marks the partition key attribute. **Required.**
490/// - `#[sort_key]` — marks the sort key attribute. Optional; omit for simple-key tables.
491/// - `#[marker_only]` — implements [`HasAttribute`](crate::HasAttribute) for
492/// the attribute (e.g. for GSI membership) but does **not** add it to
493/// `AdditionalAttributes`, because the attribute is already serialized as
494/// part of the struct's serde representation.
495///
496/// # Examples
497///
498/// **Singleton item** (constant PK + SK):
499///
500/// ```
501/// # use dynamodb_facade::test_fixtures::*;
502/// use dynamodb_facade::dynamodb_item;
503/// use serde::{Deserialize, Serialize};
504///
505/// #[derive(Debug, Clone, Serialize, Deserialize)]
506/// struct AppConfig {
507/// pub feature_flags: Vec<String>,
508/// }
509///
510/// dynamodb_item! {
511/// #[table = PlatformTable]
512/// AppConfig {
513/// #[partition_key]
514/// PK { const VALUE: &'static str = "APP_CONFIG"; }
515/// #[sort_key]
516/// SK { const VALUE: &'static str = "APP_CONFIG"; }
517/// ItemType { const VALUE: &'static str = "APP_CONFIG"; }
518/// }
519/// }
520/// ```
521///
522/// **Variable PK, constant SK**:
523///
524/// ```
525/// # use dynamodb_facade::test_fixtures::*;
526/// use dynamodb_facade::dynamodb_item;
527/// use serde::{Deserialize, Serialize};
528///
529/// #[derive(Debug, Clone, Serialize, Deserialize)]
530/// struct Course {
531/// pub id: String,
532/// pub title: String,
533/// pub email: String,
534/// }
535///
536/// dynamodb_item! {
537/// #[table = PlatformTable]
538/// Course {
539/// #[partition_key]
540/// PK {
541/// // Borrows self.id as &'id str — no clone needed.
542/// // attribute_value then receives that same &str.
543/// fn attribute_id(&self) -> &'id str { &self.id }
544/// fn attribute_value(id) -> String { format!("COURSE#{id}") }
545/// }
546/// #[sort_key]
547/// SK { const VALUE: &'static str = "COURSE"; }
548/// // email is already part of the struct and serialized by serde,
549/// // so use #[marker_only] to exclude it from AdditionalAttributes
550/// #[marker_only]
551/// Email {
552/// fn attribute_id(&self) -> &'id str { &self.email }
553/// fn attribute_value(id) -> String { id.to_owned() }
554/// }
555/// // The constant ItemType attribute is part of AdditionalAttributes
556/// // and will be added to each Course item
557/// ItemType { const VALUE: &'static str = "COURSE"; }
558/// }
559/// }
560/// ```
561#[macro_export]
562macro_rules! dynamodb_item {
563 // Syntaxic QoL
564 // Bubble-up #[...] modified attributes
565 {
566 #[table = $table:path]
567 $item:ty {
568 $(
569 #[$attr_mod:ident]
570 $modified_attr:path {$($modified_blk:tt)+}
571 )+
572 $(
573 $attr:path {$($blk:tt)+}
574 )*
575 }
576 } => {
577 $crate::dynamodb_item! {
578 @modtop
579 #[table = $table]
580 $item {
581 $(
582 #[$attr_mod]
583 $modified_attr {$($modified_blk)+}
584 )+
585 $(
586 $attr {$($blk)+}
587 )*
588 }
589 }
590 };
591 {
592 #[table = $table:path]
593 $item:ty {
594 $(
595 #[$attr_mod:ident]
596 $modified_attr:path {$($modified_blk:tt)+}
597 )*
598 $(
599 $attr_before:path {$($blk_before:tt)+}
600 )+
601 #[$attr_mod_after:ident]
602 $modified_attr_after:path {$($modified_blk_after:tt)+}
603 $($rest:tt)*
604 }
605 } => {
606 $crate::dynamodb_item! {
607 #[table = $table]
608 $item {
609 $(
610 #[$attr_mod]
611 $modified_attr {$($modified_blk)+}
612 )*
613 #[$attr_mod_after]
614 $modified_attr_after {$($modified_blk_after)+}
615 $(
616 $attr_before {$($blk_before)+}
617 )+
618 $($rest)*
619 }
620 }
621 };
622 // Bubble-up PK
623 {
624 @modtop
625 #[table = $table:path]
626 $item:ty {
627 #[partition_key]
628 $pk_attr:path {$($pk_blk:tt)+}
629 $(
630 #[$attr_mod:ident]
631 $modified_attr:path {$($modified_blk:tt)+}
632 )*
633 $(
634 $attr:path {$($blk:tt)+}
635 )*
636 }
637 } => {
638 $crate::dynamodb_item! {
639 @pktop
640 #[table = $table]
641 $item {
642 #[partition_key]
643 $pk_attr {$($pk_blk)+}
644 $(
645 #[$attr_mod]
646 $modified_attr {$($modified_blk)+}
647 )*
648 $(
649 $attr {$($blk)+}
650 )*
651 }
652 }
653 };
654 {
655 @modtop
656 #[table = $table:path]
657 $item:ty {
658 #[$first_attr_mod:ident]
659 $first_modified_attr:path {$($first_modified_blk:tt)+}
660 $(
661 #[$attr_mod:ident]
662 $modified_attr:path {$($modified_blk:tt)+}
663 )+
664 $(
665 $attr:path {$($blk:tt)+}
666 )*
667 }
668 } => {
669 $crate::dynamodb_item! {
670 @modtop
671 #[table = $table]
672 $item {
673 $(
674 #[$attr_mod]
675 $modified_attr {$($modified_blk)+}
676 )+
677 #[$first_attr_mod]
678 $first_modified_attr {$($first_modified_blk)+}
679 $(
680 $attr {$($blk)+}
681 )*
682 }
683 }
684 };
685 // Optionaly Bubble-up SK
686 {
687 @pktop
688 #[table = $table:path]
689 $item:ty {
690 #[partition_key]
691 $pk_attr:path {$($pk_blk:tt)+}
692 #[sort_key]
693 $sk_attr:path {$($sk_blk:tt)+}
694 $(
695 #[$attr_mod:ident]
696 $modified_attr:path {$($modified_blk:tt)+}
697 )*
698 $(
699 $attr:path {$($blk:tt)+}
700 )*
701 $(
702 @barier
703 $(
704 #[$attr_mod_after:ident]
705 $modified_attr_after:path {$($modified_blk_after:tt)+}
706 )+
707 )?
708 }
709 } => {
710 $crate::dynamodb_item! {
711 @allsorted
712 #[table = $table]
713 $item {
714 #[partition_key]
715 $pk_attr {$($pk_blk)+}
716 #[sort_key]
717 $sk_attr {$($sk_blk)+}
718 $(
719 $(
720 #[$attr_mod_after]
721 $modified_attr_after {$($modified_blk_after)+}
722 )+
723 )?
724 $(
725 #[$attr_mod]
726 $modified_attr {$($modified_blk)+}
727 )*
728 $(
729 $attr {$($blk)+}
730 )*
731 }
732 }
733 };
734 {
735 @pktop
736 #[table = $table:path]
737 $item:ty {
738 #[partition_key]
739 $pk_attr:path {$($pk_blk:tt)+}
740 #[$first_attr_mod:ident]
741 $first_modified_attr:path {$($first_modified_blk:tt)+}
742 $(
743 #[$attr_mod:ident]
744 $modified_attr:path {$($modified_blk:tt)+}
745 )*
746 $(
747 $attr:path {$($blk:tt)+}
748 )*
749 $(
750 @barier
751 $(
752 #[$attr_mod_after:ident]
753 $modified_attr_after:path {$($modified_blk_after:tt)+}
754 )+
755 )?
756 }
757 } => {
758 $crate::dynamodb_item! {
759 @pktop
760 #[table = $table]
761 $item {
762 #[partition_key]
763 $pk_attr {$($pk_blk)+}
764 $(
765 #[$attr_mod]
766 $modified_attr {$($modified_blk)+}
767 )*
768 $(
769 $attr {$($blk)+}
770 )*
771 @barier
772 $(
773 $(
774 #[$attr_mod_after]
775 $modified_attr_after {$($modified_blk_after)+}
776 )+
777 )?
778 #[$first_attr_mod]
779 $first_modified_attr {$($first_modified_blk)+}
780 }
781 }
782 };
783 {
784 @pktop
785 #[table = $table:path]
786 $item:ty {
787 #[partition_key]
788 $pk_attr:path {$($pk_blk:tt)+}
789 $(
790 $attr:path {$($blk:tt)+}
791 )*
792 $(
793 @barier
794 $(
795 #[$attr_mod_after:ident]
796 $modified_attr_after:path {$($modified_blk_after:tt)+}
797 )+
798 )?
799 }
800 } => {
801 $crate::dynamodb_item! {
802 @allsorted
803 #[table = $table]
804 $item {
805 #[partition_key]
806 $pk_attr {$($pk_blk)+}
807 $(
808 $(
809 #[$attr_mod_after]
810 $modified_attr_after {$($modified_blk_after)+}
811 )+
812 )?
813 $(
814 $attr {$($blk)+}
815 )*
816 }
817 }
818 };
819 // Processing
820 {
821 @allsorted
822 #[table = $table:path]
823 $item:ty {
824 #[partition_key]
825 $pk_attr:path {$($pk_blk:tt)+}
826 $(
827 #[sort_key]
828 $sk_attr:path {$($sk_blk:tt)+}
829 )?
830 $(
831 #[marker_only]
832 $marker_only_attr:path {$($marker_only_blk:tt)+}
833 )*
834 $(
835 $attr:path {$($blk:tt)+}
836 )*
837 }
838 } => {
839 $crate::dynamodb_item! {
840 @dbitem $table ; $item {
841 $($attr)*
842 }
843 }
844 $crate::has_attributes! {
845 $item {
846 $pk_attr {$($pk_blk)+}
847 $($sk_attr {$($sk_blk)+})?
848 $(
849 $marker_only_attr {$($marker_only_blk)+}
850 )*
851 $(
852 $attr {$($blk)+}
853 )*
854 }
855 }
856 };
857 {
858 @dbitem $table:path; $item:ty {
859 $($attr:path)*
860 }
861 } => {
862 impl $crate::DynamoDBItem<$table> for $item {
863 type AdditionalAttributes = $crate::attr_list![$($attr),*];
864 }
865 };
866 // === diagnostic arms: catch-all for malformed input ===
867 // User-form catch-all (table attribute present, body malformed —
868 // most commonly a missing `#[partition_key]`).
869 {
870 #[table = $table:path]
871 $item:ty {$($tt:tt)*}
872 } => {
873 ::core::compile_error!(concat!(
874 "`dynamodb_item!`: malformed body. Most common cause is a missing ",
875 "`#[partition_key]` annotation — exactly one key attribute block must ",
876 "be marked `#[partition_key]`. Expected shape:\n",
877 " #[table = TableType]\n",
878 " ItemType {\n",
879 " #[partition_key]\n",
880 " PkAttr { ... }\n",
881 " [#[sort_key]]\n",
882 " [SkAttr { ... }]\n",
883 " [#[marker_only]]\n",
884 " [OtherAttr { ... }]\n",
885 " AdditionalAttr { ... }\n",
886 " ...\n",
887 " }"
888 ));
889 };
890 // Generic fallback (missing `#[table = ...]` or otherwise unrecognised).
891 ($($tt:tt)*) => {
892 ::core::compile_error!(concat!(
893 "`dynamodb_item!` expected:\n",
894 " #[table = TableType]\n",
895 " ItemType {\n",
896 " #[partition_key]\n",
897 " PkAttr { ... }\n",
898 " [#[sort_key]]\n",
899 " [SkAttr { ... }]\n",
900 " AdditionalAttr { ... }\n",
901 " ...\n",
902 " }"
903 ));
904 };
905}
906
907/// Defines one or more DynamoDB table zero-sized types implementing
908/// [`TableDefinition`](crate::TableDefinition).
909///
910/// Each definition generates a `pub struct` with an internal key schema and
911/// a `table_name()` function. The key schema is derived from the `type`
912/// declarations:
913///
914/// - `type PartitionKey = ...` only → [`SimpleKeySchema`](crate::SimpleKeySchema)
915/// - `type PartitionKey = ...` + `type SortKey = ...` → [`CompositeKeySchema`](crate::CompositeKeySchema)
916///
917/// Multiple table definitions can appear in a single invocation.
918///
919/// # Syntax
920///
921/// ```text
922/// table_definitions! {
923/// [doc comments and attributes]
924/// TableName {
925/// type PartitionKey = PkAttr;
926/// type SortKey = SkAttr; // optional
927/// fn table_name() -> String { ... }
928/// }
929/// ...
930/// }
931/// ```
932///
933/// # Examples
934///
935/// ```
936/// # use dynamodb_facade::test_fixtures::*;
937/// use dynamodb_facade::table_definitions;
938///
939/// table_definitions! {
940/// /// The platform mono-table with composite key (PK + SK).
941/// LearningTable {
942/// type PartitionKey = PK;
943/// type SortKey = SK;
944/// fn table_name() -> String {
945/// std::env::var("TABLE_NAME").unwrap_or_else(|_| "learning".to_owned())
946/// }
947/// }
948/// }
949///
950/// use dynamodb_facade::TableDefinition;
951/// assert_eq!(LearningTable::table_name(), "learning");
952/// ```
953#[macro_export]
954macro_rules! table_definitions {
955 {
956 $(
957 $(#[$meta:meta])*
958 $table:ident {
959 $(type $ident_before:ident = $identty_before:ty;)*
960 fn $table_name_fct:ident() -> String $table_name:block
961 $(type $ident_after:ident = $identty_after:ty;)*
962 }
963 )+
964 } => {
965 $(
966 $(#[$meta])*
967 pub struct $table;
968 const _: () = {
969 $crate::key_schema! {
970 __TableKeySchema {
971 $(type $ident_before = $identty_before;)*
972 $(type $ident_after = $identty_after;)*
973 }
974 }
975
976 impl $crate::TableDefinition for $table {
977 type KeySchema = __TableKeySchema;
978 fn $table_name_fct() -> String $table_name
979 }
980 };
981 )+
982 };
983 // === diagnostic arm: catch-all for malformed input ===
984 ($($tt:tt)*) => {
985 ::core::compile_error!(concat!(
986 "`table_definitions!` expected:\n",
987 " TableName {\n",
988 " type PartitionKey = PkAttr;\n",
989 " [type SortKey = SkAttr;]\n",
990 " fn table_name() -> String { ... }\n",
991 " }\n",
992 " ... (one or more)"
993 ));
994 };
995}
996
997/// Defines one or more DynamoDB Secondary Index (LSI or GSI) zero-sized types implementing
998/// [`IndexDefinition`](crate::IndexDefinition).
999///
1000/// Each definition generates a `pub struct` associated with a specific table
1001/// type. The `#[table = TableType]` attribute is required and links the index
1002/// to its parent table. The key schema follows the same rules as
1003/// [`table_definitions!`](crate::table_definitions): `PartitionKey` alone gives a simple-key index;
1004/// adding `SortKey` gives a composite-key index.
1005///
1006/// Multiple index definitions can appear in a single invocation.
1007///
1008/// # Syntax
1009///
1010/// ```text
1011/// index_definitions! {
1012/// [doc comments and attributes]
1013/// #[table = TableType]
1014/// IndexName {
1015/// type PartitionKey = PkAttr;
1016/// type SortKey = SkAttr; // optional
1017/// fn index_name() -> String { ... }
1018/// }
1019/// ...
1020/// }
1021/// ```
1022///
1023/// # Examples
1024///
1025/// ```
1026/// # use dynamodb_facade::test_fixtures::*;
1027/// use dynamodb_facade::index_definitions;
1028///
1029/// index_definitions! {
1030/// /// GSI on item type — query all items of a given type.
1031/// #[table = PlatformTable]
1032/// CourseTypeIndex {
1033/// type PartitionKey = ItemType;
1034/// fn index_name() -> String { "iType".to_owned() }
1035/// }
1036///
1037/// /// GSI on email with composite key.
1038/// #[table = PlatformTable]
1039/// EmailSkIndex {
1040/// type PartitionKey = Email;
1041/// type SortKey = SK;
1042/// fn index_name() -> String { "iEmailSk".to_owned() }
1043/// }
1044/// }
1045///
1046/// use dynamodb_facade::IndexDefinition;
1047/// assert_eq!(CourseTypeIndex::index_name(), "iType");
1048/// assert_eq!(EmailSkIndex::index_name(), "iEmailSk");
1049/// ```
1050#[macro_export]
1051macro_rules! index_definitions {
1052 // Syntaxic QoL: Bubble $[table = ...] up
1053 {
1054 $(
1055 $(#[$($meta:tt)+])*
1056 $index:ident {$($rest:tt)+}
1057 )+
1058 } => {
1059 $(
1060 $crate::index_definitions!{
1061 @solo
1062 $(#[$($meta)+])*
1063 $index {$($rest)+}
1064 }
1065 )+
1066 };
1067 // Syntaxic QoL: Bubble $[table = ...] up
1068 {
1069 @solo
1070 #[table = $table:ty]
1071 $(#[$meta:meta])*
1072 $index:ident {$($rest:tt)+}
1073 $(#[$firsts:meta])*
1074 } => {
1075 $crate::index_definitions!{
1076 @tableup $table;
1077 $(#[$firsts])*
1078 $(#[$meta])*
1079 $index {$($rest)+}
1080 }
1081 };
1082 {
1083 @solo
1084 #[$first:meta]
1085 $(#[$($others:tt)+])*
1086 $index:ident {$($rest:tt)+}
1087 $(#[$($firsts:tt)+])*
1088 } => {
1089 $crate::index_definitions!{
1090 @solo
1091 $(#[$($others)+])*
1092 $index {$($rest)+}
1093 $(#[$($firsts)+])*
1094 #[$first]
1095 }
1096 };
1097 // Processing
1098 {
1099 @tableup $table:ty;
1100 $(#[$meta:meta])*
1101 $index:ident {
1102 $(type $ident_before:ident = $identty_before:ty;)*
1103 fn $index_name_fct:ident() -> String $index_name:block
1104 $(type $ident_after:ident = $identty_after:ty;)*
1105 }
1106 } => {
1107 $(#[$meta])*
1108 pub struct $index;
1109 const _: () = {
1110 $crate::key_schema! {
1111 __IndexKeySchema {
1112 $(type $ident_before = $identty_before;)*
1113 $(type $ident_after = $identty_after;)*
1114 }
1115 }
1116
1117 impl $crate::IndexDefinition<$table> for $index {
1118 type KeySchema = __IndexKeySchema;
1119 fn $index_name_fct() -> String $index_name
1120 }
1121 };
1122 };
1123 // === diagnostic arm: catch-all for malformed input ===
1124 ($($tt:tt)*) => {
1125 ::core::compile_error!(concat!(
1126 "`index_definitions!` expected:\n",
1127 " #[table = TableType]\n",
1128 " IndexName {\n",
1129 " type PartitionKey = PkAttr;\n",
1130 " [type SortKey = SkAttr;]\n",
1131 " fn index_name() -> String { ... }\n",
1132 " }\n",
1133 " ... (one or more)"
1134 ));
1135 };
1136}