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
612 .query_params
613 .get("tagKeys")
614 .map(|v| v.split(',').map(|s| s.to_string()).collect())
615 .unwrap_or_default();
616 let mut accounts = self.state.write();
617 let st = accounts
618 .get_mut(&req.account_id)
619 .ok_or_else(|| resource_not_found_arn(arn))?;
620 let tags = resolve_tags_mut(st, arn).ok_or_else(|| resource_not_found_arn(arn))?;
621 for k in keys {
622 tags.remove(&k);
623 }
624 Ok(AwsResponse::json_value(StatusCode::OK, json!({})))
625 }
626
627 fn list_tags_for_resource(
628 &self,
629 req: &AwsRequest,
630 arn: &str,
631 ) -> Result<AwsResponse, AwsServiceError> {
632 require_resource_arn(arn)?;
633 let accounts = self.state.read();
634 let st = accounts
635 .get(&req.account_id)
636 .ok_or_else(|| resource_not_found_arn(arn))?;
637 let tags = resolve_tags_ref(st, arn).ok_or_else(|| resource_not_found_arn(arn))?;
638 Ok(AwsResponse::json_value(
639 StatusCode::OK,
640 json!({ "tags": tag_map_json(tags) }),
641 ))
642 }
643}
644
645pub const DSQL_ACTIONS: &[&str] = &[
648 "CreateCluster",
649 "GetCluster",
650 "UpdateCluster",
651 "DeleteCluster",
652 "ListClusters",
653 "GetClusterPolicy",
654 "PutClusterPolicy",
655 "DeleteClusterPolicy",
656 "CreateStream",
657 "GetStream",
658 "DeleteStream",
659 "ListStreams",
660 "GetVpcEndpointServiceName",
661 "TagResource",
662 "UntagResource",
663 "ListTagsForResource",
664];
665
666impl Route {
667 fn mutates(&self) -> bool {
669 matches!(
670 self,
671 Route::CreateCluster
672 | Route::UpdateCluster(_)
673 | Route::DeleteCluster(_)
674 | Route::PutClusterPolicy(_)
675 | Route::DeleteClusterPolicy(_)
676 | Route::CreateStream(_)
677 | Route::DeleteStream(_, _)
678 | Route::TagResource(_)
679 | Route::UntagResource(_)
680 )
681 }
682}
683
684#[async_trait]
685impl AwsService for DsqlService {
686 fn service_name(&self) -> &str {
687 "dsql"
688 }
689
690 fn supported_actions(&self) -> &[&str] {
691 DSQL_ACTIONS
692 }
693
694 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
695 let Some(route) = Self::resolve_route(&req) else {
696 return Err(AwsServiceError::aws_error(
697 StatusCode::NOT_FOUND,
698 "UnknownOperationException",
699 format!("Unknown operation: {} {}", req.method, req.raw_path),
700 ));
701 };
702 let mutates = route.mutates();
703 let result = match route {
704 Route::CreateCluster => self.create_cluster(&req),
705 Route::ListClusters => self.list_clusters(&req),
706 Route::GetCluster(id) => self.get_cluster(&req, &id),
707 Route::UpdateCluster(id) => self.update_cluster(&req, &id),
708 Route::DeleteCluster(id) => self.delete_cluster(&req, &id),
709 Route::GetClusterPolicy(id) => self.get_cluster_policy(&req, &id),
710 Route::PutClusterPolicy(id) => self.put_cluster_policy(&req, &id),
711 Route::DeleteClusterPolicy(id) => self.delete_cluster_policy(&req, &id),
712 Route::CreateStream(cid) => self.create_stream(&req, &cid),
713 Route::ListStreams(cid) => self.list_streams(&req, &cid),
714 Route::GetStream(cid, sid) => self.get_stream(&req, &cid, &sid),
715 Route::DeleteStream(cid, sid) => self.delete_stream(&req, &cid, &sid),
716 Route::GetVpcEndpointServiceName(id) => self.get_vpc_endpoint_service_name(&req, &id),
717 Route::TagResource(arn) => self.tag_resource(&req, &arn),
718 Route::UntagResource(arn) => self.untag_resource(&req, &arn),
719 Route::ListTagsForResource(arn) => self.list_tags_for_resource(&req, &arn),
720 };
721 if mutates && result.is_ok() {
724 self.persist().await;
725 }
726 result
727 }
728}
729
730fn cluster_mutation_response(cluster: &Cluster) -> AwsResponse {
736 let mut out = json!({
737 "identifier": cluster.identifier,
738 "arn": cluster.arn,
739 "status": cluster.status,
740 "creationTime": epoch_secs(cluster),
741 "deletionProtectionEnabled": cluster.deletion_protection_enabled,
742 });
743 if let Some(mr) = &cluster.multi_region_properties {
744 out["multiRegionProperties"] = multi_region_json(mr);
745 }
746 out["encryptionDetails"] = encryption_json(&cluster.encryption_details);
747 if !cluster.endpoint.is_empty() {
748 out["endpoint"] = json!(cluster.endpoint);
749 }
750 AwsResponse::json_value(StatusCode::OK, out)
751}
752
753fn stream_mutation_response(stream: &Stream) -> AwsResponse {
754 AwsResponse::json_value(StatusCode::OK, stream_core_json(stream))
755}
756
757fn stream_core_json(stream: &Stream) -> Value {
758 json!({
759 "clusterIdentifier": stream.cluster_identifier,
760 "streamIdentifier": stream.stream_identifier,
761 "arn": stream.arn,
762 "status": stream.status,
763 "creationTime": stream.creation_time.timestamp() as f64
764 + stream.creation_time.timestamp_subsec_millis() as f64 / 1000.0,
765 "ordering": stream.ordering,
766 "format": stream.format,
767 })
768}
769
770fn stream_full_json(stream: &Stream) -> Value {
771 let mut out = stream_core_json(stream);
772 out["targetDefinition"] = stream.target_definition.clone();
773 if let Some(reason) = &stream.status_reason {
774 out["statusReason"] = json!(reason);
775 }
776 out
777}
778
779fn epoch_secs(cluster: &Cluster) -> f64 {
780 cluster.creation_time.timestamp() as f64
781 + cluster.creation_time.timestamp_subsec_millis() as f64 / 1000.0
782}
783
784fn encryption_json(e: &EncryptionDetails) -> Value {
785 let mut out = json!({
786 "encryptionType": e.encryption_type,
787 "encryptionStatus": e.encryption_status,
788 });
789 if let Some(arn) = &e.kms_key_arn {
790 out["kmsKeyArn"] = json!(arn);
791 }
792 out
793}
794
795fn multi_region_json(mr: &MultiRegionProperties) -> Value {
796 let mut out = json!({ "clusters": mr.clusters });
797 if let Some(w) = &mr.witness_region {
798 out["witnessRegion"] = json!(w);
799 }
800 out
801}
802
803fn tag_map_json(tags: &BTreeMap<String, String>) -> Value {
804 Value::Object(
805 tags.iter()
806 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
807 .collect(),
808 )
809}
810
811fn validate_str(
819 body: &Value,
820 field: &str,
821 min: usize,
822 max: usize,
823 charset_ok: impl Fn(char) -> bool,
824) -> Result<(), AwsServiceError> {
825 if let Some(s) = body.get(field).and_then(|v| v.as_str()) {
826 if s.len() < min || s.len() > max {
827 return Err(validation(&format!(
828 "Value at '{field}' failed to satisfy constraint: Member must have length between {min} and {max}."
829 )));
830 }
831 if !s.chars().all(&charset_ok) {
832 return Err(validation(&format!(
833 "Value at '{field}' failed to satisfy constraint: Member must satisfy the required pattern."
834 )));
835 }
836 }
837 Ok(())
838}
839
840fn parse_body(req: &AwsRequest) -> Result<Value, AwsServiceError> {
845 if req.body.is_empty() {
846 return Ok(json!({}));
847 }
848 serde_json::from_slice(&req.body).map_err(|e| validation(&format!("Invalid request body: {e}")))
849}
850
851fn require_resource_arn(arn: &str) -> Result<(), AwsServiceError> {
854 if arn.trim().is_empty() {
855 return Err(validation(
856 "Value at 'resourceArn' failed to satisfy constraint: Member must not be empty.",
857 ));
858 }
859 Ok(())
860}
861
862fn is_printable_ascii(c: char) -> bool {
864 ('!'..='~').contains(&c)
865}
866
867fn is_kms_key_char(c: char) -> bool {
869 c.is_ascii_alphanumeric() || matches!(c, ':' | '/' | '_' | '-')
870}
871
872fn parse_tag_map(v: Option<&Value>) -> BTreeMap<String, String> {
873 let mut out = BTreeMap::new();
874 if let Some(Value::Object(map)) = v {
875 for (k, val) in map {
876 if let Some(s) = val.as_str() {
877 out.insert(k.clone(), s.to_string());
878 }
879 }
880 }
881 out
882}
883
884fn parse_multi_region(v: Option<&Value>) -> Option<MultiRegionProperties> {
885 let obj = v?.as_object()?;
886 Some(MultiRegionProperties {
887 witness_region: obj
888 .get("witnessRegion")
889 .and_then(|v| v.as_str())
890 .map(|s| s.to_string()),
891 clusters: obj
892 .get("clusters")
893 .and_then(|v| v.as_array())
894 .map(|a| {
895 a.iter()
896 .filter_map(|c| c.as_str().map(String::from))
897 .collect()
898 })
899 .unwrap_or_default(),
900 })
901}
902
903fn build_encryption(kms_key: &Option<String>) -> EncryptionDetails {
904 match kms_key {
905 Some(k) => EncryptionDetails {
906 encryption_type: "CUSTOMER_MANAGED_KMS_KEY".to_string(),
907 kms_key_arn: Some(k.clone()),
908 encryption_status: "ENABLED".to_string(),
909 },
910 None => EncryptionDetails {
911 encryption_type: "AWS_OWNED_KMS_KEY".to_string(),
912 kms_key_arn: None,
913 encryption_status: "ENABLED".to_string(),
914 },
915 }
916}
917
918fn paginate_lenient(
922 items: &[Value],
923 next_token: Option<&str>,
924 max_results: usize,
925) -> (Vec<Value>, Option<String>) {
926 match paginate_checked(items, next_token, max_results) {
927 Ok(res) => res,
928 Err(_) => paginate_checked(items, None, max_results)
929 .expect("pagination from the start is always valid"),
930 }
931}
932
933fn pagination_params(req: &AwsRequest) -> Result<(usize, Option<String>), AwsServiceError> {
940 let max = match req.query_params.get("max-results") {
941 Some(raw) => {
942 let n = raw.parse::<i64>().map_err(|_| {
943 validation("Value for 'maxResults' must be an integer between 1 and 100.")
944 })?;
945 if !(1..=100).contains(&n) {
946 return Err(validation(
947 "Value for 'maxResults' must be between 1 and 100.",
948 ));
949 }
950 n as usize
951 }
952 None => 100,
953 };
954 let token = req.query_params.get("next-token").cloned();
955 Ok((max, token))
956}
957
958fn resolve_tags_mut<'a>(
960 st: &'a mut crate::state::DsqlState,
961 arn: &str,
962) -> Option<&'a mut BTreeMap<String, String>> {
963 let (cluster_id, stream_id) = parse_resource_arn(arn)?;
964 let cluster = st.clusters.get_mut(&cluster_id)?;
965 match stream_id {
966 Some(sid) => cluster.streams.get_mut(&sid).map(|s| &mut s.tags),
967 None => Some(&mut cluster.tags),
968 }
969}
970
971fn resolve_tags_ref<'a>(
972 st: &'a crate::state::DsqlState,
973 arn: &str,
974) -> Option<&'a BTreeMap<String, String>> {
975 let (cluster_id, stream_id) = parse_resource_arn(arn)?;
976 let cluster = st.clusters.get(&cluster_id)?;
977 match stream_id {
978 Some(sid) => cluster.streams.get(&sid).map(|s| &s.tags),
979 None => Some(&cluster.tags),
980 }
981}
982
983fn parse_resource_arn(arn: &str) -> Option<(String, Option<String>)> {
988 let fields: Vec<&str> = arn.splitn(6, ':').collect();
990 if fields.len() != 6 || fields[0] != "arn" || fields[2] != "dsql" {
991 return None;
992 }
993 let parts: Vec<&str> = fields[5].split('/').collect();
994 match parts.as_slice() {
995 ["cluster", cid] => Some(((*cid).to_string(), None)),
996 ["cluster", cid, "stream", sid] => Some(((*cid).to_string(), Some((*sid).to_string()))),
997 _ => None,
998 }
999}
1000
1001fn hex6(id: &str) -> String {
1003 let mut h: u32 = 2166136261;
1004 for b in id.bytes() {
1005 h ^= b as u32;
1006 h = h.wrapping_mul(16777619);
1007 }
1008 format!("{:06x}", h & 0x00ff_ffff)
1009}
1010
1011fn percent_decode(s: &str) -> String {
1012 percent_encoding::percent_decode_str(s)
1013 .decode_utf8_lossy()
1014 .to_string()
1015}
1016
1017fn validation(msg: &str) -> AwsServiceError {
1022 AwsServiceError::aws_error(StatusCode::BAD_REQUEST, "ValidationException", msg)
1023}
1024
1025fn conflict(msg: &str) -> AwsServiceError {
1026 AwsServiceError::aws_error(StatusCode::CONFLICT, "ConflictException", msg)
1027}
1028
1029fn quota_exceeded(msg: &str) -> AwsServiceError {
1030 AwsServiceError::aws_error(
1031 StatusCode::PAYMENT_REQUIRED,
1032 "ServiceQuotaExceededException",
1033 msg,
1034 )
1035}
1036
1037fn resource_not_found(msg: &str) -> AwsServiceError {
1038 AwsServiceError::aws_error(StatusCode::NOT_FOUND, "ResourceNotFoundException", msg)
1039}
1040
1041fn cluster_not_found(id: &str) -> AwsServiceError {
1042 resource_not_found(&format!("Cluster not found: {id}"))
1043}
1044
1045fn stream_not_found(id: &str) -> AwsServiceError {
1046 resource_not_found(&format!("Stream not found: {id}"))
1047}
1048
1049fn resource_not_found_arn(arn: &str) -> AwsServiceError {
1050 resource_not_found(&format!("Resource not found: {arn}"))
1051}
1052
1053#[cfg(test)]
1054mod tests {
1055 use super::*;
1056
1057 #[test]
1058 fn parse_resource_arn_accepts_cluster_and_stream() {
1059 assert_eq!(
1060 parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:cluster/abc"),
1061 Some(("abc".to_string(), None))
1062 );
1063 assert_eq!(
1064 parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:cluster/abc/stream/xyz"),
1065 Some(("abc".to_string(), Some("xyz".to_string())))
1066 );
1067 }
1068
1069 #[test]
1070 fn parse_resource_arn_rejects_wrong_service_or_malformed() {
1071 assert!(parse_resource_arn("arn:aws:rds:us-east-1:123456789012:cluster/abc").is_none());
1073 assert!(parse_resource_arn("cluster/abc").is_none());
1075 assert!(parse_resource_arn("xrn:aws:dsql:us-east-1:123456789012:cluster/abc").is_none());
1077 assert!(parse_resource_arn("arn:aws:dsql:us-east-1:123456789012:widget/abc").is_none());
1079 }
1080
1081 #[test]
1082 fn hex6_is_six_hex_chars() {
1083 let h = hex6("abcdefghij0123456789klmnop");
1084 assert_eq!(h.len(), 6);
1085 assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
1086 }
1087}