1use std::collections::BTreeMap;
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use chrono::Utc;
8use http::{Method, StatusCode};
9use serde_json::{json, Value};
10use tokio::sync::Mutex as AsyncMutex;
11
12use fakecloud_core::pagination::paginate_checked;
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
14use fakecloud_persistence::SnapshotStore;
15
16use crate::persistence::save_snapshot;
17use crate::state::{
18 cluster_arn, cluster_endpoint, gen_id, stream_arn, Cluster, EncryptionDetails,
19 MultiRegionProperties, SharedDsqlState, Stream,
20};
21
22const MAX_CLUSTERS_PER_ACCOUNT: usize = 100;
25
26pub struct DsqlService {
27 state: SharedDsqlState,
28 snapshot_store: Option<Arc<dyn SnapshotStore>>,
29 snapshot_lock: Arc<AsyncMutex<()>>,
30}
31
32enum Route {
34 CreateCluster,
35 ListClusters,
36 GetCluster(String),
37 UpdateCluster(String),
38 DeleteCluster(String),
39 GetClusterPolicy(String),
40 PutClusterPolicy(String),
41 DeleteClusterPolicy(String),
42 CreateStream(String),
43 ListStreams(String),
44 GetStream(String, String),
45 DeleteStream(String, String),
46 GetVpcEndpointServiceName(String),
47 TagResource(String),
48 UntagResource(String),
49 ListTagsForResource(String),
50}
51
52impl DsqlService {
53 pub fn new(state: SharedDsqlState) -> Self {
54 Self {
55 state,
56 snapshot_store: None,
57 snapshot_lock: Arc::new(AsyncMutex::new(())),
58 }
59 }
60
61 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
62 self.snapshot_store = Some(store);
63 self
64 }
65
66 pub fn start_ticker(&self) {
69 crate::ticker::spawn(
70 self.state.clone(),
71 self.snapshot_store.clone(),
72 self.snapshot_lock.clone(),
73 );
74 }
75
76 async fn persist(&self) {
77 save_snapshot(
78 &self.state,
79 self.snapshot_store.clone(),
80 &self.snapshot_lock,
81 )
82 .await;
83 }
84
85 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
88 let store = self.snapshot_store.clone()?;
89 let state = self.state.clone();
90 let lock = self.snapshot_lock.clone();
91 Some(Arc::new(move || {
92 let state = state.clone();
93 let store = store.clone();
94 let lock = lock.clone();
95 Box::pin(async move {
96 save_snapshot(&state, Some(store), &lock).await;
97 })
98 }))
99 }
100
101 fn resolve_route(req: &AwsRequest) -> Option<Route> {
102 let segs: Vec<&str> = req.path_segments.iter().map(|s| s.as_str()).collect();
103 match (&req.method, segs.as_slice()) {
104 (&Method::POST, ["cluster"]) => Some(Route::CreateCluster),
105 (&Method::GET, ["cluster"]) => Some(Route::ListClusters),
106 (&Method::GET, ["cluster", id]) => Some(Route::GetCluster((*id).to_string())),
107 (&Method::POST, ["cluster", id]) => Some(Route::UpdateCluster((*id).to_string())),
108 (&Method::DELETE, ["cluster", id]) => Some(Route::DeleteCluster((*id).to_string())),
109 (&Method::GET, ["cluster", id, "policy"]) => {
110 Some(Route::GetClusterPolicy((*id).to_string()))
111 }
112 (&Method::POST, ["cluster", id, "policy"]) => {
113 Some(Route::PutClusterPolicy((*id).to_string()))
114 }
115 (&Method::DELETE, ["cluster", id, "policy"]) => {
116 Some(Route::DeleteClusterPolicy((*id).to_string()))
117 }
118 (&Method::POST, ["stream", cid]) => Some(Route::CreateStream((*cid).to_string())),
119 (&Method::GET, ["stream", cid]) => Some(Route::ListStreams((*cid).to_string())),
120 (&Method::GET, ["stream", cid, sid]) => {
121 Some(Route::GetStream((*cid).to_string(), (*sid).to_string()))
122 }
123 (&Method::DELETE, ["stream", cid, sid]) => {
124 Some(Route::DeleteStream((*cid).to_string(), (*sid).to_string()))
125 }
126 (&Method::GET, ["clusters", id, "vpc-endpoint-service-name"]) => {
127 Some(Route::GetVpcEndpointServiceName((*id).to_string()))
128 }
129 (&Method::POST, ["tags", arn]) => Some(Route::TagResource(percent_decode(arn))),
130 (&Method::DELETE, ["tags", arn]) => Some(Route::UntagResource(percent_decode(arn))),
131 (&Method::GET, ["tags", arn]) => Some(Route::ListTagsForResource(percent_decode(arn))),
132 (&Method::POST, ["tags"]) => Some(Route::TagResource(String::new())),
136 (&Method::DELETE, ["tags"]) => Some(Route::UntagResource(String::new())),
137 (&Method::GET, ["tags"]) => Some(Route::ListTagsForResource(String::new())),
138 _ => None,
139 }
140 }
141
142 fn create_cluster(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
145 let body: Value = parse_body(req)?;
146 let region = &req.region;
147 let account = &req.account_id;
148
149 validate_str(&body, "clientToken", 1, 128, is_printable_ascii)?;
151 validate_str(&body, "kmsEncryptionKey", 1, 2048, is_kms_key_char)?;
152 validate_str(&body, "policy", 1, 20480, |_| true)?;
153
154 let client_token = body
155 .get("clientToken")
156 .and_then(|v| v.as_str())
157 .map(|s| s.to_string());
158
159 let deletion_protection = body
160 .get("deletionProtectionEnabled")
161 .and_then(|v| v.as_bool())
162 .unwrap_or(true);
164
165 let kms_encryption_key = body
166 .get("kmsEncryptionKey")
167 .and_then(|v| v.as_str())
168 .map(|s| s.to_string());
169
170 let multi_region = parse_multi_region(body.get("multiRegionProperties"));
171 let tags = parse_tag_map(body.get("tags"));
172
173 let mut accounts = self.state.write();
174 let st = accounts.get_or_create(account);
175
176 if let Some(token) = &client_token {
179 if let Some(existing) = st
180 .clusters
181 .values()
182 .find(|c| c.client_token.as_deref() == Some(token.as_str()))
183 {
184 return Ok(cluster_mutation_response(existing));
185 }
186 }
187
188 if st.clusters.len() >= MAX_CLUSTERS_PER_ACCOUNT {
189 drop(accounts);
190 return Err(quota_exceeded(
191 "The maximum number of clusters for this account has been reached.",
192 ));
193 }
194
195 let id = gen_id();
196 let encryption_details = build_encryption(&kms_encryption_key);
197 let cluster = Cluster {
198 identifier: id.clone(),
199 arn: cluster_arn(region, account, &id),
200 status: "CREATING".to_string(),
201 creation_time: Utc::now(),
202 deletion_protection_enabled: deletion_protection,
203 multi_region_properties: multi_region,
204 encryption_details,
205 endpoint: cluster_endpoint(&id, region),
206 tags,
207 policy: body
208 .get("policy")
209 .and_then(|v| v.as_str())
210 .map(|s| s.to_string()),
211 policy_version: body.get("policy").map(|_| 1).unwrap_or(0),
212 client_token,
213 deleted_at: None,
214 streams: BTreeMap::new(),
215 };
216 let resp = cluster_mutation_response(&cluster);
217 st.clusters.insert(id, cluster);
218 Ok(resp)
219 }
220
221 fn get_cluster(&self, req: &AwsRequest, id: &str) -> Result<AwsResponse, AwsServiceError> {
222 let accounts = self.state.read();
223 let cluster = accounts
224 .get(&req.account_id)
225 .and_then(|s| s.clusters.get(id))
226 .ok_or_else(|| cluster_not_found(id))?;
227 let mut out = json!({
228 "identifier": cluster.identifier,
229 "arn": cluster.arn,
230 "status": cluster.status,
231 "creationTime": epoch_secs(cluster),
232 "deletionProtectionEnabled": cluster.deletion_protection_enabled,
233 "tags": tag_map_json(&cluster.tags),
234 "encryptionDetails": encryption_json(&cluster.encryption_details),
235 "endpoint": cluster.endpoint,
236 });
237 if let Some(mr) = &cluster.multi_region_properties {
238 out["multiRegionProperties"] = multi_region_json(mr);
239 }
240 Ok(AwsResponse::json_value(StatusCode::OK, out))
241 }
242
243 fn update_cluster(&self, req: &AwsRequest, id: &str) -> Result<AwsResponse, AwsServiceError> {
244 let body: Value = parse_body(req)?;
245 let mut accounts = self.state.write();
246 let st = accounts
247 .get_mut(&req.account_id)
248 .ok_or_else(|| cluster_not_found(id))?;
249 let cluster = st
250 .clusters
251 .get_mut(id)
252 .ok_or_else(|| cluster_not_found(id))?;
253
254 if let Some(dp) = body
255 .get("deletionProtectionEnabled")
256 .and_then(|v| v.as_bool())
257 {
258 cluster.deletion_protection_enabled = dp;
259 }
260 if let Some(k) = body.get("kmsEncryptionKey").and_then(|v| v.as_str()) {
261 cluster.encryption_details = build_encryption(&Some(k.to_string()));
262 }
263 if let Some(mr) = parse_multi_region(body.get("multiRegionProperties")) {
264 cluster.multi_region_properties = Some(mr);
265 }
266 Ok(cluster_mutation_response(cluster))
267 }
268
269 fn delete_cluster(&self, req: &AwsRequest, id: &str) -> Result<AwsResponse, AwsServiceError> {
270 let mut accounts = self.state.write();
271 let st = accounts
272 .get_mut(&req.account_id)
273 .ok_or_else(|| cluster_not_found(id))?;
274 let cluster = st
275 .clusters
276 .get_mut(id)
277 .ok_or_else(|| cluster_not_found(id))?;
278 if cluster.deletion_protection_enabled {
279 return Err(conflict(
280 "Deletion protection is enabled for this cluster. Disable deletion protection before deleting.",
281 ));
282 }
283 cluster.status = "DELETING".to_string();
284 cluster.deleted_at = Some(Utc::now());
285 Ok(cluster_mutation_response(cluster))
286 }
287
288 fn list_clusters(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
289 let (max_results, next_token) = pagination_params(req)?;
290 let accounts = self.state.read();
291 let mut summaries: Vec<Value> = accounts
292 .get(&req.account_id)
293 .map(|s| {
294 s.clusters
295 .values()
296 .map(|c| json!({ "identifier": c.identifier, "arn": c.arn }))
297 .collect()
298 })
299 .unwrap_or_default();
300 summaries.sort_by(|a, b| a["identifier"].as_str().cmp(&b["identifier"].as_str()));
301 let (page, token) = paginate_lenient(&summaries, next_token.as_deref(), max_results);
302 let mut out = json!({ "clusters": page });
303 if let Some(t) = token {
304 out["nextToken"] = json!(t);
305 }
306 Ok(AwsResponse::json_value(StatusCode::OK, out))
307 }
308
309 fn get_vpc_endpoint_service_name(
310 &self,
311 req: &AwsRequest,
312 id: &str,
313 ) -> Result<AwsResponse, AwsServiceError> {
314 let accounts = self.state.read();
315 let _cluster = accounts
316 .get(&req.account_id)
317 .and_then(|s| s.clusters.get(id))
318 .ok_or_else(|| cluster_not_found(id))?;
319 let suffix = hex6(id);
320 let service_name = format!("com.amazonaws.{}.dsql-{}", req.region, suffix);
321 Ok(AwsResponse::json_value(
322 StatusCode::OK,
323 json!({ "serviceName": service_name }),
324 ))
325 }
326
327 fn put_cluster_policy(
339 &self,
340 req: &AwsRequest,
341 id: &str,
342 ) -> Result<AwsResponse, AwsServiceError> {
343 let body: Value = parse_body(req)?;
344 let policy = body
345 .get("policy")
346 .and_then(|v| v.as_str())
347 .ok_or_else(|| validation("policy is required."))?
348 .to_string();
349 if policy.is_empty() {
350 return Err(validation("policy must not be empty."));
351 }
352 let expected = body
353 .get("expectedPolicyVersion")
354 .and_then(|v| v.as_str())
355 .and_then(|s| s.parse::<u64>().ok());
356
357 let mut accounts = self.state.write();
358 let st = accounts
359 .get_mut(&req.account_id)
360 .ok_or_else(|| cluster_not_found(id))?;
361 let cluster = st
362 .clusters
363 .get_mut(id)
364 .ok_or_else(|| cluster_not_found(id))?;
365 if let Some(exp) = expected {
366 if exp != cluster.policy_version {
367 return Err(conflict(
368 "The expectedPolicyVersion does not match the current policy version.",
369 ));
370 }
371 }
372 cluster.policy = Some(policy);
373 cluster.policy_version += 1;
374 let version = cluster.policy_version.to_string();
375 Ok(AwsResponse::json_value(
376 StatusCode::OK,
377 json!({ "policyVersion": version }),
378 ))
379 }
380
381 fn get_cluster_policy(
382 &self,
383 req: &AwsRequest,
384 id: &str,
385 ) -> Result<AwsResponse, AwsServiceError> {
386 let accounts = self.state.read();
387 let cluster = accounts
388 .get(&req.account_id)
389 .and_then(|s| s.clusters.get(id))
390 .ok_or_else(|| cluster_not_found(id))?;
391 let policy = cluster
392 .policy
393 .as_ref()
394 .ok_or_else(|| resource_not_found("No policy is attached to this cluster."))?;
395 Ok(AwsResponse::json_value(
396 StatusCode::OK,
397 json!({ "policy": policy, "policyVersion": cluster.policy_version.to_string() }),
398 ))
399 }
400
401 fn delete_cluster_policy(
402 &self,
403 req: &AwsRequest,
404 id: &str,
405 ) -> Result<AwsResponse, AwsServiceError> {
406 let expected = req
407 .query_params
408 .get("expected-policy-version")
409 .and_then(|s| s.parse::<u64>().ok());
410 let mut accounts = self.state.write();
411 let st = accounts
412 .get_mut(&req.account_id)
413 .ok_or_else(|| cluster_not_found(id))?;
414 let cluster = st
415 .clusters
416 .get_mut(id)
417 .ok_or_else(|| cluster_not_found(id))?;
418 if let Some(exp) = expected {
419 if exp != cluster.policy_version {
420 return Err(conflict(
421 "The expectedPolicyVersion does not match the current policy version.",
422 ));
423 }
424 }
425 cluster.policy = None;
426 cluster.policy_version += 1;
427 let version = cluster.policy_version.to_string();
428 Ok(AwsResponse::json_value(
429 StatusCode::OK,
430 json!({ "policyVersion": version }),
431 ))
432 }
433
434 fn create_stream(
437 &self,
438 req: &AwsRequest,
439 cluster_id: &str,
440 ) -> Result<AwsResponse, AwsServiceError> {
441 let body: Value = parse_body(req)?;
442 let target = body
443 .get("targetDefinition")
444 .cloned()
445 .ok_or_else(|| validation("targetDefinition is required."))?;
446 let ordering = body
447 .get("ordering")
448 .and_then(|v| v.as_str())
449 .ok_or_else(|| validation("ordering is required."))?
450 .to_string();
451 let format = body
452 .get("format")
453 .and_then(|v| v.as_str())
454 .ok_or_else(|| validation("format is required."))?
455 .to_string();
456 let client_token = body
457 .get("clientToken")
458 .and_then(|v| v.as_str())
459 .map(|s| s.to_string());
460 let tags = parse_tag_map(body.get("tags"));
461
462 let region = req.region.clone();
463 let account = req.account_id.clone();
464 let mut accounts = self.state.write();
465 let st = accounts
466 .get_mut(&account)
467 .ok_or_else(|| cluster_not_found(cluster_id))?;
468 let cluster = st
469 .clusters
470 .get_mut(cluster_id)
471 .ok_or_else(|| cluster_not_found(cluster_id))?;
472
473 if let Some(token) = &client_token {
474 if let Some(existing) = cluster
475 .streams
476 .values()
477 .find(|s| s.client_token.as_deref() == Some(token.as_str()))
478 {
479 return Ok(stream_mutation_response(existing));
480 }
481 }
482
483 let sid = gen_id();
484 let stream = Stream {
485 cluster_identifier: cluster_id.to_string(),
486 stream_identifier: sid.clone(),
487 arn: stream_arn(®ion, &account, cluster_id, &sid),
488 status: "CREATING".to_string(),
489 creation_time: Utc::now(),
490 ordering,
491 format,
492 target_definition: target,
493 status_reason: None,
494 tags,
495 client_token,
496 deleted_at: None,
497 };
498 let resp = stream_mutation_response(&stream);
499 cluster.streams.insert(sid, stream);
500 Ok(resp)
501 }
502
503 fn get_stream(
504 &self,
505 req: &AwsRequest,
506 cluster_id: &str,
507 stream_id: &str,
508 ) -> Result<AwsResponse, AwsServiceError> {
509 let accounts = self.state.read();
510 let stream = accounts
511 .get(&req.account_id)
512 .and_then(|s| s.clusters.get(cluster_id))
513 .and_then(|c| c.streams.get(stream_id))
514 .ok_or_else(|| stream_not_found(stream_id))?;
515 let mut out = stream_full_json(stream);
516 out["tags"] = tag_map_json(&stream.tags);
517 Ok(AwsResponse::json_value(StatusCode::OK, out))
518 }
519
520 fn delete_stream(
521 &self,
522 req: &AwsRequest,
523 cluster_id: &str,
524 stream_id: &str,
525 ) -> Result<AwsResponse, AwsServiceError> {
526 let mut accounts = self.state.write();
527 let st = accounts
528 .get_mut(&req.account_id)
529 .ok_or_else(|| stream_not_found(stream_id))?;
530 let cluster = st
531 .clusters
532 .get_mut(cluster_id)
533 .ok_or_else(|| cluster_not_found(cluster_id))?;
534 let stream = cluster
535 .streams
536 .get_mut(stream_id)
537 .ok_or_else(|| stream_not_found(stream_id))?;
538 stream.status = "DELETING".to_string();
542 stream.deleted_at = Some(Utc::now());
543 let resp = json!({
544 "clusterIdentifier": stream.cluster_identifier,
545 "streamIdentifier": stream.stream_identifier,
546 "arn": stream.arn,
547 "status": stream.status,
548 "creationTime": stream.creation_time.timestamp() as f64
549 + stream.creation_time.timestamp_subsec_millis() as f64 / 1000.0,
550 });
551 Ok(AwsResponse::json_value(StatusCode::OK, resp))
552 }
553
554 fn list_streams(
555 &self,
556 req: &AwsRequest,
557 cluster_id: &str,
558 ) -> Result<AwsResponse, AwsServiceError> {
559 let (max_results, next_token) = pagination_params(req)?;
560 let accounts = self.state.read();
561 let cluster = accounts
562 .get(&req.account_id)
563 .and_then(|s| s.clusters.get(cluster_id))
564 .ok_or_else(|| cluster_not_found(cluster_id))?;
565 let mut summaries: Vec<Value> = cluster
566 .streams
567 .values()
568 .map(|s| {
569 json!({
570 "clusterIdentifier": s.cluster_identifier,
571 "streamIdentifier": s.stream_identifier,
572 "arn": s.arn,
573 "status": s.status,
574 "creationTime": s.creation_time.timestamp() as f64
575 + s.creation_time.timestamp_subsec_millis() as f64 / 1000.0,
576 })
577 })
578 .collect();
579 summaries.sort_by(|a, b| {
580 a["streamIdentifier"]
581 .as_str()
582 .cmp(&b["streamIdentifier"].as_str())
583 });
584 let (page, token) = paginate_lenient(&summaries, next_token.as_deref(), max_results);
585 let mut out = json!({ "streams": page });
586 if let Some(t) = token {
587 out["nextToken"] = json!(t);
588 }
589 Ok(AwsResponse::json_value(StatusCode::OK, out))
590 }
591
592 fn tag_resource(&self, req: &AwsRequest, arn: &str) -> Result<AwsResponse, AwsServiceError> {
595 require_resource_arn(arn)?;
596 let body: Value = parse_body(req)?;
597 let new_tags = parse_tag_map(body.get("tags"));
598 let mut accounts = self.state.write();
599 let st = accounts
600 .get_mut(&req.account_id)
601 .ok_or_else(|| resource_not_found_arn(arn))?;
602 let tags = resolve_tags_mut(st, arn).ok_or_else(|| resource_not_found_arn(arn))?;
603 for (k, v) in new_tags {
604 tags.insert(k, v);
605 }
606 Ok(AwsResponse::json_value(StatusCode::OK, json!({})))
607 }
608
609 fn untag_resource(&self, req: &AwsRequest, arn: &str) -> Result<AwsResponse, AwsServiceError> {
610 require_resource_arn(arn)?;
611 let keys: Vec<String> = req
615 .raw_query
616 .split('&')
617 .filter_map(|pair| pair.strip_prefix("tagKeys="))
618 .map(percent_decode)
619 .collect();
620 let mut accounts = self.state.write();
621 let st = accounts
622 .get_mut(&req.account_id)
623 .ok_or_else(|| resource_not_found_arn(arn))?;
624 let tags = resolve_tags_mut(st, arn).ok_or_else(|| resource_not_found_arn(arn))?;
625 for k in keys {
626 tags.remove(&k);
627 }
628 Ok(AwsResponse::json_value(StatusCode::OK, json!({})))
629 }
630
631 fn list_tags_for_resource(
632 &self,
633 req: &AwsRequest,
634 arn: &str,
635 ) -> Result<AwsResponse, AwsServiceError> {
636 require_resource_arn(arn)?;
637 let accounts = self.state.read();
638 let st = accounts
639 .get(&req.account_id)
640 .ok_or_else(|| resource_not_found_arn(arn))?;
641 let tags = resolve_tags_ref(st, arn).ok_or_else(|| resource_not_found_arn(arn))?;
642 Ok(AwsResponse::json_value(
643 StatusCode::OK,
644 json!({ "tags": tag_map_json(tags) }),
645 ))
646 }
647}
648
649pub const DSQL_ACTIONS: &[&str] = &[
652 "CreateCluster",
653 "GetCluster",
654 "UpdateCluster",
655 "DeleteCluster",
656 "ListClusters",
657 "GetClusterPolicy",
658 "PutClusterPolicy",
659 "DeleteClusterPolicy",
660 "CreateStream",
661 "GetStream",
662 "DeleteStream",
663 "ListStreams",
664 "GetVpcEndpointServiceName",
665 "TagResource",
666 "UntagResource",
667 "ListTagsForResource",
668];
669
670impl Route {
671 fn mutates(&self) -> bool {
673 matches!(
674 self,
675 Route::CreateCluster
676 | Route::UpdateCluster(_)
677 | Route::DeleteCluster(_)
678 | Route::PutClusterPolicy(_)
679 | Route::DeleteClusterPolicy(_)
680 | Route::CreateStream(_)
681 | Route::DeleteStream(_, _)
682 | Route::TagResource(_)
683 | Route::UntagResource(_)
684 )
685 }
686}
687
688#[async_trait]
689impl AwsService for DsqlService {
690 fn service_name(&self) -> &str {
691 "dsql"
692 }
693
694 fn supported_actions(&self) -> &[&str] {
695 DSQL_ACTIONS
696 }
697
698 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
699 let Some(route) = Self::resolve_route(&req) else {
700 return Err(AwsServiceError::aws_error(
701 StatusCode::NOT_FOUND,
702 "UnknownOperationException",
703 format!("Unknown operation: {} {}", req.method, req.raw_path),
704 ));
705 };
706 let mutates = route.mutates();
707 let result = match route {
708 Route::CreateCluster => self.create_cluster(&req),
709 Route::ListClusters => self.list_clusters(&req),
710 Route::GetCluster(id) => self.get_cluster(&req, &id),
711 Route::UpdateCluster(id) => self.update_cluster(&req, &id),
712 Route::DeleteCluster(id) => self.delete_cluster(&req, &id),
713 Route::GetClusterPolicy(id) => self.get_cluster_policy(&req, &id),
714 Route::PutClusterPolicy(id) => self.put_cluster_policy(&req, &id),
715 Route::DeleteClusterPolicy(id) => self.delete_cluster_policy(&req, &id),
716 Route::CreateStream(cid) => self.create_stream(&req, &cid),
717 Route::ListStreams(cid) => self.list_streams(&req, &cid),
718 Route::GetStream(cid, sid) => self.get_stream(&req, &cid, &sid),
719 Route::DeleteStream(cid, sid) => self.delete_stream(&req, &cid, &sid),
720 Route::GetVpcEndpointServiceName(id) => self.get_vpc_endpoint_service_name(&req, &id),
721 Route::TagResource(arn) => self.tag_resource(&req, &arn),
722 Route::UntagResource(arn) => self.untag_resource(&req, &arn),
723 Route::ListTagsForResource(arn) => self.list_tags_for_resource(&req, &arn),
724 };
725 if mutates && result.is_ok() {
728 self.persist().await;
729 }
730 result
731 }
732}
733
734fn cluster_mutation_response(cluster: &Cluster) -> AwsResponse {
740 let mut out = json!({
741 "identifier": cluster.identifier,
742 "arn": cluster.arn,
743 "status": cluster.status,
744 "creationTime": epoch_secs(cluster),
745 "deletionProtectionEnabled": cluster.deletion_protection_enabled,
746 });
747 if let Some(mr) = &cluster.multi_region_properties {
748 out["multiRegionProperties"] = multi_region_json(mr);
749 }
750 out["encryptionDetails"] = encryption_json(&cluster.encryption_details);
751 if !cluster.endpoint.is_empty() {
752 out["endpoint"] = json!(cluster.endpoint);
753 }
754 AwsResponse::json_value(StatusCode::OK, out)
755}
756
757fn stream_mutation_response(stream: &Stream) -> AwsResponse {
758 AwsResponse::json_value(StatusCode::OK, stream_core_json(stream))
759}
760
761fn stream_core_json(stream: &Stream) -> Value {
762 json!({
763 "clusterIdentifier": stream.cluster_identifier,
764 "streamIdentifier": stream.stream_identifier,
765 "arn": stream.arn,
766 "status": stream.status,
767 "creationTime": stream.creation_time.timestamp() as f64
768 + stream.creation_time.timestamp_subsec_millis() as f64 / 1000.0,
769 "ordering": stream.ordering,
770 "format": stream.format,
771 })
772}
773
774fn stream_full_json(stream: &Stream) -> Value {
775 let mut out = stream_core_json(stream);
776 out["targetDefinition"] = stream.target_definition.clone();
777 if let Some(reason) = &stream.status_reason {
778 out["statusReason"] = json!(reason);
779 }
780 out
781}
782
783fn epoch_secs(cluster: &Cluster) -> f64 {
784 cluster.creation_time.timestamp() as f64
785 + cluster.creation_time.timestamp_subsec_millis() as f64 / 1000.0
786}
787
788fn encryption_json(e: &EncryptionDetails) -> Value {
789 let mut out = json!({
790 "encryptionType": e.encryption_type,
791 "encryptionStatus": e.encryption_status,
792 });
793 if let Some(arn) = &e.kms_key_arn {
794 out["kmsKeyArn"] = json!(arn);
795 }
796 out
797}
798
799fn multi_region_json(mr: &MultiRegionProperties) -> Value {
800 let mut out = json!({ "clusters": mr.clusters });
801 if let Some(w) = &mr.witness_region {
802 out["witnessRegion"] = json!(w);
803 }
804 out
805}
806
807fn tag_map_json(tags: &BTreeMap<String, String>) -> Value {
808 Value::Object(
809 tags.iter()
810 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
811 .collect(),
812 )
813}
814
815fn validate_str(
823 body: &Value,
824 field: &str,
825 min: usize,
826 max: usize,
827 charset_ok: impl Fn(char) -> bool,
828) -> Result<(), AwsServiceError> {
829 if let Some(s) = body.get(field).and_then(|v| v.as_str()) {
830 if s.len() < min || s.len() > max {
831 return Err(validation(&format!(
832 "Value at '{field}' failed to satisfy constraint: Member must have length between {min} and {max}."
833 )));
834 }
835 if !s.chars().all(&charset_ok) {
836 return Err(validation(&format!(
837 "Value at '{field}' failed to satisfy constraint: Member must satisfy the required pattern."
838 )));
839 }
840 }
841 Ok(())
842}
843
844fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
849 if req.body.is_empty() {
850 return Ok(json!({}));
851 }
852 serde_json::from_slice(&req.body).map_err(|e| validation(&format!("Invalid request body: {e}")))
853}
854
855fn require_resource_arn(arn: &str) -> Result<(), AwsServiceError> {
858 if arn.trim().is_empty() {
859 return Err(validation(
860 "Value at 'resourceArn' failed to satisfy constraint: Member must not be empty.",
861 ));
862 }
863 Ok(())
864}
865
866fn is_printable_ascii(c: char) -> bool {
868 ('!'..='~').contains(&c)
869}
870
871fn is_kms_key_char(c: char) -> bool {
873 c.is_ascii_alphanumeric() || matches!(c, ':' | '/' | '_' | '-')
874}
875
876fn parse_tag_map(v: Option<&Value>) -> BTreeMap<String, String> {
877 let mut out = BTreeMap::new();
878 if let Some(Value::Object(map)) = v {
879 for (k, val) in map {
880 if let Some(s) = val.as_str() {
881 out.insert(k.clone(), s.to_string());
882 }
883 }
884 }
885 out
886}
887
888fn parse_multi_region(v: Option<&Value>) -> Option<MultiRegionProperties> {
889 let obj = v?.as_object()?;
890 Some(MultiRegionProperties {
891 witness_region: obj
892 .get("witnessRegion")
893 .and_then(|v| v.as_str())
894 .map(|s| s.to_string()),
895 clusters: obj
896 .get("clusters")
897 .and_then(|v| v.as_array())
898 .map(|a| {
899 a.iter()
900 .filter_map(|c| c.as_str().map(String::from))
901 .collect()
902 })
903 .unwrap_or_default(),
904 })
905}
906
907fn build_encryption(kms_key: &Option<String>) -> EncryptionDetails {
908 match kms_key {
909 Some(k) => EncryptionDetails {
910 encryption_type: "CUSTOMER_MANAGED_KMS_KEY".to_string(),
911 kms_key_arn: Some(k.clone()),
912 encryption_status: "ENABLED".to_string(),
913 },
914 None => EncryptionDetails {
915 encryption_type: "AWS_OWNED_KMS_KEY".to_string(),
916 kms_key_arn: None,
917 encryption_status: "ENABLED".to_string(),
918 },
919 }
920}
921
922fn paginate_lenient(
926 items: &[Value],
927 next_token: Option<&str>,
928 max_results: usize,
929) -> (Vec<Value>, Option<String>) {
930 match paginate_checked(items, next_token, max_results) {
931 Ok(res) => res,
932 Err(_) => paginate_checked(items, None, max_results)
933 .expect("pagination from the start is always valid"),
934 }
935}
936
937fn pagination_params(req: &AwsRequest) -> Result<(usize, Option<String>), AwsServiceError> {
944 let max = match req.query_params.get("max-results") {
945 Some(raw) => {
946 let n = raw.parse::<i64>().map_err(|_| {
947 validation("Value for 'maxResults' must be an integer between 1 and 100.")
948 })?;
949 if !(1..=100).contains(&n) {
950 return Err(validation(
951 "Value for 'maxResults' must be between 1 and 100.",
952 ));
953 }
954 n as usize
955 }
956 None => 100,
957 };
958 let token = req.query_params.get("next-token").cloned();
959 Ok((max, token))
960}
961
962fn resolve_tags_mut<'a>(
964 st: &'a mut crate::state::DsqlState,
965 arn: &str,
966) -> Option<&'a mut BTreeMap<String, String>> {
967 let (cluster_id, stream_id) = parse_resource_arn(arn)?;
968 let cluster = st.clusters.get_mut(&cluster_id)?;
969 match stream_id {
970 Some(sid) => cluster.streams.get_mut(&sid).map(|s| &mut s.tags),
971 None => Some(&mut cluster.tags),
972 }
973}
974
975fn resolve_tags_ref<'a>(
976 st: &'a crate::state::DsqlState,
977 arn: &str,
978) -> Option<&'a BTreeMap<String, String>> {
979 let (cluster_id, stream_id) = parse_resource_arn(arn)?;
980 let cluster = st.clusters.get(&cluster_id)?;
981 match stream_id {
982 Some(sid) => cluster.streams.get(&sid).map(|s| &s.tags),
983 None => Some(&cluster.tags),
984 }
985}
986
987fn parse_resource_arn(arn: &str) -> Option<(String, Option<String>)> {
992 let fields: Vec<&str> = arn.splitn(6, ':').collect();
994 if fields.len() != 6 || fields[0] != "arn" || fields[2] != "dsql" {
995 return None;
996 }
997 let parts: Vec<&str> = fields[5].split('/').collect();
998 match parts.as_slice() {
999 ["cluster", cid] => Some(((*cid).to_string(), None)),
1000 ["cluster", cid, "stream", sid] => Some(((*cid).to_string(), Some((*sid).to_string()))),
1001 _ => None,
1002 }
1003}
1004
1005fn hex6(id: &str) -> String {
1007 let mut h: u32 = 2166136261;
1008 for b in id.bytes() {
1009 h ^= b as u32;
1010 h = h.wrapping_mul(16777619);
1011 }
1012 format!("{:06x}", h & 0x00ff_ffff)
1013}
1014
1015fn percent_decode(s: &str) -> String {
1016 percent_encoding::percent_decode_str(s)
1017 .decode_utf8_lossy()
1018 .to_string()
1019}
1020
1021fn validation(msg: &str) -> AwsServiceError {
1026 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg)
1027}
1028
1029fn conflict(msg: &str) -> AwsServiceError {
1030 AwsServiceError::aws_error(StatusCode::CONFLICT, "ConflictException", msg)
1031}
1032
1033fn quota_exceeded(msg: &str) -> AwsServiceError {
1034 AwsServiceError::aws_error(
1035 StatusCode::PAYMENT_REQUIRED,
1036 "ServiceQuotaExceededException",
1037 msg,
1038 )
1039}
1040
1041fn resource_not_found(msg: &str) -> AwsServiceError {
1042 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFoundException", msg)
1043}
1044
1045fn cluster_not_found(id: &str) -> AwsServiceError {
1046 resource_not_found(&format!("Cluster not found: {id}"))
1047}
1048
1049fn stream_not_found(id: &str) -> AwsServiceError {
1050 resource_not_found(&format!("Stream not found: {id}"))
1051}
1052
1053fn resource_not_found_arn(arn: &str) -> AwsServiceError {
1054 resource_not_found(&format!("Resource not found: {arn}"))
1055}
1056
1057#[cfg(test)]
1058mod tests {
1059 use super::*;
1060
1061 #[test]
1062 fn parse_resource_arn_accepts_cluster_and_stream() {
1063 assert_eq!(
1064 parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:cluster/abc"),
1065 Some(("abc".to_string(), None))
1066 );
1067 assert_eq!(
1068 parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:cluster/abc/stream/xyz"),
1069 Some(("abc".to_string(), Some("xyz".to_string())))
1070 );
1071 }
1072
1073 #[test]
1074 fn parse_resource_arn_rejects_wrong_service_or_malformed() {
1075 assert!(parse_resource_arn("arn:aws:rds:us-east-1:123456789012:cluster/abc").is_none());
1077 assert!(parse_resource_arn("cluster/abc").is_none());
1079 assert!(parse_resource_arn("xrn:aws:dsql:us-east-1:123456789012:cluster/abc").is_none());
1081 assert!(parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:widget/abc").is_none());
1083 }
1084
1085 #[test]
1086 fn hex6_is_six_hex_chars() {
1087 let h = hex6("abcdefghij0123456789klmnop");
1088 assert_eq!(h.len(), 6);
1089 assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
1090 }
1091}