1use axum::{
6 extract::{Path, State},
7 http::StatusCode,
8 response::{IntoResponse, Json, Response},
9};
10use mockforge_core::{
11 workspace::{EnvironmentColor, MockEnvironmentName, SyncDirection, SyncDirectoryStructure},
12 MultiTenantWorkspaceRegistry, Workspace, WorkspaceStats,
13};
14use serde::{Deserialize, Serialize};
15use serde_json::json;
16use std::{path::PathBuf, sync::Arc};
17
18#[derive(Debug, Clone)]
20pub struct WorkspaceState {
21 pub registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>,
23}
24
25impl WorkspaceState {
26 pub fn new(registry: Arc<tokio::sync::RwLock<MultiTenantWorkspaceRegistry>>) -> Self {
28 Self { registry }
29 }
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct ApiResponse<T> {
35 pub success: bool,
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub data: Option<T>,
38 #[serde(skip_serializing_if = "Option::is_none")]
39 pub error: Option<String>,
40}
41
42impl<T: Serialize> ApiResponse<T> {
43 pub fn success(data: T) -> Self {
44 Self {
45 success: true,
46 data: Some(data),
47 error: None,
48 }
49 }
50
51 pub fn error(message: String) -> Self {
52 Self {
53 success: false,
54 data: None,
55 error: Some(message),
56 }
57 }
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct WorkspaceListItem {
63 pub id: String,
64 pub name: String,
65 pub description: Option<String>,
66 pub enabled: bool,
67 pub stats: WorkspaceStats,
68 pub created_at: String,
69 pub updated_at: String,
70}
71
72#[derive(Debug, Clone, Deserialize)]
74pub struct CreateWorkspaceRequest {
75 pub id: String,
76 pub name: String,
77 pub description: Option<String>,
78}
79
80#[derive(Debug, Clone, Deserialize)]
82pub struct UpdateWorkspaceRequest {
83 pub name: Option<String>,
84 pub description: Option<String>,
85 pub enabled: Option<bool>,
86}
87
88pub async fn list_workspaces(
90 State(state): State<WorkspaceState>,
91) -> Result<Json<ApiResponse<Vec<WorkspaceListItem>>>, Response> {
92 let registry = state.registry.read().await;
93
94 match registry.list_workspaces() {
95 Ok(workspaces) => {
96 let items: Vec<WorkspaceListItem> = workspaces
97 .into_iter()
98 .map(|(id, tenant_ws)| WorkspaceListItem {
99 id,
100 name: tenant_ws.workspace.name.clone(),
101 description: tenant_ws.workspace.description.clone(),
102 enabled: tenant_ws.enabled,
103 stats: tenant_ws.stats.clone(),
104 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
105 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
106 })
107 .collect();
108
109 Ok(Json(ApiResponse::success(items)))
110 }
111 Err(e) => {
112 tracing::error!("Failed to list workspaces: {}", e);
113 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
114 .into_response())
115 }
116 }
117}
118
119pub async fn get_workspace(
121 State(state): State<WorkspaceState>,
122 Path(workspace_id): Path<String>,
123) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
124 let registry = state.registry.read().await;
125
126 match registry.get_workspace(&workspace_id) {
127 Ok(tenant_ws) => {
128 let item = WorkspaceListItem {
129 id: workspace_id.clone(),
130 name: tenant_ws.workspace.name.clone(),
131 description: tenant_ws.workspace.description.clone(),
132 enabled: tenant_ws.enabled,
133 stats: tenant_ws.stats.clone(),
134 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
135 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
136 };
137
138 Ok(Json(ApiResponse::success(item)))
139 }
140 Err(e) => {
141 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
142 Err((
143 StatusCode::NOT_FOUND,
144 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
145 )
146 .into_response())
147 }
148 }
149}
150
151pub async fn create_workspace(
153 State(state): State<WorkspaceState>,
154 Json(request): Json<CreateWorkspaceRequest>,
155) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
156 let mut registry = state.registry.write().await;
157
158 if registry.workspace_exists(&request.id) {
160 return Err((
161 StatusCode::CONFLICT,
162 Json(json!({"error": format!("Workspace '{}' already exists", request.id)})),
163 )
164 .into_response());
165 }
166
167 let mut workspace = Workspace::new(request.name.clone());
169 workspace.description = request.description.clone();
170
171 match registry.register_workspace(request.id.clone(), workspace) {
172 Ok(_) => {
173 match registry.get_workspace(&request.id) {
175 Ok(tenant_ws) => {
176 let item = WorkspaceListItem {
177 id: request.id.clone(),
178 name: tenant_ws.workspace.name.clone(),
179 description: tenant_ws.workspace.description.clone(),
180 enabled: tenant_ws.enabled,
181 stats: tenant_ws.stats.clone(),
182 created_at: tenant_ws.workspace.created_at.to_rfc3339(),
183 updated_at: tenant_ws.workspace.updated_at.to_rfc3339(),
184 };
185
186 tracing::info!("Created workspace: {}", request.id);
187 Ok(Json(ApiResponse::success(item)))
188 }
189 Err(e) => {
190 tracing::error!("Failed to retrieve created workspace: {}", e);
191 Err((
192 StatusCode::INTERNAL_SERVER_ERROR,
193 Json(json!({"error": "Workspace created but failed to retrieve"})),
194 )
195 .into_response())
196 }
197 }
198 }
199 Err(e) => {
200 tracing::error!("Failed to create workspace: {}", e);
201 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
202 .into_response())
203 }
204 }
205}
206
207pub async fn update_workspace(
209 State(state): State<WorkspaceState>,
210 Path(workspace_id): Path<String>,
211 Json(request): Json<UpdateWorkspaceRequest>,
212) -> Result<Json<ApiResponse<WorkspaceListItem>>, Response> {
213 let mut registry = state.registry.write().await;
214
215 let mut tenant_ws = match registry.get_workspace(&workspace_id) {
217 Ok(ws) => ws,
218 Err(_e) => {
219 return Err((
220 StatusCode::NOT_FOUND,
221 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
222 )
223 .into_response());
224 }
225 };
226
227 if let Some(name) = request.name {
229 tenant_ws.workspace.name = name;
230 }
231
232 if let Some(description) = request.description {
233 tenant_ws.workspace.description = Some(description);
234 }
235
236 tenant_ws.workspace.updated_at = chrono::Utc::now();
237
238 match registry.update_workspace(&workspace_id, tenant_ws.workspace.clone()) {
240 Ok(_) => {
241 if let Some(enabled) = request.enabled {
243 if let Err(e) = registry.set_workspace_enabled(&workspace_id, enabled) {
244 tracing::error!("Failed to set workspace enabled status: {}", e);
245 }
246 }
247
248 match registry.get_workspace(&workspace_id) {
250 Ok(updated_ws) => {
251 let item = WorkspaceListItem {
252 id: workspace_id.clone(),
253 name: updated_ws.workspace.name.clone(),
254 description: updated_ws.workspace.description.clone(),
255 enabled: updated_ws.enabled,
256 stats: updated_ws.stats.clone(),
257 created_at: updated_ws.workspace.created_at.to_rfc3339(),
258 updated_at: updated_ws.workspace.updated_at.to_rfc3339(),
259 };
260
261 tracing::info!("Updated workspace: {}", workspace_id);
262 Ok(Json(ApiResponse::success(item)))
263 }
264 Err(e) => {
265 tracing::error!("Failed to retrieve updated workspace: {}", e);
266 Err((
267 StatusCode::INTERNAL_SERVER_ERROR,
268 Json(json!({"error": "Workspace updated but failed to retrieve"})),
269 )
270 .into_response())
271 }
272 }
273 }
274 Err(e) => {
275 tracing::error!("Failed to update workspace: {}", e);
276 Err((StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": e.to_string()})))
277 .into_response())
278 }
279 }
280}
281
282pub async fn set_active_workspace(
284 State(state): State<WorkspaceState>,
285 Path(workspace_id): Path<String>,
286) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
287 let registry = state.registry.read().await;
288
289 match registry.get_workspace(&workspace_id) {
290 Ok(_) => Ok(Json(ApiResponse::success(json!({
291 "workspace_id": workspace_id,
292 "active": true
293 })))),
294 Err(_) => Err((
295 StatusCode::NOT_FOUND,
296 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
297 )
298 .into_response()),
299 }
300}
301
302pub async fn delete_workspace(
304 State(state): State<WorkspaceState>,
305 Path(workspace_id): Path<String>,
306) -> Result<Json<ApiResponse<String>>, Response> {
307 let mut registry = state.registry.write().await;
308
309 match registry.remove_workspace(&workspace_id) {
310 Ok(_) => {
311 tracing::info!("Deleted workspace: {}", workspace_id);
312 Ok(Json(ApiResponse::success(format!(
313 "Workspace '{}' deleted successfully",
314 workspace_id
315 ))))
316 }
317 Err(e) => {
318 tracing::error!("Failed to delete workspace {}: {}", workspace_id, e);
319 Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response())
320 }
321 }
322}
323
324pub async fn get_workspace_stats(
326 State(state): State<WorkspaceState>,
327 Path(workspace_id): Path<String>,
328) -> Result<Json<ApiResponse<WorkspaceStats>>, Response> {
329 let registry = state.registry.read().await;
330
331 match registry.get_workspace(&workspace_id) {
332 Ok(tenant_ws) => Ok(Json(ApiResponse::success(tenant_ws.stats.clone()))),
333 Err(e) => {
334 tracing::error!("Failed to get workspace stats for {}: {}", workspace_id, e);
335 Err((
336 StatusCode::NOT_FOUND,
337 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
338 )
339 .into_response())
340 }
341 }
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct MockEnvironmentResponse {
347 pub name: String,
348 pub id: String,
349 pub workspace_id: String,
350 pub reality_config: Option<serde_json::Value>,
351 pub chaos_config: Option<serde_json::Value>,
352 pub drift_budget_config: Option<serde_json::Value>,
353}
354
355#[derive(Debug, Clone, Serialize, Deserialize)]
357pub struct MockEnvironmentManagerResponse {
358 pub workspace_id: String,
359 pub active_environment: Option<String>,
360 pub environments: Vec<MockEnvironmentResponse>,
361}
362
363pub async fn list_mock_environments(
365 State(state): State<WorkspaceState>,
366 Path(workspace_id): Path<String>,
367) -> Result<Json<ApiResponse<MockEnvironmentManagerResponse>>, Response> {
368 let registry = state.registry.read().await;
369
370 match registry.get_workspace(&workspace_id) {
371 Ok(tenant_ws) => {
372 let mock_envs = tenant_ws.workspace.get_mock_environments();
373 let environments: Vec<MockEnvironmentResponse> = mock_envs
374 .list_environments()
375 .into_iter()
376 .map(|env| MockEnvironmentResponse {
377 name: env.name.as_str().to_string(),
378 id: env.id.clone(),
379 workspace_id: env.workspace_id.clone(),
380 reality_config: env
381 .reality_config
382 .as_ref()
383 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
384 chaos_config: env
385 .chaos_config
386 .as_ref()
387 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
388 drift_budget_config: env
389 .drift_budget_config
390 .as_ref()
391 .map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
392 })
393 .collect();
394
395 let response = MockEnvironmentManagerResponse {
396 workspace_id: workspace_id.clone(),
397 active_environment: mock_envs.active_environment.map(|e| e.as_str().to_string()),
398 environments,
399 };
400
401 Ok(Json(ApiResponse::success(response)))
402 }
403 Err(e) => {
404 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
405 Err((
406 StatusCode::NOT_FOUND,
407 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
408 )
409 .into_response())
410 }
411 }
412}
413
414pub async fn get_mock_environment(
416 State(state): State<WorkspaceState>,
417 Path((workspace_id, env_name)): Path<(String, String)>,
418) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
419 let registry = state.registry.read().await;
420
421 let env_name_enum = match env_name.to_lowercase().as_str() {
422 "dev" => MockEnvironmentName::Dev,
423 "test" => MockEnvironmentName::Test,
424 "prod" => MockEnvironmentName::Prod,
425 _ => {
426 return Err((
427 StatusCode::BAD_REQUEST,
428 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
429 )
430 .into_response());
431 }
432 };
433
434 match registry.get_workspace(&workspace_id) {
435 Ok(tenant_ws) => {
436 match tenant_ws.workspace.get_mock_environment(env_name_enum) {
437 Some(env) => {
438 let response = MockEnvironmentResponse {
439 name: env.name.as_str().to_string(),
440 id: env.id.clone(),
441 workspace_id: env.workspace_id.clone(),
442 reality_config: env.reality_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
443 chaos_config: env.chaos_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
444 drift_budget_config: env.drift_budget_config.as_ref().map(|c| serde_json::to_value(c).unwrap_or(serde_json::json!({}))),
445 };
446 Ok(Json(ApiResponse::success(response)))
447 }
448 None => Err((
449 StatusCode::NOT_FOUND,
450 Json(json!({"error": format!("Environment '{}' not found in workspace '{}'", env_name, workspace_id)})),
451 )
452 .into_response()),
453 }
454 }
455 Err(e) => {
456 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
457 Err((
458 StatusCode::NOT_FOUND,
459 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
460 )
461 .into_response())
462 }
463 }
464}
465
466#[derive(Debug, Clone, Deserialize)]
468pub struct SetActiveEnvironmentRequest {
469 pub environment: String,
470}
471
472pub async fn set_active_mock_environment(
473 State(state): State<WorkspaceState>,
474 Path(workspace_id): Path<String>,
475 Json(request): Json<SetActiveEnvironmentRequest>,
476) -> Result<Json<ApiResponse<String>>, Response> {
477 let mut registry = state.registry.write().await;
478
479 let env_name = match request.environment.to_lowercase().as_str() {
480 "dev" => MockEnvironmentName::Dev,
481 "test" => MockEnvironmentName::Test,
482 "prod" => MockEnvironmentName::Prod,
483 _ => {
484 return Err((
485 StatusCode::BAD_REQUEST,
486 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", request.environment)})),
487 )
488 .into_response());
489 }
490 };
491
492 match registry.get_workspace(&workspace_id) {
493 Ok(mut tenant_ws) => {
494 match tenant_ws.workspace.set_active_mock_environment(env_name) {
495 Ok(_) => {
496 if let Err(e) =
498 registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
499 {
500 tracing::error!("Failed to save workspace: {}", e);
501 return Err((
502 StatusCode::INTERNAL_SERVER_ERROR,
503 Json(json!({"error": "Failed to save workspace"})),
504 )
505 .into_response());
506 }
507
508 tracing::info!(
509 "Set active environment to '{}' for workspace '{}'",
510 request.environment,
511 workspace_id
512 );
513 Ok(Json(ApiResponse::success(format!(
514 "Active environment set to '{}'",
515 request.environment
516 ))))
517 }
518 Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
519 .into_response()),
520 }
521 }
522 Err(e) => {
523 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
524 Err((
525 StatusCode::NOT_FOUND,
526 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
527 )
528 .into_response())
529 }
530 }
531}
532
533#[derive(Debug, Clone, Deserialize)]
535pub struct UpdateMockEnvironmentRequest {
536 pub reality_config: Option<serde_json::Value>,
537 pub chaos_config: Option<serde_json::Value>,
538 pub drift_budget_config: Option<serde_json::Value>,
539}
540
541pub async fn update_mock_environment(
542 State(state): State<WorkspaceState>,
543 Path((workspace_id, env_name)): Path<(String, String)>,
544 Json(request): Json<UpdateMockEnvironmentRequest>,
545) -> Result<Json<ApiResponse<MockEnvironmentResponse>>, Response> {
546 let mut registry = state.registry.write().await;
547
548 let env_name_enum = match env_name.to_lowercase().as_str() {
549 "dev" => MockEnvironmentName::Dev,
550 "test" => MockEnvironmentName::Test,
551 "prod" => MockEnvironmentName::Prod,
552 _ => {
553 return Err((
554 StatusCode::BAD_REQUEST,
555 Json(json!({"error": format!("Invalid environment name: '{}'. Must be 'dev', 'test', or 'prod'", env_name)})),
556 )
557 .into_response());
558 }
559 };
560
561 match registry.get_workspace(&workspace_id) {
562 Ok(mut tenant_ws) => {
563 let reality_config =
565 request.reality_config.and_then(|v| serde_json::from_value(v).ok());
566 let chaos_config = request.chaos_config.and_then(|v| serde_json::from_value(v).ok());
567 let drift_budget_config =
568 request.drift_budget_config.and_then(|v| serde_json::from_value(v).ok());
569
570 match tenant_ws.workspace.set_mock_environment_config(
572 env_name_enum,
573 reality_config,
574 chaos_config,
575 drift_budget_config,
576 ) {
577 Ok(_) => {
578 if let Err(e) =
580 registry.update_workspace(&workspace_id, tenant_ws.workspace.clone())
581 {
582 tracing::error!("Failed to save workspace: {}", e);
583 return Err((
584 StatusCode::INTERNAL_SERVER_ERROR,
585 Json(json!({"error": "Failed to save workspace"})),
586 )
587 .into_response());
588 }
589
590 match tenant_ws.workspace.get_mock_environment(env_name_enum) {
592 Some(env) => {
593 let response = MockEnvironmentResponse {
594 name: env.name.as_str().to_string(),
595 id: env.id.clone(),
596 workspace_id: env.workspace_id.clone(),
597 reality_config: env.reality_config.as_ref().map(|c| {
598 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
599 }),
600 chaos_config: env.chaos_config.as_ref().map(|c| {
601 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
602 }),
603 drift_budget_config: env.drift_budget_config.as_ref().map(|c| {
604 serde_json::to_value(c).unwrap_or(serde_json::json!({}))
605 }),
606 };
607 tracing::info!(
608 "Updated environment '{}' for workspace '{}'",
609 env_name,
610 workspace_id
611 );
612 Ok(Json(ApiResponse::success(response)))
613 }
614 None => Err((
615 StatusCode::INTERNAL_SERVER_ERROR,
616 Json(json!({"error": "Failed to retrieve updated environment"})),
617 )
618 .into_response()),
619 }
620 }
621 Err(e) => Err((StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()})))
622 .into_response()),
623 }
624 }
625 Err(e) => {
626 tracing::error!("Failed to get workspace {}: {}", workspace_id, e);
627 Err((
628 StatusCode::NOT_FOUND,
629 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
630 )
631 .into_response())
632 }
633 }
634}
635
636#[derive(Debug, Clone, Deserialize)]
637pub struct CreateEnvironmentRequest {
638 pub name: String,
639 pub description: Option<String>,
640}
641
642#[derive(Debug, Clone, Deserialize)]
643pub struct UpdateEnvironmentRequest {
644 pub name: Option<String>,
645 pub description: Option<String>,
646 pub color: Option<EnvironmentColor>,
647}
648
649#[derive(Debug, Clone, Deserialize)]
650pub struct UpdateEnvironmentsOrderRequest {
651 pub environment_ids: Vec<String>,
652}
653
654#[derive(Debug, Clone, Deserialize)]
655pub struct UpdateWorkspacesOrderRequest {
656 pub workspace_ids: Vec<String>,
657}
658
659#[derive(Debug, Clone, Serialize, Deserialize)]
660pub struct EnvironmentVariableResponse {
661 pub id: String,
662 pub key: String,
663 pub value: String,
664 pub encrypted: bool,
665 #[serde(rename = "createdAt")]
666 pub created_at: String,
667}
668
669#[derive(Debug, Clone, Deserialize)]
670pub struct SetVariableRequest {
671 pub key: String,
672 pub value: String,
673}
674
675#[derive(Debug, Clone, Deserialize)]
676pub struct AutocompleteRequest {
677 pub input: String,
678 pub cursor_position: usize,
679 pub context: Option<String>,
680}
681
682#[derive(Debug, Clone, Serialize)]
683pub struct AutocompleteSuggestion {
684 pub text: String,
685 pub display_text: Option<String>,
686 pub kind: Option<String>,
687 pub documentation: Option<String>,
688}
689
690#[derive(Debug, Clone, Serialize)]
691pub struct AutocompleteResponse {
692 pub suggestions: Vec<AutocompleteSuggestion>,
693 pub start_position: usize,
694 pub end_position: usize,
695}
696
697#[derive(Debug, Clone, Deserialize)]
698pub struct ConfigureSyncRequest {
699 pub target_directory: String,
700 pub sync_direction: SyncDirection,
701 pub realtime_monitoring: bool,
702 pub directory_structure: Option<SyncDirectoryStructure>,
703 pub filename_pattern: Option<String>,
704}
705
706#[derive(Debug, Clone, Deserialize)]
707pub struct ConfirmSyncChangesRequest {
708 pub workspace_id: String,
709 pub changes: Vec<serde_json::Value>,
710 pub apply_all: bool,
711}
712
713pub async fn list_environments(
715 State(state): State<WorkspaceState>,
716 Path(workspace_id): Path<String>,
717) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
718 let registry = state.registry.read().await;
719 let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
720 (
721 StatusCode::NOT_FOUND,
722 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
723 )
724 .into_response()
725 })?;
726
727 let workspace = &tenant_ws.workspace;
728 let global_env_id = workspace.config.global_environment.id.clone();
729 let active_env_id = workspace.get_active_environment().id.clone();
730 let mut environments = Vec::new();
731
732 for env in workspace.get_environments_ordered() {
733 environments.push(json!({
734 "id": env.id.clone(),
735 "name": env.name.clone(),
736 "description": env.description.clone(),
737 "variable_count": env.variables.len(),
738 "is_global": env.id == global_env_id,
739 "active": env.id == active_env_id,
740 "color": env.color.clone(),
741 "order": env.order,
742 }));
743 }
744
745 Ok(Json(ApiResponse::success(json!({
746 "environments": environments,
747 "total": environments.len(),
748 }))))
749}
750
751pub async fn create_environment(
753 State(state): State<WorkspaceState>,
754 Path(workspace_id): Path<String>,
755 Json(request): Json<CreateEnvironmentRequest>,
756) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
757 let mut registry = state.registry.write().await;
758 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
759 (
760 StatusCode::NOT_FOUND,
761 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
762 )
763 .into_response()
764 })?;
765
766 let env_id = tenant_ws
767 .workspace
768 .create_environment(request.name, request.description)
769 .map_err(|e| {
770 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
771 })?;
772
773 registry
774 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
775 .map_err(|e| {
776 (
777 StatusCode::INTERNAL_SERVER_ERROR,
778 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
779 )
780 .into_response()
781 })?;
782
783 Ok(Json(ApiResponse::success(json!({
784 "id": env_id,
785 "message": "Environment created"
786 }))))
787}
788
789pub async fn update_environment(
791 State(state): State<WorkspaceState>,
792 Path((workspace_id, environment_id)): Path<(String, String)>,
793 Json(request): Json<UpdateEnvironmentRequest>,
794) -> Result<Json<ApiResponse<String>>, Response> {
795 let mut registry = state.registry.write().await;
796 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
797 (
798 StatusCode::NOT_FOUND,
799 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
800 )
801 .into_response()
802 })?;
803
804 if let Some(name) = &request.name {
805 let name_conflict = tenant_ws
806 .workspace
807 .get_environments()
808 .iter()
809 .any(|env| env.id != environment_id && env.name == *name);
810 if name_conflict {
811 return Err((
812 StatusCode::BAD_REQUEST,
813 Json(json!({"error": format!("Environment with name '{}' already exists", name)})),
814 )
815 .into_response());
816 }
817 }
818
819 let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
820 (
821 StatusCode::NOT_FOUND,
822 Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
823 )
824 .into_response()
825 })?;
826
827 if let Some(name) = request.name {
828 env.name = name;
829 }
830 if let Some(description) = request.description {
831 env.description = Some(description);
832 }
833 if let Some(color) = request.color {
834 env.color = Some(color);
835 }
836 env.updated_at = chrono::Utc::now();
837
838 registry
839 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
840 .map_err(|e| {
841 (
842 StatusCode::INTERNAL_SERVER_ERROR,
843 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
844 )
845 .into_response()
846 })?;
847
848 Ok(Json(ApiResponse::success("Environment updated".to_string())))
849}
850
851pub async fn delete_environment(
853 State(state): State<WorkspaceState>,
854 Path((workspace_id, environment_id)): Path<(String, String)>,
855) -> Result<Json<ApiResponse<String>>, Response> {
856 let mut registry = state.registry.write().await;
857 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
858 (
859 StatusCode::NOT_FOUND,
860 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
861 )
862 .into_response()
863 })?;
864
865 tenant_ws.workspace.delete_environment(&environment_id).map_err(|e| {
866 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
867 })?;
868
869 registry
870 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
871 .map_err(|e| {
872 (
873 StatusCode::INTERNAL_SERVER_ERROR,
874 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
875 )
876 .into_response()
877 })?;
878
879 Ok(Json(ApiResponse::success("Environment deleted".to_string())))
880}
881
882pub async fn set_active_environment(
884 State(state): State<WorkspaceState>,
885 Path((workspace_id, environment_id)): Path<(String, String)>,
886) -> Result<Json<ApiResponse<String>>, Response> {
887 let mut registry = state.registry.write().await;
888 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
889 (
890 StatusCode::NOT_FOUND,
891 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
892 )
893 .into_response()
894 })?;
895
896 tenant_ws.workspace.set_active_environment(Some(environment_id)).map_err(|e| {
897 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
898 })?;
899
900 registry
901 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
902 .map_err(|e| {
903 (
904 StatusCode::INTERNAL_SERVER_ERROR,
905 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
906 )
907 .into_response()
908 })?;
909
910 Ok(Json(ApiResponse::success("Environment activated".to_string())))
911}
912
913pub async fn update_environments_order(
915 State(state): State<WorkspaceState>,
916 Path(workspace_id): Path<String>,
917 Json(request): Json<UpdateEnvironmentsOrderRequest>,
918) -> Result<Json<ApiResponse<String>>, Response> {
919 let mut registry = state.registry.write().await;
920 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
921 (
922 StatusCode::NOT_FOUND,
923 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
924 )
925 .into_response()
926 })?;
927
928 tenant_ws
929 .workspace
930 .update_environments_order(request.environment_ids)
931 .map_err(|e| {
932 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
933 })?;
934
935 registry
936 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
937 .map_err(|e| {
938 (
939 StatusCode::INTERNAL_SERVER_ERROR,
940 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
941 )
942 .into_response()
943 })?;
944
945 Ok(Json(ApiResponse::success("Environment order updated".to_string())))
946}
947
948pub async fn update_workspaces_order(
950 State(state): State<WorkspaceState>,
951 Json(request): Json<UpdateWorkspacesOrderRequest>,
952) -> Result<Json<ApiResponse<String>>, Response> {
953 let mut registry = state.registry.write().await;
954
955 for workspace_id in &request.workspace_ids {
956 if !registry.workspace_exists(workspace_id) {
957 return Err((
958 StatusCode::NOT_FOUND,
959 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
960 )
961 .into_response());
962 }
963 }
964
965 for (idx, workspace_id) in request.workspace_ids.iter().enumerate() {
966 let mut tenant_ws = registry.get_workspace(workspace_id).map_err(|_| {
967 (
968 StatusCode::NOT_FOUND,
969 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
970 )
971 .into_response()
972 })?;
973 tenant_ws.workspace.order = idx as i32;
974 tenant_ws.workspace.updated_at = chrono::Utc::now();
975 registry
976 .update_workspace(workspace_id, tenant_ws.workspace.clone())
977 .map_err(|e| {
978 (
979 StatusCode::INTERNAL_SERVER_ERROR,
980 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
981 )
982 .into_response()
983 })?;
984 }
985
986 Ok(Json(ApiResponse::success("Workspace order updated".to_string())))
987}
988
989pub async fn get_environment_variables(
991 State(state): State<WorkspaceState>,
992 Path((workspace_id, environment_id)): Path<(String, String)>,
993) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
994 let registry = state.registry.write().await;
995 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
996 (
997 StatusCode::NOT_FOUND,
998 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
999 )
1000 .into_response()
1001 })?;
1002
1003 tenant_ws.workspace.set_active_environment(Some(environment_id)).map_err(|e| {
1004 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1005 })?;
1006
1007 let now = chrono::Utc::now().to_rfc3339();
1008 let mut variables = Vec::new();
1009 for (key, value) in tenant_ws.workspace.get_all_variables() {
1010 variables.push(EnvironmentVariableResponse {
1011 id: key.clone(),
1012 key,
1013 value,
1014 encrypted: false,
1015 created_at: now.clone(),
1016 });
1017 }
1018
1019 Ok(Json(ApiResponse::success(json!({
1020 "variables": variables
1021 }))))
1022}
1023
1024pub async fn set_environment_variable(
1026 State(state): State<WorkspaceState>,
1027 Path((workspace_id, environment_id)): Path<(String, String)>,
1028 Json(request): Json<SetVariableRequest>,
1029) -> Result<Json<ApiResponse<String>>, Response> {
1030 let mut registry = state.registry.write().await;
1031 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1032 (
1033 StatusCode::NOT_FOUND,
1034 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1035 )
1036 .into_response()
1037 })?;
1038
1039 let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
1040 (
1041 StatusCode::NOT_FOUND,
1042 Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
1043 )
1044 .into_response()
1045 })?;
1046
1047 env.set_variable(request.key, request.value);
1048
1049 registry
1050 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1051 .map_err(|e| {
1052 (
1053 StatusCode::INTERNAL_SERVER_ERROR,
1054 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1055 )
1056 .into_response()
1057 })?;
1058
1059 Ok(Json(ApiResponse::success("Environment variable set".to_string())))
1060}
1061
1062pub async fn remove_environment_variable(
1064 State(state): State<WorkspaceState>,
1065 Path((workspace_id, environment_id, variable_name)): Path<(String, String, String)>,
1066) -> Result<Json<ApiResponse<String>>, Response> {
1067 let mut registry = state.registry.write().await;
1068 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1069 (
1070 StatusCode::NOT_FOUND,
1071 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1072 )
1073 .into_response()
1074 })?;
1075
1076 let env = tenant_ws.workspace.get_environment_mut(&environment_id).ok_or_else(|| {
1077 (
1078 StatusCode::NOT_FOUND,
1079 Json(json!({"error": format!("Environment '{}' not found", environment_id)})),
1080 )
1081 .into_response()
1082 })?;
1083
1084 if !env.remove_variable(&variable_name) {
1085 return Err((
1086 StatusCode::NOT_FOUND,
1087 Json(json!({"error": format!("Variable '{}' not found", variable_name)})),
1088 )
1089 .into_response());
1090 }
1091
1092 registry
1093 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1094 .map_err(|e| {
1095 (
1096 StatusCode::INTERNAL_SERVER_ERROR,
1097 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1098 )
1099 .into_response()
1100 })?;
1101
1102 Ok(Json(ApiResponse::success("Environment variable removed".to_string())))
1103}
1104
1105pub async fn get_autocomplete_suggestions(
1107 State(state): State<WorkspaceState>,
1108 Path(workspace_id): Path<String>,
1109 Json(request): Json<AutocompleteRequest>,
1110) -> Result<Json<ApiResponse<AutocompleteResponse>>, Response> {
1111 let registry = state.registry.read().await;
1112 let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1113 (
1114 StatusCode::NOT_FOUND,
1115 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1116 )
1117 .into_response()
1118 })?;
1119
1120 let input = request.input;
1121 let cursor = request.cursor_position.min(input.len());
1122 let bytes = input.as_bytes();
1123 let mut start = cursor;
1124 while start > 0 {
1125 let ch = bytes[start - 1] as char;
1126 if ch.is_ascii_alphanumeric() || ch == '_' || ch == '.' || ch == '-' {
1127 start -= 1;
1128 } else {
1129 break;
1130 }
1131 }
1132 let prefix = &input[start..cursor];
1133 let prefix_lower = prefix.to_lowercase();
1134
1135 let mut suggestions: Vec<AutocompleteSuggestion> = Vec::new();
1136 for (key, _) in tenant_ws.workspace.get_all_variables() {
1137 if prefix.is_empty() || key.to_lowercase().contains(&prefix_lower) {
1138 suggestions.push(AutocompleteSuggestion {
1139 text: key.clone(),
1140 display_text: Some(key),
1141 kind: Some("variable".to_string()),
1142 documentation: Some("Workspace environment variable".to_string()),
1143 });
1144 }
1145 }
1146
1147 let builtins = [
1148 ("now", "Current timestamp"),
1149 ("uuid", "Generate UUID"),
1150 ("rand.int", "Random integer"),
1151 ("rand.float", "Random float"),
1152 ("faker.name", "Random name"),
1153 ("faker.email", "Random email"),
1154 ];
1155 for (token, doc) in builtins {
1156 if prefix.is_empty() || token.contains(prefix) {
1157 suggestions.push(AutocompleteSuggestion {
1158 text: token.to_string(),
1159 display_text: Some(token.to_string()),
1160 kind: Some("builtin".to_string()),
1161 documentation: Some(doc.to_string()),
1162 });
1163 }
1164 }
1165
1166 suggestions.sort_by(|a, b| a.text.cmp(&b.text));
1167 suggestions.dedup_by(|a, b| a.text == b.text);
1168 suggestions.truncate(20);
1169
1170 Ok(Json(ApiResponse::success(AutocompleteResponse {
1171 suggestions,
1172 start_position: start,
1173 end_position: cursor,
1174 })))
1175}
1176
1177pub async fn get_sync_status(
1179 State(state): State<WorkspaceState>,
1180 Path(workspace_id): Path<String>,
1181) -> Result<Json<ApiResponse<serde_json::Value>>, Response> {
1182 let registry = state.registry.read().await;
1183 let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1184 (
1185 StatusCode::NOT_FOUND,
1186 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1187 )
1188 .into_response()
1189 })?;
1190
1191 let sync = tenant_ws.workspace.get_sync_config();
1192 Ok(Json(ApiResponse::success(json!({
1193 "workspace_id": workspace_id,
1194 "enabled": sync.enabled,
1195 "target_directory": sync.target_directory,
1196 "sync_direction": sync.sync_direction,
1197 "realtime_monitoring": sync.realtime_monitoring,
1198 "last_sync": sync.last_sync,
1199 "status": if sync.enabled { "ready" } else { "disabled" },
1200 }))))
1201}
1202
1203pub async fn configure_sync(
1205 State(state): State<WorkspaceState>,
1206 Path(workspace_id): Path<String>,
1207 Json(request): Json<ConfigureSyncRequest>,
1208) -> Result<Json<ApiResponse<String>>, Response> {
1209 let mut registry = state.registry.write().await;
1210 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1211 (
1212 StatusCode::NOT_FOUND,
1213 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1214 )
1215 .into_response()
1216 })?;
1217
1218 let mut sync = tenant_ws.workspace.get_sync_config().clone();
1219 sync.enabled = true;
1220 sync.target_directory = Some(request.target_directory);
1221 sync.sync_direction = request.sync_direction;
1222 sync.realtime_monitoring = request.realtime_monitoring;
1223 if let Some(directory_structure) = request.directory_structure {
1224 sync.directory_structure = directory_structure;
1225 }
1226 if let Some(filename_pattern) = request.filename_pattern {
1227 sync.filename_pattern = filename_pattern;
1228 }
1229
1230 tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1231 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1232 })?;
1233
1234 registry
1235 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1236 .map_err(|e| {
1237 (
1238 StatusCode::INTERNAL_SERVER_ERROR,
1239 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1240 )
1241 .into_response()
1242 })?;
1243
1244 Ok(Json(ApiResponse::success("Sync configured".to_string())))
1245}
1246
1247pub async fn disable_sync(
1249 State(state): State<WorkspaceState>,
1250 Path(workspace_id): Path<String>,
1251) -> Result<Json<ApiResponse<String>>, Response> {
1252 let mut registry = state.registry.write().await;
1253 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1254 (
1255 StatusCode::NOT_FOUND,
1256 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1257 )
1258 .into_response()
1259 })?;
1260
1261 tenant_ws.workspace.disable_sync().map_err(|e| {
1262 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1263 })?;
1264
1265 registry
1266 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1267 .map_err(|e| {
1268 (
1269 StatusCode::INTERNAL_SERVER_ERROR,
1270 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1271 )
1272 .into_response()
1273 })?;
1274
1275 Ok(Json(ApiResponse::success("Sync disabled".to_string())))
1276}
1277
1278pub async fn trigger_sync(
1280 State(state): State<WorkspaceState>,
1281 Path(workspace_id): Path<String>,
1282) -> Result<Json<ApiResponse<String>>, Response> {
1283 let mut registry = state.registry.write().await;
1284 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1285 (
1286 StatusCode::NOT_FOUND,
1287 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1288 )
1289 .into_response()
1290 })?;
1291
1292 let mut sync = tenant_ws.workspace.get_sync_config().clone();
1293 if !sync.enabled {
1294 return Err((StatusCode::BAD_REQUEST, Json(json!({"error": "Sync is not enabled"})))
1295 .into_response());
1296 }
1297 sync.last_sync = Some(chrono::Utc::now());
1298 tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1299 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1300 })?;
1301
1302 registry
1303 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1304 .map_err(|e| {
1305 (
1306 StatusCode::INTERNAL_SERVER_ERROR,
1307 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1308 )
1309 .into_response()
1310 })?;
1311
1312 Ok(Json(ApiResponse::success("Sync triggered".to_string())))
1313}
1314
1315#[derive(Debug, Clone, Serialize)]
1316struct SyncChangeItem {
1317 change_type: String,
1318 path: String,
1319 description: String,
1320 requires_confirmation: bool,
1321}
1322
1323fn collect_sync_changes(
1324 target_directory: PathBuf,
1325 last_sync: Option<chrono::DateTime<chrono::Utc>>,
1326) -> Vec<SyncChangeItem> {
1327 const MAX_CHANGES: usize = 250;
1328
1329 if !target_directory.exists() {
1330 return vec![SyncChangeItem {
1331 change_type: "created".to_string(),
1332 path: target_directory.display().to_string(),
1333 description: "Sync target directory does not exist yet and will be created during sync"
1334 .to_string(),
1335 requires_confirmation: false,
1336 }];
1337 }
1338
1339 let mut changes = Vec::new();
1340 let mut stack = vec![target_directory.clone()];
1341
1342 while let Some(current_dir) = stack.pop() {
1343 let entries = match std::fs::read_dir(¤t_dir) {
1344 Ok(entries) => entries,
1345 Err(_) => continue,
1346 };
1347
1348 for entry in entries.flatten() {
1349 if changes.len() >= MAX_CHANGES {
1350 break;
1351 }
1352
1353 let path = entry.path();
1354 let metadata = match entry.metadata() {
1355 Ok(metadata) => metadata,
1356 Err(_) => continue,
1357 };
1358
1359 if metadata.is_dir() {
1360 stack.push(path);
1361 continue;
1362 }
1363
1364 let modified_after_sync = match (metadata.modified(), last_sync) {
1365 (Ok(modified), Some(last_sync_ts)) => {
1366 let modified_utc = chrono::DateTime::<chrono::Utc>::from(modified);
1367 modified_utc > last_sync_ts
1368 }
1369 (Ok(_), None) => true,
1370 (Err(_), _) => false,
1371 };
1372
1373 if !modified_after_sync {
1374 continue;
1375 }
1376
1377 let rel_path = path
1378 .strip_prefix(&target_directory)
1379 .map(|p| p.display().to_string())
1380 .unwrap_or_else(|_| path.display().to_string());
1381
1382 changes.push(SyncChangeItem {
1383 change_type: "modified".to_string(),
1384 path: rel_path.clone(),
1385 description: format!("Detected filesystem change in '{}'", rel_path),
1386 requires_confirmation: true,
1387 });
1388 }
1389
1390 if changes.len() >= MAX_CHANGES {
1391 break;
1392 }
1393 }
1394
1395 changes
1396}
1397
1398pub async fn get_sync_changes(
1400 State(state): State<WorkspaceState>,
1401 Path(workspace_id): Path<String>,
1402) -> Result<Json<ApiResponse<Vec<serde_json::Value>>>, Response> {
1403 let registry = state.registry.read().await;
1404 let tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1405 (
1406 StatusCode::NOT_FOUND,
1407 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1408 )
1409 .into_response()
1410 })?;
1411
1412 let sync = tenant_ws.workspace.get_sync_config().clone();
1413 let changes: Vec<serde_json::Value> = if !sync.enabled {
1414 Vec::new()
1415 } else if let Some(target_directory) = sync.target_directory.clone() {
1416 let target_directory = PathBuf::from(target_directory);
1417 tokio::task::spawn_blocking(move || collect_sync_changes(target_directory, sync.last_sync))
1418 .await
1419 .map_err(|e| {
1420 (
1421 StatusCode::INTERNAL_SERVER_ERROR,
1422 Json(json!({"error": format!("Failed to inspect sync directory: {}", e)})),
1423 )
1424 .into_response()
1425 })?
1426 .into_iter()
1427 .map(|change| serde_json::to_value(change).unwrap_or_default())
1428 .collect()
1429 } else {
1430 Vec::new()
1431 };
1432
1433 Ok(Json(ApiResponse::success(changes)))
1434}
1435
1436pub async fn confirm_sync_changes(
1438 State(state): State<WorkspaceState>,
1439 Path(workspace_id): Path<String>,
1440 Json(request): Json<ConfirmSyncChangesRequest>,
1441) -> Result<Json<ApiResponse<String>>, Response> {
1442 let mut registry = state.registry.write().await;
1443 let mut tenant_ws = registry.get_workspace(&workspace_id).map_err(|_| {
1444 (
1445 StatusCode::NOT_FOUND,
1446 Json(json!({"error": format!("Workspace '{}' not found", workspace_id)})),
1447 )
1448 .into_response()
1449 })?;
1450
1451 if request.workspace_id != workspace_id {
1452 return Err((
1453 StatusCode::BAD_REQUEST,
1454 Json(json!({"error": "workspace_id in body must match path"})),
1455 )
1456 .into_response());
1457 }
1458
1459 let mut sync = tenant_ws.workspace.get_sync_config().clone();
1460 sync.last_sync = Some(chrono::Utc::now());
1461 tenant_ws.workspace.configure_sync(sync).map_err(|e| {
1462 (StatusCode::BAD_REQUEST, Json(json!({"error": e.to_string()}))).into_response()
1463 })?;
1464
1465 registry
1466 .update_workspace(&workspace_id, tenant_ws.workspace.clone())
1467 .map_err(|e| {
1468 (
1469 StatusCode::INTERNAL_SERVER_ERROR,
1470 Json(json!({"error": format!("Failed to save workspace: {}", e)})),
1471 )
1472 .into_response()
1473 })?;
1474
1475 Ok(Json(ApiResponse::success(format!(
1476 "Sync changes confirmed ({} changes, apply_all={})",
1477 request.changes.len(),
1478 request.apply_all
1479 ))))
1480}
1481
1482#[cfg(test)]
1483mod tests {
1484 use super::*;
1485 use mockforge_core::MultiTenantConfig;
1486
1487 fn create_test_state() -> WorkspaceState {
1488 let config = MultiTenantConfig::default();
1489 let registry = MultiTenantWorkspaceRegistry::new(config);
1490 WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)))
1491 }
1492
1493 #[test]
1496 fn test_workspace_state_creation() {
1497 let state = create_test_state();
1498 let _ = state;
1500 }
1501
1502 #[test]
1503 fn test_workspace_state_clone() {
1504 let state = create_test_state();
1505 let cloned = state.clone();
1506 let _ = cloned;
1508 }
1509
1510 #[test]
1511 fn test_workspace_state_debug() {
1512 let state = create_test_state();
1513 let debug = format!("{:?}", state);
1514 assert!(debug.contains("WorkspaceState"));
1515 }
1516
1517 #[test]
1520 fn test_api_response_success() {
1521 let response: ApiResponse<String> = ApiResponse::success("test data".to_string());
1522 assert!(response.success);
1523 assert!(response.data.is_some());
1524 assert!(response.error.is_none());
1525 }
1526
1527 #[test]
1528 fn test_api_response_error() {
1529 let response: ApiResponse<String> = ApiResponse::error("test error".to_string());
1530 assert!(!response.success);
1531 assert!(response.data.is_none());
1532 assert!(response.error.is_some());
1533 }
1534
1535 #[test]
1536 fn test_api_response_serialization() {
1537 let response = ApiResponse::success("data".to_string());
1538 let json = serde_json::to_string(&response).unwrap();
1539 assert!(json.contains("success"));
1540 assert!(json.contains("data"));
1541 }
1542
1543 #[test]
1544 fn test_api_response_error_serialization() {
1545 let response: ApiResponse<()> = ApiResponse::error("something went wrong".to_string());
1546 let json = serde_json::to_string(&response).unwrap();
1547 assert!(json.contains("error"));
1548 assert!(json.contains("something went wrong"));
1549 }
1550
1551 #[test]
1554 fn test_create_workspace_request_minimal() {
1555 let request = CreateWorkspaceRequest {
1556 id: "ws-123".to_string(),
1557 name: "My Workspace".to_string(),
1558 description: None,
1559 };
1560
1561 assert_eq!(request.id, "ws-123");
1562 assert_eq!(request.name, "My Workspace");
1563 assert!(request.description.is_none());
1564 }
1565
1566 #[test]
1567 fn test_create_workspace_request_full() {
1568 let request = CreateWorkspaceRequest {
1569 id: "ws-456".to_string(),
1570 name: "Full Workspace".to_string(),
1571 description: Some("A complete workspace".to_string()),
1572 };
1573
1574 assert!(request.description.is_some());
1575 }
1576
1577 #[test]
1578 fn test_create_workspace_request_deserialization() {
1579 let json = r#"{
1580 "id": "test-ws",
1581 "name": "Test",
1582 "description": "Test workspace"
1583 }"#;
1584
1585 let request: CreateWorkspaceRequest = serde_json::from_str(json).unwrap();
1586 assert_eq!(request.id, "test-ws");
1587 assert_eq!(request.name, "Test");
1588 }
1589
1590 #[test]
1593 fn test_update_workspace_request_empty() {
1594 let request = UpdateWorkspaceRequest {
1595 name: None,
1596 description: None,
1597 enabled: None,
1598 };
1599
1600 assert!(request.name.is_none());
1601 assert!(request.description.is_none());
1602 assert!(request.enabled.is_none());
1603 }
1604
1605 #[test]
1606 fn test_update_workspace_request_partial() {
1607 let request = UpdateWorkspaceRequest {
1608 name: Some("New Name".to_string()),
1609 description: None,
1610 enabled: Some(false),
1611 };
1612
1613 assert!(request.name.is_some());
1614 assert!(request.enabled.is_some());
1615 }
1616
1617 #[test]
1618 fn test_update_workspace_request_deserialization() {
1619 let json = r#"{
1620 "name": "Updated",
1621 "enabled": true
1622 }"#;
1623
1624 let request: UpdateWorkspaceRequest = serde_json::from_str(json).unwrap();
1625 assert_eq!(request.name, Some("Updated".to_string()));
1626 assert_eq!(request.enabled, Some(true));
1627 }
1628
1629 #[test]
1632 fn test_workspace_list_item_creation() {
1633 let item = WorkspaceListItem {
1634 id: "item-1".to_string(),
1635 name: "Test Item".to_string(),
1636 description: Some("Description".to_string()),
1637 enabled: true,
1638 stats: WorkspaceStats::default(),
1639 created_at: "2024-01-01T00:00:00Z".to_string(),
1640 updated_at: "2024-01-02T00:00:00Z".to_string(),
1641 };
1642
1643 assert_eq!(item.id, "item-1");
1644 assert!(item.enabled);
1645 }
1646
1647 #[test]
1648 fn test_workspace_list_item_serialization() {
1649 let item = WorkspaceListItem {
1650 id: "ser-test".to_string(),
1651 name: "Serialize Test".to_string(),
1652 description: None,
1653 enabled: false,
1654 stats: WorkspaceStats::default(),
1655 created_at: "2024-01-01T00:00:00Z".to_string(),
1656 updated_at: "2024-01-01T00:00:00Z".to_string(),
1657 };
1658
1659 let json = serde_json::to_string(&item).unwrap();
1660 assert!(json.contains("ser-test"));
1661 assert!(json.contains("Serialize Test"));
1662 }
1663
1664 #[test]
1665 fn test_workspace_list_item_clone() {
1666 let item = WorkspaceListItem {
1667 id: "clone-test".to_string(),
1668 name: "Clone Test".to_string(),
1669 description: None,
1670 enabled: true,
1671 stats: WorkspaceStats::default(),
1672 created_at: "2024-01-01T00:00:00Z".to_string(),
1673 updated_at: "2024-01-01T00:00:00Z".to_string(),
1674 };
1675
1676 let cloned = item.clone();
1677 assert_eq!(cloned.id, item.id);
1678 assert_eq!(cloned.enabled, item.enabled);
1679 }
1680
1681 #[test]
1684 fn test_mock_environment_response_creation() {
1685 let response = MockEnvironmentResponse {
1686 name: "dev".to_string(),
1687 id: "env-123".to_string(),
1688 workspace_id: "ws-456".to_string(),
1689 reality_config: None,
1690 chaos_config: None,
1691 drift_budget_config: None,
1692 };
1693
1694 assert_eq!(response.name, "dev");
1695 assert_eq!(response.id, "env-123");
1696 }
1697
1698 #[test]
1699 fn test_mock_environment_response_with_configs() {
1700 let response = MockEnvironmentResponse {
1701 name: "test".to_string(),
1702 id: "env-test".to_string(),
1703 workspace_id: "ws-test".to_string(),
1704 reality_config: Some(serde_json::json!({"level": "high"})),
1705 chaos_config: Some(serde_json::json!({"enabled": true})),
1706 drift_budget_config: Some(serde_json::json!({"max_drift": 0.1})),
1707 };
1708
1709 assert!(response.reality_config.is_some());
1710 assert!(response.chaos_config.is_some());
1711 assert!(response.drift_budget_config.is_some());
1712 }
1713
1714 #[test]
1715 fn test_mock_environment_response_serialization() {
1716 let response = MockEnvironmentResponse {
1717 name: "prod".to_string(),
1718 id: "env-prod".to_string(),
1719 workspace_id: "ws-prod".to_string(),
1720 reality_config: None,
1721 chaos_config: None,
1722 drift_budget_config: None,
1723 };
1724
1725 let json = serde_json::to_string(&response).unwrap();
1726 assert!(json.contains("prod"));
1727 assert!(json.contains("env-prod"));
1728 }
1729
1730 #[test]
1733 fn test_mock_environment_manager_response_empty() {
1734 let response = MockEnvironmentManagerResponse {
1735 workspace_id: "ws-empty".to_string(),
1736 active_environment: None,
1737 environments: vec![],
1738 };
1739
1740 assert!(response.active_environment.is_none());
1741 assert!(response.environments.is_empty());
1742 }
1743
1744 #[test]
1745 fn test_mock_environment_manager_response_with_environments() {
1746 let response = MockEnvironmentManagerResponse {
1747 workspace_id: "ws-full".to_string(),
1748 active_environment: Some("dev".to_string()),
1749 environments: vec![
1750 MockEnvironmentResponse {
1751 name: "dev".to_string(),
1752 id: "env-dev".to_string(),
1753 workspace_id: "ws-full".to_string(),
1754 reality_config: None,
1755 chaos_config: None,
1756 drift_budget_config: None,
1757 },
1758 MockEnvironmentResponse {
1759 name: "test".to_string(),
1760 id: "env-test".to_string(),
1761 workspace_id: "ws-full".to_string(),
1762 reality_config: None,
1763 chaos_config: None,
1764 drift_budget_config: None,
1765 },
1766 ],
1767 };
1768
1769 assert_eq!(response.active_environment, Some("dev".to_string()));
1770 assert_eq!(response.environments.len(), 2);
1771 }
1772
1773 #[test]
1776 fn test_set_active_environment_request_creation() {
1777 let request = SetActiveEnvironmentRequest {
1778 environment: "prod".to_string(),
1779 };
1780
1781 assert_eq!(request.environment, "prod");
1782 }
1783
1784 #[test]
1785 fn test_set_active_environment_request_deserialization() {
1786 let json = r#"{"environment": "test"}"#;
1787 let request: SetActiveEnvironmentRequest = serde_json::from_str(json).unwrap();
1788 assert_eq!(request.environment, "test");
1789 }
1790
1791 #[test]
1794 fn test_update_mock_environment_request_empty() {
1795 let request = UpdateMockEnvironmentRequest {
1796 reality_config: None,
1797 chaos_config: None,
1798 drift_budget_config: None,
1799 };
1800
1801 assert!(request.reality_config.is_none());
1802 }
1803
1804 #[test]
1805 fn test_update_mock_environment_request_with_configs() {
1806 let request = UpdateMockEnvironmentRequest {
1807 reality_config: Some(serde_json::json!({"level": "medium"})),
1808 chaos_config: Some(serde_json::json!({"rate": 0.5})),
1809 drift_budget_config: None,
1810 };
1811
1812 assert!(request.reality_config.is_some());
1813 assert!(request.chaos_config.is_some());
1814 }
1815
1816 #[tokio::test]
1819 async fn test_create_workspace() {
1820 let state = create_test_state();
1821
1822 let request = CreateWorkspaceRequest {
1823 id: "test".to_string(),
1824 name: "Test Workspace".to_string(),
1825 description: Some("Test description".to_string()),
1826 };
1827
1828 let result = create_workspace(State(state.clone()), Json(request)).await.unwrap();
1829
1830 assert!(result.0.success);
1831 assert_eq!(result.0.data.as_ref().unwrap().id, "test");
1832 }
1833
1834 #[tokio::test]
1835 async fn test_list_workspaces() {
1836 let state = create_test_state();
1837
1838 let request = CreateWorkspaceRequest {
1840 id: "test".to_string(),
1841 name: "Test Workspace".to_string(),
1842 description: None,
1843 };
1844
1845 let _ = create_workspace(State(state.clone()), Json(request)).await;
1846
1847 let result = list_workspaces(State(state)).await.unwrap();
1848
1849 assert!(result.0.success);
1850 assert!(!result.0.data.unwrap().is_empty());
1851 }
1852
1853 #[tokio::test]
1854 async fn test_get_workspace() {
1855 let state = create_test_state();
1856
1857 let request = CreateWorkspaceRequest {
1859 id: "get-test".to_string(),
1860 name: "Get Test Workspace".to_string(),
1861 description: None,
1862 };
1863
1864 let _ = create_workspace(State(state.clone()), Json(request)).await;
1865
1866 let result = get_workspace(State(state), Path("get-test".to_string())).await.unwrap();
1867
1868 assert!(result.0.success);
1869 assert_eq!(result.0.data.as_ref().unwrap().id, "get-test");
1870 }
1871
1872 #[tokio::test]
1873 async fn test_get_workspace_not_found() {
1874 let state = create_test_state();
1875
1876 let result = get_workspace(State(state), Path("nonexistent".to_string())).await;
1877
1878 assert!(result.is_err());
1879 }
1880
1881 #[tokio::test]
1882 async fn test_create_duplicate_workspace() {
1883 let state = create_test_state();
1884
1885 let request = CreateWorkspaceRequest {
1886 id: "duplicate".to_string(),
1887 name: "First".to_string(),
1888 description: None,
1889 };
1890
1891 let _ = create_workspace(State(state.clone()), Json(request)).await;
1892
1893 let request2 = CreateWorkspaceRequest {
1894 id: "duplicate".to_string(),
1895 name: "Second".to_string(),
1896 description: None,
1897 };
1898
1899 let result = create_workspace(State(state), Json(request2)).await;
1900 assert!(result.is_err());
1901 }
1902
1903 #[tokio::test]
1904 async fn test_delete_workspace() {
1905 let state = create_test_state();
1906
1907 let request = CreateWorkspaceRequest {
1909 id: "delete-test".to_string(),
1910 name: "Delete Test".to_string(),
1911 description: None,
1912 };
1913
1914 let _ = create_workspace(State(state.clone()), Json(request)).await;
1915
1916 let result = delete_workspace(State(state.clone()), Path("delete-test".to_string())).await;
1917
1918 assert!(result.is_ok());
1919 assert!(result.unwrap().0.success);
1920
1921 let get_result = get_workspace(State(state), Path("delete-test".to_string())).await;
1923 assert!(get_result.is_err());
1924 }
1925
1926 #[tokio::test]
1927 async fn test_update_workspace() {
1928 let state = create_test_state();
1929
1930 let create_request = CreateWorkspaceRequest {
1932 id: "update-test".to_string(),
1933 name: "Original Name".to_string(),
1934 description: None,
1935 };
1936
1937 let _ = create_workspace(State(state.clone()), Json(create_request)).await;
1938
1939 let update_request = UpdateWorkspaceRequest {
1941 name: Some("Updated Name".to_string()),
1942 description: Some("New description".to_string()),
1943 enabled: Some(false),
1944 };
1945
1946 let result = update_workspace(
1947 State(state.clone()),
1948 Path("update-test".to_string()),
1949 Json(update_request),
1950 )
1951 .await;
1952
1953 assert!(result.is_ok());
1954 let response = result.unwrap();
1955 assert!(response.0.success);
1956 assert_eq!(response.0.data.as_ref().unwrap().name, "Updated Name");
1957 }
1958
1959 #[tokio::test]
1960 async fn test_get_workspace_stats() {
1961 let state = create_test_state();
1962
1963 let request = CreateWorkspaceRequest {
1965 id: "stats-test".to_string(),
1966 name: "Stats Test".to_string(),
1967 description: None,
1968 };
1969
1970 let _ = create_workspace(State(state.clone()), Json(request)).await;
1971
1972 let result = get_workspace_stats(State(state), Path("stats-test".to_string())).await;
1973
1974 assert!(result.is_ok());
1975 assert!(result.unwrap().0.success);
1976 }
1977}