rusty_cdk_core/dynamodb/
builder.rs

1use crate::dynamodb::{AttributeDefinition, KeySchema, Table, TableProperties};
2use crate::dynamodb::{OnDemandThroughput, ProvisionedThroughput, TableRef};
3use crate::shared::Id;
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/// Supports both pay-per-request and provisioned billing modes. The builder enforces the correct configuration based on the chosen billing mode.
64///
65/// # Example
66///
67/// ```rust
68/// use rusty_cdk_core::stack::StackBuilder;
69/// use rusty_cdk_core::dynamodb::{TableBuilder, Key, AttributeType};
70/// use rusty_cdk_core::wrappers::*;
71/// use rusty_cdk_macros::{string_with_only_alphanumerics_and_underscores, non_zero_number};
72///
73/// let mut stack_builder = StackBuilder::new();
74///
75/// let partition_key = Key::new(
76///     string_with_only_alphanumerics_and_underscores!("id"),
77///     AttributeType::String
78/// );
79/// let sort_key = Key::new(
80///     string_with_only_alphanumerics_and_underscores!("timestamp"),
81///     AttributeType::Number
82/// );
83///
84/// let on_demand_table = TableBuilder::new("on-demand-table", partition_key)
85///     .sort_key(sort_key)
86///     .pay_per_request_billing()
87///     .build(&mut stack_builder);
88/// ```
89pub struct TableBuilder<T: TableBuilderState> {
90    state: PhantomData<T>,
91    id: Id,
92    table_name: Option<String>,
93    partition_key: Option<Key>,
94    sort_key: Option<Key>,
95    billing_mode: Option<BillingMode>,
96    read_capacity: Option<u32>,
97    write_capacity: Option<u32>,
98    max_read_capacity: Option<u32>,
99    max_write_capacity: Option<u32>,
100}
101
102impl TableBuilder<StartState> {
103    /// Creates a new DynamoDB table builder.
104    ///
105    /// # Arguments
106    /// * `id` - Unique identifier for the table
107    /// * `key` - Partition key definition for the table
108    pub fn new(id: &str, key: Key) -> Self {
109        TableBuilder {
110            state: Default::default(),
111            id: Id(id.to_string()),
112            table_name: None,
113            partition_key: Some(key),
114            sort_key: None,
115            billing_mode: None,
116            read_capacity: None,
117            write_capacity: None,
118            max_read_capacity: None,
119            max_write_capacity: None,
120        }
121    }
122}
123
124impl<T: TableBuilderState> TableBuilder<T> {
125    pub fn sort_key(self, key: Key) -> Self {
126        Self {
127            sort_key: Some(key),
128            ..self
129        }
130    }
131
132    pub fn table_name(self, name: StringWithOnlyAlphaNumericsAndUnderscores) -> Self {
133        Self {
134            table_name: Some(name.0),
135            ..self
136        }
137    }
138
139    /// Configures the table to use pay-per-request billing.
140    ///
141    /// With this mode, you pay per request and can optionally set max throughput limits.
142    pub fn pay_per_request_billing(self) -> TableBuilder<PayPerRequestState> {
143        TableBuilder {
144            billing_mode: Some(BillingMode::PayPerRequest),
145            state: Default::default(),
146            id: self.id,
147            table_name: self.table_name,
148            partition_key: self.partition_key,
149            sort_key: self.sort_key,
150            max_read_capacity: self.max_read_capacity,
151            max_write_capacity: self.max_write_capacity,
152            read_capacity: None,
153            write_capacity: None,
154        }
155    }
156
157    /// Configures the table to use provisioned billing.
158    ///
159    /// With this mode, you must specify read and write capacity units.
160    pub fn provisioned_billing(self) -> TableBuilder<ProvisionedStateStart> {
161        TableBuilder {
162            billing_mode: Some(BillingMode::Provisioned),
163            state: Default::default(),
164            id: self.id,
165            table_name: self.table_name,
166            partition_key: self.partition_key,
167            sort_key: self.sort_key,
168            read_capacity: self.read_capacity,
169            write_capacity: self.write_capacity,
170            max_read_capacity: None,
171            max_write_capacity: None,
172        }
173    }
174
175    fn build_internal(self, stack_builder: &mut StackBuilder) -> TableRef {
176        let Key { key, key_type } = self.partition_key.unwrap();
177        let mut key_schema = vec![KeySchema {
178            attribute_name: key.clone(),
179            key_type: "HASH".to_string(),
180        }];
181        let mut key_attributes = vec![AttributeDefinition {
182            attribute_name: key,
183            attribute_type: key_type.into(),
184        }];
185
186        if let Some(Key { key, key_type }) = self.sort_key {
187            let sort_key = KeySchema {
188                attribute_name: key.clone(),
189                key_type: "RANGE".to_string(),
190            };
191            let sort_key_attributes = AttributeDefinition {
192                attribute_name: key,
193                attribute_type: key_type.into(),
194            };
195            key_schema.push(sort_key);
196            key_attributes.push(sort_key_attributes);
197        }
198
199        let billing_mode = self
200            .billing_mode
201            .expect("billing mode should be set, as this is enforced by the builder");
202
203        let provisioned_throughput = if billing_mode == BillingMode::Provisioned {
204            Some(ProvisionedThroughput {
205                read_capacity: self
206                    .read_capacity
207                    .expect("for provisioned billing mode, read capacity should be set"),
208                write_capacity: self
209                    .write_capacity
210                    .expect("for provisioned billing mode, write capacity should be set"),
211            })
212        } else {
213            None
214        };
215
216        let on_demand_throughput = if billing_mode == BillingMode::PayPerRequest {
217            Some(OnDemandThroughput {
218                max_read_capacity: self.max_read_capacity,
219                max_write_capacity: self.max_write_capacity,
220            })
221        } else {
222            None
223        };
224
225        let properties = TableProperties {
226            key_schema,
227            attribute_definitions: key_attributes,
228            billing_mode: billing_mode.into(),
229            provisioned_throughput,
230            on_demand_throughput,
231        };
232
233        let resource_id = Resource::generate_id("DynamoDBTable");
234        stack_builder.add_resource(Table {
235            id: self.id,
236            resource_id: resource_id.clone(),
237            r#type: "AWS::DynamoDB::Table".to_string(),
238            properties,
239        });
240
241        TableRef::new(resource_id)
242    }
243}
244
245impl TableBuilder<PayPerRequestState> {
246    pub fn max_read_capacity(self, capacity: NonZeroNumber) -> Self {
247        Self {
248            max_read_capacity: Some(capacity.0),
249            ..self
250        }
251    }
252
253    pub fn max_write_capacity(self, capacity: NonZeroNumber) -> Self {
254        Self {
255            max_write_capacity: Some(capacity.0),
256            ..self
257        }
258    }
259
260    pub fn build(self, stack_builder: &mut StackBuilder) -> TableRef {
261        self.build_internal(stack_builder)
262    }
263}
264
265impl TableBuilder<ProvisionedStateStart> {
266    pub fn read_capacity(self, capacity: NonZeroNumber) -> TableBuilder<ProvisionedStateReadSet> {
267        TableBuilder {
268            read_capacity: Some(capacity.0),
269            state: Default::default(),
270            id: self.id,
271            table_name: self.table_name,
272            partition_key: self.partition_key,
273            sort_key: self.sort_key,
274            billing_mode: self.billing_mode,
275            write_capacity: self.write_capacity,
276            max_read_capacity: self.read_capacity,
277            max_write_capacity: self.max_write_capacity,
278        }
279    }
280}
281
282impl TableBuilder<ProvisionedStateReadSet> {
283    pub fn write_capacity(self, capacity: NonZeroNumber) -> TableBuilder<ProvisionedStateWriteSet> {
284        TableBuilder {
285            write_capacity: Some(capacity.0),
286            state: Default::default(),
287            id: self.id,
288            table_name: self.table_name,
289            partition_key: self.partition_key,
290            sort_key: self.sort_key,
291            billing_mode: self.billing_mode,
292            read_capacity: self.read_capacity,
293            max_read_capacity: self.max_read_capacity,
294            max_write_capacity: self.max_write_capacity,
295        }
296    }
297}
298
299impl TableBuilder<ProvisionedStateWriteSet> {
300    pub fn build(self, stack_builder: &mut StackBuilder) -> TableRef {
301        self.build_internal(stack_builder)
302    }
303}