Skip to main content

rustack_dynamodb_model/
types.rs

1//! Shared DynamoDB types for the 12 MVP operations.
2//!
3//! All types follow the DynamoDB JSON wire format with `PascalCase` field names.
4//! Structs use `#[serde(rename_all = "PascalCase")]` to match the DynamoDB API.
5//!
6//! Enum variants use idiomatic Rust `PascalCase` naming with `#[serde(rename)]`
7//! attributes to map to the `SCREAMING_SNAKE_CASE` wire format that DynamoDB uses.
8//!
9//! # MVP Operations Covered
10//!
11//! - Table management: `CreateTable`, `DeleteTable`, `DescribeTable`, `ListTables`
12//! - Item CRUD: `PutItem`, `GetItem`, `UpdateItem`, `DeleteItem`
13//! - Queries: `Query`, `Scan`
14//! - Batch: `BatchGetItem`, `BatchWriteItem`
15
16use std::collections::HashMap;
17
18use serde::{Deserialize, Serialize};
19
20use crate::attribute_value::AttributeValue;
21
22// ---------------------------------------------------------------------------
23// Enums
24// ---------------------------------------------------------------------------
25
26/// Key type within a key schema element.
27///
28/// `Hash` denotes the partition key; `Range` denotes the sort key.
29#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
30pub enum KeyType {
31    /// Partition key.
32    #[serde(rename = "HASH")]
33    Hash,
34    /// Sort key.
35    #[serde(rename = "RANGE")]
36    Range,
37}
38
39impl KeyType {
40    /// Returns the DynamoDB wire-format string representation of this key type.
41    #[must_use]
42    pub fn as_str(&self) -> &'static str {
43        match self {
44            Self::Hash => "HASH",
45            Self::Range => "RANGE",
46        }
47    }
48}
49
50impl std::fmt::Display for KeyType {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.write_str(self.as_str())
53    }
54}
55
56/// Scalar attribute types supported in key schema and attribute definitions.
57///
58/// DynamoDB only allows `S`, `N`, and `B` for key attributes, but the wire
59/// protocol may receive other values which must be rejected with a
60/// `ValidationException` rather than a deserialization error.
61#[derive(Debug, Clone, PartialEq, Eq, Hash)]
62pub enum ScalarAttributeType {
63    /// String type.
64    S,
65    /// Number type.
66    N,
67    /// Binary type.
68    B,
69    /// An unknown/invalid attribute type received from the client.
70    Unknown(String),
71}
72
73impl ScalarAttributeType {
74    /// Returns the DynamoDB wire-format string representation.
75    #[must_use]
76    pub fn as_str(&self) -> &str {
77        match self {
78            Self::S => "S",
79            Self::N => "N",
80            Self::B => "B",
81            Self::Unknown(s) => s.as_str(),
82        }
83    }
84
85    /// Returns `true` if this is a valid key attribute type (S, N, or B).
86    #[must_use]
87    pub fn is_valid_key_type(&self) -> bool {
88        matches!(self, Self::S | Self::N | Self::B)
89    }
90}
91
92impl Serialize for ScalarAttributeType {
93    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
94        serializer.serialize_str(self.as_str())
95    }
96}
97
98impl<'de> Deserialize<'de> for ScalarAttributeType {
99    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
100        let s = String::deserialize(deserializer)?;
101        match s.as_str() {
102            "S" => Ok(Self::S),
103            "N" => Ok(Self::N),
104            "B" => Ok(Self::B),
105            _ => Ok(Self::Unknown(s)),
106        }
107    }
108}
109
110impl std::fmt::Display for ScalarAttributeType {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        f.write_str(self.as_str())
113    }
114}
115
116/// Current status of a DynamoDB table.
117#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
118pub enum TableStatus {
119    /// The table is being created.
120    #[serde(rename = "CREATING")]
121    Creating,
122    /// The table is ready for use.
123    #[serde(rename = "ACTIVE")]
124    Active,
125    /// The table is being deleted.
126    #[serde(rename = "DELETING")]
127    Deleting,
128    /// The table is being updated (e.g., GSI changes).
129    #[serde(rename = "UPDATING")]
130    Updating,
131    /// The table is being archived.
132    #[serde(rename = "ARCHIVING")]
133    Archiving,
134    /// The table has been archived.
135    #[serde(rename = "ARCHIVED")]
136    Archived,
137    /// The table is inaccessible due to encryption credentials issues.
138    #[serde(rename = "INACCESSIBLE_ENCRYPTION_CREDENTIALS")]
139    InaccessibleEncryptionCredentials,
140}
141
142impl TableStatus {
143    /// Returns the DynamoDB wire-format string representation.
144    #[must_use]
145    pub fn as_str(&self) -> &'static str {
146        match self {
147            Self::Creating => "CREATING",
148            Self::Active => "ACTIVE",
149            Self::Deleting => "DELETING",
150            Self::Updating => "UPDATING",
151            Self::Archiving => "ARCHIVING",
152            Self::Archived => "ARCHIVED",
153            Self::InaccessibleEncryptionCredentials => "INACCESSIBLE_ENCRYPTION_CREDENTIALS",
154        }
155    }
156}
157
158impl std::fmt::Display for TableStatus {
159    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
160        f.write_str(self.as_str())
161    }
162}
163
164/// Billing mode for a DynamoDB table.
165#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
166pub enum BillingMode {
167    /// Provisioned capacity mode with explicit RCU/WCU settings.
168    Provisioned,
169    /// On-demand capacity mode (pay per request).
170    #[default]
171    PayPerRequest,
172    /// An unknown billing mode value received from the client.
173    Unknown(String),
174}
175
176impl BillingMode {
177    /// Returns the DynamoDB wire-format string representation.
178    #[must_use]
179    pub fn as_str(&self) -> &str {
180        match self {
181            Self::Provisioned => "PROVISIONED",
182            Self::PayPerRequest => "PAY_PER_REQUEST",
183            Self::Unknown(s) => s.as_str(),
184        }
185    }
186}
187
188impl Serialize for BillingMode {
189    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
190        serializer.serialize_str(self.as_str())
191    }
192}
193
194impl<'de> Deserialize<'de> for BillingMode {
195    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
196        let s = String::deserialize(deserializer)?;
197        match s.as_str() {
198            "PROVISIONED" => Ok(Self::Provisioned),
199            "PAY_PER_REQUEST" => Ok(Self::PayPerRequest),
200            _ => Ok(Self::Unknown(s)),
201        }
202    }
203}
204
205impl std::fmt::Display for BillingMode {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        f.write_str(self.as_str())
208    }
209}
210
211/// Projection type for secondary indexes.
212#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
213pub enum ProjectionType {
214    /// All attributes from the table are projected into the index.
215    #[default]
216    #[serde(rename = "ALL")]
217    All,
218    /// Only the index and primary keys are projected.
219    #[serde(rename = "KEYS_ONLY")]
220    KeysOnly,
221    /// Only specified non-key attributes are projected alongside keys.
222    #[serde(rename = "INCLUDE")]
223    Include,
224}
225
226impl ProjectionType {
227    /// Returns the DynamoDB wire-format string representation.
228    #[must_use]
229    pub fn as_str(&self) -> &'static str {
230        match self {
231            Self::All => "ALL",
232            Self::KeysOnly => "KEYS_ONLY",
233            Self::Include => "INCLUDE",
234        }
235    }
236}
237
238impl std::fmt::Display for ProjectionType {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        f.write_str(self.as_str())
241    }
242}
243
244/// Stream view type controlling what data is captured in DynamoDB Streams.
245#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
246pub enum StreamViewType {
247    /// Only the key attributes of the modified item.
248    #[serde(rename = "KEYS_ONLY")]
249    KeysOnly,
250    /// The entire item as it appears after modification.
251    #[serde(rename = "NEW_IMAGE")]
252    NewImage,
253    /// The entire item as it appeared before modification.
254    #[serde(rename = "OLD_IMAGE")]
255    OldImage,
256    /// Both the new and old item images.
257    #[serde(rename = "NEW_AND_OLD_IMAGES")]
258    NewAndOldImages,
259}
260
261impl StreamViewType {
262    /// Returns the DynamoDB wire-format string representation.
263    #[must_use]
264    pub fn as_str(&self) -> &'static str {
265        match self {
266            Self::KeysOnly => "KEYS_ONLY",
267            Self::NewImage => "NEW_IMAGE",
268            Self::OldImage => "OLD_IMAGE",
269            Self::NewAndOldImages => "NEW_AND_OLD_IMAGES",
270        }
271    }
272}
273
274impl std::fmt::Display for StreamViewType {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        f.write_str(self.as_str())
277    }
278}
279
280/// SSE (Server-Side Encryption) type.
281#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
282pub enum SseType {
283    /// AWS-owned key encryption (default, free).
284    #[default]
285    #[serde(rename = "AES256")]
286    Aes256,
287    /// AWS KMS managed key encryption.
288    #[serde(rename = "KMS")]
289    Kms,
290}
291
292impl SseType {
293    /// Returns the DynamoDB wire-format string representation.
294    #[must_use]
295    pub fn as_str(&self) -> &'static str {
296        match self {
297            Self::Aes256 => "AES256",
298            Self::Kms => "KMS",
299        }
300    }
301}
302
303impl std::fmt::Display for SseType {
304    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
305        f.write_str(self.as_str())
306    }
307}
308
309/// SSE status.
310#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
311pub enum SseStatus {
312    /// SSE is being enabled.
313    #[serde(rename = "ENABLING")]
314    Enabling,
315    /// SSE is active.
316    #[serde(rename = "ENABLED")]
317    Enabled,
318    /// SSE is being disabled.
319    #[serde(rename = "DISABLING")]
320    Disabling,
321    /// SSE is disabled.
322    #[serde(rename = "DISABLED")]
323    Disabled,
324    /// SSE is being updated.
325    #[serde(rename = "UPDATING")]
326    Updating,
327}
328
329impl SseStatus {
330    /// Returns the DynamoDB wire-format string representation.
331    #[must_use]
332    pub fn as_str(&self) -> &'static str {
333        match self {
334            Self::Enabling => "ENABLING",
335            Self::Enabled => "ENABLED",
336            Self::Disabling => "DISABLING",
337            Self::Disabled => "DISABLED",
338            Self::Updating => "UPDATING",
339        }
340    }
341}
342
343impl std::fmt::Display for SseStatus {
344    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
345        f.write_str(self.as_str())
346    }
347}
348
349/// Index status for global secondary indexes.
350#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
351pub enum IndexStatus {
352    /// The index is being created.
353    #[serde(rename = "CREATING")]
354    Creating,
355    /// The index is being updated.
356    #[serde(rename = "UPDATING")]
357    Updating,
358    /// The index is being deleted.
359    #[serde(rename = "DELETING")]
360    Deleting,
361    /// The index is active and ready for use.
362    #[serde(rename = "ACTIVE")]
363    Active,
364}
365
366impl IndexStatus {
367    /// Returns the DynamoDB wire-format string representation.
368    #[must_use]
369    pub fn as_str(&self) -> &'static str {
370        match self {
371            Self::Creating => "CREATING",
372            Self::Updating => "UPDATING",
373            Self::Deleting => "DELETING",
374            Self::Active => "ACTIVE",
375        }
376    }
377}
378
379impl std::fmt::Display for IndexStatus {
380    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
381        f.write_str(self.as_str())
382    }
383}
384
385/// Determines what values are returned by write operations.
386#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
387pub enum ReturnValue {
388    /// Nothing is returned.
389    #[default]
390    #[serde(rename = "NONE")]
391    None,
392    /// Returns all attributes of the item as they appeared before the operation.
393    #[serde(rename = "ALL_OLD")]
394    AllOld,
395    /// Returns only the updated attributes as they appeared before the operation.
396    #[serde(rename = "UPDATED_OLD")]
397    UpdatedOld,
398    /// Returns all attributes of the item as they appear after the operation.
399    #[serde(rename = "ALL_NEW")]
400    AllNew,
401    /// Returns only the updated attributes as they appear after the operation.
402    #[serde(rename = "UPDATED_NEW")]
403    UpdatedNew,
404}
405
406impl ReturnValue {
407    /// Returns the DynamoDB wire-format string representation.
408    #[must_use]
409    pub fn as_str(&self) -> &'static str {
410        match self {
411            Self::None => "NONE",
412            Self::AllOld => "ALL_OLD",
413            Self::UpdatedOld => "UPDATED_OLD",
414            Self::AllNew => "ALL_NEW",
415            Self::UpdatedNew => "UPDATED_NEW",
416        }
417    }
418}
419
420impl std::fmt::Display for ReturnValue {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        f.write_str(self.as_str())
423    }
424}
425
426/// Controls whether consumed capacity information is returned.
427#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
428pub enum ReturnConsumedCapacity {
429    /// Return consumed capacity for the table and any indexes involved.
430    #[serde(rename = "INDEXES")]
431    Indexes,
432    /// Return only the total consumed capacity.
433    #[serde(rename = "TOTAL")]
434    Total,
435    /// Do not return consumed capacity (default).
436    #[default]
437    #[serde(rename = "NONE")]
438    None,
439}
440
441impl ReturnConsumedCapacity {
442    /// Returns the DynamoDB wire-format string representation.
443    #[must_use]
444    pub fn as_str(&self) -> &'static str {
445        match self {
446            Self::Indexes => "INDEXES",
447            Self::Total => "TOTAL",
448            Self::None => "NONE",
449        }
450    }
451
452    /// Returns `true` if capacity tracking should be performed.
453    #[must_use]
454    pub fn should_report(&self) -> bool {
455        !matches!(self, Self::None)
456    }
457
458    /// Returns `true` if per-index capacity should be reported.
459    #[must_use]
460    pub fn should_report_indexes(&self) -> bool {
461        matches!(self, Self::Indexes)
462    }
463}
464
465impl std::fmt::Display for ReturnConsumedCapacity {
466    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
467        f.write_str(self.as_str())
468    }
469}
470
471/// Controls whether item collection metrics are returned for writes.
472#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
473pub enum ReturnItemCollectionMetrics {
474    /// Return item collection size estimates.
475    #[serde(rename = "SIZE")]
476    Size,
477    /// Do not return item collection metrics (default).
478    #[default]
479    #[serde(rename = "NONE")]
480    None,
481}
482
483impl ReturnItemCollectionMetrics {
484    /// Returns the DynamoDB wire-format string representation.
485    #[must_use]
486    pub fn as_str(&self) -> &'static str {
487        match self {
488            Self::Size => "SIZE",
489            Self::None => "NONE",
490        }
491    }
492
493    /// Returns `true` if metrics should be collected.
494    #[must_use]
495    pub fn should_report(&self) -> bool {
496        matches!(self, Self::Size)
497    }
498}
499
500impl std::fmt::Display for ReturnItemCollectionMetrics {
501    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
502        f.write_str(self.as_str())
503    }
504}
505
506/// Attributes to retrieve in a `Query` or `Scan` operation.
507#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
508pub enum Select {
509    /// All attributes of the item.
510    #[default]
511    #[serde(rename = "ALL_ATTRIBUTES")]
512    AllAttributes,
513    /// All projected attributes (for index queries).
514    #[serde(rename = "ALL_PROJECTED_ATTRIBUTES")]
515    AllProjectedAttributes,
516    /// Only the attributes specified in `ProjectionExpression`.
517    #[serde(rename = "SPECIFIC_ATTRIBUTES")]
518    SpecificAttributes,
519    /// Only the count of matching items (no item data).
520    #[serde(rename = "COUNT")]
521    Count,
522}
523
524impl Select {
525    /// Returns the DynamoDB wire-format string representation.
526    #[must_use]
527    pub fn as_str(&self) -> &'static str {
528        match self {
529            Self::AllAttributes => "ALL_ATTRIBUTES",
530            Self::AllProjectedAttributes => "ALL_PROJECTED_ATTRIBUTES",
531            Self::SpecificAttributes => "SPECIFIC_ATTRIBUTES",
532            Self::Count => "COUNT",
533        }
534    }
535}
536
537impl std::fmt::Display for Select {
538    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
539        f.write_str(self.as_str())
540    }
541}
542
543/// Logical operator for combining multiple conditions (legacy API).
544#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
545pub enum ConditionalOperator {
546    /// All conditions must be true.
547    #[default]
548    #[serde(rename = "AND")]
549    And,
550    /// At least one condition must be true.
551    #[serde(rename = "OR")]
552    Or,
553}
554
555impl ConditionalOperator {
556    /// Returns the DynamoDB wire-format string representation.
557    #[must_use]
558    pub fn as_str(&self) -> &'static str {
559        match self {
560            Self::And => "AND",
561            Self::Or => "OR",
562        }
563    }
564}
565
566impl std::fmt::Display for ConditionalOperator {
567    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568        f.write_str(self.as_str())
569    }
570}
571
572/// Comparison operator for legacy `Condition` filters.
573///
574/// These are used with the legacy `ScanFilter`, `QueryFilter`, `KeyConditions`,
575/// and `Expected` parameters. Modern applications should use expression-based
576/// APIs (`FilterExpression`, `KeyConditionExpression`, `ConditionExpression`).
577#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
578pub enum ComparisonOperator {
579    /// Equal to.
580    #[serde(rename = "EQ")]
581    Eq,
582    /// Not equal to.
583    #[serde(rename = "NE")]
584    Ne,
585    /// Less than or equal to.
586    #[serde(rename = "LE")]
587    Le,
588    /// Less than.
589    #[serde(rename = "LT")]
590    Lt,
591    /// Greater than or equal to.
592    #[serde(rename = "GE")]
593    Ge,
594    /// Greater than.
595    #[serde(rename = "GT")]
596    Gt,
597    /// Attribute does not exist.
598    #[serde(rename = "NOT_NULL")]
599    NotNull,
600    /// Attribute exists (with any value, including null).
601    #[serde(rename = "NULL")]
602    Null,
603    /// Attribute value contains the specified substring or set member.
604    #[serde(rename = "CONTAINS")]
605    Contains,
606    /// Attribute value does not contain the specified substring or set member.
607    #[serde(rename = "NOT_CONTAINS")]
608    NotContains,
609    /// Attribute value begins with the specified substring.
610    #[serde(rename = "BEGINS_WITH")]
611    BeginsWith,
612    /// Attribute value is a member of the specified list.
613    #[serde(rename = "IN")]
614    In,
615    /// Attribute value is between two values (inclusive).
616    #[serde(rename = "BETWEEN")]
617    Between,
618}
619
620impl ComparisonOperator {
621    /// Returns the DynamoDB wire-format string representation.
622    #[must_use]
623    pub fn as_str(&self) -> &'static str {
624        match self {
625            Self::Eq => "EQ",
626            Self::Ne => "NE",
627            Self::Le => "LE",
628            Self::Lt => "LT",
629            Self::Ge => "GE",
630            Self::Gt => "GT",
631            Self::NotNull => "NOT_NULL",
632            Self::Null => "NULL",
633            Self::Contains => "CONTAINS",
634            Self::NotContains => "NOT_CONTAINS",
635            Self::BeginsWith => "BEGINS_WITH",
636            Self::In => "IN",
637            Self::Between => "BETWEEN",
638        }
639    }
640}
641
642impl std::fmt::Display for ComparisonOperator {
643    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
644        f.write_str(self.as_str())
645    }
646}
647
648// ---------------------------------------------------------------------------
649// Structs - Key Schema & Attributes
650// ---------------------------------------------------------------------------
651
652/// An element of the key schema for a table or index.
653///
654/// Specifies an attribute name and whether it serves as a `HASH` (partition)
655/// or `RANGE` (sort) key.
656#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
657#[serde(rename_all = "PascalCase")]
658pub struct KeySchemaElement {
659    /// The name of the key attribute.
660    pub attribute_name: String,
661    /// The role of the attribute in the key schema (`HASH` or `RANGE`).
662    pub key_type: KeyType,
663}
664
665/// An attribute definition specifying the attribute name and its scalar type.
666///
667/// Used in `CreateTable` to declare attributes that participate in key schemas
668/// or secondary indexes.
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(rename_all = "PascalCase")]
671pub struct AttributeDefinition {
672    /// The name of the attribute.
673    pub attribute_name: String,
674    /// The scalar data type of the attribute (`S`, `N`, or `B`).
675    pub attribute_type: ScalarAttributeType,
676}
677
678// ---------------------------------------------------------------------------
679// Structs - Billing & Throughput
680// ---------------------------------------------------------------------------
681
682/// Summary of the billing mode for a table.
683#[derive(Debug, Clone, Default, Serialize, Deserialize)]
684#[serde(rename_all = "PascalCase")]
685pub struct BillingModeSummary {
686    /// The billing mode currently in effect.
687    #[serde(skip_serializing_if = "Option::is_none")]
688    pub billing_mode: Option<BillingMode>,
689    /// The date and time (epoch seconds) when the billing mode was last set.
690    #[serde(skip_serializing_if = "Option::is_none")]
691    pub last_update_to_pay_per_request_date_time: Option<f64>,
692}
693
694/// Provisioned throughput settings for a table or GSI (input).
695///
696/// Specifies the read and write capacity units to provision.
697/// For on-demand tables this is accepted but not enforced.
698#[derive(Debug, Clone, Default, Serialize, Deserialize)]
699#[serde(rename_all = "PascalCase")]
700pub struct ProvisionedThroughput {
701    /// The maximum number of strongly consistent reads per second.
702    pub read_capacity_units: i64,
703    /// The maximum number of writes per second.
704    pub write_capacity_units: i64,
705}
706
707/// Provisioned throughput description (output) including timestamps.
708///
709/// Returned in `DescribeTable` responses with additional metadata about
710/// when capacity was last changed.
711#[derive(Debug, Clone, Default, Serialize, Deserialize)]
712#[serde(rename_all = "PascalCase")]
713pub struct ProvisionedThroughputDescription {
714    /// The number of read capacity units provisioned.
715    pub read_capacity_units: i64,
716    /// The number of write capacity units provisioned.
717    pub write_capacity_units: i64,
718    /// The number of provisioned throughput decreases for this day.
719    #[serde(skip_serializing_if = "Option::is_none")]
720    pub number_of_decreases_today: Option<i64>,
721    /// The date and time (epoch seconds) of the last provisioned throughput increase.
722    #[serde(skip_serializing_if = "Option::is_none")]
723    pub last_increase_date_time: Option<f64>,
724    /// The date and time (epoch seconds) of the last provisioned throughput decrease.
725    #[serde(skip_serializing_if = "Option::is_none")]
726    pub last_decrease_date_time: Option<f64>,
727}
728
729// ---------------------------------------------------------------------------
730// Structs - Projection
731// ---------------------------------------------------------------------------
732
733/// Projection settings for a secondary index.
734///
735/// Controls which attributes are copied (projected) from the base table
736/// into the index.
737#[derive(Debug, Clone, Default, Serialize, Deserialize)]
738#[serde(rename_all = "PascalCase")]
739pub struct Projection {
740    /// The set of attributes projected into the index.
741    #[serde(skip_serializing_if = "Option::is_none")]
742    pub projection_type: Option<ProjectionType>,
743    /// The non-key attributes to project when `projection_type` is `INCLUDE`.
744    #[serde(default, skip_serializing_if = "Vec::is_empty")]
745    pub non_key_attributes: Vec<String>,
746}
747
748// ---------------------------------------------------------------------------
749// Structs - Secondary Indexes (Input)
750// ---------------------------------------------------------------------------
751
752/// Global secondary index definition (input for `CreateTable`).
753///
754/// A GSI has its own key schema, projection, and optional provisioned throughput.
755#[derive(Debug, Clone, Serialize, Deserialize)]
756#[serde(rename_all = "PascalCase")]
757pub struct GlobalSecondaryIndex {
758    /// The name of the global secondary index.
759    pub index_name: String,
760    /// The key schema for this index (partition key, optional sort key).
761    pub key_schema: Vec<KeySchemaElement>,
762    /// The attributes projected into this index.
763    pub projection: Projection,
764    /// The provisioned throughput for this index (required for `PROVISIONED` mode).
765    #[serde(skip_serializing_if = "Option::is_none")]
766    pub provisioned_throughput: Option<ProvisionedThroughput>,
767}
768
769/// Global secondary index description (output from `DescribeTable`).
770///
771/// Contains runtime metadata in addition to the index definition.
772#[derive(Debug, Clone, Default, Serialize, Deserialize)]
773#[serde(rename_all = "PascalCase")]
774pub struct GlobalSecondaryIndexDescription {
775    /// The name of the global secondary index.
776    #[serde(skip_serializing_if = "Option::is_none")]
777    pub index_name: Option<String>,
778    /// The key schema for this index.
779    #[serde(default, skip_serializing_if = "Vec::is_empty")]
780    pub key_schema: Vec<KeySchemaElement>,
781    /// The projection settings for this index.
782    #[serde(skip_serializing_if = "Option::is_none")]
783    pub projection: Option<Projection>,
784    /// The current status of the index.
785    #[serde(skip_serializing_if = "Option::is_none")]
786    pub index_status: Option<IndexStatus>,
787    /// Whether the index is currently backfilling.
788    #[serde(skip_serializing_if = "Option::is_none")]
789    pub backfilling: Option<bool>,
790    /// The provisioned throughput settings for this index.
791    #[serde(skip_serializing_if = "Option::is_none")]
792    pub provisioned_throughput: Option<ProvisionedThroughputDescription>,
793    /// The total size of the index in bytes.
794    #[serde(skip_serializing_if = "Option::is_none")]
795    pub index_size_bytes: Option<i64>,
796    /// The number of items in the index.
797    #[serde(skip_serializing_if = "Option::is_none")]
798    pub item_count: Option<i64>,
799    /// The Amazon Resource Name (ARN) of the index.
800    #[serde(skip_serializing_if = "Option::is_none")]
801    pub index_arn: Option<String>,
802}
803
804/// Local secondary index definition (input for `CreateTable`).
805///
806/// An LSI shares the partition key with the base table but uses a different sort key.
807/// LSIs must be defined at table creation time and cannot be modified afterward.
808#[derive(Debug, Clone, Serialize, Deserialize)]
809#[serde(rename_all = "PascalCase")]
810pub struct LocalSecondaryIndex {
811    /// The name of the local secondary index.
812    pub index_name: String,
813    /// The key schema for this index.
814    pub key_schema: Vec<KeySchemaElement>,
815    /// The attributes projected into this index.
816    pub projection: Projection,
817}
818
819/// Local secondary index description (output from `DescribeTable`).
820///
821/// Contains runtime metadata in addition to the index definition.
822#[derive(Debug, Clone, Default, Serialize, Deserialize)]
823#[serde(rename_all = "PascalCase")]
824pub struct LocalSecondaryIndexDescription {
825    /// The name of the local secondary index.
826    #[serde(skip_serializing_if = "Option::is_none")]
827    pub index_name: Option<String>,
828    /// The key schema for this index.
829    #[serde(default, skip_serializing_if = "Vec::is_empty")]
830    pub key_schema: Vec<KeySchemaElement>,
831    /// The projection settings for this index.
832    #[serde(skip_serializing_if = "Option::is_none")]
833    pub projection: Option<Projection>,
834    /// The total size of the index in bytes.
835    #[serde(skip_serializing_if = "Option::is_none")]
836    pub index_size_bytes: Option<i64>,
837    /// The number of items in the index.
838    #[serde(skip_serializing_if = "Option::is_none")]
839    pub item_count: Option<i64>,
840    /// The Amazon Resource Name (ARN) of the index.
841    #[serde(skip_serializing_if = "Option::is_none")]
842    pub index_arn: Option<String>,
843}
844
845// ---------------------------------------------------------------------------
846// Structs - Streams
847// ---------------------------------------------------------------------------
848
849/// Stream specification for enabling DynamoDB Streams on a table.
850#[derive(Debug, Clone, Default, Serialize, Deserialize)]
851#[serde(rename_all = "PascalCase")]
852pub struct StreamSpecification {
853    /// Whether DynamoDB Streams is enabled on the table.
854    pub stream_enabled: bool,
855    /// What information is written to the stream when an item is modified.
856    #[serde(skip_serializing_if = "Option::is_none")]
857    pub stream_view_type: Option<StreamViewType>,
858}
859
860// ---------------------------------------------------------------------------
861// Structs - Server-Side Encryption
862// ---------------------------------------------------------------------------
863
864/// SSE specification (input for `CreateTable` / `UpdateTable`).
865///
866/// Specifies the desired encryption settings for a table.
867#[derive(Debug, Clone, Default, Serialize, Deserialize)]
868#[serde(rename_all = "PascalCase")]
869pub struct SSESpecification {
870    /// Whether server-side encryption is enabled.
871    #[serde(skip_serializing_if = "Option::is_none")]
872    pub enabled: Option<bool>,
873    /// The encryption type (`AES256` or `KMS`).
874    #[serde(rename = "SSEType", skip_serializing_if = "Option::is_none")]
875    pub sse_type: Option<SseType>,
876    /// The KMS key ARN for `KMS` encryption type.
877    #[serde(rename = "KMSMasterKeyId", skip_serializing_if = "Option::is_none")]
878    pub kms_master_key_id: Option<String>,
879}
880
881/// SSE description (output from `DescribeTable`).
882///
883/// Describes the current encryption state of a table.
884#[derive(Debug, Clone, Default, Serialize, Deserialize)]
885#[serde(rename_all = "PascalCase")]
886pub struct SSEDescription {
887    /// The current status of server-side encryption.
888    #[serde(skip_serializing_if = "Option::is_none")]
889    pub status: Option<SseStatus>,
890    /// The encryption type.
891    #[serde(rename = "SSEType", skip_serializing_if = "Option::is_none")]
892    pub sse_type: Option<SseType>,
893    /// The KMS key ARN used for encryption.
894    #[serde(rename = "KMSMasterKeyId", skip_serializing_if = "Option::is_none")]
895    pub kms_master_key_id: Option<String>,
896    /// The date and time (epoch seconds) when the KMS key became inaccessible.
897    #[serde(skip_serializing_if = "Option::is_none")]
898    pub inaccessible_encryption_date_time: Option<f64>,
899}
900
901// ---------------------------------------------------------------------------
902// Structs - Time to Live
903// ---------------------------------------------------------------------------
904
905/// Time-to-Live specification for enabling or disabling TTL on a table.
906#[derive(Debug, Clone, Serialize, Deserialize)]
907#[serde(rename_all = "PascalCase")]
908pub struct TimeToLiveSpecification {
909    /// The name of the TTL attribute used to store the expiration time.
910    pub attribute_name: String,
911    /// Whether TTL is enabled (`true`) or disabled (`false`).
912    pub enabled: bool,
913}
914
915/// Time-to-Live description with status information.
916#[derive(Debug, Clone, Serialize, Deserialize)]
917#[serde(rename_all = "PascalCase")]
918pub struct TimeToLiveDescription {
919    /// The name of the TTL attribute.
920    #[serde(skip_serializing_if = "Option::is_none")]
921    pub attribute_name: Option<String>,
922    /// The TTL status: `ENABLED`, `DISABLED`, `ENABLING`, or `DISABLING`.
923    #[serde(skip_serializing_if = "Option::is_none")]
924    pub time_to_live_status: Option<String>,
925}
926
927// ---------------------------------------------------------------------------
928// Structs - Transaction Types
929// ---------------------------------------------------------------------------
930
931/// A single write action within a `TransactWriteItems` request.
932///
933/// Exactly one of the four fields must be set.
934#[derive(Debug, Clone, Serialize, Deserialize)]
935#[serde(rename_all = "PascalCase")]
936pub struct TransactWriteItem {
937    /// A condition check against an existing item (no mutation).
938    #[serde(skip_serializing_if = "Option::is_none")]
939    pub condition_check: Option<ConditionCheck>,
940    /// A put (insert or replace) action.
941    #[serde(skip_serializing_if = "Option::is_none")]
942    pub put: Option<TransactPut>,
943    /// A delete action.
944    #[serde(skip_serializing_if = "Option::is_none")]
945    pub delete: Option<TransactDelete>,
946    /// An update action.
947    #[serde(skip_serializing_if = "Option::is_none")]
948    pub update: Option<TransactUpdate>,
949}
950
951/// A condition check within a transaction (no mutation).
952#[derive(Debug, Clone, Serialize, Deserialize)]
953#[serde(rename_all = "PascalCase")]
954pub struct ConditionCheck {
955    /// The table containing the item.
956    pub table_name: String,
957    /// The primary key of the item to check.
958    pub key: HashMap<String, AttributeValue>,
959    /// The condition expression that must evaluate to true.
960    pub condition_expression: String,
961    /// Substitution tokens for attribute names.
962    #[serde(skip_serializing_if = "Option::is_none")]
963    pub expression_attribute_names: Option<HashMap<String, String>>,
964    /// Substitution tokens for attribute values.
965    #[serde(skip_serializing_if = "Option::is_none")]
966    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
967    /// Determines whether to return item attributes on condition check failure.
968    #[serde(skip_serializing_if = "Option::is_none")]
969    pub return_values_on_condition_check_failure: Option<String>,
970}
971
972/// A put action within a transaction.
973#[derive(Debug, Clone, Serialize, Deserialize)]
974#[serde(rename_all = "PascalCase")]
975pub struct TransactPut {
976    /// The table to put the item into.
977    pub table_name: String,
978    /// The item to put.
979    pub item: HashMap<String, AttributeValue>,
980    /// An optional condition expression.
981    #[serde(skip_serializing_if = "Option::is_none")]
982    pub condition_expression: Option<String>,
983    /// Substitution tokens for attribute names.
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub expression_attribute_names: Option<HashMap<String, String>>,
986    /// Substitution tokens for attribute values.
987    #[serde(skip_serializing_if = "Option::is_none")]
988    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
989    /// Determines whether to return item attributes on condition check failure.
990    #[serde(skip_serializing_if = "Option::is_none")]
991    pub return_values_on_condition_check_failure: Option<String>,
992}
993
994/// A delete action within a transaction.
995#[derive(Debug, Clone, Serialize, Deserialize)]
996#[serde(rename_all = "PascalCase")]
997pub struct TransactDelete {
998    /// The table containing the item to delete.
999    pub table_name: String,
1000    /// The primary key of the item to delete.
1001    pub key: HashMap<String, AttributeValue>,
1002    /// An optional condition expression.
1003    #[serde(skip_serializing_if = "Option::is_none")]
1004    pub condition_expression: Option<String>,
1005    /// Substitution tokens for attribute names.
1006    #[serde(skip_serializing_if = "Option::is_none")]
1007    pub expression_attribute_names: Option<HashMap<String, String>>,
1008    /// Substitution tokens for attribute values.
1009    #[serde(skip_serializing_if = "Option::is_none")]
1010    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
1011    /// Determines whether to return item attributes on condition check failure.
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub return_values_on_condition_check_failure: Option<String>,
1014}
1015
1016/// An update action within a transaction.
1017#[derive(Debug, Clone, Serialize, Deserialize)]
1018#[serde(rename_all = "PascalCase")]
1019pub struct TransactUpdate {
1020    /// The table containing the item to update.
1021    pub table_name: String,
1022    /// The primary key of the item to update.
1023    pub key: HashMap<String, AttributeValue>,
1024    /// The update expression defining the mutations.
1025    pub update_expression: String,
1026    /// An optional condition expression.
1027    #[serde(skip_serializing_if = "Option::is_none")]
1028    pub condition_expression: Option<String>,
1029    /// Substitution tokens for attribute names.
1030    #[serde(skip_serializing_if = "Option::is_none")]
1031    pub expression_attribute_names: Option<HashMap<String, String>>,
1032    /// Substitution tokens for attribute values.
1033    #[serde(skip_serializing_if = "Option::is_none")]
1034    pub expression_attribute_values: Option<HashMap<String, AttributeValue>>,
1035    /// Determines whether to return item attributes on condition check failure.
1036    #[serde(skip_serializing_if = "Option::is_none")]
1037    pub return_values_on_condition_check_failure: Option<String>,
1038}
1039
1040/// A single get action within a `TransactGetItems` request.
1041#[derive(Debug, Clone, Serialize, Deserialize)]
1042#[serde(rename_all = "PascalCase")]
1043pub struct TransactGetItem {
1044    /// The get operation to perform.
1045    pub get: Get,
1046}
1047
1048/// A get operation targeting a single item by primary key.
1049#[derive(Debug, Clone, Serialize, Deserialize)]
1050#[serde(rename_all = "PascalCase")]
1051pub struct Get {
1052    /// The table containing the item.
1053    pub table_name: String,
1054    /// The primary key of the item to retrieve.
1055    pub key: HashMap<String, AttributeValue>,
1056    /// A projection expression to limit returned attributes.
1057    #[serde(skip_serializing_if = "Option::is_none")]
1058    pub projection_expression: Option<String>,
1059    /// Substitution tokens for attribute names.
1060    #[serde(skip_serializing_if = "Option::is_none")]
1061    pub expression_attribute_names: Option<HashMap<String, String>>,
1062}
1063
1064/// A reason why a transaction item was cancelled.
1065#[derive(Debug, Clone, Serialize, Deserialize)]
1066#[serde(rename_all = "PascalCase")]
1067pub struct CancellationReason {
1068    /// The cancellation reason code.
1069    #[serde(skip_serializing_if = "Option::is_none")]
1070    pub code: Option<String>,
1071    /// A human-readable cancellation reason message.
1072    #[serde(skip_serializing_if = "Option::is_none")]
1073    pub message: Option<String>,
1074    /// The item that failed the condition check, if applicable.
1075    #[serde(skip_serializing_if = "Option::is_none")]
1076    pub item: Option<HashMap<String, AttributeValue>>,
1077}
1078
1079/// A response item from a `TransactGetItems` operation.
1080#[derive(Debug, Clone, Serialize, Deserialize)]
1081#[serde(rename_all = "PascalCase")]
1082pub struct ItemResponse {
1083    /// The retrieved item, or `None` if not found.
1084    #[serde(skip_serializing_if = "Option::is_none")]
1085    pub item: Option<HashMap<String, AttributeValue>>,
1086}
1087
1088// ---------------------------------------------------------------------------
1089// Structs - Tags
1090// ---------------------------------------------------------------------------
1091
1092/// A key-value tag associated with a DynamoDB resource.
1093#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1094#[serde(rename_all = "PascalCase")]
1095pub struct Tag {
1096    /// The tag key (up to 128 Unicode characters).
1097    pub key: String,
1098    /// The tag value (up to 256 Unicode characters).
1099    pub value: String,
1100}
1101
1102// ---------------------------------------------------------------------------
1103// Structs - Table Description
1104// ---------------------------------------------------------------------------
1105
1106/// Comprehensive description of a DynamoDB table.
1107///
1108/// Returned by `DescribeTable`, `CreateTable`, and `DeleteTable` responses.
1109#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1110#[serde(rename_all = "PascalCase")]
1111pub struct TableDescription {
1112    /// The name of the table.
1113    #[serde(skip_serializing_if = "Option::is_none")]
1114    pub table_name: Option<String>,
1115    /// The current status of the table.
1116    #[serde(skip_serializing_if = "Option::is_none")]
1117    pub table_status: Option<TableStatus>,
1118    /// The key schema for the table.
1119    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1120    pub key_schema: Vec<KeySchemaElement>,
1121    /// The attribute definitions for the table.
1122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1123    pub attribute_definitions: Vec<AttributeDefinition>,
1124    /// The date and time (epoch seconds) when the table was created.
1125    #[serde(skip_serializing_if = "Option::is_none")]
1126    pub creation_date_time: Option<f64>,
1127    /// The number of items in the table.
1128    #[serde(skip_serializing_if = "Option::is_none")]
1129    pub item_count: Option<i64>,
1130    /// The total size of the table in bytes.
1131    #[serde(skip_serializing_if = "Option::is_none")]
1132    pub table_size_bytes: Option<i64>,
1133    /// The Amazon Resource Name (ARN) of the table.
1134    #[serde(skip_serializing_if = "Option::is_none")]
1135    pub table_arn: Option<String>,
1136    /// A unique identifier for the table.
1137    #[serde(skip_serializing_if = "Option::is_none")]
1138    pub table_id: Option<String>,
1139    /// The billing mode summary.
1140    #[serde(skip_serializing_if = "Option::is_none")]
1141    pub billing_mode_summary: Option<BillingModeSummary>,
1142    /// The provisioned throughput settings.
1143    #[serde(skip_serializing_if = "Option::is_none")]
1144    pub provisioned_throughput: Option<ProvisionedThroughputDescription>,
1145    /// The global secondary indexes on the table.
1146    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1147    pub global_secondary_indexes: Vec<GlobalSecondaryIndexDescription>,
1148    /// The local secondary indexes on the table.
1149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1150    pub local_secondary_indexes: Vec<LocalSecondaryIndexDescription>,
1151    /// The stream specification for the table.
1152    #[serde(skip_serializing_if = "Option::is_none")]
1153    pub stream_specification: Option<StreamSpecification>,
1154    /// The latest stream ARN if streams are enabled.
1155    #[serde(skip_serializing_if = "Option::is_none")]
1156    pub latest_stream_arn: Option<String>,
1157    /// The latest stream label if streams are enabled.
1158    #[serde(skip_serializing_if = "Option::is_none")]
1159    pub latest_stream_label: Option<String>,
1160    /// The server-side encryption description.
1161    #[serde(rename = "SSEDescription", skip_serializing_if = "Option::is_none")]
1162    pub sse_description: Option<SSEDescription>,
1163    /// The deletion protection setting.
1164    #[serde(skip_serializing_if = "Option::is_none")]
1165    pub deletion_protection_enabled: Option<bool>,
1166}
1167
1168// ---------------------------------------------------------------------------
1169// Structs - Consumed Capacity
1170// ---------------------------------------------------------------------------
1171
1172/// Capacity units consumed by an individual table or index.
1173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1174#[serde(rename_all = "PascalCase")]
1175pub struct Capacity {
1176    /// The total read capacity units consumed.
1177    #[serde(skip_serializing_if = "Option::is_none")]
1178    pub read_capacity_units: Option<f64>,
1179    /// The total write capacity units consumed.
1180    #[serde(skip_serializing_if = "Option::is_none")]
1181    pub write_capacity_units: Option<f64>,
1182    /// The total capacity units consumed (read + write).
1183    #[serde(skip_serializing_if = "Option::is_none")]
1184    pub capacity_units: Option<f64>,
1185}
1186
1187/// Total capacity consumed by an operation across table and indexes.
1188///
1189/// Returned when `ReturnConsumedCapacity` is set to `TOTAL` or `INDEXES`.
1190#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1191#[serde(rename_all = "PascalCase")]
1192pub struct ConsumedCapacity {
1193    /// The name of the table that was affected.
1194    #[serde(skip_serializing_if = "Option::is_none")]
1195    pub table_name: Option<String>,
1196    /// The total capacity units consumed by the operation.
1197    #[serde(skip_serializing_if = "Option::is_none")]
1198    pub capacity_units: Option<f64>,
1199    /// The total read capacity units consumed.
1200    #[serde(skip_serializing_if = "Option::is_none")]
1201    pub read_capacity_units: Option<f64>,
1202    /// The total write capacity units consumed.
1203    #[serde(skip_serializing_if = "Option::is_none")]
1204    pub write_capacity_units: Option<f64>,
1205    /// The capacity consumed by the table (excluding indexes).
1206    #[serde(skip_serializing_if = "Option::is_none")]
1207    pub table: Option<Capacity>,
1208    /// The capacity consumed by each local secondary index.
1209    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1210    pub local_secondary_indexes: HashMap<String, Capacity>,
1211    /// The capacity consumed by each global secondary index.
1212    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1213    pub global_secondary_indexes: HashMap<String, Capacity>,
1214}
1215
1216// ---------------------------------------------------------------------------
1217// Structs - Item Collection Metrics
1218// ---------------------------------------------------------------------------
1219
1220/// Metrics about an item collection (items sharing the same partition key).
1221///
1222/// Returned for tables with local secondary indexes when
1223/// `ReturnItemCollectionMetrics` is `SIZE`.
1224#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1225#[serde(rename_all = "PascalCase")]
1226pub struct ItemCollectionMetrics {
1227    /// The partition key value of the item collection.
1228    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1229    pub item_collection_key: HashMap<String, AttributeValue>,
1230    /// An estimate of the item collection size in gigabytes (lower and upper bound).
1231    #[serde(
1232        rename = "SizeEstimateRangeGB",
1233        default,
1234        skip_serializing_if = "Vec::is_empty"
1235    )]
1236    pub size_estimate_range_gb: Vec<f64>,
1237}
1238
1239// ---------------------------------------------------------------------------
1240// Structs - Legacy Condition (for ScanFilter / QueryFilter / KeyConditions)
1241// ---------------------------------------------------------------------------
1242
1243/// A condition for legacy filtering operations.
1244///
1245/// Used with `ScanFilter`, `QueryFilter`, `KeyConditions`, and `Expected`
1246/// parameters. Modern applications should use expression-based APIs instead.
1247#[derive(Debug, Clone, Serialize, Deserialize)]
1248#[serde(rename_all = "PascalCase")]
1249pub struct Condition {
1250    /// The comparison operator.
1251    pub comparison_operator: ComparisonOperator,
1252    /// The attribute values to compare against.
1253    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1254    pub attribute_value_list: Vec<AttributeValue>,
1255}
1256
1257// ---------------------------------------------------------------------------
1258// Structs - Legacy API Types
1259// ---------------------------------------------------------------------------
1260
1261/// Action to perform on an attribute during an `UpdateItem` operation (legacy API).
1262///
1263/// Used with `AttributeUpdates` parameter. Modern applications should use
1264/// `UpdateExpression` instead.
1265#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
1266pub enum AttributeAction {
1267    /// Set the attribute value.
1268    #[default]
1269    #[serde(rename = "PUT")]
1270    Put,
1271    /// Delete the attribute (for scalars) or remove elements from a set.
1272    #[serde(rename = "DELETE")]
1273    Delete,
1274    /// Add to a number or set attribute.
1275    #[serde(rename = "ADD")]
1276    Add,
1277}
1278
1279impl AttributeAction {
1280    /// Returns the DynamoDB wire-format string representation.
1281    #[must_use]
1282    pub fn as_str(&self) -> &'static str {
1283        match self {
1284            Self::Put => "PUT",
1285            Self::Delete => "DELETE",
1286            Self::Add => "ADD",
1287        }
1288    }
1289}
1290
1291impl std::fmt::Display for AttributeAction {
1292    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1293        f.write_str(self.as_str())
1294    }
1295}
1296
1297/// An attribute value update for the legacy `AttributeUpdates` parameter.
1298#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1299#[serde(rename_all = "PascalCase")]
1300pub struct AttributeValueUpdate {
1301    /// The new value for the attribute.
1302    #[serde(skip_serializing_if = "Option::is_none")]
1303    pub value: Option<AttributeValue>,
1304    /// The action to perform on the attribute.
1305    #[serde(skip_serializing_if = "Option::is_none")]
1306    pub action: Option<AttributeAction>,
1307}
1308
1309/// Expected attribute value for the legacy `Expected` parameter (conditional writes).
1310#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1311#[serde(rename_all = "PascalCase")]
1312pub struct ExpectedAttributeValue {
1313    /// The value to compare against (legacy simple form).
1314    #[serde(skip_serializing_if = "Option::is_none")]
1315    pub value: Option<AttributeValue>,
1316    /// Whether the attribute must exist (`true`) or not exist (`false`).
1317    #[serde(skip_serializing_if = "Option::is_none")]
1318    pub exists: Option<bool>,
1319    /// The comparison operator (extended form).
1320    #[serde(skip_serializing_if = "Option::is_none")]
1321    pub comparison_operator: Option<ComparisonOperator>,
1322    /// The attribute values to compare against (extended form).
1323    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1324    pub attribute_value_list: Vec<AttributeValue>,
1325}
1326
1327// ---------------------------------------------------------------------------
1328// Structs - Batch Operations
1329// ---------------------------------------------------------------------------
1330
1331/// A set of keys and optional projection for `BatchGetItem`.
1332///
1333/// Describes the items to retrieve from a single table.
1334#[derive(Debug, Clone, Serialize, Deserialize)]
1335#[serde(rename_all = "PascalCase")]
1336pub struct KeysAndAttributes {
1337    /// The primary keys of the items to retrieve.
1338    pub keys: Vec<HashMap<String, AttributeValue>>,
1339    /// The attributes to retrieve. If not specified, all attributes are returned.
1340    #[serde(skip_serializing_if = "Option::is_none")]
1341    pub projection_expression: Option<String>,
1342    /// Expression attribute names for substitution in `projection_expression`.
1343    #[serde(skip_serializing_if = "Option::is_none")]
1344    pub expression_attribute_names: Option<HashMap<String, String>>,
1345    /// Whether to use a consistent read.
1346    #[serde(skip_serializing_if = "Option::is_none")]
1347    pub consistent_read: Option<bool>,
1348    /// Legacy: attribute names to retrieve (use `projection_expression` instead).
1349    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1350    pub attributes_to_get: Vec<String>,
1351}
1352
1353/// A single write request within a `BatchWriteItem` operation.
1354///
1355/// Exactly one of `put_request` or `delete_request` must be specified.
1356#[derive(Debug, Clone, Serialize, Deserialize)]
1357#[serde(rename_all = "PascalCase")]
1358pub struct WriteRequest {
1359    /// A request to put an item.
1360    #[serde(skip_serializing_if = "Option::is_none")]
1361    pub put_request: Option<PutRequest>,
1362    /// A request to delete an item.
1363    #[serde(skip_serializing_if = "Option::is_none")]
1364    pub delete_request: Option<DeleteRequest>,
1365}
1366
1367/// A request to put an item within a `BatchWriteItem` operation.
1368#[derive(Debug, Clone, Serialize, Deserialize)]
1369#[serde(rename_all = "PascalCase")]
1370pub struct PutRequest {
1371    /// The item attributes to put.
1372    pub item: HashMap<String, AttributeValue>,
1373}
1374
1375/// A request to delete an item within a `BatchWriteItem` operation.
1376#[derive(Debug, Clone, Serialize, Deserialize)]
1377#[serde(rename_all = "PascalCase")]
1378pub struct DeleteRequest {
1379    /// The primary key of the item to delete.
1380    pub key: HashMap<String, AttributeValue>,
1381}
1382
1383// ---------------------------------------------------------------------------
1384// Type aliases for common DynamoDB item shapes
1385// ---------------------------------------------------------------------------
1386
1387/// A DynamoDB item represented as a map of attribute names to values.
1388pub type Item = HashMap<String, AttributeValue>;
1389
1390/// A DynamoDB key represented as a map of key attribute names to values.
1391pub type Key = HashMap<String, AttributeValue>;
1392
1393/// Expression attribute names mapping (`#name` placeholders to attribute names).
1394pub type ExpressionAttributeNames = HashMap<String, String>;
1395
1396/// Expression attribute values mapping (`:value` placeholders to attribute values).
1397pub type ExpressionAttributeValues = HashMap<String, AttributeValue>;
1398
1399#[cfg(test)]
1400mod tests {
1401    use super::*;
1402
1403    #[test]
1404    fn test_should_serialize_key_schema_element() {
1405        let elem = KeySchemaElement {
1406            attribute_name: "pk".to_owned(),
1407            key_type: KeyType::Hash,
1408        };
1409        let json = serde_json::to_string(&elem).expect("serialize KeySchemaElement");
1410        assert_eq!(json, r#"{"AttributeName":"pk","KeyType":"HASH"}"#);
1411    }
1412
1413    #[test]
1414    fn test_should_roundtrip_attribute_definition() {
1415        let def = AttributeDefinition {
1416            attribute_name: "id".to_owned(),
1417            attribute_type: ScalarAttributeType::S,
1418        };
1419        let json = serde_json::to_string(&def).expect("serialize AttributeDefinition");
1420        let parsed: AttributeDefinition =
1421            serde_json::from_str(&json).expect("deserialize AttributeDefinition");
1422        assert_eq!(def.attribute_name, parsed.attribute_name);
1423        assert_eq!(def.attribute_type, parsed.attribute_type);
1424    }
1425
1426    #[test]
1427    fn test_should_serialize_table_status() {
1428        let status = TableStatus::Active;
1429        let json = serde_json::to_string(&status).expect("serialize TableStatus");
1430        assert_eq!(json, r#""ACTIVE""#);
1431    }
1432
1433    #[test]
1434    fn test_should_default_billing_mode_to_pay_per_request() {
1435        let mode = BillingMode::default();
1436        assert_eq!(mode, BillingMode::PayPerRequest);
1437    }
1438
1439    #[test]
1440    fn test_should_default_return_value_to_none() {
1441        let rv = ReturnValue::default();
1442        assert_eq!(rv, ReturnValue::None);
1443    }
1444
1445    #[test]
1446    fn test_should_serialize_provisioned_throughput() {
1447        let pt = ProvisionedThroughput {
1448            read_capacity_units: 5,
1449            write_capacity_units: 10,
1450        };
1451        let json = serde_json::to_string(&pt).expect("serialize ProvisionedThroughput");
1452        assert_eq!(json, r#"{"ReadCapacityUnits":5,"WriteCapacityUnits":10}"#);
1453    }
1454
1455    #[test]
1456    fn test_should_skip_none_fields_in_table_description() {
1457        let desc = TableDescription {
1458            table_name: Some("TestTable".to_owned()),
1459            table_status: Some(TableStatus::Active),
1460            ..Default::default()
1461        };
1462        let json = serde_json::to_string(&desc).expect("serialize TableDescription");
1463        assert!(json.contains(r#""TableName":"TestTable""#));
1464        assert!(json.contains(r#""TableStatus":"ACTIVE""#));
1465        // Fields that are None or empty should be absent
1466        assert!(!json.contains("TableArn"));
1467        assert!(!json.contains("KeySchema"));
1468        assert!(!json.contains("GlobalSecondaryIndexes"));
1469    }
1470
1471    #[test]
1472    fn test_should_roundtrip_projection() {
1473        let proj = Projection {
1474            projection_type: Some(ProjectionType::Include),
1475            non_key_attributes: vec!["email".to_owned(), "name".to_owned()],
1476        };
1477        let json = serde_json::to_string(&proj).expect("serialize Projection");
1478        let parsed: Projection = serde_json::from_str(&json).expect("deserialize Projection");
1479        assert_eq!(proj.projection_type, parsed.projection_type);
1480        assert_eq!(proj.non_key_attributes, parsed.non_key_attributes);
1481    }
1482
1483    #[test]
1484    fn test_should_serialize_write_request_with_put() {
1485        let mut item = HashMap::new();
1486        item.insert("id".to_owned(), AttributeValue::S("123".to_owned()));
1487        let req = WriteRequest {
1488            put_request: Some(PutRequest { item }),
1489            delete_request: Option::None,
1490        };
1491        let json = serde_json::to_string(&req).expect("serialize WriteRequest");
1492        assert!(json.contains("PutRequest"));
1493        assert!(!json.contains("DeleteRequest"));
1494    }
1495
1496    #[test]
1497    fn test_should_serialize_write_request_with_delete() {
1498        let mut key = HashMap::new();
1499        key.insert("id".to_owned(), AttributeValue::S("456".to_owned()));
1500        let req = WriteRequest {
1501            put_request: Option::None,
1502            delete_request: Some(DeleteRequest { key }),
1503        };
1504        let json = serde_json::to_string(&req).expect("serialize WriteRequest");
1505        assert!(json.contains("DeleteRequest"));
1506        assert!(!json.contains("PutRequest"));
1507    }
1508
1509    #[test]
1510    fn test_should_serialize_condition() {
1511        let cond = Condition {
1512            comparison_operator: ComparisonOperator::Eq,
1513            attribute_value_list: vec![AttributeValue::S("test".to_owned())],
1514        };
1515        let json = serde_json::to_string(&cond).expect("serialize Condition");
1516        assert!(json.contains(r#""ComparisonOperator":"EQ""#));
1517        assert!(json.contains("AttributeValueList"));
1518    }
1519
1520    #[test]
1521    fn test_should_roundtrip_consumed_capacity() {
1522        let cap = ConsumedCapacity {
1523            table_name: Some("Orders".to_owned()),
1524            capacity_units: Some(5.0),
1525            ..Default::default()
1526        };
1527        let json = serde_json::to_string(&cap).expect("serialize ConsumedCapacity");
1528        let parsed: ConsumedCapacity =
1529            serde_json::from_str(&json).expect("deserialize ConsumedCapacity");
1530        assert_eq!(cap.table_name, parsed.table_name);
1531        assert_eq!(cap.capacity_units, parsed.capacity_units);
1532    }
1533
1534    #[test]
1535    fn test_should_serialize_sse_specification() {
1536        let sse = SSESpecification {
1537            enabled: Some(true),
1538            sse_type: Some(SseType::Kms),
1539            kms_master_key_id: Some("arn:aws:kms:us-east-1:123456789012:key/abc".to_owned()),
1540        };
1541        let json = serde_json::to_string(&sse).expect("serialize SSESpecification");
1542        assert!(json.contains(r#""SSEType":"KMS""#));
1543        assert!(json.contains(r#""KMSMasterKeyId":"arn:aws:kms"#));
1544    }
1545
1546    #[test]
1547    fn test_should_serialize_tag() {
1548        let tag = Tag {
1549            key: "Environment".to_owned(),
1550            value: "Production".to_owned(),
1551        };
1552        let json = serde_json::to_string(&tag).expect("serialize Tag");
1553        assert_eq!(json, r#"{"Key":"Environment","Value":"Production"}"#);
1554    }
1555
1556    #[test]
1557    fn test_should_serialize_stream_specification() {
1558        let spec = StreamSpecification {
1559            stream_enabled: true,
1560            stream_view_type: Some(StreamViewType::NewAndOldImages),
1561        };
1562        let json = serde_json::to_string(&spec).expect("serialize StreamSpecification");
1563        assert!(json.contains(r#""StreamEnabled":true"#));
1564        assert!(json.contains(r#""StreamViewType":"NEW_AND_OLD_IMAGES""#));
1565    }
1566
1567    #[test]
1568    fn test_should_serialize_keys_and_attributes() {
1569        let mut key = HashMap::new();
1570        key.insert("pk".to_owned(), AttributeValue::S("user-1".to_owned()));
1571        let ka = KeysAndAttributes {
1572            keys: vec![key],
1573            projection_expression: Some("id, #n".to_owned()),
1574            expression_attribute_names: Some(HashMap::from([("#n".to_owned(), "name".to_owned())])),
1575            consistent_read: Some(true),
1576            attributes_to_get: Vec::new(),
1577        };
1578        let json = serde_json::to_string(&ka).expect("serialize KeysAndAttributes");
1579        assert!(json.contains("ProjectionExpression"));
1580        assert!(json.contains("ExpressionAttributeNames"));
1581        assert!(json.contains("ConsistentRead"));
1582        assert!(!json.contains("AttributesToGet"));
1583    }
1584
1585    #[test]
1586    fn test_should_roundtrip_global_secondary_index() {
1587        let gsi = GlobalSecondaryIndex {
1588            index_name: "gsi-email".to_owned(),
1589            key_schema: vec![KeySchemaElement {
1590                attribute_name: "email".to_owned(),
1591                key_type: KeyType::Hash,
1592            }],
1593            projection: Projection {
1594                projection_type: Some(ProjectionType::All),
1595                non_key_attributes: Vec::new(),
1596            },
1597            provisioned_throughput: Option::None,
1598        };
1599        let json = serde_json::to_string(&gsi).expect("serialize GlobalSecondaryIndex");
1600        let parsed: GlobalSecondaryIndex =
1601            serde_json::from_str(&json).expect("deserialize GlobalSecondaryIndex");
1602        assert_eq!(gsi.index_name, parsed.index_name);
1603        assert_eq!(gsi.key_schema.len(), parsed.key_schema.len());
1604    }
1605
1606    #[test]
1607    fn test_should_display_all_enum_variants() {
1608        // Verify Display impl produces correct DynamoDB wire-format strings
1609        assert_eq!(KeyType::Hash.to_string(), "HASH");
1610        assert_eq!(KeyType::Range.to_string(), "RANGE");
1611        assert_eq!(ScalarAttributeType::S.to_string(), "S");
1612        assert_eq!(ScalarAttributeType::N.to_string(), "N");
1613        assert_eq!(ScalarAttributeType::B.to_string(), "B");
1614        assert_eq!(TableStatus::Active.to_string(), "ACTIVE");
1615        assert_eq!(TableStatus::Creating.to_string(), "CREATING");
1616        assert_eq!(BillingMode::PayPerRequest.to_string(), "PAY_PER_REQUEST");
1617        assert_eq!(ProjectionType::KeysOnly.to_string(), "KEYS_ONLY");
1618        assert_eq!(
1619            StreamViewType::NewAndOldImages.to_string(),
1620            "NEW_AND_OLD_IMAGES"
1621        );
1622        assert_eq!(SseType::Kms.to_string(), "KMS");
1623        assert_eq!(SseStatus::Enabled.to_string(), "ENABLED");
1624        assert_eq!(IndexStatus::Active.to_string(), "ACTIVE");
1625        assert_eq!(ReturnValue::AllNew.to_string(), "ALL_NEW");
1626        assert_eq!(ReturnConsumedCapacity::Indexes.to_string(), "INDEXES");
1627        assert_eq!(ReturnItemCollectionMetrics::Size.to_string(), "SIZE");
1628        assert_eq!(Select::Count.to_string(), "COUNT");
1629        assert_eq!(ConditionalOperator::And.to_string(), "AND");
1630        assert_eq!(ComparisonOperator::BeginsWith.to_string(), "BEGINS_WITH");
1631    }
1632
1633    #[test]
1634    fn test_should_report_consumed_capacity_settings() {
1635        assert!(!ReturnConsumedCapacity::None.should_report());
1636        assert!(ReturnConsumedCapacity::Total.should_report());
1637        assert!(ReturnConsumedCapacity::Indexes.should_report());
1638        assert!(!ReturnConsumedCapacity::Total.should_report_indexes());
1639        assert!(ReturnConsumedCapacity::Indexes.should_report_indexes());
1640    }
1641
1642    #[test]
1643    fn test_should_report_item_collection_metrics_settings() {
1644        assert!(!ReturnItemCollectionMetrics::None.should_report());
1645        assert!(ReturnItemCollectionMetrics::Size.should_report());
1646    }
1647
1648    #[test]
1649    fn test_should_deserialize_table_description_from_dynamodb_json() {
1650        let json = r#"{
1651            "TableName": "Users",
1652            "TableStatus": "ACTIVE",
1653            "KeySchema": [
1654                {"AttributeName": "pk", "KeyType": "HASH"},
1655                {"AttributeName": "sk", "KeyType": "RANGE"}
1656            ],
1657            "AttributeDefinitions": [
1658                {"AttributeName": "pk", "AttributeType": "S"},
1659                {"AttributeName": "sk", "AttributeType": "S"}
1660            ],
1661            "CreationDateTime": 1709136000.0,
1662            "ItemCount": 42,
1663            "TableSizeBytes": 8192,
1664            "TableArn": "arn:aws:dynamodb:us-east-1:123456789012:table/Users",
1665            "TableId": "abc-123-def",
1666            "ProvisionedThroughput": {
1667                "ReadCapacityUnits": 5,
1668                "WriteCapacityUnits": 10,
1669                "NumberOfDecreasesToday": 0
1670            },
1671            "BillingModeSummary": {
1672                "BillingMode": "PROVISIONED"
1673            }
1674        }"#;
1675        let desc: TableDescription =
1676            serde_json::from_str(json).expect("deserialize TableDescription");
1677        assert_eq!(desc.table_name.as_deref(), Some("Users"));
1678        assert_eq!(desc.table_status, Some(TableStatus::Active));
1679        assert_eq!(desc.key_schema.len(), 2);
1680        assert_eq!(desc.attribute_definitions.len(), 2);
1681        assert_eq!(desc.item_count, Some(42));
1682        assert_eq!(desc.table_size_bytes, Some(8192));
1683        assert_eq!(desc.table_id.as_deref(), Some("abc-123-def"));
1684        assert_eq!(
1685            desc.billing_mode_summary
1686                .as_ref()
1687                .and_then(|b| b.billing_mode.as_ref()),
1688            Some(&BillingMode::Provisioned)
1689        );
1690    }
1691
1692    #[test]
1693    fn test_should_roundtrip_all_comparison_operators() {
1694        let operators = [
1695            ComparisonOperator::Eq,
1696            ComparisonOperator::Ne,
1697            ComparisonOperator::Le,
1698            ComparisonOperator::Lt,
1699            ComparisonOperator::Ge,
1700            ComparisonOperator::Gt,
1701            ComparisonOperator::NotNull,
1702            ComparisonOperator::Null,
1703            ComparisonOperator::Contains,
1704            ComparisonOperator::NotContains,
1705            ComparisonOperator::BeginsWith,
1706            ComparisonOperator::In,
1707            ComparisonOperator::Between,
1708        ];
1709        for op in &operators {
1710            let json = serde_json::to_string(op).expect("serialize ComparisonOperator");
1711            let parsed: ComparisonOperator =
1712                serde_json::from_str(&json).expect("deserialize ComparisonOperator");
1713            assert_eq!(op, &parsed);
1714        }
1715    }
1716
1717    #[test]
1718    fn test_should_roundtrip_all_return_values() {
1719        let values = [
1720            ReturnValue::None,
1721            ReturnValue::AllOld,
1722            ReturnValue::UpdatedOld,
1723            ReturnValue::AllNew,
1724            ReturnValue::UpdatedNew,
1725        ];
1726        for rv in &values {
1727            let json = serde_json::to_string(rv).expect("serialize ReturnValue");
1728            let parsed: ReturnValue = serde_json::from_str(&json).expect("deserialize ReturnValue");
1729            assert_eq!(rv, &parsed);
1730        }
1731    }
1732
1733    #[test]
1734    fn test_should_serialize_item_collection_metrics() {
1735        let mut key = HashMap::new();
1736        key.insert("pk".to_owned(), AttributeValue::S("user-1".to_owned()));
1737        let metrics = ItemCollectionMetrics {
1738            item_collection_key: key,
1739            size_estimate_range_gb: vec![0.5, 1.0],
1740        };
1741        let json = serde_json::to_string(&metrics).expect("serialize ItemCollectionMetrics");
1742        assert!(json.contains("ItemCollectionKey"));
1743        assert!(json.contains("SizeEstimateRangeGB"));
1744    }
1745
1746    #[test]
1747    fn test_should_serialize_billing_mode_summary() {
1748        let summary = BillingModeSummary {
1749            billing_mode: Some(BillingMode::PayPerRequest),
1750            last_update_to_pay_per_request_date_time: Some(1_709_136_000.0),
1751        };
1752        let json = serde_json::to_string(&summary).expect("serialize BillingModeSummary");
1753        assert!(json.contains(r#""BillingMode":"PAY_PER_REQUEST""#));
1754        assert!(json.contains("LastUpdateToPayPerRequestDateTime"));
1755    }
1756
1757    #[test]
1758    fn test_should_serialize_gsi_description_with_all_fields() {
1759        let desc = GlobalSecondaryIndexDescription {
1760            index_name: Some("gsi-status".to_owned()),
1761            key_schema: vec![KeySchemaElement {
1762                attribute_name: "status".to_owned(),
1763                key_type: KeyType::Hash,
1764            }],
1765            projection: Some(Projection {
1766                projection_type: Some(ProjectionType::All),
1767                ..Default::default()
1768            }),
1769            index_status: Some(IndexStatus::Active),
1770            backfilling: Some(false),
1771            provisioned_throughput: Some(ProvisionedThroughputDescription {
1772                read_capacity_units: 5,
1773                write_capacity_units: 5,
1774                ..Default::default()
1775            }),
1776            index_size_bytes: Some(1024),
1777            item_count: Some(10),
1778            index_arn: Some(
1779                "arn:aws:dynamodb:us-east-1:123456789012:table/T/index/gsi-status".to_owned(),
1780            ),
1781        };
1782        let json = serde_json::to_string(&desc).expect("serialize GlobalSecondaryIndexDescription");
1783        let parsed: GlobalSecondaryIndexDescription =
1784            serde_json::from_str(&json).expect("deserialize GlobalSecondaryIndexDescription");
1785        assert_eq!(desc.index_name, parsed.index_name);
1786        assert_eq!(desc.index_status, parsed.index_status);
1787        assert_eq!(desc.item_count, parsed.item_count);
1788    }
1789}