Skip to main content

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}