1pub mod core;
12pub mod environment;
13pub mod mock_environment;
14pub mod registry;
15pub mod request;
16pub mod scenario_promotion;
17pub mod sync;
18
19pub use environment::*;
21pub use mock_environment::*;
22pub use registry::*;
23pub use request::*;
24pub use scenario_promotion::*;
25pub use sync::*;
26
27use crate::config::AuthConfig;
29use crate::encryption::AutoEncryptionConfig;
30use crate::fidelity::FidelityScore;
31use crate::reality::RealityLevel;
32use crate::routing::{HttpMethod, Route, RouteRegistry};
33use crate::{Error, Result};
34use chrono::{DateTime, Utc};
35use serde::{Deserialize, Serialize};
36use std::collections::HashMap;
37use uuid::Uuid;
38
39pub type EntityId = String;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Workspace {
45 pub id: EntityId,
47 pub name: String,
49 pub description: Option<String>,
51 pub created_at: DateTime<Utc>,
53 pub updated_at: DateTime<Utc>,
55 pub tags: Vec<String>,
57 pub config: WorkspaceConfig,
59 pub folders: Vec<Folder>,
61 pub requests: Vec<MockRequest>,
63 #[serde(default)]
65 pub order: i32,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct FolderInheritanceConfig {
71 #[serde(default)]
73 pub headers: HashMap<String, String>,
74 pub auth: Option<AuthConfig>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct Folder {
81 pub id: EntityId,
83 pub name: String,
85 pub description: Option<String>,
87 pub parent_id: Option<EntityId>,
89 pub created_at: DateTime<Utc>,
91 pub updated_at: DateTime<Utc>,
93 pub tags: Vec<String>,
95 #[serde(default)]
97 pub inheritance: FolderInheritanceConfig,
98 pub folders: Vec<Folder>,
100 pub requests: Vec<MockRequest>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct MockRequest {
107 pub id: EntityId,
109 pub name: String,
111 pub description: Option<String>,
113 pub method: HttpMethod,
115 pub path: String,
117 pub headers: HashMap<String, String>,
119 pub query_params: HashMap<String, String>,
121 pub body: Option<String>,
123 pub response: MockResponse,
125 pub response_history: Vec<ResponseHistoryEntry>,
127 pub created_at: DateTime<Utc>,
129 pub updated_at: DateTime<Utc>,
131 pub tags: Vec<String>,
133 pub auth: Option<AuthConfig>,
135 pub priority: i32,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct MockResponse {
142 pub status_code: u16,
144 pub headers: HashMap<String, String>,
146 pub body: Option<String>,
148 pub content_type: Option<String>,
150 pub delay_ms: Option<u64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ResponseHistoryEntry {
157 pub id: String,
159 pub executed_at: DateTime<Utc>,
161 pub request_method: HttpMethod,
163 pub request_path: String,
165 pub request_headers: HashMap<String, String>,
167 pub request_body: Option<String>,
169 pub response_status_code: u16,
171 pub response_headers: HashMap<String, String>,
173 pub response_body: Option<String>,
175 pub response_time_ms: u64,
177 pub response_size_bytes: u64,
179 pub error_message: Option<String>,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct EnvironmentColor {
186 pub hex: String,
188 pub name: Option<String>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct Environment {
195 pub id: EntityId,
197 pub name: String,
199 pub description: Option<String>,
201 pub color: Option<EnvironmentColor>,
203 pub variables: HashMap<String, String>,
205 pub created_at: DateTime<Utc>,
207 pub updated_at: DateTime<Utc>,
209 #[serde(default)]
211 pub order: i32,
212 #[serde(default)]
214 pub sharable: bool,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
219pub struct SyncConfig {
220 pub enabled: bool,
222 pub target_directory: Option<String>,
224 pub directory_structure: SyncDirectoryStructure,
226 pub sync_direction: SyncDirection,
228 pub include_metadata: bool,
230 pub realtime_monitoring: bool,
232 pub filename_pattern: String,
234 pub exclude_pattern: Option<String>,
236 pub force_overwrite: bool,
238 pub last_sync: Option<DateTime<Utc>>,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
244pub enum SyncDirectoryStructure {
245 Flat,
247 Nested,
249 Grouped,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
255pub enum SyncDirection {
256 Manual,
258 WorkspaceToDirectory,
260 Bidirectional,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct WorkspaceConfig {
267 pub base_url: Option<String>,
269 pub default_headers: HashMap<String, String>,
271 pub auth: Option<AuthConfig>,
273 pub global_environment: Environment,
275 pub environments: Vec<Environment>,
277 pub active_environment_id: Option<EntityId>,
279 pub sync: SyncConfig,
281 #[serde(default)]
283 pub auto_encryption: AutoEncryptionConfig,
284 #[serde(default)]
287 pub reality_level: Option<RealityLevel>,
288 #[serde(default, skip_serializing_if = "Option::is_none")]
291 pub fidelity_score: Option<FidelityScore>,
292}
293
294#[derive(Debug, Clone)]
296pub struct WorkspaceRegistry {
297 workspaces: HashMap<EntityId, Workspace>,
298 active_workspace: Option<EntityId>,
299}
300
301impl Workspace {
302 pub fn new(name: String) -> Self {
304 let now = Utc::now();
305 Self {
306 id: Uuid::new_v4().to_string(),
307 name,
308 description: None,
309 created_at: now,
310 updated_at: now,
311 tags: Vec::new(),
312 config: WorkspaceConfig::default(),
313 folders: Vec::new(),
314 requests: Vec::new(),
315 order: 0, }
317 }
318
319 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
321 let folder = Folder::new(name);
322 let id = folder.id.clone();
323 self.folders.push(folder);
324 self.updated_at = Utc::now();
325 Ok(id)
326 }
327
328 pub fn create_environment(
330 &mut self,
331 name: String,
332 description: Option<String>,
333 ) -> Result<EntityId> {
334 if self.config.environments.iter().any(|env| env.name == name) {
336 return Err(Error::generic(format!("Environment with name '{}' already exists", name)));
337 }
338
339 let mut environment = Environment::new(name);
340 environment.description = description;
341
342 environment.order = self.config.environments.len() as i32;
344
345 let id = environment.id.clone();
346
347 self.config.environments.push(environment);
348 self.updated_at = Utc::now();
349 Ok(id)
350 }
351
352 pub fn get_environments(&self) -> Vec<&Environment> {
354 let mut all_envs = vec![&self.config.global_environment];
355 all_envs.extend(self.config.environments.iter());
356 all_envs
357 }
358
359 pub fn get_environment(&self, id: &str) -> Option<&Environment> {
361 if self.config.global_environment.id == id {
362 Some(&self.config.global_environment)
363 } else {
364 self.config.environments.iter().find(|env| env.id == id)
365 }
366 }
367
368 pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
370 if self.config.global_environment.id == id {
371 Some(&mut self.config.global_environment)
372 } else {
373 self.config.environments.iter_mut().find(|env| env.id == id)
374 }
375 }
376
377 pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
379 if let Some(ref id) = environment_id {
380 if self.get_environment(id).is_none() {
381 return Err(Error::generic(format!("Environment with ID '{}' not found", id)));
382 }
383 }
384 self.config.active_environment_id = environment_id;
385 self.updated_at = Utc::now();
386 Ok(())
387 }
388
389 pub fn get_active_environment(&self) -> &Environment {
391 if let Some(ref active_id) = self.config.active_environment_id {
392 self.get_environment(active_id).unwrap_or(&self.config.global_environment)
393 } else {
394 &self.config.global_environment
395 }
396 }
397
398 pub fn get_active_environment_id(&self) -> Option<&str> {
400 self.config.active_environment_id.as_deref()
401 }
402
403 pub fn get_variable(&self, key: &str) -> Option<&String> {
405 let active_env = self.get_active_environment();
407 active_env.get_variable(key).or_else(|| {
408 if active_env.id != self.config.global_environment.id {
409 self.config.global_environment.get_variable(key)
410 } else {
411 None
412 }
413 })
414 }
415
416 pub fn get_all_variables(&self) -> HashMap<String, String> {
418 let mut variables = HashMap::new();
419
420 variables.extend(self.config.global_environment.variables.clone());
422
423 let active_env = self.get_active_environment();
425 if active_env.id != self.config.global_environment.id {
426 variables.extend(active_env.variables.clone());
427 }
428
429 variables
430 }
431
432 pub fn delete_environment(&mut self, id: &str) -> Result<()> {
434 if id == self.config.global_environment.id {
435 return Err(Error::generic("Cannot delete global environment".to_string()));
436 }
437
438 let position = self.config.environments.iter().position(|env| env.id == id);
439 if let Some(pos) = position {
440 self.config.environments.remove(pos);
441
442 if self.config.active_environment_id.as_deref() == Some(id) {
444 self.config.active_environment_id = None;
445 }
446
447 self.updated_at = Utc::now();
448 Ok(())
449 } else {
450 Err(Error::generic(format!("Environment with ID '{}' not found", id)))
451 }
452 }
453
454 pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
456 for env_id in &environment_ids {
458 if !self.config.environments.iter().any(|env| env.id == *env_id) {
459 return Err(Error::generic(format!("Environment with ID '{}' not found", env_id)));
460 }
461 }
462
463 for (index, env_id) in environment_ids.iter().enumerate() {
465 if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
466 env.order = index as i32;
467 env.updated_at = Utc::now();
468 }
469 }
470
471 self.updated_at = Utc::now();
472 Ok(())
473 }
474
475 pub fn get_environments_ordered(&self) -> Vec<&Environment> {
477 let mut all_envs = vec![&self.config.global_environment];
478 all_envs.extend(self.config.environments.iter());
479 all_envs.sort_by_key(|env| env.order);
480 all_envs
481 }
482
483 pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
485 self.config.sync = config;
486 self.updated_at = Utc::now();
487 Ok(())
488 }
489
490 pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
492 self.config.sync.enabled = true;
493 self.config.sync.target_directory = Some(target_directory);
494 self.config.sync.realtime_monitoring = true; self.updated_at = Utc::now();
496 Ok(())
497 }
498
499 pub fn disable_sync(&mut self) -> Result<()> {
501 self.config.sync.enabled = false;
502 self.updated_at = Utc::now();
503 Ok(())
504 }
505
506 pub fn get_sync_config(&self) -> &SyncConfig {
508 &self.config.sync
509 }
510
511 pub fn is_sync_enabled(&self) -> bool {
513 self.config.sync.enabled
514 }
515
516 pub fn get_sync_directory(&self) -> Option<&str> {
518 self.config.sync.target_directory.as_deref()
519 }
520
521 pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
523 self.config.sync.target_directory = directory;
524 self.updated_at = Utc::now();
525 Ok(())
526 }
527
528 pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
530 self.config.sync.sync_direction = direction;
531 self.updated_at = Utc::now();
532 Ok(())
533 }
534
535 pub fn get_sync_direction(&self) -> &SyncDirection {
537 &self.config.sync.sync_direction
538 }
539
540 pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
542 self.config.sync.realtime_monitoring = enabled;
543 self.updated_at = Utc::now();
544 Ok(())
545 }
546
547 pub fn is_realtime_monitoring_enabled(&self) -> bool {
549 self.config.sync.realtime_monitoring && self.config.sync.enabled
550 }
551
552 pub fn to_filtered_for_sync(&self) -> Workspace {
554 let mut filtered = self.clone();
555
556 filtered.config.global_environment.variables =
559 self.filter_sensitive_variables(&self.config.global_environment.variables);
560
561 filtered.config.environments = filtered
563 .config
564 .environments
565 .into_iter()
566 .filter(|env| env.sharable)
567 .map(|mut env| {
568 env.variables = self.filter_sensitive_variables(&env.variables);
569 env
570 })
571 .collect();
572
573 filtered
574 }
575
576 fn filter_sensitive_variables(
578 &self,
579 variables: &HashMap<String, String>,
580 ) -> HashMap<String, String> {
581 let sensitive_keys = [
582 "password",
584 "secret",
585 "key",
586 "token",
587 "credential",
588 "api_key",
589 "apikey",
590 "api_secret",
591 "db_password",
592 "database_password",
593 "aws_secret_key",
594 "aws_session_token",
595 "private_key",
596 "authorization",
597 "auth_token",
598 "access_token",
599 "refresh_token",
600 "cookie",
601 "session",
602 "csrf",
603 "jwt",
604 "bearer",
605 ];
606
607 variables
608 .iter()
609 .filter(|(key, _)| {
610 let key_lower = key.to_lowercase();
611 !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
612 })
613 .map(|(k, v)| (k.clone(), v.clone()))
614 .collect()
615 }
616
617 pub fn should_sync(&self) -> bool {
619 self.config.sync.enabled && self.config.sync.target_directory.is_some()
620 }
621
622 pub fn get_sync_filename(&self) -> String {
624 let pattern = &self.config.sync.filename_pattern;
626
627 let filename = pattern
629 .replace("{name}", &sanitize_filename(&self.name))
630 .replace("{id}", &self.id);
631
632 if filename.ends_with(".yaml") || filename.ends_with(".yml") {
633 filename
634 } else {
635 format!("{}.yaml", filename)
636 }
637 }
638}
639
640fn sanitize_filename(name: &str) -> String {
642 name.chars()
643 .map(|c| match c {
644 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
645 c if c.is_control() => '_',
646 c if c.is_whitespace() => '-',
647 c => c,
648 })
649 .collect::<String>()
650 .to_lowercase()
651}
652
653impl Folder {
654 pub fn new(name: String) -> Self {
656 let now = Utc::now();
657 Self {
658 id: Uuid::new_v4().to_string(),
659 name,
660 description: None,
661 parent_id: None,
662 created_at: now,
663 updated_at: now,
664 tags: Vec::new(),
665 inheritance: FolderInheritanceConfig::default(),
666 folders: Vec::new(),
667 requests: Vec::new(),
668 }
669 }
670
671 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
673 let mut folder = Folder::new(name);
674 folder.parent_id = Some(self.id.clone());
675 let id = folder.id.clone();
676 self.folders.push(folder);
677 self.updated_at = Utc::now();
678 Ok(id)
679 }
680
681 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
683 let id = request.id.clone();
684 self.requests.push(request);
685 self.updated_at = Utc::now();
686 Ok(id)
687 }
688
689 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
691 for folder in &self.folders {
692 if folder.id == id {
693 return Some(folder);
694 }
695 if let Some(found) = folder.find_folder(id) {
696 return Some(found);
697 }
698 }
699 None
700 }
701
702 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
704 for folder in &mut self.folders {
705 if folder.id == id {
706 return Some(folder);
707 }
708 if let Some(found) = folder.find_folder_mut(id) {
709 return Some(found);
710 }
711 }
712 None
713 }
714
715 pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
717 for request in &self.requests {
719 if request.id == id {
720 return Some(request);
721 }
722 }
723
724 for folder in &self.folders {
726 if let Some(found) = folder.find_request(id) {
727 return Some(found);
728 }
729 }
730 None
731 }
732
733 pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
735 let mut routes = Vec::new();
736
737 for request in &self.requests {
739 routes.push(
740 Route::new(request.method.clone(), request.path.clone())
741 .with_priority(request.priority)
742 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
743 .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
744 .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
745 );
746 }
747
748 for folder in &self.folders {
750 routes.extend(folder.get_routes(workspace_id));
751 }
752
753 routes
754 }
755}
756
757impl Workspace {
758 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
760 for folder in &self.folders {
761 if folder.id == id {
762 return Some(folder);
763 }
764 if let Some(found) = folder.find_folder(id) {
765 return Some(found);
766 }
767 }
768 None
769 }
770
771 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
773 for folder in &mut self.folders {
774 if folder.id == id {
775 return Some(folder);
776 }
777 if let Some(found) = folder.find_folder_mut(id) {
778 return Some(found);
779 }
780 }
781 None
782 }
783
784 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
786 let id = request.id.clone();
787 self.requests.push(request);
788 self.updated_at = Utc::now();
789 Ok(id)
790 }
791
792 pub fn get_routes(&self) -> Vec<Route> {
794 let mut routes = Vec::new();
795
796 for request in &self.requests {
798 routes.push(
799 Route::new(request.method.clone(), request.path.clone())
800 .with_priority(request.priority)
801 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
802 .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
803 );
804 }
805
806 for folder in &self.folders {
808 routes.extend(folder.get_routes(&self.id));
809 }
810
811 routes
812 }
813
814 pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
816 for folder in folder_path.iter().rev() {
818 if let Some(auth) = &folder.inheritance.auth {
819 return Some(auth);
820 }
821 }
822
823 self.config.auth.as_ref()
825 }
826
827 pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
829 let mut effective_headers = HashMap::new();
830
831 for (key, value) in &self.config.default_headers {
833 effective_headers.insert(key.clone(), value.clone());
834 }
835
836 for folder in folder_path {
838 for (key, value) in &folder.inheritance.headers {
839 effective_headers.insert(key.clone(), value.clone());
840 }
841 }
842
843 effective_headers
844 }
845}
846
847impl Folder {
848 pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
850 let mut path = Vec::new();
851 let mut current = Some(self);
852
853 while let Some(folder) = current {
854 path.push(folder);
855 current =
856 folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
857 }
858
859 path.reverse(); path
861 }
862}
863
864impl MockRequest {
865 pub fn apply_inheritance(
867 &mut self,
868 effective_headers: HashMap<String, String>,
869 effective_auth: Option<&AuthConfig>,
870 ) {
871 for (key, value) in effective_headers {
873 self.headers.entry(key).or_insert(value);
874 }
875
876 if let Some(auth) = effective_auth {
879 self.auth = Some(auth.clone());
880 }
881 }
882
883 pub fn create_inherited_request(
885 mut self,
886 workspace: &Workspace,
887 folder_path: &[&Folder],
888 ) -> Self {
889 let effective_headers = workspace.get_effective_headers(folder_path);
890 let effective_auth = workspace.get_effective_auth(folder_path);
891
892 self.apply_inheritance(effective_headers, effective_auth);
893 self
894 }
895
896 pub fn new(method: HttpMethod, path: String, name: String) -> Self {
898 let now = Utc::now();
899 Self {
900 id: Uuid::new_v4().to_string(),
901 name,
902 description: None,
903 method,
904 path,
905 headers: HashMap::new(),
906 query_params: HashMap::new(),
907 body: None,
908 response: MockResponse::default(),
909 response_history: Vec::new(),
910 created_at: now,
911 updated_at: now,
912 tags: Vec::new(),
913 auth: None,
914 priority: 0,
915 }
916 }
917
918 pub fn with_response(mut self, response: MockResponse) -> Self {
920 self.response = response;
921 self
922 }
923
924 pub fn with_header(mut self, key: String, value: String) -> Self {
926 self.headers.insert(key, value);
927 self
928 }
929
930 pub fn with_query_param(mut self, key: String, value: String) -> Self {
932 self.query_params.insert(key, value);
933 self
934 }
935
936 pub fn with_body(mut self, body: String) -> Self {
938 self.body = Some(body);
939 self
940 }
941
942 pub fn with_tag(mut self, tag: String) -> Self {
944 self.tags.push(tag);
945 self
946 }
947
948 pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
950 self.response_history.push(entry);
951 if self.response_history.len() > 100 {
953 self.response_history.remove(0);
954 }
955 self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
957 }
958
959 pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
961 &self.response_history
962 }
963}
964
965impl Default for MockResponse {
966 fn default() -> Self {
967 Self {
968 status_code: 200,
969 headers: HashMap::new(),
970 body: Some("{}".to_string()),
971 content_type: Some("application/json".to_string()),
972 delay_ms: None,
973 }
974 }
975}
976
977impl Environment {
978 pub fn new(name: String) -> Self {
980 let now = Utc::now();
981 Self {
982 id: Uuid::new_v4().to_string(),
983 name,
984 description: None,
985 color: None,
986 variables: HashMap::new(),
987 created_at: now,
988 updated_at: now,
989 order: 0, sharable: false, }
992 }
993
994 pub fn new_global() -> Self {
996 let mut env = Self::new("Global".to_string());
997 env.description =
998 Some("Global environment variables available in all contexts".to_string());
999 env
1000 }
1001
1002 pub fn set_variable(&mut self, key: String, value: String) {
1004 self.variables.insert(key, value);
1005 self.updated_at = Utc::now();
1006 }
1007
1008 pub fn remove_variable(&mut self, key: &str) -> bool {
1010 let removed = self.variables.remove(key).is_some();
1011 if removed {
1012 self.updated_at = Utc::now();
1013 }
1014 removed
1015 }
1016
1017 pub fn get_variable(&self, key: &str) -> Option<&String> {
1019 self.variables.get(key)
1020 }
1021
1022 pub fn set_color(&mut self, color: EnvironmentColor) {
1024 self.color = Some(color);
1025 self.updated_at = Utc::now();
1026 }
1027}
1028
1029impl Default for SyncConfig {
1030 fn default() -> Self {
1031 Self {
1032 enabled: false,
1033 target_directory: None,
1034 directory_structure: SyncDirectoryStructure::Nested,
1035 sync_direction: SyncDirection::Manual,
1036 include_metadata: true,
1037 realtime_monitoring: false,
1038 filename_pattern: "{name}".to_string(),
1039 exclude_pattern: None,
1040 force_overwrite: false,
1041 last_sync: None,
1042 }
1043 }
1044}
1045
1046impl Default for WorkspaceConfig {
1047 fn default() -> Self {
1048 Self {
1049 base_url: None,
1050 default_headers: HashMap::new(),
1051 auth: None,
1052 global_environment: Environment::new_global(),
1053 environments: Vec::new(),
1054 active_environment_id: None,
1055 sync: SyncConfig::default(),
1056 auto_encryption: AutoEncryptionConfig::default(),
1057 reality_level: None,
1058 fidelity_score: None,
1059 }
1060 }
1061}
1062
1063impl WorkspaceRegistry {
1064 pub fn new() -> Self {
1066 Self {
1067 workspaces: HashMap::new(),
1068 active_workspace: None,
1069 }
1070 }
1071
1072 pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1074 let id = workspace.id.clone();
1075
1076 if workspace.order == 0 && !self.workspaces.is_empty() {
1078 workspace.order = self.workspaces.len() as i32;
1079 }
1080
1081 self.workspaces.insert(id.clone(), workspace);
1082 Ok(id)
1083 }
1084
1085 pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1087 self.workspaces.get(id)
1088 }
1089
1090 pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1092 self.workspaces.get_mut(id)
1093 }
1094
1095 pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1097 if self.workspaces.remove(id).is_some() {
1098 if self.active_workspace.as_deref() == Some(id) {
1100 self.active_workspace = None;
1101 }
1102 Ok(())
1103 } else {
1104 Err(Error::generic(format!("Workspace with ID '{}' not found", id)))
1105 }
1106 }
1107
1108 pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1110 if let Some(ref workspace_id) = id {
1111 if !self.workspaces.contains_key(workspace_id) {
1112 return Err(Error::generic(format!(
1113 "Workspace with ID '{}' not found",
1114 workspace_id
1115 )));
1116 }
1117 }
1118 self.active_workspace = id;
1119 Ok(())
1120 }
1121
1122 pub fn get_active_workspace(&self) -> Option<&Workspace> {
1124 self.active_workspace.as_ref().and_then(|id| self.workspaces.get(id))
1125 }
1126
1127 pub fn get_active_workspace_id(&self) -> Option<&str> {
1129 self.active_workspace.as_deref()
1130 }
1131
1132 pub fn get_workspaces(&self) -> Vec<&Workspace> {
1134 self.workspaces.values().collect()
1135 }
1136
1137 pub fn get_workspaces_ordered(&self) -> Vec<&Workspace> {
1139 let mut workspaces: Vec<&Workspace> = self.workspaces.values().collect();
1140 workspaces.sort_by_key(|w| w.order);
1141 workspaces
1142 }
1143
1144 pub fn update_workspaces_order(&mut self, workspace_ids: Vec<String>) -> Result<()> {
1146 for workspace_id in &workspace_ids {
1148 if !self.workspaces.contains_key(workspace_id) {
1149 return Err(Error::generic(format!(
1150 "Workspace with ID '{}' not found",
1151 workspace_id
1152 )));
1153 }
1154 }
1155
1156 for (index, workspace_id) in workspace_ids.iter().enumerate() {
1158 if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1159 workspace.order = index as i32;
1160 workspace.updated_at = Utc::now();
1161 }
1162 }
1163
1164 Ok(())
1165 }
1166
1167 pub fn get_all_routes(&self) -> Vec<Route> {
1169 let mut all_routes = Vec::new();
1170 for workspace in self.workspaces.values() {
1171 all_routes.extend(workspace.get_routes());
1172 }
1173 all_routes
1174 }
1175
1176 pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1178 let mut registry = RouteRegistry::new();
1179 let routes = self.get_all_routes();
1180
1181 for route in routes {
1182 registry.add_http_route(route)?;
1183 }
1184
1185 Ok(registry)
1186 }
1187}
1188
1189impl Default for WorkspaceRegistry {
1190 fn default() -> Self {
1191 Self::new()
1192 }
1193}
1194
1195#[cfg(test)]
1196mod tests {
1197 use super::*;
1198
1199 use crate::ApiKeyConfig;
1200
1201 #[test]
1202 fn test_workspace_creation() {
1203 let workspace = Workspace::new("Test Workspace".to_string());
1204 assert_eq!(workspace.name, "Test Workspace");
1205 assert!(!workspace.id.is_empty());
1206 assert!(workspace.folders.is_empty());
1207 assert!(workspace.requests.is_empty());
1208 }
1209
1210 #[test]
1211 fn test_folder_creation() {
1212 let folder = Folder::new("Test Folder".to_string());
1213 assert_eq!(folder.name, "Test Folder");
1214 assert!(!folder.id.is_empty());
1215 assert!(folder.folders.is_empty());
1216 assert!(folder.requests.is_empty());
1217 }
1218
1219 #[test]
1220 fn test_request_creation() {
1221 let request =
1222 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1223 assert_eq!(request.name, "Test Request");
1224 assert_eq!(request.method, HttpMethod::GET);
1225 assert_eq!(request.path, "/test");
1226 assert_eq!(request.response.status_code, 200);
1227 }
1228
1229 #[test]
1230 fn test_workspace_hierarchy() {
1231 let mut workspace = Workspace::new("Test Workspace".to_string());
1232
1233 let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1235 assert_eq!(workspace.folders.len(), 1);
1236
1237 let request =
1239 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1240 workspace.add_request(request).unwrap();
1241 assert_eq!(workspace.requests.len(), 1);
1242
1243 let folder = workspace.find_folder_mut(&folder_id).unwrap();
1245 let folder_request = MockRequest::new(
1246 HttpMethod::POST,
1247 "/folder-test".to_string(),
1248 "Folder Request".to_string(),
1249 );
1250 folder.add_request(folder_request).unwrap();
1251 assert_eq!(folder.requests.len(), 1);
1252 }
1253
1254 #[test]
1255 fn test_workspace_registry() {
1256 let mut registry = WorkspaceRegistry::new();
1257
1258 let workspace = Workspace::new("Test Workspace".to_string());
1259 let workspace_id = registry.add_workspace(workspace).unwrap();
1260
1261 registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1263 assert!(registry.get_active_workspace().is_some());
1264
1265 let retrieved = registry.get_workspace(&workspace_id).unwrap();
1267 assert_eq!(retrieved.name, "Test Workspace");
1268
1269 registry.remove_workspace(&workspace_id).unwrap();
1271 assert!(registry.get_workspace(&workspace_id).is_none());
1272 }
1273
1274 #[test]
1275 fn test_inheritance_header_priority() {
1276 let mut workspace = Workspace::new("Test Workspace".to_string());
1277 workspace
1278 .config
1279 .default_headers
1280 .insert("X-Common".to_string(), "workspace-value".to_string());
1281 workspace
1282 .config
1283 .default_headers
1284 .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1285
1286 let mut folder = Folder::new("Test Folder".to_string());
1288 folder
1289 .inheritance
1290 .headers
1291 .insert("X-Common".to_string(), "folder-value".to_string());
1292 folder
1293 .inheritance
1294 .headers
1295 .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1296
1297 let folder_path = vec![&folder];
1299 let effective_headers = workspace.get_effective_headers(&folder_path);
1300
1301 assert_eq!(effective_headers.get("X-Common").unwrap(), "folder-value"); assert_eq!(effective_headers.get("X-Workspace-Only").unwrap(), "workspace-only-value"); assert_eq!(effective_headers.get("X-Folder-Only").unwrap(), "folder-only-value");
1304 }
1306
1307 #[test]
1308 fn test_inheritance_request_headers_override() {
1309 let mut workspace = Workspace::new("Test Workspace".to_string());
1310 workspace
1311 .config
1312 .default_headers
1313 .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1314
1315 let folder_path = vec![];
1316 let effective_headers = workspace.get_effective_headers(&folder_path);
1317 let mut request = MockRequest::new(
1318 crate::routing::HttpMethod::GET,
1319 "/test".to_string(),
1320 "Test Request".to_string(),
1321 );
1322
1323 request
1325 .headers
1326 .insert("Authorization".to_string(), "Bearer request-token".to_string());
1327
1328 request.apply_inheritance(effective_headers, None);
1330
1331 assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1332 }
1333
1334 #[test]
1335 fn test_inheritance_nested_folders() {
1336 let mut workspace = Workspace::new("Test Workspace".to_string());
1337 workspace
1338 .config
1339 .default_headers
1340 .insert("X-Level".to_string(), "workspace".to_string());
1341
1342 let mut parent_folder = Folder::new("Parent Folder".to_string());
1344 parent_folder
1345 .inheritance
1346 .headers
1347 .insert("X-Level".to_string(), "parent".to_string());
1348 parent_folder
1349 .inheritance
1350 .headers
1351 .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1352
1353 let mut child_folder = Folder::new("Child Folder".to_string());
1355 child_folder
1356 .inheritance
1357 .headers
1358 .insert("X-Level".to_string(), "child".to_string());
1359 child_folder
1360 .inheritance
1361 .headers
1362 .insert("X-Child-Only".to_string(), "child-value".to_string());
1363
1364 let folder_path = vec![&parent_folder, &child_folder];
1366 let effective_headers = workspace.get_effective_headers(&folder_path);
1367
1368 assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1370 assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1371 assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1372 }
1373
1374 #[test]
1375 fn test_inheritance_auth_from_folder() {
1376 let workspace = Workspace::new("Test Workspace".to_string());
1378
1379 let mut folder = Folder::new("Test Folder".to_string());
1381 let auth = AuthConfig {
1382 require_auth: true,
1383 api_key: Some(ApiKeyConfig {
1384 header_name: "X-API-Key".to_string(),
1385 query_name: Some("api_key".to_string()),
1386 keys: vec!["folder-key".to_string()],
1387 }),
1388 ..Default::default()
1389 };
1390 folder.inheritance.auth = Some(auth);
1391
1392 let folder_path = vec![&folder];
1393 let effective_auth = workspace.get_effective_auth(&folder_path);
1394
1395 assert!(effective_auth.is_some());
1396 let auth_config = effective_auth.unwrap();
1397 assert!(auth_config.require_auth);
1398 let api_key_config = auth_config.api_key.as_ref().unwrap();
1399 assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1400 }
1401}