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
18pub 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 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, ®ion);
83
84 let response = match action.as_str() {
85 "CreateDatabase" => {
86 self.handle_create_database(&state, body_bytes, account_id, ®ion)
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, ®ion)
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(®ion).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, ®ion).await;
121 }
122 response
123 }
124
125 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 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 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 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 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 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 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 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
699fn 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
709fn 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
721fn 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
748fn 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}