Skip to main content

winterbaume_timestreamwrite/
handlers.rs

1use std::collections::HashMap;
2use std::future::Future;
3use std::pin::Pin;
4use std::sync::Arc;
5
6use serde_json::{Value, json};
7use uuid::Uuid;
8use winterbaume_core::{
9    BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, StatefulService,
10    default_account_id,
11};
12
13use crate::state::{TimestreamWriteError, TimestreamWriteState};
14use crate::types::{self as statetypes, Dimension, Record};
15use crate::views::TimestreamWriteStateView;
16use crate::wire;
17
18/// Timestream Write service handler that processes awsJson1.0 protocol requests.
19pub struct TimestreamWriteService {
20    pub(crate) state: Arc<BackendState<TimestreamWriteState>>,
21    pub(crate) notifier: StateChangeNotifier<TimestreamWriteStateView>,
22}
23
24impl TimestreamWriteService {
25    pub fn new() -> Self {
26        Self {
27            state: Arc::new(BackendState::new()),
28            notifier: StateChangeNotifier::new(),
29        }
30    }
31}
32
33impl Default for TimestreamWriteService {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl MockService for TimestreamWriteService {
40    fn service_name(&self) -> &str {
41        "timestream"
42    }
43
44    fn url_patterns(&self) -> Vec<&str> {
45        vec![r"https?://ingest\.timestream\.(.+)\.amazonaws\.com"]
46    }
47
48    fn handle(
49        &self,
50        request: MockRequest,
51    ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
52        Box::pin(async move { self.dispatch(request).await })
53    }
54}
55
56impl TimestreamWriteService {
57    async fn dispatch(&self, request: MockRequest) -> MockResponse {
58        let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
59        let account_id = default_account_id();
60
61        // Extract action from X-Amz-Target header
62        // Format: "Timestream_20181101.CreateDatabase"
63        let action = request
64            .headers
65            .get("x-amz-target")
66            .and_then(|v| v.to_str().ok())
67            .and_then(|v| v.split('.').next_back())
68            .map(|s| s.to_string());
69
70        let action = match action {
71            Some(a) => a,
72            None => {
73                return json_error_response(400, "MissingAction", "Missing X-Amz-Target header");
74            }
75        };
76
77        if serde_json::from_slice::<Value>(&request.body).is_err() {
78            return json_error_response(400, "SerializationException", "Invalid JSON body");
79        }
80        let body_bytes: &[u8] = &request.body;
81
82        let state = self.state.get(account_id, &region);
83
84        let response = match action.as_str() {
85            "CreateDatabase" => {
86                self.handle_create_database(&state, body_bytes, account_id, &region)
87                    .await
88            }
89            "DescribeDatabase" => self.handle_describe_database(&state, body_bytes).await,
90            "DeleteDatabase" => self.handle_delete_database(&state, body_bytes).await,
91            "ListDatabases" => self.handle_list_databases(&state).await,
92            "UpdateDatabase" => self.handle_update_database(&state, body_bytes).await,
93            "CreateTable" => {
94                self.handle_create_table(&state, body_bytes, account_id, &region)
95                    .await
96            }
97            "DescribeTable" => self.handle_describe_table(&state, body_bytes).await,
98            "DeleteTable" => self.handle_delete_table(&state, body_bytes).await,
99            "ListTables" => self.handle_list_tables(&state, body_bytes).await,
100            "UpdateTable" => self.handle_update_table(&state, body_bytes).await,
101            "WriteRecords" => self.handle_write_records(&state, body_bytes).await,
102            "DescribeEndpoints" => self.handle_describe_endpoints(&region).await,
103            "TagResource" => self.handle_tag_resource(&state, body_bytes).await,
104            "UntagResource" => self.handle_untag_resource(&state, body_bytes).await,
105            "ListTagsForResource" => self.handle_list_tags_for_resource(&state, body_bytes).await,
106            "CreateBatchLoadTask" => self.handle_create_batch_load_task(&state, body_bytes).await,
107            "DescribeBatchLoadTask" => {
108                self.handle_describe_batch_load_task(&state, body_bytes)
109                    .await
110            }
111            "ListBatchLoadTasks" => self.handle_list_batch_load_tasks(&state, body_bytes).await,
112            "ResumeBatchLoadTask" => self.handle_resume_batch_load_task(&state, body_bytes).await,
113            _ => json_error_response(
114                400,
115                "UnknownOperationException",
116                &format!("Unknown operation: {action}"),
117            ),
118        };
119        if response.status / 100 == 2 {
120            self.notify_state_changed(account_id, &region).await;
121        }
122        response
123    }
124
125    // --- Database handlers ---
126
127    async fn handle_create_database(
128        &self,
129        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
130        body: &[u8],
131        account_id: &str,
132        region: &str,
133    ) -> MockResponse {
134        let input = match wire::deserialize_create_database_request(body) {
135            Ok(v) => v,
136            Err(e) => return json_error_response(400, "ValidationException", &e),
137        };
138        if input.database_name.is_empty() {
139            return json_error_response(400, "ValidationException", "DatabaseName is required");
140        }
141        let kms_key_id = input.kms_key_id.as_deref();
142
143        let mut state = state.write().await;
144        match state.create_database(&input.database_name, account_id, region, kms_key_id) {
145            Ok(db) => wire::serialize_create_database_response(&wire::CreateDatabaseResponse {
146                database: Some(database_to_wire(db)),
147            }),
148            Err(e) => timestream_error_response(&e),
149        }
150    }
151
152    async fn handle_describe_database(
153        &self,
154        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
155        body: &[u8],
156    ) -> MockResponse {
157        let input = match wire::deserialize_describe_database_request(body) {
158            Ok(v) => v,
159            Err(e) => return json_error_response(400, "ValidationException", &e),
160        };
161        if input.database_name.is_empty() {
162            return json_error_response(400, "ValidationException", "DatabaseName is required");
163        }
164
165        let state = state.read().await;
166        match state.describe_database(&input.database_name) {
167            Ok(db) => wire::serialize_describe_database_response(&wire::DescribeDatabaseResponse {
168                database: Some(database_to_wire(db)),
169            }),
170            Err(e) => timestream_error_response(&e),
171        }
172    }
173
174    async fn handle_delete_database(
175        &self,
176        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
177        body: &[u8],
178    ) -> MockResponse {
179        let input = match wire::deserialize_delete_database_request(body) {
180            Ok(v) => v,
181            Err(e) => return json_error_response(400, "ValidationException", &e),
182        };
183        if input.database_name.is_empty() {
184            return json_error_response(400, "ValidationException", "DatabaseName is required");
185        }
186
187        let mut state = state.write().await;
188        match state.delete_database(&input.database_name) {
189            Ok(()) => wire::serialize_delete_database_response(),
190            Err(e) => timestream_error_response(&e),
191        }
192    }
193
194    async fn handle_list_databases(
195        &self,
196        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
197    ) -> MockResponse {
198        let state = state.read().await;
199        let databases = state.list_databases();
200
201        let db_list: Vec<wire::Database> =
202            databases.iter().map(|db| database_to_wire(db)).collect();
203
204        wire::serialize_list_databases_response(&wire::ListDatabasesResponse {
205            databases: Some(db_list),
206            next_token: None,
207        })
208    }
209
210    async fn handle_update_database(
211        &self,
212        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
213        body: &[u8],
214    ) -> MockResponse {
215        let input = match wire::deserialize_update_database_request(body) {
216            Ok(v) => v,
217            Err(e) => return json_error_response(400, "ValidationException", &e),
218        };
219        if input.database_name.is_empty() {
220            return json_error_response(400, "ValidationException", "DatabaseName is required");
221        }
222        if input.kms_key_id.is_empty() {
223            return json_error_response(400, "ValidationException", "KmsKeyId is required");
224        }
225
226        let mut state = state.write().await;
227        match state.update_database(&input.database_name, &input.kms_key_id) {
228            Ok(db) => wire::serialize_update_database_response(&wire::UpdateDatabaseResponse {
229                database: Some(database_to_wire(db)),
230            }),
231            Err(e) => timestream_error_response(&e),
232        }
233    }
234
235    // --- Table handlers ---
236
237    async fn handle_create_table(
238        &self,
239        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
240        body: &[u8],
241        account_id: &str,
242        region: &str,
243    ) -> MockResponse {
244        let input = match wire::deserialize_create_table_request(body) {
245            Ok(v) => v,
246            Err(e) => return json_error_response(400, "ValidationException", &e),
247        };
248        if input.database_name.is_empty() {
249            return json_error_response(400, "ValidationException", "DatabaseName is required");
250        }
251        if input.table_name.is_empty() {
252            return json_error_response(400, "ValidationException", "TableName is required");
253        }
254
255        let retention_properties =
256            input
257                .retention_properties
258                .map(|rp| statetypes::RetentionProperties {
259                    memory_store_retention_period_in_hours: rp
260                        .memory_store_retention_period_in_hours,
261                    magnetic_store_retention_period_in_days: rp
262                        .magnetic_store_retention_period_in_days,
263                });
264
265        let magnetic_store_write_properties = input.magnetic_store_write_properties.map(|mswp| {
266            statetypes::MagneticStoreWriteProperties {
267                enable_magnetic_store_writes: mswp.enable_magnetic_store_writes,
268            }
269        });
270
271        let mut state = state.write().await;
272        match state.create_table(
273            &input.database_name,
274            &input.table_name,
275            account_id,
276            region,
277            retention_properties,
278            magnetic_store_write_properties,
279        ) {
280            Ok(table) => wire::serialize_create_table_response(&wire::CreateTableResponse {
281                table: Some(table_to_wire(table)),
282            }),
283            Err(e) => timestream_error_response(&e),
284        }
285    }
286
287    async fn handle_describe_table(
288        &self,
289        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
290        body: &[u8],
291    ) -> MockResponse {
292        let input = match wire::deserialize_describe_table_request(body) {
293            Ok(v) => v,
294            Err(e) => return json_error_response(400, "ValidationException", &e),
295        };
296        if input.database_name.is_empty() {
297            return json_error_response(400, "ValidationException", "DatabaseName is required");
298        }
299        if input.table_name.is_empty() {
300            return json_error_response(400, "ValidationException", "TableName is required");
301        }
302
303        let state = state.read().await;
304        match state.describe_table(&input.database_name, &input.table_name) {
305            Ok(table) => wire::serialize_describe_table_response(&wire::DescribeTableResponse {
306                table: Some(table_to_wire(table)),
307            }),
308            Err(e) => timestream_error_response(&e),
309        }
310    }
311
312    async fn handle_delete_table(
313        &self,
314        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
315        body: &[u8],
316    ) -> MockResponse {
317        let input = match wire::deserialize_delete_table_request(body) {
318            Ok(v) => v,
319            Err(e) => return json_error_response(400, "ValidationException", &e),
320        };
321        if input.database_name.is_empty() {
322            return json_error_response(400, "ValidationException", "DatabaseName is required");
323        }
324        if input.table_name.is_empty() {
325            return json_error_response(400, "ValidationException", "TableName is required");
326        }
327
328        let mut state = state.write().await;
329        match state.delete_table(&input.database_name, &input.table_name) {
330            Ok(()) => wire::serialize_delete_table_response(),
331            Err(e) => timestream_error_response(&e),
332        }
333    }
334
335    async fn handle_list_tables(
336        &self,
337        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
338        body: &[u8],
339    ) -> MockResponse {
340        let input = match wire::deserialize_list_tables_request(body) {
341            Ok(v) => v,
342            Err(e) => return json_error_response(400, "ValidationException", &e),
343        };
344        let database_name = match input.database_name.as_deref() {
345            Some(n) if !n.is_empty() => n,
346            _ => {
347                return json_error_response(400, "ValidationException", "DatabaseName is required");
348            }
349        };
350
351        let state = state.read().await;
352        match state.list_tables(database_name) {
353            Ok(tables) => {
354                let table_list: Vec<wire::Table> =
355                    tables.iter().map(|t| table_to_wire(t)).collect();
356                wire::serialize_list_tables_response(&wire::ListTablesResponse {
357                    tables: Some(table_list),
358                    next_token: None,
359                })
360            }
361            Err(e) => timestream_error_response(&e),
362        }
363    }
364
365    async fn handle_update_table(
366        &self,
367        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
368        body: &[u8],
369    ) -> MockResponse {
370        let input = match wire::deserialize_update_table_request(body) {
371            Ok(v) => v,
372            Err(e) => return json_error_response(400, "ValidationException", &e),
373        };
374        if input.database_name.is_empty() {
375            return json_error_response(400, "ValidationException", "DatabaseName is required");
376        }
377        if input.table_name.is_empty() {
378            return json_error_response(400, "ValidationException", "TableName is required");
379        }
380
381        let retention_properties =
382            input
383                .retention_properties
384                .map(|rp| statetypes::RetentionProperties {
385                    memory_store_retention_period_in_hours: rp
386                        .memory_store_retention_period_in_hours,
387                    magnetic_store_retention_period_in_days: rp
388                        .magnetic_store_retention_period_in_days,
389                });
390
391        let magnetic_store_write_properties = input.magnetic_store_write_properties.map(|mswp| {
392            statetypes::MagneticStoreWriteProperties {
393                enable_magnetic_store_writes: mswp.enable_magnetic_store_writes,
394            }
395        });
396
397        let mut state = state.write().await;
398        match state.update_table(
399            &input.database_name,
400            &input.table_name,
401            retention_properties,
402            magnetic_store_write_properties,
403        ) {
404            Ok(table) => wire::serialize_update_table_response(&wire::UpdateTableResponse {
405                table: Some(table_to_wire(table)),
406            }),
407            Err(e) => timestream_error_response(&e),
408        }
409    }
410
411    // --- WriteRecords ---
412
413    async fn handle_write_records(
414        &self,
415        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
416        body: &[u8],
417    ) -> MockResponse {
418        let input = match wire::deserialize_write_records_request(body) {
419            Ok(v) => v,
420            Err(e) => return json_error_response(400, "ValidationException", &e),
421        };
422        if input.database_name.is_empty() {
423            return json_error_response(400, "ValidationException", "DatabaseName is required");
424        }
425        if input.table_name.is_empty() {
426            return json_error_response(400, "ValidationException", "TableName is required");
427        }
428
429        // Common attributes that apply to all records
430        let common = input.common_attributes.unwrap_or_default();
431        let common_dimensions: Vec<Dimension> = common
432            .dimensions
433            .unwrap_or_default()
434            .into_iter()
435            .map(wire_dimension_to_state)
436            .collect();
437        let common_measure_name = common.measure_name;
438        let common_measure_value = common.measure_value;
439        let common_measure_value_type = common.measure_value_type;
440        let common_time = common.time;
441        let common_time_unit = common.time_unit;
442
443        let mut records = Vec::new();
444        for r in input.records {
445            let mut dims = common_dimensions.clone();
446            if let Some(extra) = r.dimensions {
447                dims.extend(extra.into_iter().map(wire_dimension_to_state));
448            }
449
450            let measure_name = r.measure_name.or_else(|| common_measure_name.clone());
451            let measure_value = r.measure_value.or_else(|| common_measure_value.clone());
452            let measure_value_type = r
453                .measure_value_type
454                .or_else(|| common_measure_value_type.clone());
455            let time = r.time.or_else(|| common_time.clone());
456            let time_unit = r.time_unit.or_else(|| common_time_unit.clone());
457
458            records.push(Record {
459                dimensions: dims,
460                measure_name,
461                measure_value,
462                measure_value_type,
463                time,
464                time_unit,
465            });
466        }
467
468        let mut state = state.write().await;
469        match state.write_records(&input.database_name, &input.table_name, records) {
470            Ok(count) => wire::serialize_write_records_response(&wire::WriteRecordsResponse {
471                records_ingested: Some(wire::RecordsIngested {
472                    total: Some(count),
473                    memory_store: Some(count),
474                    magnetic_store: Some(0),
475                }),
476            }),
477            Err(e) => timestream_error_response(&e),
478        }
479    }
480
481    // --- DescribeEndpoints ---
482
483    async fn handle_describe_endpoints(&self, region: &str) -> MockResponse {
484        wire::serialize_describe_endpoints_response(&wire::DescribeEndpointsResponse {
485            endpoints: Some(vec![wire::Endpoint {
486                address: Some(format!("ingest.timestream.{region}.amazonaws.com")),
487                cache_period_in_minutes: Some(1440),
488            }]),
489        })
490    }
491
492    // --- Tag operations ---
493
494    async fn handle_tag_resource(
495        &self,
496        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
497        body: &[u8],
498    ) -> MockResponse {
499        let input = match wire::deserialize_tag_resource_request(body) {
500            Ok(v) => v,
501            Err(e) => return json_error_response(400, "ValidationException", &e),
502        };
503        if input.resource_a_r_n.is_empty() {
504            return json_error_response(400, "ValidationException", "ResourceARN is required");
505        }
506        let tag_map: HashMap<String, String> =
507            input.tags.into_iter().map(|t| (t.key, t.value)).collect();
508
509        let mut state = state.write().await;
510        match state.tag_resource(&input.resource_a_r_n, tag_map) {
511            Ok(()) => wire::serialize_tag_resource_response(&wire::TagResourceResponse {}),
512            Err(e) => timestream_error_response(&e),
513        }
514    }
515
516    async fn handle_untag_resource(
517        &self,
518        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
519        body: &[u8],
520    ) -> MockResponse {
521        let input = match wire::deserialize_untag_resource_request(body) {
522            Ok(v) => v,
523            Err(e) => return json_error_response(400, "ValidationException", &e),
524        };
525        if input.resource_a_r_n.is_empty() {
526            return json_error_response(400, "ValidationException", "ResourceARN is required");
527        }
528
529        let mut state = state.write().await;
530        match state.untag_resource(&input.resource_a_r_n, &input.tag_keys) {
531            Ok(()) => wire::serialize_untag_resource_response(&wire::UntagResourceResponse {}),
532            Err(e) => timestream_error_response(&e),
533        }
534    }
535
536    async fn handle_list_tags_for_resource(
537        &self,
538        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
539        body: &[u8],
540    ) -> MockResponse {
541        let input = match wire::deserialize_list_tags_for_resource_request(body) {
542            Ok(v) => v,
543            Err(e) => return json_error_response(400, "ValidationException", &e),
544        };
545        if input.resource_a_r_n.is_empty() {
546            return json_error_response(400, "ValidationException", "ResourceARN is required");
547        }
548
549        let state = state.read().await;
550        match state.list_tags_for_resource(&input.resource_a_r_n) {
551            Ok(tags) => {
552                let tag_list: Vec<wire::Tag> = tags
553                    .iter()
554                    .map(|(k, v)| wire::Tag {
555                        key: k.clone(),
556                        value: v.clone(),
557                    })
558                    .collect();
559                wire::serialize_list_tags_for_resource_response(
560                    &wire::ListTagsForResourceResponse {
561                        tags: Some(tag_list),
562                    },
563                )
564            }
565            Err(e) => timestream_error_response(&e),
566        }
567    }
568
569    // --- BatchLoadTask handlers ---
570
571    async fn handle_create_batch_load_task(
572        &self,
573        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
574        body: &[u8],
575    ) -> MockResponse {
576        let input = match wire::deserialize_create_batch_load_task_request(body) {
577            Ok(v) => v,
578            Err(e) => return json_error_response(400, "ValidationException", &e),
579        };
580        if input.target_database_name.is_empty() {
581            return json_error_response(
582                400,
583                "ValidationException",
584                "TargetDatabaseName is required",
585            );
586        }
587        if input.target_table_name.is_empty() {
588            return json_error_response(400, "ValidationException", "TargetTableName is required");
589        }
590        // Bridge typed wire shapes back into Value for state storage.
591        let data_source_configuration =
592            serde_json::to_value(&input.data_source_configuration).unwrap_or(Value::Null);
593        let report_configuration =
594            serde_json::to_value(&input.report_configuration).unwrap_or(Value::Null);
595        let data_model_configuration = input
596            .data_model_configuration
597            .as_ref()
598            .map(|v| serde_json::to_value(v).unwrap_or(Value::Null));
599
600        let task_id = Uuid::new_v4().to_string();
601
602        let mut state = state.write().await;
603        let task = state.create_batch_load_task(
604            task_id,
605            &input.target_database_name,
606            &input.target_table_name,
607            data_source_configuration,
608            report_configuration,
609            data_model_configuration,
610        );
611        wire::serialize_create_batch_load_task_response(&wire::CreateBatchLoadTaskResponse {
612            task_id: Some(task.task_id.clone()),
613        })
614    }
615
616    async fn handle_describe_batch_load_task(
617        &self,
618        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
619        body: &[u8],
620    ) -> MockResponse {
621        let input = match wire::deserialize_describe_batch_load_task_request(body) {
622            Ok(v) => v,
623            Err(e) => return json_error_response(400, "ValidationException", &e),
624        };
625        if input.task_id.is_empty() {
626            return json_error_response(400, "ValidationException", "TaskId is required");
627        }
628
629        let state = state.read().await;
630        match state.describe_batch_load_task(&input.task_id) {
631            Ok(task) => {
632                let desc = batch_load_task_to_description(task);
633                wire::serialize_describe_batch_load_task_response(
634                    &wire::DescribeBatchLoadTaskResponse {
635                        batch_load_task_description: Some(desc),
636                    },
637                )
638            }
639            Err(e) => timestream_error_response(&e),
640        }
641    }
642
643    async fn handle_list_batch_load_tasks(
644        &self,
645        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
646        body: &[u8],
647    ) -> MockResponse {
648        let input = match wire::deserialize_list_batch_load_tasks_request(body) {
649            Ok(v) => v,
650            Err(e) => return json_error_response(400, "ValidationException", &e),
651        };
652        let task_status_filter = input.task_status;
653
654        let state = state.read().await;
655        let tasks = state.list_batch_load_tasks(task_status_filter.as_deref());
656
657        let task_list: Vec<wire::BatchLoadTask> = tasks
658            .iter()
659            .map(|t| wire::BatchLoadTask {
660                task_id: Some(t.task_id.clone()),
661                task_status: Some(t.task_status.clone()),
662                database_name: Some(t.database_name.clone()),
663                table_name: Some(t.table_name.clone()),
664                creation_time: Some(t.creation_time.timestamp() as f64),
665                last_updated_time: Some(t.last_updated_time.timestamp() as f64),
666                resumable_until: None,
667            })
668            .collect();
669
670        wire::serialize_list_batch_load_tasks_response(&wire::ListBatchLoadTasksResponse {
671            batch_load_tasks: Some(task_list),
672            next_token: None,
673        })
674    }
675
676    async fn handle_resume_batch_load_task(
677        &self,
678        state: &Arc<tokio::sync::RwLock<TimestreamWriteState>>,
679        body: &[u8],
680    ) -> MockResponse {
681        let input = match wire::deserialize_resume_batch_load_task_request(body) {
682            Ok(v) => v,
683            Err(e) => return json_error_response(400, "ValidationException", &e),
684        };
685        if input.task_id.is_empty() {
686            return json_error_response(400, "ValidationException", "TaskId is required");
687        }
688
689        let mut state = state.write().await;
690        match state.resume_batch_load_task(&input.task_id) {
691            Ok(()) => wire::serialize_resume_batch_load_task_response(
692                &wire::ResumeBatchLoadTaskResponse {},
693            ),
694            Err(e) => timestream_error_response(&e),
695        }
696    }
697}
698
699// --- Utility functions ---
700
701fn wire_dimension_to_state(d: wire::Dimension) -> Dimension {
702    Dimension {
703        name: d.name,
704        value: d.value,
705        dimension_value_type: d.dimension_value_type,
706    }
707}
708
709/// Convert a state `Database` to the wire `Database` model type.
710fn database_to_wire(db: &statetypes::Database) -> wire::Database {
711    wire::Database {
712        database_name: Some(db.database_name.clone()),
713        arn: Some(db.arn.clone()),
714        table_count: Some(db.table_count),
715        creation_time: Some(db.creation_time.timestamp() as f64),
716        last_updated_time: Some(db.last_updated_time.timestamp() as f64),
717        kms_key_id: db.kms_key_id.clone(),
718    }
719}
720
721/// Convert a state `Table` to the wire `Table` model type.
722fn table_to_wire(table: &statetypes::Table) -> wire::Table {
723    wire::Table {
724        table_name: Some(table.table_name.clone()),
725        database_name: Some(table.database_name.clone()),
726        arn: Some(table.arn.clone()),
727        table_status: Some(table.table_status.clone()),
728        creation_time: Some(table.creation_time.timestamp() as f64),
729        last_updated_time: Some(table.last_updated_time.timestamp() as f64),
730        retention_properties: Some(wire::RetentionProperties {
731            memory_store_retention_period_in_hours: table
732                .retention_properties
733                .memory_store_retention_period_in_hours,
734            magnetic_store_retention_period_in_days: table
735                .retention_properties
736                .magnetic_store_retention_period_in_days,
737        }),
738        magnetic_store_write_properties: Some(wire::MagneticStoreWriteProperties {
739            enable_magnetic_store_writes: table
740                .magnetic_store_write_properties
741                .enable_magnetic_store_writes,
742            ..Default::default()
743        }),
744        schema: None,
745    }
746}
747
748/// Convert a state `BatchLoadTask` to the wire `BatchLoadTaskDescription` model type.
749fn batch_load_task_to_description(
750    task: &statetypes::BatchLoadTask,
751) -> wire::BatchLoadTaskDescription {
752    let dsc = &task.data_source_configuration;
753    let data_source_configuration = Some(wire::DataSourceConfiguration {
754        data_format: dsc
755            .get("DataFormat")
756            .and_then(|v| v.as_str())
757            .unwrap_or("")
758            .to_string(),
759        data_source_s3_configuration: {
760            let s3cfg = dsc.get("DataSourceS3Configuration");
761            wire::DataSourceS3Configuration {
762                bucket_name: s3cfg
763                    .and_then(|v| v.get("BucketName"))
764                    .and_then(|v| v.as_str())
765                    .unwrap_or("")
766                    .to_string(),
767                object_key_prefix: s3cfg
768                    .and_then(|v| v.get("ObjectKeyPrefix"))
769                    .and_then(|v| v.as_str())
770                    .map(|s| s.to_string()),
771            }
772        },
773        csv_configuration: dsc
774            .get("CsvConfiguration")
775            .map(|csv| wire::CsvConfiguration {
776                column_separator: csv
777                    .get("ColumnSeparator")
778                    .and_then(|v| v.as_str())
779                    .map(|s| s.to_string()),
780                escape_char: csv
781                    .get("EscapeChar")
782                    .and_then(|v| v.as_str())
783                    .map(|s| s.to_string()),
784                null_value: csv
785                    .get("NullValue")
786                    .and_then(|v| v.as_str())
787                    .map(|s| s.to_string()),
788                quote_char: csv
789                    .get("QuoteChar")
790                    .and_then(|v| v.as_str())
791                    .map(|s| s.to_string()),
792                trim_white_space: csv.get("TrimWhiteSpace").and_then(|v| v.as_bool()),
793            }),
794    });
795
796    let rc = &task.report_configuration;
797    let report_configuration = Some(wire::ReportConfiguration {
798        report_s3_configuration: rc.get("ReportS3Configuration").map(|s3| {
799            wire::ReportS3Configuration {
800                bucket_name: s3
801                    .get("BucketName")
802                    .and_then(|v| v.as_str())
803                    .unwrap_or("")
804                    .to_string(),
805                encryption_option: s3
806                    .get("EncryptionOption")
807                    .and_then(|v| v.as_str())
808                    .map(|s| s.to_string()),
809                kms_key_id: s3
810                    .get("KmsKeyId")
811                    .and_then(|v| v.as_str())
812                    .map(|s| s.to_string()),
813                object_key_prefix: s3
814                    .get("ObjectKeyPrefix")
815                    .and_then(|v| v.as_str())
816                    .map(|s| s.to_string()),
817            }
818        }),
819    });
820
821    let data_model_configuration =
822        task.data_model_configuration
823            .as_ref()
824            .map(|dmc| wire::DataModelConfiguration {
825                data_model: dmc.get("DataModel").map(|dm| wire::DataModel {
826                    dimension_mappings: dm
827                        .get("DimensionMappings")
828                        .and_then(|v| v.as_array())
829                        .map(|arr| {
830                            arr.iter()
831                                .map(|m| wire::DimensionMapping {
832                                    source_column: m
833                                        .get("SourceColumn")
834                                        .and_then(|v| v.as_str())
835                                        .map(|s| s.to_string()),
836                                    destination_column: m
837                                        .get("DestinationColumn")
838                                        .and_then(|v| v.as_str())
839                                        .map(|s| s.to_string()),
840                                })
841                                .collect()
842                        })
843                        .unwrap_or_default(),
844                    measure_name_column: dm
845                        .get("MeasureNameColumn")
846                        .and_then(|v| v.as_str())
847                        .map(|s| s.to_string()),
848                    time_column: dm
849                        .get("TimeColumn")
850                        .and_then(|v| v.as_str())
851                        .map(|s| s.to_string()),
852                    time_unit: dm
853                        .get("TimeUnit")
854                        .and_then(|v| v.as_str())
855                        .map(|s| s.to_string()),
856                    mixed_measure_mappings: None,
857                    multi_measure_mappings: None,
858                }),
859                data_model_s3_configuration: dmc.get("DataModelS3Configuration").map(|s3| {
860                    wire::DataModelS3Configuration {
861                        bucket_name: s3
862                            .get("BucketName")
863                            .and_then(|v| v.as_str())
864                            .map(|s| s.to_string()),
865                        object_key: s3
866                            .get("ObjectKey")
867                            .and_then(|v| v.as_str())
868                            .map(|s| s.to_string()),
869                    }
870                }),
871            });
872
873    wire::BatchLoadTaskDescription {
874        task_id: Some(task.task_id.clone()),
875        task_status: Some(task.task_status.clone()),
876        target_database_name: Some(task.database_name.clone()),
877        target_table_name: Some(task.table_name.clone()),
878        data_source_configuration,
879        report_configuration,
880        data_model_configuration,
881        error_message: task.error_message.clone(),
882        creation_time: Some(task.creation_time.timestamp() as f64),
883        last_updated_time: Some(task.last_updated_time.timestamp() as f64),
884        resumable_until: None,
885        progress_report: Some(wire::BatchLoadProgressReport::default()),
886        record_version: None,
887    }
888}
889
890fn timestream_error_response(err: &TimestreamWriteError) -> MockResponse {
891    let (status, error_type) = match err {
892        TimestreamWriteError::DatabaseAlreadyExists { .. }
893        | TimestreamWriteError::TableAlreadyExists { .. } => (409u16, "ConflictException"),
894        TimestreamWriteError::DatabaseNotEmpty { .. }
895        | TimestreamWriteError::TaskCannotBeResumed { .. } => (400, "ValidationException"),
896        TimestreamWriteError::DatabaseNotFound { .. }
897        | TimestreamWriteError::TableNotFound { .. }
898        | TimestreamWriteError::ResourceNotFound { .. }
899        | TimestreamWriteError::TaskNotFound { .. } => (404, "ResourceNotFoundException"),
900    };
901    let body = json!({
902        "__type": error_type,
903        "message": err.to_string(),
904    });
905    MockResponse::json(status, body.to_string())
906}
907
908fn json_error_response(status: u16, error_type: &str, message: &str) -> MockResponse {
909    let body = json!({
910        "__type": error_type,
911        "message": message,
912    });
913    MockResponse::json(status, body.to_string())
914}