Skip to main content

winterbaume_keyspaces/
state.rs

1use std::collections::HashMap;
2
3use chrono::Utc;
4use thiserror::Error;
5
6use crate::types::*;
7
8#[derive(Debug, Default)]
9pub struct KeyspacesState {
10    /// Keyspaces keyed by name.
11    pub keyspaces: HashMap<String, Keyspace>,
12    /// Tables keyed by (keyspace_name, table_name).
13    pub tables: HashMap<(String, String), Table>,
14    /// User-defined types keyed by (keyspace_name, type_name).
15    pub types: HashMap<(String, String), UserDefinedType>,
16}
17
18/// Domain-specific error enum. Contains no HTTP status codes or AWS error type strings.
19#[derive(Debug, Error)]
20pub enum KeyspacesError {
21    #[error("Resource not found: {resource_type} {name}")]
22    NotFound {
23        resource_type: &'static str,
24        name: String,
25    },
26    #[error("Resource already exists: {resource_type} {name}")]
27    AlreadyExists {
28        resource_type: &'static str,
29        name: String,
30    },
31    #[error("{message}")]
32    Validation { message: String },
33    #[error("Resource {name} has a conflict: {detail}")]
34    Conflict { name: String, detail: String },
35}
36
37impl KeyspacesState {
38    // ---- Keyspace CRUD ----
39
40    pub fn create_keyspace(
41        &mut self,
42        name: &str,
43        replication_strategy: &str,
44        replication_regions: Vec<String>,
45        tags: HashMap<String, String>,
46        account_id: &str,
47        region: &str,
48    ) -> Result<String, KeyspacesError> {
49        if self.keyspaces.contains_key(name) {
50            return Err(KeyspacesError::AlreadyExists {
51                resource_type: "Keyspace",
52                name: name.to_string(),
53            });
54        }
55        let arn = format!("arn:aws:cassandra:{region}:{account_id}:/keyspace/{name}/");
56        let ks = Keyspace {
57            name: name.to_string(),
58            arn: arn.clone(),
59            replication_strategy: replication_strategy.to_string(),
60            replication_regions,
61            tags,
62            creation_timestamp: Utc::now(),
63            status: "ACTIVE".to_string(),
64        };
65        self.keyspaces.insert(name.to_string(), ks);
66        Ok(arn)
67    }
68
69    pub fn get_keyspace(&self, name: &str) -> Result<&Keyspace, KeyspacesError> {
70        self.keyspaces
71            .get(name)
72            .ok_or_else(|| KeyspacesError::NotFound {
73                resource_type: "Keyspace",
74                name: name.to_string(),
75            })
76    }
77
78    pub fn delete_keyspace(&mut self, name: &str) -> Result<(), KeyspacesError> {
79        if self.keyspaces.remove(name).is_none() {
80            return Err(KeyspacesError::NotFound {
81                resource_type: "Keyspace",
82                name: name.to_string(),
83            });
84        }
85        // Also remove all tables in this keyspace.
86        self.tables.retain(|(ks, _), _| ks != name);
87        // Also remove all types in this keyspace.
88        self.types.retain(|(ks, _), _| ks != name);
89        Ok(())
90    }
91
92    pub fn update_keyspace(
93        &mut self,
94        name: &str,
95        replication_strategy: &str,
96        replication_regions: Vec<String>,
97    ) -> Result<String, KeyspacesError> {
98        let ks = self
99            .keyspaces
100            .get_mut(name)
101            .ok_or_else(|| KeyspacesError::NotFound {
102                resource_type: "Keyspace",
103                name: name.to_string(),
104            })?;
105        ks.replication_strategy = replication_strategy.to_string();
106        ks.replication_regions = replication_regions;
107        Ok(ks.arn.clone())
108    }
109
110    pub fn list_keyspaces(&self) -> Vec<&Keyspace> {
111        let mut ks: Vec<_> = self.keyspaces.values().collect();
112        ks.sort_by_key(|k| &k.name);
113        ks
114    }
115
116    // ---- Table CRUD ----
117
118    #[allow(clippy::too_many_arguments)]
119    pub fn create_table(
120        &mut self,
121        keyspace_name: &str,
122        table_name: &str,
123        schema: SchemaDefinition,
124        capacity_mode: &str,
125        read_capacity_units: Option<i64>,
126        write_capacity_units: Option<i64>,
127        encryption_type: &str,
128        kms_key_identifier: Option<String>,
129        point_in_time_recovery: bool,
130        ttl_status: &str,
131        default_time_to_live: Option<i32>,
132        comment: &str,
133        client_side_timestamps: bool,
134        tags: HashMap<String, String>,
135        account_id: &str,
136        region: &str,
137    ) -> Result<String, KeyspacesError> {
138        // Verify keyspace exists.
139        if !self.keyspaces.contains_key(keyspace_name) {
140            return Err(KeyspacesError::NotFound {
141                resource_type: "Keyspace",
142                name: keyspace_name.to_string(),
143            });
144        }
145        let key = (keyspace_name.to_string(), table_name.to_string());
146        if self.tables.contains_key(&key) {
147            return Err(KeyspacesError::AlreadyExists {
148                resource_type: "Table",
149                name: format!("{keyspace_name}/{table_name}"),
150            });
151        }
152        let arn = format!(
153            "arn:aws:cassandra:{region}:{account_id}:/keyspace/{keyspace_name}/table/{table_name}"
154        );
155        let table = Table {
156            keyspace_name: keyspace_name.to_string(),
157            table_name: table_name.to_string(),
158            arn: arn.clone(),
159            schema_definition: schema,
160            capacity_mode: capacity_mode.to_string(),
161            read_capacity_units,
162            write_capacity_units,
163            encryption_type: encryption_type.to_string(),
164            kms_key_identifier,
165            point_in_time_recovery_enabled: point_in_time_recovery,
166            ttl_status: ttl_status.to_string(),
167            default_time_to_live,
168            comment: comment.to_string(),
169            client_side_timestamps_enabled: client_side_timestamps,
170            tags,
171            creation_timestamp: Utc::now(),
172            status: "ACTIVE".to_string(),
173        };
174        self.tables.insert(key, table);
175        Ok(arn)
176    }
177
178    pub fn get_table(
179        &self,
180        keyspace_name: &str,
181        table_name: &str,
182    ) -> Result<&Table, KeyspacesError> {
183        let key = (keyspace_name.to_string(), table_name.to_string());
184        self.tables
185            .get(&key)
186            .ok_or_else(|| KeyspacesError::NotFound {
187                resource_type: "Table",
188                name: format!("{keyspace_name}/{table_name}"),
189            })
190    }
191
192    pub fn delete_table(
193        &mut self,
194        keyspace_name: &str,
195        table_name: &str,
196    ) -> Result<(), KeyspacesError> {
197        let key = (keyspace_name.to_string(), table_name.to_string());
198        if self.tables.remove(&key).is_none() {
199            return Err(KeyspacesError::NotFound {
200                resource_type: "Table",
201                name: format!("{keyspace_name}/{table_name}"),
202            });
203        }
204        Ok(())
205    }
206
207    #[allow(clippy::too_many_arguments)]
208    pub fn update_table(
209        &mut self,
210        keyspace_name: &str,
211        table_name: &str,
212        capacity_mode: Option<&str>,
213        read_capacity_units: Option<i64>,
214        write_capacity_units: Option<i64>,
215        encryption_type: Option<&str>,
216        kms_key_identifier: Option<String>,
217        point_in_time_recovery: Option<bool>,
218        ttl_status: Option<&str>,
219        default_time_to_live: Option<i32>,
220        client_side_timestamps: Option<bool>,
221    ) -> Result<String, KeyspacesError> {
222        let key = (keyspace_name.to_string(), table_name.to_string());
223        let table = self
224            .tables
225            .get_mut(&key)
226            .ok_or_else(|| KeyspacesError::NotFound {
227                resource_type: "Table",
228                name: format!("{keyspace_name}/{table_name}"),
229            })?;
230        if let Some(cm) = capacity_mode {
231            table.capacity_mode = cm.to_string();
232        }
233        if let Some(rcu) = read_capacity_units {
234            table.read_capacity_units = Some(rcu);
235        }
236        if let Some(wcu) = write_capacity_units {
237            table.write_capacity_units = Some(wcu);
238        }
239        if let Some(et) = encryption_type {
240            table.encryption_type = et.to_string();
241        }
242        if kms_key_identifier.is_some() {
243            table.kms_key_identifier = kms_key_identifier;
244        }
245        if let Some(pitr) = point_in_time_recovery {
246            table.point_in_time_recovery_enabled = pitr;
247        }
248        if let Some(ts) = ttl_status {
249            table.ttl_status = ts.to_string();
250        }
251        if let Some(dttl) = default_time_to_live {
252            table.default_time_to_live = Some(dttl);
253        }
254        if let Some(cst) = client_side_timestamps {
255            table.client_side_timestamps_enabled = cst;
256        }
257        Ok(table.arn.clone())
258    }
259
260    pub fn list_tables(&self, keyspace_name: &str) -> Result<Vec<&Table>, KeyspacesError> {
261        if !self.keyspaces.contains_key(keyspace_name) {
262            return Err(KeyspacesError::NotFound {
263                resource_type: "Keyspace",
264                name: keyspace_name.to_string(),
265            });
266        }
267        let mut tables: Vec<_> = self
268            .tables
269            .values()
270            .filter(|t| t.keyspace_name == keyspace_name)
271            .collect();
272        tables.sort_by_key(|t| &t.table_name);
273        Ok(tables)
274    }
275
276    pub fn restore_table(
277        &mut self,
278        source_keyspace_name: &str,
279        source_table_name: &str,
280        target_keyspace_name: &str,
281        target_table_name: &str,
282        account_id: &str,
283        region: &str,
284    ) -> Result<String, KeyspacesError> {
285        // Verify source table exists.
286        let source_key = (
287            source_keyspace_name.to_string(),
288            source_table_name.to_string(),
289        );
290        let source = self
291            .tables
292            .get(&source_key)
293            .ok_or_else(|| KeyspacesError::NotFound {
294                resource_type: "Table",
295                name: format!("{source_keyspace_name}/{source_table_name}"),
296            })?
297            .clone();
298
299        // Verify target keyspace exists.
300        if !self.keyspaces.contains_key(target_keyspace_name) {
301            return Err(KeyspacesError::NotFound {
302                resource_type: "Keyspace",
303                name: target_keyspace_name.to_string(),
304            });
305        }
306
307        let target_key = (
308            target_keyspace_name.to_string(),
309            target_table_name.to_string(),
310        );
311        if self.tables.contains_key(&target_key) {
312            return Err(KeyspacesError::AlreadyExists {
313                resource_type: "Table",
314                name: format!("{target_keyspace_name}/{target_table_name}"),
315            });
316        }
317
318        let arn = format!(
319            "arn:aws:cassandra:{region}:{account_id}:/keyspace/{target_keyspace_name}/table/{target_table_name}"
320        );
321        let table = Table {
322            keyspace_name: target_keyspace_name.to_string(),
323            table_name: target_table_name.to_string(),
324            arn: arn.clone(),
325            schema_definition: source.schema_definition,
326            capacity_mode: source.capacity_mode,
327            read_capacity_units: source.read_capacity_units,
328            write_capacity_units: source.write_capacity_units,
329            encryption_type: source.encryption_type,
330            kms_key_identifier: source.kms_key_identifier,
331            point_in_time_recovery_enabled: source.point_in_time_recovery_enabled,
332            ttl_status: source.ttl_status,
333            default_time_to_live: source.default_time_to_live,
334            comment: source.comment,
335            client_side_timestamps_enabled: source.client_side_timestamps_enabled,
336            tags: source.tags,
337            creation_timestamp: Utc::now(),
338            status: "ACTIVE".to_string(),
339        };
340        self.tables.insert(target_key, table);
341        Ok(arn)
342    }
343
344    // ---- Type CRUD ----
345
346    pub fn create_type(
347        &mut self,
348        keyspace_name: &str,
349        type_name: &str,
350        field_definitions: Vec<FieldDefinition>,
351    ) -> Result<String, KeyspacesError> {
352        if !self.keyspaces.contains_key(keyspace_name) {
353            return Err(KeyspacesError::NotFound {
354                resource_type: "Keyspace",
355                name: keyspace_name.to_string(),
356            });
357        }
358        let key = (keyspace_name.to_string(), type_name.to_string());
359        if self.types.contains_key(&key) {
360            return Err(KeyspacesError::AlreadyExists {
361                resource_type: "Type",
362                name: format!("{keyspace_name}/{type_name}"),
363            });
364        }
365        // Types don't have a separate ARN field in the API, but GetType returns keyspaceArn.
366        // The response uses keyspaceArn, not a type-specific ARN.
367        let result = format!("{keyspace_name}.{type_name}");
368        let udt = UserDefinedType {
369            keyspace_name: keyspace_name.to_string(),
370            type_name: type_name.to_string(),
371            field_definitions,
372            creation_timestamp: Utc::now(),
373            status: "ACTIVE".to_string(),
374        };
375        self.types.insert(key, udt);
376        Ok(result)
377    }
378
379    pub fn get_type(
380        &self,
381        keyspace_name: &str,
382        type_name: &str,
383    ) -> Result<&UserDefinedType, KeyspacesError> {
384        let key = (keyspace_name.to_string(), type_name.to_string());
385        self.types
386            .get(&key)
387            .ok_or_else(|| KeyspacesError::NotFound {
388                resource_type: "Type",
389                name: format!("{keyspace_name}/{type_name}"),
390            })
391    }
392
393    pub fn delete_type(
394        &mut self,
395        keyspace_name: &str,
396        type_name: &str,
397    ) -> Result<(), KeyspacesError> {
398        let key = (keyspace_name.to_string(), type_name.to_string());
399        if self.types.remove(&key).is_none() {
400            return Err(KeyspacesError::NotFound {
401                resource_type: "Type",
402                name: format!("{keyspace_name}/{type_name}"),
403            });
404        }
405        Ok(())
406    }
407
408    pub fn list_types(&self, keyspace_name: &str) -> Result<Vec<&UserDefinedType>, KeyspacesError> {
409        if !self.keyspaces.contains_key(keyspace_name) {
410            return Err(KeyspacesError::NotFound {
411                resource_type: "Keyspace",
412                name: keyspace_name.to_string(),
413            });
414        }
415        let mut types: Vec<_> = self
416            .types
417            .values()
418            .filter(|t| t.keyspace_name == keyspace_name)
419            .collect();
420        types.sort_by_key(|t| &t.type_name);
421        Ok(types)
422    }
423
424    // ---- Tag operations ----
425
426    /// Get tags for a resource by ARN. Returns empty map if no tags found.
427    pub fn get_tags_for_resource(
428        &self,
429        arn: &str,
430    ) -> Result<HashMap<String, String>, KeyspacesError> {
431        // Find resource by ARN.
432        for ks in self.keyspaces.values() {
433            if ks.arn == arn {
434                return Ok(ks.tags.clone());
435            }
436        }
437        for table in self.tables.values() {
438            if table.arn == arn {
439                return Ok(table.tags.clone());
440            }
441        }
442        Err(KeyspacesError::NotFound {
443            resource_type: "Resource",
444            name: arn.to_string(),
445        })
446    }
447
448    pub fn tag_resource(
449        &mut self,
450        arn: &str,
451        tags: HashMap<String, String>,
452    ) -> Result<(), KeyspacesError> {
453        // Find resource by ARN and merge tags.
454        for ks in self.keyspaces.values_mut() {
455            if ks.arn == arn {
456                ks.tags.extend(tags);
457                return Ok(());
458            }
459        }
460        for table in self.tables.values_mut() {
461            if table.arn == arn {
462                table.tags.extend(tags);
463                return Ok(());
464            }
465        }
466        Err(KeyspacesError::NotFound {
467            resource_type: "Resource",
468            name: arn.to_string(),
469        })
470    }
471
472    pub fn untag_resource(&mut self, arn: &str, tag_keys: &[String]) -> Result<(), KeyspacesError> {
473        // Find resource by ARN and remove tags.
474        for ks in self.keyspaces.values_mut() {
475            if ks.arn == arn {
476                for key in tag_keys {
477                    ks.tags.remove(key);
478                }
479                return Ok(());
480            }
481        }
482        for table in self.tables.values_mut() {
483            if table.arn == arn {
484                for key in tag_keys {
485                    table.tags.remove(key);
486                }
487                return Ok(());
488            }
489        }
490        Err(KeyspacesError::NotFound {
491            resource_type: "Resource",
492            name: arn.to_string(),
493        })
494    }
495}