Skip to main content

rusty_cdk_core/dynamodb/
builder.rs

1use crate::dynamodb::{AttributeDefinition, KeySchema, Table, TableProperties, TableType};
2use crate::dynamodb::{OnDemandThroughput, ProvisionedThroughput, TableRef};
3use crate::shared::{DeletionPolicy, Id, UpdateDeletePolicyDTO, UpdateReplacePolicy};
4use crate::stack::{Resource, StackBuilder};
5use crate::wrappers::{NonZeroNumber, StringWithOnlyAlphaNumericsAndUnderscores};
6use std::marker::PhantomData;
7use crate::type_state;
8
9#[derive(Debug, Clone, PartialEq)]
10pub enum BillingMode {
11    PayPerRequest,
12    Provisioned,
13}
14
15impl From<BillingMode> for String {
16    fn from(value: BillingMode) -> Self {
17        match value {
18            BillingMode::PayPerRequest => "PAY_PER_REQUEST".to_string(),
19            BillingMode::Provisioned => "PROVISIONED".to_string(),
20        }
21    }
22}
23
24#[derive(Debug, Clone)]
25pub enum AttributeType {
26    String,
27    Number,
28    Binary,
29}
30
31impl From<AttributeType> for String {
32    fn from(value: AttributeType) -> Self {
33        match value {
34            AttributeType::String => "S".to_string(),
35            AttributeType::Number => "N".to_string(),
36            AttributeType::Binary => "B".to_string(),
37        }
38    }
39}
40
41pub struct Key {
42    key: String,
43    key_type: AttributeType,
44}
45
46impl Key {
47    pub fn new(key: StringWithOnlyAlphaNumericsAndUnderscores, key_type: AttributeType) -> Self {
48        Self { key: key.0, key_type }
49    }
50}
51
52type_state!(
53    TableBuilderState,
54    StartState,
55    ProvisionedStateStart,
56    ProvisionedStateReadSet,
57    ProvisionedStateWriteSet,
58    PayPerRequestState,
59);
60
61/// Builder for DynamoDB tables.
62///
63/// See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-dynamodb-table.html
64///
65/// Supports both pay-per-request and provisioned billing modes. The builder enforces the correct configuration based on the chosen billing mode.
66///
67/// # Example
68///
69/// ```rust
70/// use rusty_cdk_core::stack::StackBuilder;
71/// use rusty_cdk_core::dynamodb::{TableBuilder, Key, AttributeType};
72/// use rusty_cdk_core::wrappers::*;
73/// use rusty_cdk_macros::{string_with_only_alphanumerics_and_underscores, non_zero_number};
74///
75/// let mut stack_builder = StackBuilder::new();
76///
77/// let partition_key = Key::new(
78///     string_with_only_alphanumerics_and_underscores!("id"),
79///     AttributeType::String
80/// );
81/// let sort_key = Key::new(
82///     string_with_only_alphanumerics_and_underscores!("timestamp"),
83///     AttributeType::Number
84/// );
85///
86/// let on_demand_table = TableBuilder::new("on-demand-table", partition_key)
87///     .sort_key(sort_key)
88///     .pay_per_request_billing()
89///     .build(&mut stack_builder);
90/// ```
91pub struct TableBuilder<T: TableBuilderState> {
92    state: PhantomData<T>,
93    id: Id,
94    table_name: Option<String>,
95    partition_key: Option<Key>,
96    sort_key: Option<Key>,
97    billing_mode: Option<BillingMode>,
98    read_capacity: Option<u32>,
99    write_capacity: Option<u32>,
100    max_read_capacity: Option<u32>,
101    max_write_capacity: Option<u32>,
102    deletion_policy: Option<String>,
103    update_replace_policy: Option<String>,
104}
105
106impl TableBuilder<StartState> {
107    /// Creates a new DynamoDB table builder.
108    ///
109    /// # Arguments
110    /// * `id` - Unique identifier for the table
111    /// * `key` - Partition key definition for the table
112    pub fn new(id: &str, key: Key) -> Self {
113        TableBuilder {
114            state: Default::default(),
115            id: Id(id.to_string()),
116            table_name: None,
117            partition_key: Some(key),
118            sort_key: None,
119            billing_mode: None,
120            read_capacity: None,
121            write_capacity: None,
122            max_read_capacity: None,
123            max_write_capacity: None,
124            deletion_policy: None,
125            update_replace_policy: None,
126        }
127    }
128}
129
130impl<T: TableBuilderState> TableBuilder<T> {
131    /// Sets the sort key for the table. The sort key is also known as the range key.
132    pub fn sort_key(self, key: Key) -> Self {
133        Self {
134            sort_key: Some(key),
135            ..self
136        }
137    }
138
139    /// Sets the name of the table. If not specified, a name will be generated.
140    pub fn table_name(self, name: StringWithOnlyAlphaNumericsAndUnderscores) -> Self {
141        Self {
142            table_name: Some(name.0),
143            ..self
144        }
145    }
146
147    pub fn update_replace_and_deletion_policy(self, update_replace_policy: UpdateReplacePolicy, deletion_policy: DeletionPolicy) -> Self {
148        Self {
149            deletion_policy: Some(deletion_policy.into()),
150            update_replace_policy: Some(update_replace_policy.into()),
151            ..self
152        }
153    }
154
155    /// Configures the table to use pay-per-request billing.
156    ///
157    /// With this mode, you pay per request and can optionally set max throughput limits.
158    pub fn pay_per_request_billing(self) -> TableBuilder<PayPerRequestState> {
159        TableBuilder {
160            billing_mode: Some(BillingMode::PayPerRequest),
161            state: Default::default(),
162            id: self.id,
163            table_name: self.table_name,
164            partition_key: self.partition_key,
165            sort_key: self.sort_key,
166            max_read_capacity: self.max_read_capacity,
167            max_write_capacity: self.max_write_capacity,
168            deletion_policy: self.deletion_policy,
169            update_replace_policy: self.update_replace_policy,
170            read_capacity: None,
171            write_capacity: None,
172        }
173    }
174
175    /// Configures the table to use provisioned billing.
176    ///
177    /// With this mode, you must specify read and write capacity units.
178    pub fn provisioned_billing(self) -> TableBuilder<ProvisionedStateStart> {
179        TableBuilder {
180            billing_mode: Some(BillingMode::Provisioned),
181            state: Default::default(),
182            id: self.id,
183            table_name: self.table_name,
184            partition_key: self.partition_key,
185            sort_key: self.sort_key,
186            read_capacity: self.read_capacity,
187            write_capacity: self.write_capacity,
188            deletion_policy: self.deletion_policy,
189            update_replace_policy: self.update_replace_policy,
190            max_read_capacity: None,
191            max_write_capacity: None,
192        }
193    }
194
195    fn build_internal(self, stack_builder: &mut StackBuilder) -> TableRef {
196        let Key { key, key_type } = self.partition_key.unwrap();
197        let mut key_schema = vec![KeySchema {
198            attribute_name: key.clone(),
199            key_type: "HASH".to_string(),
200        }];
201        let mut key_attributes = vec![AttributeDefinition {
202            attribute_name: key,
203            attribute_type: key_type.into(),
204        }];
205
206        if let Some(Key { key, key_type }) = self.sort_key {
207            let sort_key = KeySchema {
208                attribute_name: key.clone(),
209                key_type: "RANGE".to_string(),
210            };
211            let sort_key_attributes = AttributeDefinition {
212                attribute_name: key,
213                attribute_type: key_type.into(),
214            };
215            key_schema.push(sort_key);
216            key_attributes.push(sort_key_attributes);
217        }
218
219        let billing_mode = self
220            .billing_mode
221            .expect("billing mode should be set, as this is enforced by the builder");
222
223        let provisioned_throughput = if billing_mode == BillingMode::Provisioned {
224            Some(ProvisionedThroughput {
225                read_capacity: self
226                    .read_capacity
227                    .expect("for provisioned billing mode, read capacity should be set"),
228                write_capacity: self
229                    .write_capacity
230                    .expect("for provisioned billing mode, write capacity should be set"),
231            })
232        } else {
233            None
234        };
235
236        let on_demand_throughput = if billing_mode == BillingMode::PayPerRequest {
237            Some(OnDemandThroughput {
238                max_read_capacity: self.max_read_capacity,
239                max_write_capacity: self.max_write_capacity,
240            })
241        } else {
242            None
243        };
244
245        let properties = TableProperties {
246            key_schema,
247            attribute_definitions: key_attributes,
248            billing_mode: billing_mode.into(),
249            provisioned_throughput,
250            on_demand_throughput,
251        };
252
253        let resource_id = Resource::generate_id("DynamoDBTable");
254        stack_builder.add_resource(Table {
255            id: self.id,
256            resource_id: resource_id.clone(),
257            r#type: TableType::TableType,
258            properties,
259            update_delete_policy_dto: UpdateDeletePolicyDTO { deletion_policy: self.deletion_policy, update_replace_policy: self.update_replace_policy },
260        });
261
262        TableRef::internal_new(resource_id)
263    }
264}
265
266impl TableBuilder<PayPerRequestState> {
267    /// Sets the maximum read capacity for a pay-per-request table. This is an optional property.
268    pub fn max_read_capacity(self, capacity: NonZeroNumber) -> Self {
269        Self {
270            max_read_capacity: Some(capacity.0),
271            ..self
272        }
273    }
274
275    /// Sets the maximum write capacity for a pay-per-request table. This is an optional property.
276    pub fn max_write_capacity(self, capacity: NonZeroNumber) -> Self {
277        Self {
278            max_write_capacity: Some(capacity.0),
279            ..self
280        }
281    }
282
283    pub fn build(self, stack_builder: &mut StackBuilder) -> TableRef {
284        self.build_internal(stack_builder)
285    }
286}
287
288impl TableBuilder<ProvisionedStateStart> {
289    /// Sets the read capacity for a provisioned table. This is a required property for provisioned tables.
290    pub fn read_capacity(self, capacity: NonZeroNumber) -> TableBuilder<ProvisionedStateReadSet> {
291        TableBuilder {
292            read_capacity: Some(capacity.0),
293            state: Default::default(),
294            id: self.id,
295            table_name: self.table_name,
296            partition_key: self.partition_key,
297            sort_key: self.sort_key,
298            billing_mode: self.billing_mode,
299            write_capacity: self.write_capacity,
300            max_read_capacity: self.read_capacity,
301            max_write_capacity: self.max_write_capacity,
302            deletion_policy: self.deletion_policy,
303            update_replace_policy: self.update_replace_policy,
304        }
305    }
306}
307
308impl TableBuilder<ProvisionedStateReadSet> {
309    /// Sets the write capacity for a provisioned table. This is a required property for provisioned tables.
310    pub fn write_capacity(self, capacity: NonZeroNumber) -> TableBuilder<ProvisionedStateWriteSet> {
311        TableBuilder {
312            write_capacity: Some(capacity.0),
313            state: Default::default(),
314            id: self.id,
315            table_name: self.table_name,
316            partition_key: self.partition_key,
317            sort_key: self.sort_key,
318            billing_mode: self.billing_mode,
319            read_capacity: self.read_capacity,
320            max_read_capacity: self.max_read_capacity,
321            max_write_capacity: self.max_write_capacity,
322            deletion_policy: self.deletion_policy,
323            update_replace_policy: self.update_replace_policy,
324        }
325    }
326}
327
328impl TableBuilder<ProvisionedStateWriteSet> {
329    pub fn build(self, stack_builder: &mut StackBuilder) -> TableRef {
330        self.build_internal(stack_builder)
331    }
332}