1use std::future::Future;
2use std::pin::Pin;
3use std::sync::Arc;
4
5use serde_json::{Value, json};
6use winterbaume_core::{
7 BackendState, MockRequest, MockResponse, MockService, StateChangeNotifier, StatefulService,
8 default_account_id,
9};
10
11use crate::model;
12use crate::state::WorkSpacesState;
13use crate::types::{
14 ClientProperties, IpRule, RebootRequest, RebuildRequest, SelfservicePermissions, StartRequest,
15 StopRequest, Tag, TerminateRequest, Workspace, WorkspaceCreationProperties, WorkspaceDirectory,
16 WorkspaceProperties, WorkspaceRequest, WorkspacesPool,
17};
18use crate::views::WorkSpacesStateView;
19use crate::wire;
20
21fn rfc3339_to_epoch(s: &str) -> Option<f64> {
23 chrono::DateTime::parse_from_rfc3339(s)
24 .ok()
25 .map(|dt| dt.timestamp() as f64)
26}
27
28pub struct WorkSpacesService {
30 pub(crate) state: Arc<BackendState<WorkSpacesState>>,
31 pub(crate) notifier: StateChangeNotifier<WorkSpacesStateView>,
32}
33
34impl WorkSpacesService {
35 pub fn new() -> Self {
36 Self {
37 state: Arc::new(BackendState::new()),
38 notifier: StateChangeNotifier::new(),
39 }
40 }
41}
42
43impl Default for WorkSpacesService {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl MockService for WorkSpacesService {
50 fn service_name(&self) -> &str {
51 "workspaces"
52 }
53
54 fn url_patterns(&self) -> Vec<&str> {
55 vec![
56 r"https?://workspaces\..*\.amazonaws\.com",
57 r"https?://workspaces\.amazonaws\.com",
58 ]
59 }
60
61 fn handle(
62 &self,
63 request: MockRequest,
64 ) -> Pin<Box<dyn Future<Output = MockResponse> + Send + '_>> {
65 Box::pin(async move { self.dispatch(request).await })
66 }
67}
68
69impl WorkSpacesService {
70 async fn dispatch(&self, request: MockRequest) -> MockResponse {
71 let region = winterbaume_core::auth::extract_region_from_uri(&request.uri);
72 let account_id = default_account_id();
73
74 let action = request
77 .headers
78 .get("x-amz-target")
79 .and_then(|v| v.to_str().ok())
80 .and_then(|v| v.split('.').next_back())
81 .map(|s| s.to_string());
82
83 let action = match action {
84 Some(a) => a,
85 None => {
86 return json_error_response(400, "MissingAction", "Missing X-Amz-Target header");
87 }
88 };
89
90 let body_bytes: &[u8] = if request.body.is_empty() {
93 b"{}"
94 } else {
95 if serde_json::from_slice::<Value>(&request.body).is_err() {
96 return json_error_response(400, "SerializationException", "Invalid JSON body");
97 }
98 &request.body
99 };
100
101 let state = self.state.get(account_id, ®ion);
102
103 let response = match action.as_str() {
104 "CreateWorkspaces" => {
105 self.handle_create_workspaces(&state, body_bytes, account_id, ®ion)
106 .await
107 }
108 "DescribeWorkspaces" => self.handle_describe_workspaces(&state, body_bytes).await,
109 "TerminateWorkspaces" => self.handle_terminate_workspaces(&state, body_bytes).await,
110 "DescribeWorkspaceDirectories" => {
111 self.handle_describe_workspace_directories(&state, body_bytes)
112 .await
113 }
114 "CreateTags" => self.handle_create_tags(&state, body_bytes).await,
115 "DescribeTags" => self.handle_describe_tags(&state, body_bytes).await,
116 "CreateWorkspaceImage" => {
117 self.handle_create_workspace_image(&state, body_bytes, account_id)
118 .await
119 }
120 "DescribeWorkspaceImages" => {
121 self.handle_describe_workspace_images(&state, body_bytes)
122 .await
123 }
124 "DescribeWorkspaceImagePermissions" => {
125 self.handle_describe_workspace_image_permissions(&state, body_bytes)
126 .await
127 }
128 "UpdateWorkspaceImagePermission" => {
129 self.handle_update_workspace_image_permission(&state, body_bytes)
130 .await
131 }
132 "DescribeClientProperties" => {
133 self.handle_describe_client_properties(&state, body_bytes)
134 .await
135 }
136 "ModifyClientProperties" => {
137 self.handle_modify_client_properties(&state, body_bytes)
138 .await
139 }
140 "ModifySelfservicePermissions" => {
141 self.handle_modify_selfservice_permissions(&state, body_bytes)
142 .await
143 }
144 "ModifyWorkspaceCreationProperties" => {
145 self.handle_modify_workspace_creation_properties(&state, body_bytes)
146 .await
147 }
148 "RegisterWorkspaceDirectory" => {
149 self.handle_register_workspace_directory(&state, body_bytes, account_id, ®ion)
150 .await
151 }
152 "DeregisterWorkspaceDirectory" => {
153 self.handle_deregister_workspace_directory(&state, body_bytes)
154 .await
155 }
156 "DeleteTags" => self.handle_delete_tags(&state, body_bytes).await,
157 "StartWorkspaces" => self.handle_start_workspaces(&state, body_bytes).await,
158 "StopWorkspaces" => self.handle_stop_workspaces(&state, body_bytes).await,
159 "RebootWorkspaces" => self.handle_reboot_workspaces(&state, body_bytes).await,
160 "RebuildWorkspaces" => self.handle_rebuild_workspaces(&state, body_bytes).await,
161 "RestoreWorkspace" => self.handle_restore_workspace(&state, body_bytes).await,
162 "ModifyWorkspaceProperties" => {
163 self.handle_modify_workspace_properties(&state, body_bytes)
164 .await
165 }
166 "ModifyWorkspaceState" => self.handle_modify_workspace_state(&state, body_bytes).await,
167 "DescribeWorkspacesConnectionStatus" => {
168 self.handle_describe_workspaces_connection_status(&state, body_bytes)
169 .await
170 }
171 "CreateIpGroup" => {
172 self.handle_create_ip_group(&state, body_bytes, account_id)
173 .await
174 }
175 "DescribeIpGroups" => self.handle_describe_ip_groups(&state, body_bytes).await,
176 "DeleteIpGroup" => self.handle_delete_ip_group(&state, body_bytes).await,
177 "AuthorizeIpRules" => self.handle_authorize_ip_rules(&state, body_bytes).await,
178 "RevokeIpRules" => self.handle_revoke_ip_rules(&state, body_bytes).await,
179 "UpdateRulesOfIpGroup" => {
180 self.handle_update_rules_of_ip_group(&state, body_bytes)
181 .await
182 }
183 "AssociateIpGroups" => self.handle_associate_ip_groups(&state, body_bytes).await,
184 "DisassociateIpGroups" => self.handle_disassociate_ip_groups(&state, body_bytes).await,
185 "CreateConnectionAlias" => {
186 self.handle_create_connection_alias(&state, body_bytes, account_id)
187 .await
188 }
189 "DescribeConnectionAliases" => {
190 self.handle_describe_connection_aliases(&state, body_bytes)
191 .await
192 }
193 "DeleteConnectionAlias" => {
194 self.handle_delete_connection_alias(&state, body_bytes)
195 .await
196 }
197 "AssociateConnectionAlias" => {
198 self.handle_associate_connection_alias(&state, body_bytes)
199 .await
200 }
201 "DisassociateConnectionAlias" => {
202 self.handle_disassociate_connection_alias(&state, body_bytes)
203 .await
204 }
205 "DescribeConnectionAliasPermissions" => {
206 self.handle_describe_connection_alias_permissions(&state, body_bytes)
207 .await
208 }
209 "UpdateConnectionAliasPermission" => {
210 self.handle_update_connection_alias_permission(&state, body_bytes)
211 .await
212 }
213 "DescribeWorkspaceBundles" => {
214 self.handle_describe_workspace_bundles(&state, body_bytes)
215 .await
216 }
217 "CreateWorkspaceBundle" => {
218 self.handle_create_workspace_bundle(&state, body_bytes, account_id)
219 .await
220 }
221 "DeleteWorkspaceBundle" => {
222 self.handle_delete_workspace_bundle(&state, body_bytes)
223 .await
224 }
225 "DeleteWorkspaceImage" => self.handle_delete_workspace_image(&state, body_bytes).await,
226 "CreateWorkspacesPool" => {
227 self.handle_create_workspaces_pool(&state, body_bytes, account_id, ®ion)
228 .await
229 }
230 "DescribeWorkspacesPools" => {
231 self.handle_describe_workspaces_pools(&state, body_bytes)
232 .await
233 }
234 "TerminateWorkspacesPool" => {
235 self.handle_terminate_workspaces_pool(&state, body_bytes)
236 .await
237 }
238 "UpdateWorkspacesPool" => self.handle_update_workspaces_pool(&state, body_bytes).await,
239 "StartWorkspacesPool" => self.handle_start_workspaces_pool(&state, body_bytes).await,
240 "StopWorkspacesPool" => self.handle_stop_workspaces_pool(&state, body_bytes).await,
241 _ => json_error_response(
242 400,
243 "InvalidAction",
244 &format!("Could not find operation {action} for WorkSpaces"),
245 ),
246 };
247 if response.status / 100 == 2 {
248 self.notify_state_changed(account_id, ®ion).await;
249 }
250 response
251 }
252
253 async fn handle_create_workspaces(
254 &self,
255 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
256 body: &[u8],
257 account_id: &str,
258 region: &str,
259 ) -> MockResponse {
260 let input = match wire::deserialize_create_workspaces_request(body) {
261 Ok(v) => v,
262 Err(e) => return json_error_response(400, "ValidationException", &e),
263 };
264 let requests: Vec<WorkspaceRequest> = input
265 .workspaces
266 .into_iter()
267 .filter_map(|item| {
268 if item.directory_id.is_empty()
269 || item.user_name.is_empty()
270 || item.bundle_id.is_empty()
271 {
272 return None;
273 }
274 let workspace_properties =
275 item.workspace_properties.map(|props| WorkspaceProperties {
276 running_mode: props.running_mode,
277 running_mode_auto_stop_timeout_in_minutes: props
278 .running_mode_auto_stop_timeout_in_minutes,
279 root_volume_size_gib: props.root_volume_size_gib,
280 user_volume_size_gib: props.user_volume_size_gib,
281 compute_type_name: props.compute_type_name,
282 });
283 let tags: Option<Vec<Tag>> = item.tags.map(|arr| {
284 arr.into_iter()
285 .map(|t| Tag {
286 key: t.key,
287 value: t.value.unwrap_or_default(),
288 })
289 .collect()
290 });
291 Some(WorkspaceRequest {
292 directory_id: item.directory_id,
293 user_name: item.user_name,
294 bundle_id: item.bundle_id,
295 volume_encryption_key: item.volume_encryption_key,
296 user_volume_encryption_enabled: item.user_volume_encryption_enabled,
297 root_volume_encryption_enabled: item.root_volume_encryption_enabled,
298 workspace_properties,
299 tags,
300 })
301 })
302 .collect();
303
304 let mut state = state.write().await;
305 let (pending, failed) = state.create_workspaces(&requests, account_id, region);
306
307 let pending_wire: Vec<model::Workspace> = pending.iter().map(workspace_to_model).collect();
308 let failed_wire: Vec<model::FailedCreateWorkspaceRequest> = failed
309 .iter()
310 .map(|f| model::FailedCreateWorkspaceRequest {
311 workspace_request: f
312 .workspace_request
313 .as_ref()
314 .map(|r| model::WorkspaceRequest {
315 directory_id: r.directory_id.clone(),
316 user_name: r.user_name.clone(),
317 bundle_id: r.bundle_id.clone(),
318 ..Default::default()
319 }),
320 error_code: Some(f.error_code.clone()),
321 error_message: Some(f.error_message.clone()),
322 })
323 .collect();
324
325 let result = model::CreateWorkspacesResult {
326 pending_requests: Some(pending_wire),
327 failed_requests: Some(failed_wire),
328 };
329 wire::serialize_create_workspaces_response(&result)
330 }
331
332 async fn handle_describe_workspaces(
333 &self,
334 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
335 body: &[u8],
336 ) -> MockResponse {
337 let input = match wire::deserialize_describe_workspaces_request(body) {
338 Ok(v) => v,
339 Err(e) => return json_error_response(400, "ValidationException", &e),
340 };
341 let workspace_ids = input.workspace_ids;
342 let directory_id = input.directory_id.as_deref();
343 let user_name = input.user_name.as_deref();
344
345 let state = state.read().await;
346 let workspaces =
347 state.describe_workspaces(workspace_ids.as_deref(), directory_id, user_name);
348
349 let entries: Vec<model::Workspace> =
350 workspaces.iter().map(|ws| workspace_to_model(ws)).collect();
351
352 let result = model::DescribeWorkspacesResult {
353 workspaces: Some(entries),
354 next_token: None,
355 };
356 wire::serialize_describe_workspaces_response(&result)
357 }
358
359 async fn handle_terminate_workspaces(
360 &self,
361 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
362 body: &[u8],
363 ) -> MockResponse {
364 let input = match wire::deserialize_terminate_workspaces_request(body) {
365 Ok(v) => v,
366 Err(e) => return json_error_response(400, "ValidationException", &e),
367 };
368 let requests: Vec<TerminateRequest> = input
369 .terminate_workspace_requests
370 .into_iter()
371 .map(|item| TerminateRequest {
372 workspace_id: item.workspace_id,
373 })
374 .collect();
375
376 let mut state = state.write().await;
377 let failed = state.terminate_workspaces(&requests);
378
379 let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
380 .iter()
381 .map(|f| model::FailedWorkspaceChangeRequest {
382 workspace_id: Some(f.workspace_id.clone()),
383 error_code: Some(f.error_code.clone()),
384 error_message: Some(f.error_message.clone()),
385 })
386 .collect();
387
388 let result = model::TerminateWorkspacesResult {
389 failed_requests: Some(failed_wire),
390 };
391 wire::serialize_terminate_workspaces_response(&result)
392 }
393
394 async fn handle_describe_workspace_directories(
395 &self,
396 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
397 body: &[u8],
398 ) -> MockResponse {
399 let input = match wire::deserialize_describe_workspace_directories_request(body) {
400 Ok(v) => v,
401 Err(e) => return json_error_response(400, "ValidationException", &e),
402 };
403 let directory_ids = input.directory_ids;
404
405 let state = state.read().await;
406 let directories = state.describe_workspace_directories(directory_ids.as_deref());
407
408 let entries: Vec<model::WorkspaceDirectory> = directories
409 .iter()
410 .map(|dir| directory_to_model(dir))
411 .collect();
412
413 let result = model::DescribeWorkspaceDirectoriesResult {
414 directories: Some(entries),
415 next_token: None,
416 };
417 wire::serialize_describe_workspace_directories_response(&result)
418 }
419
420 async fn handle_create_tags(
421 &self,
422 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
423 body: &[u8],
424 ) -> MockResponse {
425 let input = match wire::deserialize_create_tags_request(body) {
426 Ok(v) => v,
427 Err(e) => return json_error_response(400, "ValidationException", &e),
428 };
429 if input.resource_id.is_empty() {
430 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
431 }
432 let resource_id = input.resource_id;
433 let tags: Vec<Tag> = input
434 .tags
435 .into_iter()
436 .map(|t| Tag {
437 key: t.key,
438 value: t.value.unwrap_or_default(),
439 })
440 .collect();
441
442 let mut state = state.write().await;
443 state.create_tags(&resource_id, &tags);
444
445 let result = model::CreateTagsResult {};
446 wire::serialize_create_tags_response(&result)
447 }
448
449 async fn handle_describe_tags(
450 &self,
451 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
452 body: &[u8],
453 ) -> MockResponse {
454 let input = match wire::deserialize_describe_tags_request(body) {
455 Ok(v) => v,
456 Err(e) => return json_error_response(400, "ValidationException", &e),
457 };
458 if input.resource_id.is_empty() {
459 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
460 }
461 let resource_id = input.resource_id;
462
463 let state = state.read().await;
464 let tags = state.describe_tags(&resource_id);
465
466 let tag_list: Vec<model::Tag> = tags
467 .iter()
468 .map(|t| model::Tag {
469 key: t.key.clone(),
470 value: Some(t.value.clone()),
471 })
472 .collect();
473
474 let result = model::DescribeTagsResult {
475 tag_list: Some(tag_list),
476 };
477 wire::serialize_describe_tags_response(&result)
478 }
479
480 async fn handle_create_workspace_image(
481 &self,
482 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
483 body: &[u8],
484 account_id: &str,
485 ) -> MockResponse {
486 let input = match wire::deserialize_create_workspace_image_request(body) {
487 Ok(v) => v,
488 Err(e) => return json_error_response(400, "ValidationException", &e),
489 };
490 if input.name.is_empty() {
491 return json_error_response(400, "ValidationException", "Missing 'Name' field");
492 }
493 let name = input.name;
494 if input.description.is_empty() {
495 return json_error_response(400, "ValidationException", "Missing 'Description' field");
496 }
497 let description = input.description;
498 if input.workspace_id.is_empty() {
499 return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
500 }
501 let workspace_id = input.workspace_id;
502
503 let tags: Option<Vec<Tag>> = input.tags.map(|arr| {
504 arr.into_iter()
505 .map(|t| Tag {
506 key: t.key,
507 value: t.value.unwrap_or_default(),
508 })
509 .collect()
510 });
511
512 let mut state = state.write().await;
513 match state.create_workspace_image(
514 &name,
515 &description,
516 &workspace_id,
517 account_id,
518 tags.as_deref(),
519 ) {
520 Ok(image) => {
521 let result = model::CreateWorkspaceImageResult {
522 image_id: Some(image.image_id),
523 name: Some(image.name),
524 description: Some(image.description),
525 state: Some(image.state),
526 operating_system: Some(model::OperatingSystem {
527 r#type: image.operating_system_type,
528 }),
529 owner_account_id: Some(image.owner_account_id),
530 required_tenancy: Some(image.required_tenancy),
531 created: rfc3339_to_epoch(&image.created),
532 };
533 wire::serialize_create_workspace_image_response(&result)
534 }
535 Err(e) => workspaces_error_response(&e),
536 }
537 }
538
539 async fn handle_describe_workspace_images(
540 &self,
541 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
542 body: &[u8],
543 ) -> MockResponse {
544 let input = match wire::deserialize_describe_workspace_images_request(body) {
545 Ok(v) => v,
546 Err(e) => return json_error_response(400, "ValidationException", &e),
547 };
548 let image_ids = input.image_ids;
549
550 let state = state.read().await;
551 let images = state.describe_workspace_images(image_ids.as_deref());
552
553 let entries: Vec<model::WorkspaceImage> = images
554 .iter()
555 .map(|img| model::WorkspaceImage {
556 image_id: Some(img.image_id.clone()),
557 name: Some(img.name.clone()),
558 description: Some(img.description.clone()),
559 state: Some(img.state.clone()),
560 operating_system: Some(model::OperatingSystem {
561 r#type: img.operating_system_type.clone(),
562 }),
563 owner_account_id: Some(img.owner_account_id.clone()),
564 required_tenancy: Some(img.required_tenancy.clone()),
565 created: rfc3339_to_epoch(&img.created),
566 ..Default::default()
567 })
568 .collect();
569
570 let result = model::DescribeWorkspaceImagesResult {
571 images: Some(entries),
572 next_token: None,
573 };
574 wire::serialize_describe_workspace_images_response(&result)
575 }
576
577 async fn handle_describe_workspace_image_permissions(
578 &self,
579 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
580 body: &[u8],
581 ) -> MockResponse {
582 let input = match wire::deserialize_describe_workspace_image_permissions_request(body) {
583 Ok(v) => v,
584 Err(e) => return json_error_response(400, "ValidationException", &e),
585 };
586 if input.image_id.is_empty() {
587 return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
588 }
589 let image_id = input.image_id;
590
591 let state = state.read().await;
592 match state.describe_workspace_image_permissions(&image_id) {
593 Ok(shared_ids) => {
594 let perms: Vec<model::ImagePermission> = shared_ids
595 .iter()
596 .map(|id| model::ImagePermission {
597 shared_account_id: Some(id.clone()),
598 })
599 .collect();
600
601 let result = model::DescribeWorkspaceImagePermissionsResult {
602 image_id: Some(image_id),
603 image_permissions: Some(perms),
604 next_token: None,
605 };
606 wire::serialize_describe_workspace_image_permissions_response(&result)
607 }
608 Err(e) => workspaces_error_response(&e),
609 }
610 }
611
612 async fn handle_update_workspace_image_permission(
613 &self,
614 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
615 body: &[u8],
616 ) -> MockResponse {
617 let input = match wire::deserialize_update_workspace_image_permission_request(body) {
618 Ok(v) => v,
619 Err(e) => return json_error_response(400, "ValidationException", &e),
620 };
621 if input.image_id.is_empty() {
622 return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
623 }
624 let image_id = input.image_id;
625 if input.shared_account_id.is_empty() {
626 return json_error_response(
627 400,
628 "ValidationException",
629 "Missing 'SharedAccountId' field",
630 );
631 }
632 let shared_account_id = input.shared_account_id;
633 let allow_copy_image = input.allow_copy_image;
634
635 let mut state = state.write().await;
636 match state.update_workspace_image_permission(
637 &image_id,
638 &shared_account_id,
639 allow_copy_image,
640 ) {
641 Ok(()) => {
642 let result = model::UpdateWorkspaceImagePermissionResult {};
643 wire::serialize_update_workspace_image_permission_response(&result)
644 }
645 Err(e) => workspaces_error_response(&e),
646 }
647 }
648
649 async fn handle_describe_client_properties(
650 &self,
651 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
652 body: &[u8],
653 ) -> MockResponse {
654 let input = match wire::deserialize_describe_client_properties_request(body) {
655 Ok(v) => v,
656 Err(e) => return json_error_response(400, "ValidationException", &e),
657 };
658 let resource_ids = input.resource_ids;
659
660 let state = state.read().await;
661 let props = state.describe_client_properties(&resource_ids);
662
663 let entries: Vec<model::ClientPropertiesResult> = props
664 .iter()
665 .map(|(id, cp)| model::ClientPropertiesResult {
666 resource_id: Some(id.clone()),
667 client_properties: Some(model::ClientProperties {
668 reconnect_enabled: cp.reconnect_enabled.clone(),
669 log_upload_enabled: cp.log_upload_enabled.clone(),
670 }),
671 })
672 .collect();
673
674 let result = model::DescribeClientPropertiesResult {
675 client_properties_list: Some(entries),
676 };
677 wire::serialize_describe_client_properties_response(&result)
678 }
679
680 async fn handle_modify_client_properties(
681 &self,
682 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
683 body: &[u8],
684 ) -> MockResponse {
685 let input = match wire::deserialize_modify_client_properties_request(body) {
686 Ok(v) => v,
687 Err(e) => return json_error_response(400, "ValidationException", &e),
688 };
689 if input.resource_id.is_empty() {
690 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
691 }
692 let resource_id = input.resource_id;
693
694 let client_props = ClientProperties {
695 reconnect_enabled: input.client_properties.reconnect_enabled,
696 log_upload_enabled: input.client_properties.log_upload_enabled,
697 };
698
699 let mut state = state.write().await;
700 state.modify_client_properties(&resource_id, &client_props);
701
702 let result = model::ModifyClientPropertiesResult {};
703 wire::serialize_modify_client_properties_response(&result)
704 }
705
706 async fn handle_modify_selfservice_permissions(
707 &self,
708 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
709 body: &[u8],
710 ) -> MockResponse {
711 let input = match wire::deserialize_modify_selfservice_permissions_request(body) {
712 Ok(v) => v,
713 Err(e) => return json_error_response(400, "ValidationException", &e),
714 };
715 if input.resource_id.is_empty() {
716 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
717 }
718 let resource_id = input.resource_id;
719
720 let sp = input.selfservice_permissions;
721 let perms = SelfservicePermissions {
722 restart_workspace: sp.restart_workspace,
723 increase_volume_size: sp.increase_volume_size,
724 change_compute_type: sp.change_compute_type,
725 switch_running_mode: sp.switch_running_mode,
726 rebuild_workspace: sp.rebuild_workspace,
727 };
728
729 let mut state = state.write().await;
730 match state.modify_selfservice_permissions(&resource_id, &perms) {
731 Ok(()) => {
732 let result = model::ModifySelfservicePermissionsResult {};
733 wire::serialize_modify_selfservice_permissions_response(&result)
734 }
735 Err(e) => workspaces_error_response(&e),
736 }
737 }
738
739 async fn handle_modify_workspace_creation_properties(
740 &self,
741 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
742 body: &[u8],
743 ) -> MockResponse {
744 let input = match wire::deserialize_modify_workspace_creation_properties_request(body) {
745 Ok(v) => v,
746 Err(e) => return json_error_response(400, "ValidationException", &e),
747 };
748 if input.resource_id.is_empty() {
749 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
750 }
751 let resource_id = input.resource_id;
752
753 let wcp = input.workspace_creation_properties;
754 let props = WorkspaceCreationProperties {
755 custom_security_group_id: wcp.custom_security_group_id,
756 default_ou: wcp.default_ou,
757 enable_internet_access: wcp.enable_internet_access,
758 enable_maintenance_mode: wcp.enable_maintenance_mode,
759 user_enabled_as_local_administrator: wcp.user_enabled_as_local_administrator,
760 instance_iam_role_arn: wcp.instance_iam_role_arn,
761 };
762
763 let mut state = state.write().await;
764 match state.modify_workspace_creation_properties(&resource_id, &props) {
765 Ok(()) => {
766 let result = model::ModifyWorkspaceCreationPropertiesResult {};
767 wire::serialize_modify_workspace_creation_properties_response(&result)
768 }
769 Err(e) => workspaces_error_response(&e),
770 }
771 }
772
773 async fn handle_register_workspace_directory(
774 &self,
775 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
776 body: &[u8],
777 account_id: &str,
778 region: &str,
779 ) -> MockResponse {
780 let input = match wire::deserialize_register_workspace_directory_request(body) {
781 Ok(v) => v,
782 Err(e) => return json_error_response(400, "ValidationException", &e),
783 };
784 let directory_id = match input.directory_id {
785 Some(id) if !id.is_empty() => id,
786 _ => {
787 return json_error_response(
788 400,
789 "ValidationException",
790 "Missing 'DirectoryId' field",
791 );
792 }
793 };
794
795 let mut state = state.write().await;
796 match state.register_workspace_directory(&directory_id, account_id, region) {
797 Ok(dir) => {
798 let result = model::RegisterWorkspaceDirectoryResult {
799 directory_id: Some(dir.directory_id),
800 state: Some(dir.state),
801 };
802 wire::serialize_register_workspace_directory_response(&result)
803 }
804 Err(e) => workspaces_error_response(&e),
805 }
806 }
807
808 async fn handle_deregister_workspace_directory(
809 &self,
810 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
811 body: &[u8],
812 ) -> MockResponse {
813 let input = match wire::deserialize_deregister_workspace_directory_request(body) {
814 Ok(v) => v,
815 Err(e) => return json_error_response(400, "ValidationException", &e),
816 };
817 if input.directory_id.is_empty() {
818 return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
819 }
820 let directory_id = input.directory_id;
821
822 let mut state = state.write().await;
823 match state.deregister_workspace_directory(&directory_id) {
824 Ok(()) => {
825 let result = model::DeregisterWorkspaceDirectoryResult {};
826 wire::serialize_deregister_workspace_directory_response(&result)
827 }
828 Err(e) => workspaces_error_response(&e),
829 }
830 }
831
832 async fn handle_delete_tags(
833 &self,
834 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
835 body: &[u8],
836 ) -> MockResponse {
837 let input = match wire::deserialize_delete_tags_request(body) {
838 Ok(v) => v,
839 Err(e) => return json_error_response(400, "ValidationException", &e),
840 };
841 if input.resource_id.is_empty() {
842 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
843 }
844 let resource_id = input.resource_id;
845 let tag_keys = input.tag_keys;
846
847 let mut state = state.write().await;
848 state.delete_tags(&resource_id, &tag_keys);
849
850 let result = model::DeleteTagsResult {};
851 wire::serialize_delete_tags_response(&result)
852 }
853
854 async fn handle_start_workspaces(
855 &self,
856 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
857 body: &[u8],
858 ) -> MockResponse {
859 let input = match wire::deserialize_start_workspaces_request(body) {
860 Ok(v) => v,
861 Err(e) => return json_error_response(400, "ValidationException", &e),
862 };
863 let requests: Vec<StartRequest> = input
864 .start_workspace_requests
865 .into_iter()
866 .filter_map(|item| {
867 item.workspace_id
868 .map(|workspace_id| StartRequest { workspace_id })
869 })
870 .collect();
871
872 let mut state = state.write().await;
873 let failed = state.start_workspaces(&requests);
874
875 let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
876 .iter()
877 .map(|f| model::FailedWorkspaceChangeRequest {
878 workspace_id: Some(f.workspace_id.clone()),
879 error_code: Some(f.error_code.clone()),
880 error_message: Some(f.error_message.clone()),
881 })
882 .collect();
883 let result = model::StartWorkspacesResult {
884 failed_requests: Some(failed_wire),
885 };
886 wire::serialize_start_workspaces_response(&result)
887 }
888
889 async fn handle_stop_workspaces(
890 &self,
891 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
892 body: &[u8],
893 ) -> MockResponse {
894 let input = match wire::deserialize_stop_workspaces_request(body) {
895 Ok(v) => v,
896 Err(e) => return json_error_response(400, "ValidationException", &e),
897 };
898 let requests: Vec<StopRequest> = input
899 .stop_workspace_requests
900 .into_iter()
901 .filter_map(|item| {
902 item.workspace_id
903 .map(|workspace_id| StopRequest { workspace_id })
904 })
905 .collect();
906
907 let mut state = state.write().await;
908 let failed = state.stop_workspaces(&requests);
909
910 let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
911 .iter()
912 .map(|f| model::FailedWorkspaceChangeRequest {
913 workspace_id: Some(f.workspace_id.clone()),
914 error_code: Some(f.error_code.clone()),
915 error_message: Some(f.error_message.clone()),
916 })
917 .collect();
918 let result = model::StopWorkspacesResult {
919 failed_requests: Some(failed_wire),
920 };
921 wire::serialize_stop_workspaces_response(&result)
922 }
923
924 async fn handle_reboot_workspaces(
925 &self,
926 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
927 body: &[u8],
928 ) -> MockResponse {
929 let input = match wire::deserialize_reboot_workspaces_request(body) {
930 Ok(v) => v,
931 Err(e) => return json_error_response(400, "ValidationException", &e),
932 };
933 let requests: Vec<RebootRequest> = input
934 .reboot_workspace_requests
935 .into_iter()
936 .map(|item| RebootRequest {
937 workspace_id: item.workspace_id,
938 })
939 .collect();
940
941 let mut state = state.write().await;
942 let failed = state.reboot_workspaces(&requests);
943
944 let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
945 .iter()
946 .map(|f| model::FailedWorkspaceChangeRequest {
947 workspace_id: Some(f.workspace_id.clone()),
948 error_code: Some(f.error_code.clone()),
949 error_message: Some(f.error_message.clone()),
950 })
951 .collect();
952 let result = model::RebootWorkspacesResult {
953 failed_requests: Some(failed_wire),
954 };
955 wire::serialize_reboot_workspaces_response(&result)
956 }
957
958 async fn handle_rebuild_workspaces(
959 &self,
960 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
961 body: &[u8],
962 ) -> MockResponse {
963 let input = match wire::deserialize_rebuild_workspaces_request(body) {
964 Ok(v) => v,
965 Err(e) => return json_error_response(400, "ValidationException", &e),
966 };
967 let requests: Vec<RebuildRequest> = input
968 .rebuild_workspace_requests
969 .into_iter()
970 .map(|item| RebuildRequest {
971 workspace_id: item.workspace_id,
972 })
973 .collect();
974
975 let mut state = state.write().await;
976 let failed = state.rebuild_workspaces(&requests);
977
978 let failed_wire: Vec<model::FailedWorkspaceChangeRequest> = failed
979 .iter()
980 .map(|f| model::FailedWorkspaceChangeRequest {
981 workspace_id: Some(f.workspace_id.clone()),
982 error_code: Some(f.error_code.clone()),
983 error_message: Some(f.error_message.clone()),
984 })
985 .collect();
986 let result = model::RebuildWorkspacesResult {
987 failed_requests: Some(failed_wire),
988 };
989 wire::serialize_rebuild_workspaces_response(&result)
990 }
991
992 async fn handle_restore_workspace(
993 &self,
994 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
995 body: &[u8],
996 ) -> MockResponse {
997 let input = match wire::deserialize_restore_workspace_request(body) {
998 Ok(v) => v,
999 Err(e) => return json_error_response(400, "ValidationException", &e),
1000 };
1001 if input.workspace_id.is_empty() {
1002 return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1003 }
1004 let workspace_id = input.workspace_id;
1005
1006 let mut state = state.write().await;
1007 match state.restore_workspace(&workspace_id) {
1008 Ok(()) => {
1009 let result = model::RestoreWorkspaceResult {};
1010 wire::serialize_restore_workspace_response(&result)
1011 }
1012 Err(e) => workspaces_error_response(&e),
1013 }
1014 }
1015
1016 async fn handle_modify_workspace_properties(
1017 &self,
1018 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1019 body: &[u8],
1020 ) -> MockResponse {
1021 let input = match wire::deserialize_modify_workspace_properties_request(body) {
1022 Ok(v) => v,
1023 Err(e) => return json_error_response(400, "ValidationException", &e),
1024 };
1025 if input.workspace_id.is_empty() {
1026 return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1027 }
1028 let workspace_id = input.workspace_id;
1029
1030 let (
1031 running_mode,
1032 root_volume_size_gib,
1033 user_volume_size_gib,
1034 compute_type_name,
1035 running_mode_auto_stop,
1036 ) = match input.workspace_properties {
1037 Some(p) => (
1038 p.running_mode,
1039 p.root_volume_size_gib,
1040 p.user_volume_size_gib,
1041 p.compute_type_name,
1042 p.running_mode_auto_stop_timeout_in_minutes,
1043 ),
1044 None => (None, None, None, None, None),
1045 };
1046
1047 let mut state = state.write().await;
1048 match state.modify_workspace_properties(
1049 &workspace_id,
1050 running_mode.as_deref(),
1051 root_volume_size_gib,
1052 user_volume_size_gib,
1053 compute_type_name.as_deref(),
1054 running_mode_auto_stop,
1055 ) {
1056 Ok(()) => {
1057 let result = model::ModifyWorkspacePropertiesResult {};
1058 wire::serialize_modify_workspace_properties_response(&result)
1059 }
1060 Err(e) => workspaces_error_response(&e),
1061 }
1062 }
1063
1064 async fn handle_modify_workspace_state(
1065 &self,
1066 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1067 body: &[u8],
1068 ) -> MockResponse {
1069 let input = match wire::deserialize_modify_workspace_state_request(body) {
1070 Ok(v) => v,
1071 Err(e) => return json_error_response(400, "ValidationException", &e),
1072 };
1073 if input.workspace_id.is_empty() {
1074 return json_error_response(400, "ValidationException", "Missing 'WorkspaceId' field");
1075 }
1076 let workspace_id = input.workspace_id;
1077 if input.workspace_state.is_empty() {
1078 return json_error_response(
1079 400,
1080 "ValidationException",
1081 "Missing 'WorkspaceState' field",
1082 );
1083 }
1084 let workspace_state = input.workspace_state;
1085
1086 let mut state = state.write().await;
1087 match state.modify_workspace_state(&workspace_id, &workspace_state) {
1088 Ok(()) => {
1089 let result = model::ModifyWorkspaceStateResult {};
1090 wire::serialize_modify_workspace_state_response(&result)
1091 }
1092 Err(e) => workspaces_error_response(&e),
1093 }
1094 }
1095
1096 async fn handle_describe_workspaces_connection_status(
1097 &self,
1098 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1099 body: &[u8],
1100 ) -> MockResponse {
1101 let input = match wire::deserialize_describe_workspaces_connection_status_request(body) {
1102 Ok(v) => v,
1103 Err(e) => return json_error_response(400, "ValidationException", &e),
1104 };
1105 let workspace_ids = input.workspace_ids;
1106
1107 let state = state.read().await;
1108 let statuses = state.describe_workspaces_connection_status(workspace_ids.as_deref());
1109
1110 let entries: Vec<model::WorkspaceConnectionStatus> = statuses
1111 .iter()
1112 .map(|(id, st)| model::WorkspaceConnectionStatus {
1113 workspace_id: Some(id.to_string()),
1114 connection_state: Some(if *st == "AVAILABLE" {
1115 "CONNECTED".to_string()
1116 } else {
1117 "DISCONNECTED".to_string()
1118 }),
1119 ..Default::default()
1120 })
1121 .collect();
1122
1123 let result = model::DescribeWorkspacesConnectionStatusResult {
1124 workspaces_connection_status: Some(entries),
1125 next_token: None,
1126 };
1127 wire::serialize_describe_workspaces_connection_status_response(&result)
1128 }
1129
1130 async fn handle_create_ip_group(
1133 &self,
1134 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1135 body: &[u8],
1136 account_id: &str,
1137 ) -> MockResponse {
1138 let input = match wire::deserialize_create_ip_group_request(body) {
1139 Ok(v) => v,
1140 Err(e) => return json_error_response(400, "ValidationException", &e),
1141 };
1142 if input.group_name.is_empty() {
1143 return json_error_response(400, "ValidationException", "Missing 'GroupName' field");
1144 }
1145 let group_name = input.group_name;
1146 let group_desc = input.group_desc;
1147 let user_rules: Vec<IpRule> = input
1148 .user_rules
1149 .unwrap_or_default()
1150 .into_iter()
1151 .filter_map(|item| {
1152 item.ip_rule.map(|ip_rule| IpRule {
1153 ip_rule,
1154 rule_desc: item.rule_desc,
1155 })
1156 })
1157 .collect();
1158
1159 let mut state = state.write().await;
1160 let group =
1161 state.create_ip_group(&group_name, group_desc.as_deref(), user_rules, account_id);
1162
1163 let result = model::CreateIpGroupResult {
1164 group_id: Some(group.group_id),
1165 };
1166 wire::serialize_create_ip_group_response(&result)
1167 }
1168
1169 async fn handle_describe_ip_groups(
1170 &self,
1171 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1172 body: &[u8],
1173 ) -> MockResponse {
1174 let input = match wire::deserialize_describe_ip_groups_request(body) {
1175 Ok(v) => v,
1176 Err(e) => return json_error_response(400, "ValidationException", &e),
1177 };
1178 let group_ids = input.group_ids;
1179
1180 let state = state.read().await;
1181 let groups = state.describe_ip_groups(group_ids.as_deref());
1182
1183 let entries: Vec<model::WorkspacesIpGroup> = groups
1184 .iter()
1185 .map(|g| model::WorkspacesIpGroup {
1186 group_id: Some(g.group_id.clone()),
1187 group_name: Some(g.group_name.clone()),
1188 group_desc: g.group_desc.clone(),
1189 user_rules: Some(
1190 g.user_rules
1191 .iter()
1192 .map(|r| model::IpRuleItem {
1193 ip_rule: Some(r.ip_rule.clone()),
1194 rule_desc: r.rule_desc.clone(),
1195 })
1196 .collect(),
1197 ),
1198 })
1199 .collect();
1200
1201 let result = model::DescribeIpGroupsResult {
1202 result: Some(entries),
1203 next_token: None,
1204 };
1205 wire::serialize_describe_ip_groups_response(&result)
1206 }
1207
1208 async fn handle_delete_ip_group(
1209 &self,
1210 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1211 body: &[u8],
1212 ) -> MockResponse {
1213 let input = match wire::deserialize_delete_ip_group_request(body) {
1214 Ok(v) => v,
1215 Err(e) => return json_error_response(400, "ValidationException", &e),
1216 };
1217 if input.group_id.is_empty() {
1218 return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1219 }
1220 let group_id = input.group_id;
1221
1222 let mut state = state.write().await;
1223 match state.delete_ip_group(&group_id) {
1224 Ok(()) => {
1225 let result = model::DeleteIpGroupResult {};
1226 wire::serialize_delete_ip_group_response(&result)
1227 }
1228 Err(e) => workspaces_error_response(&e),
1229 }
1230 }
1231
1232 async fn handle_authorize_ip_rules(
1233 &self,
1234 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1235 body: &[u8],
1236 ) -> MockResponse {
1237 let input = match wire::deserialize_authorize_ip_rules_request(body) {
1238 Ok(v) => v,
1239 Err(e) => return json_error_response(400, "ValidationException", &e),
1240 };
1241 if input.group_id.is_empty() {
1242 return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1243 }
1244 let group_id = input.group_id;
1245 let user_rules: Vec<IpRule> = input
1246 .user_rules
1247 .into_iter()
1248 .filter_map(|item| {
1249 item.ip_rule.map(|ip_rule| IpRule {
1250 ip_rule,
1251 rule_desc: item.rule_desc,
1252 })
1253 })
1254 .collect();
1255
1256 let mut state = state.write().await;
1257 match state.authorize_ip_rules(&group_id, user_rules) {
1258 Ok(()) => {
1259 let result = model::AuthorizeIpRulesResult {};
1260 wire::serialize_authorize_ip_rules_response(&result)
1261 }
1262 Err(e) => workspaces_error_response(&e),
1263 }
1264 }
1265
1266 async fn handle_revoke_ip_rules(
1267 &self,
1268 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1269 body: &[u8],
1270 ) -> MockResponse {
1271 let input = match wire::deserialize_revoke_ip_rules_request(body) {
1272 Ok(v) => v,
1273 Err(e) => return json_error_response(400, "ValidationException", &e),
1274 };
1275 if input.group_id.is_empty() {
1276 return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1277 }
1278 let group_id = input.group_id;
1279 let user_rules = input.user_rules;
1280
1281 let mut state = state.write().await;
1282 match state.revoke_ip_rules(&group_id, &user_rules) {
1283 Ok(()) => {
1284 let result = model::RevokeIpRulesResult {};
1285 wire::serialize_revoke_ip_rules_response(&result)
1286 }
1287 Err(e) => workspaces_error_response(&e),
1288 }
1289 }
1290
1291 async fn handle_update_rules_of_ip_group(
1292 &self,
1293 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1294 body: &[u8],
1295 ) -> MockResponse {
1296 let input = match wire::deserialize_update_rules_of_ip_group_request(body) {
1297 Ok(v) => v,
1298 Err(e) => return json_error_response(400, "ValidationException", &e),
1299 };
1300 if input.group_id.is_empty() {
1301 return json_error_response(400, "ValidationException", "Missing 'GroupId' field");
1302 }
1303 let group_id = input.group_id;
1304 let user_rules: Vec<IpRule> = input
1305 .user_rules
1306 .into_iter()
1307 .filter_map(|item| {
1308 item.ip_rule.map(|ip_rule| IpRule {
1309 ip_rule,
1310 rule_desc: item.rule_desc,
1311 })
1312 })
1313 .collect();
1314
1315 let mut state = state.write().await;
1316 match state.update_rules_of_ip_group(&group_id, user_rules) {
1317 Ok(()) => {
1318 let result = model::UpdateRulesOfIpGroupResult {};
1319 wire::serialize_update_rules_of_ip_group_response(&result)
1320 }
1321 Err(e) => workspaces_error_response(&e),
1322 }
1323 }
1324
1325 async fn handle_associate_ip_groups(
1326 &self,
1327 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1328 body: &[u8],
1329 ) -> MockResponse {
1330 let input = match wire::deserialize_associate_ip_groups_request(body) {
1331 Ok(v) => v,
1332 Err(e) => return json_error_response(400, "ValidationException", &e),
1333 };
1334 if input.directory_id.is_empty() {
1335 return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1336 }
1337 let directory_id = input.directory_id;
1338 let group_ids = input.group_ids;
1339
1340 let mut state = state.write().await;
1341 match state.associate_ip_groups(&directory_id, &group_ids) {
1342 Ok(()) => {
1343 let result = model::AssociateIpGroupsResult {};
1344 wire::serialize_associate_ip_groups_response(&result)
1345 }
1346 Err(e) => workspaces_error_response(&e),
1347 }
1348 }
1349
1350 async fn handle_disassociate_ip_groups(
1351 &self,
1352 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1353 body: &[u8],
1354 ) -> MockResponse {
1355 let input = match wire::deserialize_disassociate_ip_groups_request(body) {
1356 Ok(v) => v,
1357 Err(e) => return json_error_response(400, "ValidationException", &e),
1358 };
1359 if input.directory_id.is_empty() {
1360 return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1361 }
1362 let directory_id = input.directory_id;
1363 let group_ids = input.group_ids;
1364
1365 let mut state = state.write().await;
1366 match state.disassociate_ip_groups(&directory_id, &group_ids) {
1367 Ok(()) => {
1368 let result = model::DisassociateIpGroupsResult {};
1369 wire::serialize_disassociate_ip_groups_response(&result)
1370 }
1371 Err(e) => workspaces_error_response(&e),
1372 }
1373 }
1374
1375 async fn handle_create_connection_alias(
1378 &self,
1379 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1380 body: &[u8],
1381 account_id: &str,
1382 ) -> MockResponse {
1383 let input = match wire::deserialize_create_connection_alias_request(body) {
1384 Ok(v) => v,
1385 Err(e) => return json_error_response(400, "ValidationException", &e),
1386 };
1387 if input.connection_string.is_empty() {
1388 return json_error_response(
1389 400,
1390 "ValidationException",
1391 "Missing 'ConnectionString' field",
1392 );
1393 }
1394 let connection_string = input.connection_string;
1395
1396 let mut state = state.write().await;
1397 let alias = state.create_connection_alias(&connection_string, account_id);
1398
1399 let result = model::CreateConnectionAliasResult {
1400 alias_id: Some(alias.alias_id),
1401 };
1402 wire::serialize_create_connection_alias_response(&result)
1403 }
1404
1405 async fn handle_describe_connection_aliases(
1406 &self,
1407 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1408 body: &[u8],
1409 ) -> MockResponse {
1410 let input = match wire::deserialize_describe_connection_aliases_request(body) {
1411 Ok(v) => v,
1412 Err(e) => return json_error_response(400, "ValidationException", &e),
1413 };
1414 let alias_ids = input.alias_ids;
1415 let resource_id = input.resource_id.as_deref();
1416
1417 let state = state.read().await;
1418 let aliases = state.describe_connection_aliases(alias_ids.as_deref(), resource_id);
1419
1420 let entries: Vec<model::ConnectionAlias> = aliases
1421 .iter()
1422 .map(|a| model::ConnectionAlias {
1423 alias_id: Some(a.alias_id.clone()),
1424 connection_string: Some(a.connection_string.clone()),
1425 owner_account_id: Some(a.owner_account_id.clone()),
1426 state: Some(a.state.clone()),
1427 associations: if a.associations.is_empty() {
1428 None
1429 } else {
1430 Some(
1431 a.associations
1432 .iter()
1433 .map(|assoc| model::ConnectionAliasAssociation {
1434 connection_identifier: Some(assoc.connection_identifier.clone()),
1435 resource_id: assoc.resource_id.clone(),
1436 associated_account_id: assoc.associated_account_id.clone(),
1437 ..Default::default()
1438 })
1439 .collect(),
1440 )
1441 },
1442 })
1443 .collect();
1444
1445 let result = model::DescribeConnectionAliasesResult {
1446 connection_aliases: Some(entries),
1447 next_token: None,
1448 };
1449 wire::serialize_describe_connection_aliases_response(&result)
1450 }
1451
1452 async fn handle_delete_connection_alias(
1453 &self,
1454 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1455 body: &[u8],
1456 ) -> MockResponse {
1457 let input = match wire::deserialize_delete_connection_alias_request(body) {
1458 Ok(v) => v,
1459 Err(e) => return json_error_response(400, "ValidationException", &e),
1460 };
1461 if input.alias_id.is_empty() {
1462 return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1463 }
1464 let alias_id = input.alias_id;
1465
1466 let mut state = state.write().await;
1467 match state.delete_connection_alias(&alias_id) {
1468 Ok(()) => {
1469 let result = model::DeleteConnectionAliasResult {};
1470 wire::serialize_delete_connection_alias_response(&result)
1471 }
1472 Err(e) => workspaces_error_response(&e),
1473 }
1474 }
1475
1476 async fn handle_associate_connection_alias(
1477 &self,
1478 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1479 body: &[u8],
1480 ) -> MockResponse {
1481 let input = match wire::deserialize_associate_connection_alias_request(body) {
1482 Ok(v) => v,
1483 Err(e) => return json_error_response(400, "ValidationException", &e),
1484 };
1485 if input.alias_id.is_empty() {
1486 return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1487 }
1488 let alias_id = input.alias_id;
1489 if input.resource_id.is_empty() {
1490 return json_error_response(400, "ValidationException", "Missing 'ResourceId' field");
1491 }
1492 let resource_id = input.resource_id;
1493
1494 let mut state = state.write().await;
1495 match state.associate_connection_alias(&alias_id, &resource_id) {
1496 Ok(connection_identifier) => {
1497 let result = model::AssociateConnectionAliasResult {
1498 connection_identifier: Some(connection_identifier),
1499 };
1500 wire::serialize_associate_connection_alias_response(&result)
1501 }
1502 Err(e) => workspaces_error_response(&e),
1503 }
1504 }
1505
1506 async fn handle_disassociate_connection_alias(
1507 &self,
1508 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1509 body: &[u8],
1510 ) -> MockResponse {
1511 let input = match wire::deserialize_disassociate_connection_alias_request(body) {
1512 Ok(v) => v,
1513 Err(e) => return json_error_response(400, "ValidationException", &e),
1514 };
1515 if input.alias_id.is_empty() {
1516 return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1517 }
1518 let alias_id = input.alias_id;
1519
1520 let mut state = state.write().await;
1521 match state.disassociate_connection_alias(&alias_id) {
1522 Ok(()) => {
1523 let result = model::DisassociateConnectionAliasResult {};
1524 wire::serialize_disassociate_connection_alias_response(&result)
1525 }
1526 Err(e) => workspaces_error_response(&e),
1527 }
1528 }
1529
1530 async fn handle_describe_connection_alias_permissions(
1531 &self,
1532 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1533 body: &[u8],
1534 ) -> MockResponse {
1535 let input = match wire::deserialize_describe_connection_alias_permissions_request(body) {
1536 Ok(v) => v,
1537 Err(e) => return json_error_response(400, "ValidationException", &e),
1538 };
1539 if input.alias_id.is_empty() {
1540 return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1541 }
1542 let alias_id = input.alias_id;
1543
1544 let state = state.read().await;
1545 match state.describe_connection_alias_permissions(&alias_id) {
1546 Ok(perms) => {
1547 let entries: Vec<model::ConnectionAliasPermission> = perms
1548 .iter()
1549 .map(|p| model::ConnectionAliasPermission {
1550 shared_account_id: p.shared_account_id.clone(),
1551 allow_association: p.allow_association,
1552 })
1553 .collect();
1554
1555 let result = model::DescribeConnectionAliasPermissionsResult {
1556 alias_id: Some(alias_id),
1557 connection_alias_permissions: Some(entries),
1558 next_token: None,
1559 };
1560 wire::serialize_describe_connection_alias_permissions_response(&result)
1561 }
1562 Err(e) => workspaces_error_response(&e),
1563 }
1564 }
1565
1566 async fn handle_update_connection_alias_permission(
1567 &self,
1568 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1569 body: &[u8],
1570 ) -> MockResponse {
1571 let input = match wire::deserialize_update_connection_alias_permission_request(body) {
1572 Ok(v) => v,
1573 Err(e) => return json_error_response(400, "ValidationException", &e),
1574 };
1575 if input.alias_id.is_empty() {
1576 return json_error_response(400, "ValidationException", "Missing 'AliasId' field");
1577 }
1578 let alias_id = input.alias_id;
1579 let perm = input.connection_alias_permission;
1580 if perm.shared_account_id.is_empty() {
1581 return json_error_response(
1582 400,
1583 "ValidationException",
1584 "Missing 'SharedAccountId' in ConnectionAliasPermission",
1585 );
1586 }
1587 let shared_account_id = perm.shared_account_id;
1588 let allow_association = perm.allow_association;
1589
1590 let mut state = state.write().await;
1591 match state.update_connection_alias_permission(
1592 &alias_id,
1593 &shared_account_id,
1594 allow_association,
1595 ) {
1596 Ok(()) => {
1597 let result = model::UpdateConnectionAliasPermissionResult {};
1598 wire::serialize_update_connection_alias_permission_response(&result)
1599 }
1600 Err(e) => workspaces_error_response(&e),
1601 }
1602 }
1603
1604 async fn handle_describe_workspace_bundles(
1607 &self,
1608 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1609 body: &[u8],
1610 ) -> MockResponse {
1611 let input = match wire::deserialize_describe_workspace_bundles_request(body) {
1612 Ok(v) => v,
1613 Err(e) => return json_error_response(400, "ValidationException", &e),
1614 };
1615 let bundle_ids = input.bundle_ids;
1616 let owner = input.owner.as_deref();
1617
1618 let state = state.read().await;
1619 let bundles = state.describe_workspace_bundles(bundle_ids.as_deref(), owner);
1620
1621 let entries: Vec<model::WorkspaceBundle> = bundles
1622 .iter()
1623 .map(|b| model::WorkspaceBundle {
1624 bundle_id: Some(b.bundle_id.clone()),
1625 name: Some(b.name.clone()),
1626 owner: b.owner.clone(),
1627 description: b.description.clone(),
1628 bundle_type: b.bundle_type.clone(),
1629 compute_type: b.compute_type_name.as_ref().map(|n| model::ComputeType {
1630 name: Some(n.clone()),
1631 }),
1632 creation_time: rfc3339_to_epoch(&b.creation_time),
1633 root_storage: b.root_storage_capacity.map(|c| model::RootStorage {
1634 capacity: c.to_string(),
1635 }),
1636 user_storage: b.user_storage_capacity.map(|c| model::UserStorage {
1637 capacity: c.to_string(),
1638 }),
1639 ..Default::default()
1640 })
1641 .collect();
1642
1643 let result = model::DescribeWorkspaceBundlesResult {
1644 bundles: Some(entries),
1645 next_token: None,
1646 };
1647 wire::serialize_describe_workspace_bundles_response(&result)
1648 }
1649
1650 async fn handle_create_workspace_bundle(
1651 &self,
1652 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1653 body: &[u8],
1654 account_id: &str,
1655 ) -> MockResponse {
1656 let input = match wire::deserialize_create_workspace_bundle_request(body) {
1657 Ok(v) => v,
1658 Err(e) => return json_error_response(400, "ValidationException", &e),
1659 };
1660 if input.bundle_name.is_empty() {
1661 return json_error_response(400, "ValidationException", "Missing 'BundleName' field");
1662 }
1663 let bundle_name = input.bundle_name;
1664 let bundle_description = if input.bundle_description.is_empty() {
1665 None
1666 } else {
1667 Some(input.bundle_description)
1668 };
1669 if input.image_id.is_empty() {
1670 return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
1671 }
1672 let image_id = input.image_id;
1673 let compute_type = input
1674 .compute_type
1675 .name
1676 .unwrap_or_else(|| "VALUE".to_string());
1677 let user_storage = input.user_storage.capacity.parse::<i32>().unwrap_or(50);
1678 let root_storage = input
1679 .root_storage
1680 .and_then(|rs| rs.capacity.parse::<i32>().ok());
1681
1682 let mut state = state.write().await;
1683 match state.create_workspace_bundle(
1684 &bundle_name,
1685 bundle_description.as_deref(),
1686 &image_id,
1687 &compute_type,
1688 user_storage,
1689 root_storage,
1690 account_id,
1691 ) {
1692 Ok(bundle) => {
1693 let wb = model::WorkspaceBundle {
1694 bundle_id: Some(bundle.bundle_id.clone()),
1695 name: Some(bundle.name.clone()),
1696 owner: bundle.owner.clone(),
1697 description: bundle.description.clone(),
1698 compute_type: bundle
1699 .compute_type_name
1700 .as_ref()
1701 .map(|n| model::ComputeType {
1702 name: Some(n.clone()),
1703 }),
1704 creation_time: rfc3339_to_epoch(&bundle.creation_time),
1705 root_storage: bundle.root_storage_capacity.map(|c| model::RootStorage {
1706 capacity: c.to_string(),
1707 }),
1708 user_storage: bundle.user_storage_capacity.map(|c| model::UserStorage {
1709 capacity: c.to_string(),
1710 }),
1711 ..Default::default()
1712 };
1713 let result = model::CreateWorkspaceBundleResult {
1714 workspace_bundle: Some(wb),
1715 };
1716 wire::serialize_create_workspace_bundle_response(&result)
1717 }
1718 Err(e) => workspaces_error_response(&e),
1719 }
1720 }
1721
1722 async fn handle_delete_workspace_bundle(
1723 &self,
1724 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1725 body: &[u8],
1726 ) -> MockResponse {
1727 let input = match wire::deserialize_delete_workspace_bundle_request(body) {
1728 Ok(v) => v,
1729 Err(e) => return json_error_response(400, "ValidationException", &e),
1730 };
1731 let bundle_id = match input.bundle_id {
1732 Some(id) if !id.is_empty() => id,
1733 _ => {
1734 return json_error_response(400, "ValidationException", "Missing 'BundleId' field");
1735 }
1736 };
1737
1738 let mut state = state.write().await;
1739 match state.delete_workspace_bundle(&bundle_id) {
1740 Ok(()) => {
1741 let result = model::DeleteWorkspaceBundleResult {};
1742 wire::serialize_delete_workspace_bundle_response(&result)
1743 }
1744 Err(e) => workspaces_error_response(&e),
1745 }
1746 }
1747
1748 async fn handle_delete_workspace_image(
1749 &self,
1750 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1751 body: &[u8],
1752 ) -> MockResponse {
1753 let input = match wire::deserialize_delete_workspace_image_request(body) {
1754 Ok(v) => v,
1755 Err(e) => return json_error_response(400, "ValidationException", &e),
1756 };
1757 if input.image_id.is_empty() {
1758 return json_error_response(400, "ValidationException", "Missing 'ImageId' field");
1759 }
1760 let image_id = input.image_id;
1761
1762 let mut state = state.write().await;
1763 match state.delete_workspace_image(&image_id) {
1764 Ok(()) => {
1765 let result = model::DeleteWorkspaceImageResult {};
1766 wire::serialize_delete_workspace_image_response(&result)
1767 }
1768 Err(e) => workspaces_error_response(&e),
1769 }
1770 }
1771
1772 async fn handle_create_workspaces_pool(
1775 &self,
1776 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1777 body: &[u8],
1778 account_id: &str,
1779 region: &str,
1780 ) -> MockResponse {
1781 let input = match wire::deserialize_create_workspaces_pool_request(body) {
1782 Ok(v) => v,
1783 Err(e) => return json_error_response(400, "ValidationException", &e),
1784 };
1785 if input.pool_name.is_empty() {
1786 return json_error_response(400, "ValidationException", "Missing 'PoolName' field");
1787 }
1788 let pool_name = input.pool_name;
1789 if input.bundle_id.is_empty() {
1790 return json_error_response(400, "ValidationException", "Missing 'BundleId' field");
1791 }
1792 let bundle_id = input.bundle_id;
1793 if input.directory_id.is_empty() {
1794 return json_error_response(400, "ValidationException", "Missing 'DirectoryId' field");
1795 }
1796 let directory_id = input.directory_id;
1797 let description = if input.description.is_empty() {
1798 None
1799 } else {
1800 Some(input.description)
1801 };
1802
1803 let mut state = state.write().await;
1804 let pool = state.create_workspaces_pool(
1805 &pool_name,
1806 description.as_deref(),
1807 &bundle_id,
1808 &directory_id,
1809 account_id,
1810 region,
1811 );
1812
1813 let pool_model = pool_to_model(&pool);
1814 let result = model::CreateWorkspacesPoolResult {
1815 workspaces_pool: Some(pool_model),
1816 };
1817 wire::serialize_create_workspaces_pool_response(&result)
1818 }
1819
1820 async fn handle_describe_workspaces_pools(
1821 &self,
1822 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1823 body: &[u8],
1824 ) -> MockResponse {
1825 let input = match wire::deserialize_describe_workspaces_pools_request(body) {
1826 Ok(v) => v,
1827 Err(e) => return json_error_response(400, "ValidationException", &e),
1828 };
1829 let pool_ids = input.pool_ids;
1830
1831 let state = state.read().await;
1832 let pools = state.describe_workspaces_pools(pool_ids.as_deref());
1833
1834 let entries: Vec<model::WorkspacesPool> = pools.iter().map(|p| pool_to_model(p)).collect();
1835
1836 let result = model::DescribeWorkspacesPoolsResult {
1837 workspaces_pools: Some(entries),
1838 next_token: None,
1839 };
1840 wire::serialize_describe_workspaces_pools_response(&result)
1841 }
1842
1843 async fn handle_terminate_workspaces_pool(
1844 &self,
1845 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1846 body: &[u8],
1847 ) -> MockResponse {
1848 let input = match wire::deserialize_terminate_workspaces_pool_request(body) {
1849 Ok(v) => v,
1850 Err(e) => return json_error_response(400, "ValidationException", &e),
1851 };
1852 if input.pool_id.is_empty() {
1853 return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1854 }
1855 let pool_id = input.pool_id;
1856
1857 let mut state = state.write().await;
1858 match state.terminate_workspaces_pool(&pool_id) {
1859 Ok(pool) => {
1860 let pool_model = pool_to_model(&pool);
1861 let result = model::TerminateWorkspacesPoolResult {};
1862 let _ = pool_model;
1863 wire::serialize_terminate_workspaces_pool_response(&result)
1864 }
1865 Err(e) => workspaces_error_response(&e),
1866 }
1867 }
1868
1869 async fn handle_update_workspaces_pool(
1870 &self,
1871 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1872 body: &[u8],
1873 ) -> MockResponse {
1874 let input = match wire::deserialize_update_workspaces_pool_request(body) {
1875 Ok(v) => v,
1876 Err(e) => return json_error_response(400, "ValidationException", &e),
1877 };
1878 if input.pool_id.is_empty() {
1879 return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1880 }
1881 let pool_id = input.pool_id;
1882 let description = input.description.as_deref();
1883 let bundle_id = input.bundle_id.as_deref();
1884 let directory_id = input.directory_id.as_deref();
1885
1886 let mut state = state.write().await;
1887 match state.update_workspaces_pool(&pool_id, description, bundle_id, directory_id) {
1888 Ok(pool) => {
1889 let pool_model = pool_to_model(&pool);
1890 let result = model::UpdateWorkspacesPoolResult {
1891 workspaces_pool: Some(pool_model),
1892 };
1893 wire::serialize_update_workspaces_pool_response(&result)
1894 }
1895 Err(e) => workspaces_error_response(&e),
1896 }
1897 }
1898
1899 async fn handle_start_workspaces_pool(
1900 &self,
1901 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1902 body: &[u8],
1903 ) -> MockResponse {
1904 let input = match wire::deserialize_start_workspaces_pool_request(body) {
1905 Ok(v) => v,
1906 Err(e) => return json_error_response(400, "ValidationException", &e),
1907 };
1908 if input.pool_id.is_empty() {
1909 return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1910 }
1911 let pool_id = input.pool_id;
1912
1913 let mut state = state.write().await;
1914 match state.start_workspaces_pool(&pool_id) {
1915 Ok(()) => {
1916 let result = model::StartWorkspacesPoolResult {};
1917 wire::serialize_start_workspaces_pool_response(&result)
1918 }
1919 Err(e) => workspaces_error_response(&e),
1920 }
1921 }
1922
1923 async fn handle_stop_workspaces_pool(
1924 &self,
1925 state: &Arc<tokio::sync::RwLock<WorkSpacesState>>,
1926 body: &[u8],
1927 ) -> MockResponse {
1928 let input = match wire::deserialize_stop_workspaces_pool_request(body) {
1929 Ok(v) => v,
1930 Err(e) => return json_error_response(400, "ValidationException", &e),
1931 };
1932 if input.pool_id.is_empty() {
1933 return json_error_response(400, "ValidationException", "Missing 'PoolId' field");
1934 }
1935 let pool_id = input.pool_id;
1936
1937 let mut state = state.write().await;
1938 match state.stop_workspaces_pool(&pool_id) {
1939 Ok(()) => {
1940 let result = model::StopWorkspacesPoolResult {};
1941 wire::serialize_stop_workspaces_pool_response(&result)
1942 }
1943 Err(e) => workspaces_error_response(&e),
1944 }
1945 }
1946}
1947
1948fn workspace_to_model(ws: &Workspace) -> model::Workspace {
1952 model::Workspace {
1953 workspace_id: Some(ws.workspace_id.clone()),
1954 directory_id: Some(ws.directory_id.clone()),
1955 user_name: Some(ws.user_name.clone()),
1956 bundle_id: Some(ws.bundle_id.clone()),
1957 state: Some(ws.state.clone()),
1958 ip_address: Some(ws.ip_address.clone()),
1959 computer_name: Some(ws.computer_name.clone()),
1960 subnet_id: Some(ws.subnet_id.clone()),
1961 root_volume_encryption_enabled: Some(ws.root_volume_encryption_enabled),
1962 user_volume_encryption_enabled: Some(ws.user_volume_encryption_enabled),
1963 volume_encryption_key: ws.volume_encryption_key.clone(),
1964 workspace_properties: Some(model::WorkspaceProperties {
1965 running_mode: Some(ws.running_mode.clone()),
1966 running_mode_auto_stop_timeout_in_minutes: Some(
1967 ws.running_mode_auto_stop_timeout_in_minutes,
1968 ),
1969 root_volume_size_gib: Some(ws.root_volume_size_gib),
1970 user_volume_size_gib: Some(ws.user_volume_size_gib),
1971 ..Default::default()
1972 }),
1973 ..Default::default()
1974 }
1975}
1976
1977fn directory_to_model(dir: &WorkspaceDirectory) -> model::WorkspaceDirectory {
1979 model::WorkspaceDirectory {
1980 directory_id: Some(dir.directory_id.clone()),
1981 directory_name: Some(dir.directory_name.clone()),
1982 directory_type: Some(dir.directory_type.clone()),
1983 alias: Some(dir.alias.clone()),
1984 state: Some(dir.state.clone()),
1985 registration_code: Some(dir.registration_code.clone()),
1986 workspace_security_group_id: Some(dir.workspace_security_group_id.clone()),
1987 iam_role_id: Some(dir.iam_role_id.clone()),
1988 ..Default::default()
1989 }
1990}
1991
1992fn pool_to_model(pool: &WorkspacesPool) -> model::WorkspacesPool {
1994 model::WorkspacesPool {
1995 pool_id: Some(pool.pool_id.clone()),
1996 pool_arn: Some(pool.pool_arn.clone()),
1997 pool_name: Some(pool.pool_name.clone()),
1998 description: pool.description.clone(),
1999 state: Some(pool.state.clone()),
2000 bundle_id: Some(pool.bundle_id.clone()),
2001 directory_id: Some(pool.directory_id.clone()),
2002 created_at: rfc3339_to_epoch(&pool.created_at),
2003 ..Default::default()
2004 }
2005}
2006
2007fn json_error_response(status: u16, code: &str, message: &str) -> MockResponse {
2008 let body = json!({
2009 "__type": code,
2010 "message": message,
2011 });
2012 MockResponse::json(status, body.to_string())
2013}
2014
2015fn workspaces_error_response(e: &crate::state::WorkSpacesError) -> MockResponse {
2016 use crate::state::WorkSpacesError;
2017 let (status, error_type) = match e {
2018 WorkSpacesError::WorkspaceNotFound(_) => (400u16, "ResourceNotFoundException"),
2019 WorkSpacesError::ImageNotFound(_) => (400u16, "ResourceNotFoundException"),
2020 WorkSpacesError::DirectoryNotFound(_) => (400u16, "ResourceNotFoundException"),
2021 WorkSpacesError::DirectoryAlreadyRegistered(_) => {
2022 (400u16, "ResourceAlreadyExistsException")
2023 }
2024 WorkSpacesError::IpGroupNotFound(_) => (400u16, "ResourceNotFoundException"),
2025 WorkSpacesError::ConnectionAliasNotFound(_) => (400u16, "ResourceNotFoundException"),
2026 WorkSpacesError::BundleNotFound(_) => (400u16, "ResourceNotFoundException"),
2027 WorkSpacesError::PoolNotFound(_) => (400u16, "ResourceNotFoundException"),
2028 };
2029 json_error_response(status, error_type, &e.to_string())
2030}