1pub mod core;
12pub mod environment;
13pub mod registry;
14pub mod request;
15pub mod sync;
16
17pub use environment::*;
19pub use registry::*;
20pub use request::*;
21pub use sync::*;
22
23use crate::config::AuthConfig;
25use crate::encryption::AutoEncryptionConfig;
26use crate::reality::RealityLevel;
27use crate::routing::{HttpMethod, Route, RouteRegistry};
28use crate::{Error, Result};
29use chrono::{DateTime, Utc};
30use serde::{Deserialize, Serialize};
31use std::collections::HashMap;
32use uuid::Uuid;
33
34pub type EntityId = String;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct Workspace {
40 pub id: EntityId,
42 pub name: String,
44 pub description: Option<String>,
46 pub created_at: DateTime<Utc>,
48 pub updated_at: DateTime<Utc>,
50 pub tags: Vec<String>,
52 pub config: WorkspaceConfig,
54 pub folders: Vec<Folder>,
56 pub requests: Vec<MockRequest>,
58 #[serde(default)]
60 pub order: i32,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize, Default)]
65pub struct FolderInheritanceConfig {
66 #[serde(default)]
68 pub headers: HashMap<String, String>,
69 pub auth: Option<AuthConfig>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct Folder {
76 pub id: EntityId,
78 pub name: String,
80 pub description: Option<String>,
82 pub parent_id: Option<EntityId>,
84 pub created_at: DateTime<Utc>,
86 pub updated_at: DateTime<Utc>,
88 pub tags: Vec<String>,
90 #[serde(default)]
92 pub inheritance: FolderInheritanceConfig,
93 pub folders: Vec<Folder>,
95 pub requests: Vec<MockRequest>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct MockRequest {
102 pub id: EntityId,
104 pub name: String,
106 pub description: Option<String>,
108 pub method: HttpMethod,
110 pub path: String,
112 pub headers: HashMap<String, String>,
114 pub query_params: HashMap<String, String>,
116 pub body: Option<String>,
118 pub response: MockResponse,
120 pub response_history: Vec<ResponseHistoryEntry>,
122 pub created_at: DateTime<Utc>,
124 pub updated_at: DateTime<Utc>,
126 pub tags: Vec<String>,
128 pub auth: Option<AuthConfig>,
130 pub priority: i32,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct MockResponse {
137 pub status_code: u16,
139 pub headers: HashMap<String, String>,
141 pub body: Option<String>,
143 pub content_type: Option<String>,
145 pub delay_ms: Option<u64>,
147}
148
149#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct ResponseHistoryEntry {
152 pub id: String,
154 pub executed_at: DateTime<Utc>,
156 pub request_method: HttpMethod,
158 pub request_path: String,
160 pub request_headers: HashMap<String, String>,
162 pub request_body: Option<String>,
164 pub response_status_code: u16,
166 pub response_headers: HashMap<String, String>,
168 pub response_body: Option<String>,
170 pub response_time_ms: u64,
172 pub response_size_bytes: u64,
174 pub error_message: Option<String>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct EnvironmentColor {
181 pub hex: String,
183 pub name: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct Environment {
190 pub id: EntityId,
192 pub name: String,
194 pub description: Option<String>,
196 pub color: Option<EnvironmentColor>,
198 pub variables: HashMap<String, String>,
200 pub created_at: DateTime<Utc>,
202 pub updated_at: DateTime<Utc>,
204 #[serde(default)]
206 pub order: i32,
207 #[serde(default)]
209 pub sharable: bool,
210}
211
212#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct SyncConfig {
215 pub enabled: bool,
217 pub target_directory: Option<String>,
219 pub directory_structure: SyncDirectoryStructure,
221 pub sync_direction: SyncDirection,
223 pub include_metadata: bool,
225 pub realtime_monitoring: bool,
227 pub filename_pattern: String,
229 pub exclude_pattern: Option<String>,
231 pub force_overwrite: bool,
233 pub last_sync: Option<DateTime<Utc>>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize)]
239pub enum SyncDirectoryStructure {
240 Flat,
242 Nested,
244 Grouped,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize)]
250pub enum SyncDirection {
251 Manual,
253 WorkspaceToDirectory,
255 Bidirectional,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct WorkspaceConfig {
262 pub base_url: Option<String>,
264 pub default_headers: HashMap<String, String>,
266 pub auth: Option<AuthConfig>,
268 pub global_environment: Environment,
270 pub environments: Vec<Environment>,
272 pub active_environment_id: Option<EntityId>,
274 pub sync: SyncConfig,
276 #[serde(default)]
278 pub auto_encryption: AutoEncryptionConfig,
279 #[serde(default)]
282 pub reality_level: Option<RealityLevel>,
283}
284
285#[derive(Debug, Clone)]
287pub struct WorkspaceRegistry {
288 workspaces: HashMap<EntityId, Workspace>,
289 active_workspace: Option<EntityId>,
290}
291
292impl Workspace {
293 pub fn new(name: String) -> Self {
295 let now = Utc::now();
296 Self {
297 id: Uuid::new_v4().to_string(),
298 name,
299 description: None,
300 created_at: now,
301 updated_at: now,
302 tags: Vec::new(),
303 config: WorkspaceConfig::default(),
304 folders: Vec::new(),
305 requests: Vec::new(),
306 order: 0, }
308 }
309
310 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
312 let folder = Folder::new(name);
313 let id = folder.id.clone();
314 self.folders.push(folder);
315 self.updated_at = Utc::now();
316 Ok(id)
317 }
318
319 pub fn create_environment(
321 &mut self,
322 name: String,
323 description: Option<String>,
324 ) -> Result<EntityId> {
325 if self.config.environments.iter().any(|env| env.name == name) {
327 return Err(Error::generic(format!("Environment with name '{}' already exists", name)));
328 }
329
330 let mut environment = Environment::new(name);
331 environment.description = description;
332
333 environment.order = self.config.environments.len() as i32;
335
336 let id = environment.id.clone();
337
338 self.config.environments.push(environment);
339 self.updated_at = Utc::now();
340 Ok(id)
341 }
342
343 pub fn get_environments(&self) -> Vec<&Environment> {
345 let mut all_envs = vec![&self.config.global_environment];
346 all_envs.extend(self.config.environments.iter());
347 all_envs
348 }
349
350 pub fn get_environment(&self, id: &str) -> Option<&Environment> {
352 if self.config.global_environment.id == id {
353 Some(&self.config.global_environment)
354 } else {
355 self.config.environments.iter().find(|env| env.id == id)
356 }
357 }
358
359 pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
361 if self.config.global_environment.id == id {
362 Some(&mut self.config.global_environment)
363 } else {
364 self.config.environments.iter_mut().find(|env| env.id == id)
365 }
366 }
367
368 pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
370 if let Some(ref id) = environment_id {
371 if self.get_environment(id).is_none() {
372 return Err(Error::generic(format!("Environment with ID '{}' not found", id)));
373 }
374 }
375 self.config.active_environment_id = environment_id;
376 self.updated_at = Utc::now();
377 Ok(())
378 }
379
380 pub fn get_active_environment(&self) -> &Environment {
382 if let Some(ref active_id) = self.config.active_environment_id {
383 self.get_environment(active_id).unwrap_or(&self.config.global_environment)
384 } else {
385 &self.config.global_environment
386 }
387 }
388
389 pub fn get_active_environment_id(&self) -> Option<&str> {
391 self.config.active_environment_id.as_deref()
392 }
393
394 pub fn get_variable(&self, key: &str) -> Option<&String> {
396 let active_env = self.get_active_environment();
398 active_env.get_variable(key).or_else(|| {
399 if active_env.id != self.config.global_environment.id {
400 self.config.global_environment.get_variable(key)
401 } else {
402 None
403 }
404 })
405 }
406
407 pub fn get_all_variables(&self) -> HashMap<String, String> {
409 let mut variables = HashMap::new();
410
411 variables.extend(self.config.global_environment.variables.clone());
413
414 let active_env = self.get_active_environment();
416 if active_env.id != self.config.global_environment.id {
417 variables.extend(active_env.variables.clone());
418 }
419
420 variables
421 }
422
423 pub fn delete_environment(&mut self, id: &str) -> Result<()> {
425 if id == self.config.global_environment.id {
426 return Err(Error::generic("Cannot delete global environment".to_string()));
427 }
428
429 let position = self.config.environments.iter().position(|env| env.id == id);
430 if let Some(pos) = position {
431 self.config.environments.remove(pos);
432
433 if self.config.active_environment_id.as_deref() == Some(id) {
435 self.config.active_environment_id = None;
436 }
437
438 self.updated_at = Utc::now();
439 Ok(())
440 } else {
441 Err(Error::generic(format!("Environment with ID '{}' not found", id)))
442 }
443 }
444
445 pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
447 for env_id in &environment_ids {
449 if !self.config.environments.iter().any(|env| env.id == *env_id) {
450 return Err(Error::generic(format!("Environment with ID '{}' not found", env_id)));
451 }
452 }
453
454 for (index, env_id) in environment_ids.iter().enumerate() {
456 if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
457 env.order = index as i32;
458 env.updated_at = Utc::now();
459 }
460 }
461
462 self.updated_at = Utc::now();
463 Ok(())
464 }
465
466 pub fn get_environments_ordered(&self) -> Vec<&Environment> {
468 let mut all_envs = vec![&self.config.global_environment];
469 all_envs.extend(self.config.environments.iter());
470 all_envs.sort_by_key(|env| env.order);
471 all_envs
472 }
473
474 pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
476 self.config.sync = config;
477 self.updated_at = Utc::now();
478 Ok(())
479 }
480
481 pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
483 self.config.sync.enabled = true;
484 self.config.sync.target_directory = Some(target_directory);
485 self.config.sync.realtime_monitoring = true; self.updated_at = Utc::now();
487 Ok(())
488 }
489
490 pub fn disable_sync(&mut self) -> Result<()> {
492 self.config.sync.enabled = false;
493 self.updated_at = Utc::now();
494 Ok(())
495 }
496
497 pub fn get_sync_config(&self) -> &SyncConfig {
499 &self.config.sync
500 }
501
502 pub fn is_sync_enabled(&self) -> bool {
504 self.config.sync.enabled
505 }
506
507 pub fn get_sync_directory(&self) -> Option<&str> {
509 self.config.sync.target_directory.as_deref()
510 }
511
512 pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
514 self.config.sync.target_directory = directory;
515 self.updated_at = Utc::now();
516 Ok(())
517 }
518
519 pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
521 self.config.sync.sync_direction = direction;
522 self.updated_at = Utc::now();
523 Ok(())
524 }
525
526 pub fn get_sync_direction(&self) -> &SyncDirection {
528 &self.config.sync.sync_direction
529 }
530
531 pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
533 self.config.sync.realtime_monitoring = enabled;
534 self.updated_at = Utc::now();
535 Ok(())
536 }
537
538 pub fn is_realtime_monitoring_enabled(&self) -> bool {
540 self.config.sync.realtime_monitoring && self.config.sync.enabled
541 }
542
543 pub fn to_filtered_for_sync(&self) -> Workspace {
545 let mut filtered = self.clone();
546
547 filtered.config.global_environment.variables =
550 self.filter_sensitive_variables(&self.config.global_environment.variables);
551
552 filtered.config.environments = filtered
554 .config
555 .environments
556 .into_iter()
557 .filter(|env| env.sharable)
558 .map(|mut env| {
559 env.variables = self.filter_sensitive_variables(&env.variables);
560 env
561 })
562 .collect();
563
564 filtered
565 }
566
567 fn filter_sensitive_variables(
569 &self,
570 variables: &HashMap<String, String>,
571 ) -> HashMap<String, String> {
572 let sensitive_keys = [
573 "password",
575 "secret",
576 "key",
577 "token",
578 "credential",
579 "api_key",
580 "apikey",
581 "api_secret",
582 "db_password",
583 "database_password",
584 "aws_secret_key",
585 "aws_session_token",
586 "private_key",
587 "authorization",
588 "auth_token",
589 "access_token",
590 "refresh_token",
591 "cookie",
592 "session",
593 "csrf",
594 "jwt",
595 "bearer",
596 ];
597
598 variables
599 .iter()
600 .filter(|(key, _)| {
601 let key_lower = key.to_lowercase();
602 !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
603 })
604 .map(|(k, v)| (k.clone(), v.clone()))
605 .collect()
606 }
607
608 pub fn should_sync(&self) -> bool {
610 self.config.sync.enabled && self.config.sync.target_directory.is_some()
611 }
612
613 pub fn get_sync_filename(&self) -> String {
615 let pattern = &self.config.sync.filename_pattern;
617
618 let filename = pattern
620 .replace("{name}", &sanitize_filename(&self.name))
621 .replace("{id}", &self.id);
622
623 if filename.ends_with(".yaml") || filename.ends_with(".yml") {
624 filename
625 } else {
626 format!("{}.yaml", filename)
627 }
628 }
629}
630
631fn sanitize_filename(name: &str) -> String {
633 name.chars()
634 .map(|c| match c {
635 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
636 c if c.is_control() => '_',
637 c if c.is_whitespace() => '-',
638 c => c,
639 })
640 .collect::<String>()
641 .to_lowercase()
642}
643
644impl Folder {
645 pub fn new(name: String) -> Self {
647 let now = Utc::now();
648 Self {
649 id: Uuid::new_v4().to_string(),
650 name,
651 description: None,
652 parent_id: None,
653 created_at: now,
654 updated_at: now,
655 tags: Vec::new(),
656 inheritance: FolderInheritanceConfig::default(),
657 folders: Vec::new(),
658 requests: Vec::new(),
659 }
660 }
661
662 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
664 let mut folder = Folder::new(name);
665 folder.parent_id = Some(self.id.clone());
666 let id = folder.id.clone();
667 self.folders.push(folder);
668 self.updated_at = Utc::now();
669 Ok(id)
670 }
671
672 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
674 let id = request.id.clone();
675 self.requests.push(request);
676 self.updated_at = Utc::now();
677 Ok(id)
678 }
679
680 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
682 for folder in &self.folders {
683 if folder.id == id {
684 return Some(folder);
685 }
686 if let Some(found) = folder.find_folder(id) {
687 return Some(found);
688 }
689 }
690 None
691 }
692
693 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
695 for folder in &mut self.folders {
696 if folder.id == id {
697 return Some(folder);
698 }
699 if let Some(found) = folder.find_folder_mut(id) {
700 return Some(found);
701 }
702 }
703 None
704 }
705
706 pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
708 for request in &self.requests {
710 if request.id == id {
711 return Some(request);
712 }
713 }
714
715 for folder in &self.folders {
717 if let Some(found) = folder.find_request(id) {
718 return Some(found);
719 }
720 }
721 None
722 }
723
724 pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
726 let mut routes = Vec::new();
727
728 for request in &self.requests {
730 routes.push(
731 Route::new(request.method.clone(), request.path.clone())
732 .with_priority(request.priority)
733 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
734 .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
735 .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
736 );
737 }
738
739 for folder in &self.folders {
741 routes.extend(folder.get_routes(workspace_id));
742 }
743
744 routes
745 }
746}
747
748impl Workspace {
749 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
751 for folder in &self.folders {
752 if folder.id == id {
753 return Some(folder);
754 }
755 if let Some(found) = folder.find_folder(id) {
756 return Some(found);
757 }
758 }
759 None
760 }
761
762 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
764 for folder in &mut self.folders {
765 if folder.id == id {
766 return Some(folder);
767 }
768 if let Some(found) = folder.find_folder_mut(id) {
769 return Some(found);
770 }
771 }
772 None
773 }
774
775 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
777 let id = request.id.clone();
778 self.requests.push(request);
779 self.updated_at = Utc::now();
780 Ok(id)
781 }
782
783 pub fn get_routes(&self) -> Vec<Route> {
785 let mut routes = Vec::new();
786
787 for request in &self.requests {
789 routes.push(
790 Route::new(request.method.clone(), request.path.clone())
791 .with_priority(request.priority)
792 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
793 .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
794 );
795 }
796
797 for folder in &self.folders {
799 routes.extend(folder.get_routes(&self.id));
800 }
801
802 routes
803 }
804
805 pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
807 for folder in folder_path.iter().rev() {
809 if let Some(auth) = &folder.inheritance.auth {
810 return Some(auth);
811 }
812 }
813
814 self.config.auth.as_ref()
816 }
817
818 pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
820 let mut effective_headers = HashMap::new();
821
822 for (key, value) in &self.config.default_headers {
824 effective_headers.insert(key.clone(), value.clone());
825 }
826
827 for folder in folder_path {
829 for (key, value) in &folder.inheritance.headers {
830 effective_headers.insert(key.clone(), value.clone());
831 }
832 }
833
834 effective_headers
835 }
836}
837
838impl Folder {
839 pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
841 let mut path = Vec::new();
842 let mut current = Some(self);
843
844 while let Some(folder) = current {
845 path.push(folder);
846 current =
847 folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
848 }
849
850 path.reverse(); path
852 }
853}
854
855impl MockRequest {
856 pub fn apply_inheritance(
858 &mut self,
859 effective_headers: HashMap<String, String>,
860 effective_auth: Option<&AuthConfig>,
861 ) {
862 for (key, value) in effective_headers {
864 self.headers.entry(key).or_insert(value);
865 }
866
867 if let Some(auth) = effective_auth {
870 self.auth = Some(auth.clone());
871 }
872 }
873
874 pub fn create_inherited_request(
876 mut self,
877 workspace: &Workspace,
878 folder_path: &[&Folder],
879 ) -> Self {
880 let effective_headers = workspace.get_effective_headers(folder_path);
881 let effective_auth = workspace.get_effective_auth(folder_path);
882
883 self.apply_inheritance(effective_headers, effective_auth);
884 self
885 }
886
887 pub fn new(method: HttpMethod, path: String, name: String) -> Self {
889 let now = Utc::now();
890 Self {
891 id: Uuid::new_v4().to_string(),
892 name,
893 description: None,
894 method,
895 path,
896 headers: HashMap::new(),
897 query_params: HashMap::new(),
898 body: None,
899 response: MockResponse::default(),
900 response_history: Vec::new(),
901 created_at: now,
902 updated_at: now,
903 tags: Vec::new(),
904 auth: None,
905 priority: 0,
906 }
907 }
908
909 pub fn with_response(mut self, response: MockResponse) -> Self {
911 self.response = response;
912 self
913 }
914
915 pub fn with_header(mut self, key: String, value: String) -> Self {
917 self.headers.insert(key, value);
918 self
919 }
920
921 pub fn with_query_param(mut self, key: String, value: String) -> Self {
923 self.query_params.insert(key, value);
924 self
925 }
926
927 pub fn with_body(mut self, body: String) -> Self {
929 self.body = Some(body);
930 self
931 }
932
933 pub fn with_tag(mut self, tag: String) -> Self {
935 self.tags.push(tag);
936 self
937 }
938
939 pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
941 self.response_history.push(entry);
942 if self.response_history.len() > 100 {
944 self.response_history.remove(0);
945 }
946 self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
948 }
949
950 pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
952 &self.response_history
953 }
954}
955
956impl Default for MockResponse {
957 fn default() -> Self {
958 Self {
959 status_code: 200,
960 headers: HashMap::new(),
961 body: Some("{}".to_string()),
962 content_type: Some("application/json".to_string()),
963 delay_ms: None,
964 }
965 }
966}
967
968impl Environment {
969 pub fn new(name: String) -> Self {
971 let now = Utc::now();
972 Self {
973 id: Uuid::new_v4().to_string(),
974 name,
975 description: None,
976 color: None,
977 variables: HashMap::new(),
978 created_at: now,
979 updated_at: now,
980 order: 0, sharable: false, }
983 }
984
985 pub fn new_global() -> Self {
987 let mut env = Self::new("Global".to_string());
988 env.description =
989 Some("Global environment variables available in all contexts".to_string());
990 env
991 }
992
993 pub fn set_variable(&mut self, key: String, value: String) {
995 self.variables.insert(key, value);
996 self.updated_at = Utc::now();
997 }
998
999 pub fn remove_variable(&mut self, key: &str) -> bool {
1001 let removed = self.variables.remove(key).is_some();
1002 if removed {
1003 self.updated_at = Utc::now();
1004 }
1005 removed
1006 }
1007
1008 pub fn get_variable(&self, key: &str) -> Option<&String> {
1010 self.variables.get(key)
1011 }
1012
1013 pub fn set_color(&mut self, color: EnvironmentColor) {
1015 self.color = Some(color);
1016 self.updated_at = Utc::now();
1017 }
1018}
1019
1020impl Default for SyncConfig {
1021 fn default() -> Self {
1022 Self {
1023 enabled: false,
1024 target_directory: None,
1025 directory_structure: SyncDirectoryStructure::Nested,
1026 sync_direction: SyncDirection::Manual,
1027 include_metadata: true,
1028 realtime_monitoring: false,
1029 filename_pattern: "{name}".to_string(),
1030 exclude_pattern: None,
1031 force_overwrite: false,
1032 last_sync: None,
1033 }
1034 }
1035}
1036
1037impl Default for WorkspaceConfig {
1038 fn default() -> Self {
1039 Self {
1040 base_url: None,
1041 default_headers: HashMap::new(),
1042 auth: None,
1043 global_environment: Environment::new_global(),
1044 environments: Vec::new(),
1045 active_environment_id: None,
1046 sync: SyncConfig::default(),
1047 auto_encryption: AutoEncryptionConfig::default(),
1048 reality_level: None,
1049 }
1050 }
1051}
1052
1053impl WorkspaceRegistry {
1054 pub fn new() -> Self {
1056 Self {
1057 workspaces: HashMap::new(),
1058 active_workspace: None,
1059 }
1060 }
1061
1062 pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1064 let id = workspace.id.clone();
1065
1066 if workspace.order == 0 && !self.workspaces.is_empty() {
1068 workspace.order = self.workspaces.len() as i32;
1069 }
1070
1071 self.workspaces.insert(id.clone(), workspace);
1072 Ok(id)
1073 }
1074
1075 pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1077 self.workspaces.get(id)
1078 }
1079
1080 pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1082 self.workspaces.get_mut(id)
1083 }
1084
1085 pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1087 if self.workspaces.remove(id).is_some() {
1088 if self.active_workspace.as_deref() == Some(id) {
1090 self.active_workspace = None;
1091 }
1092 Ok(())
1093 } else {
1094 Err(Error::generic(format!("Workspace with ID '{}' not found", id)))
1095 }
1096 }
1097
1098 pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1100 if let Some(ref workspace_id) = id {
1101 if !self.workspaces.contains_key(workspace_id) {
1102 return Err(Error::generic(format!(
1103 "Workspace with ID '{}' not found",
1104 workspace_id
1105 )));
1106 }
1107 }
1108 self.active_workspace = id;
1109 Ok(())
1110 }
1111
1112 pub fn get_active_workspace(&self) -> Option<&Workspace> {
1114 self.active_workspace.as_ref().and_then(|id| self.workspaces.get(id))
1115 }
1116
1117 pub fn get_active_workspace_id(&self) -> Option<&str> {
1119 self.active_workspace.as_deref()
1120 }
1121
1122 pub fn get_workspaces(&self) -> Vec<&Workspace> {
1124 self.workspaces.values().collect()
1125 }
1126
1127 pub fn get_workspaces_ordered(&self) -> Vec<&Workspace> {
1129 let mut workspaces: Vec<&Workspace> = self.workspaces.values().collect();
1130 workspaces.sort_by_key(|w| w.order);
1131 workspaces
1132 }
1133
1134 pub fn update_workspaces_order(&mut self, workspace_ids: Vec<String>) -> Result<()> {
1136 for workspace_id in &workspace_ids {
1138 if !self.workspaces.contains_key(workspace_id) {
1139 return Err(Error::generic(format!(
1140 "Workspace with ID '{}' not found",
1141 workspace_id
1142 )));
1143 }
1144 }
1145
1146 for (index, workspace_id) in workspace_ids.iter().enumerate() {
1148 if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1149 workspace.order = index as i32;
1150 workspace.updated_at = Utc::now();
1151 }
1152 }
1153
1154 Ok(())
1155 }
1156
1157 pub fn get_all_routes(&self) -> Vec<Route> {
1159 let mut all_routes = Vec::new();
1160 for workspace in self.workspaces.values() {
1161 all_routes.extend(workspace.get_routes());
1162 }
1163 all_routes
1164 }
1165
1166 pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1168 let mut registry = RouteRegistry::new();
1169 let routes = self.get_all_routes();
1170
1171 for route in routes {
1172 registry.add_http_route(route)?;
1173 }
1174
1175 Ok(registry)
1176 }
1177}
1178
1179impl Default for WorkspaceRegistry {
1180 fn default() -> Self {
1181 Self::new()
1182 }
1183}
1184
1185#[cfg(test)]
1186mod tests {
1187 use super::*;
1188
1189 use crate::ApiKeyConfig;
1190
1191 #[test]
1192 fn test_workspace_creation() {
1193 let workspace = Workspace::new("Test Workspace".to_string());
1194 assert_eq!(workspace.name, "Test Workspace");
1195 assert!(!workspace.id.is_empty());
1196 assert!(workspace.folders.is_empty());
1197 assert!(workspace.requests.is_empty());
1198 }
1199
1200 #[test]
1201 fn test_folder_creation() {
1202 let folder = Folder::new("Test Folder".to_string());
1203 assert_eq!(folder.name, "Test Folder");
1204 assert!(!folder.id.is_empty());
1205 assert!(folder.folders.is_empty());
1206 assert!(folder.requests.is_empty());
1207 }
1208
1209 #[test]
1210 fn test_request_creation() {
1211 let request =
1212 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1213 assert_eq!(request.name, "Test Request");
1214 assert_eq!(request.method, HttpMethod::GET);
1215 assert_eq!(request.path, "/test");
1216 assert_eq!(request.response.status_code, 200);
1217 }
1218
1219 #[test]
1220 fn test_workspace_hierarchy() {
1221 let mut workspace = Workspace::new("Test Workspace".to_string());
1222
1223 let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1225 assert_eq!(workspace.folders.len(), 1);
1226
1227 let request =
1229 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1230 workspace.add_request(request).unwrap();
1231 assert_eq!(workspace.requests.len(), 1);
1232
1233 let folder = workspace.find_folder_mut(&folder_id).unwrap();
1235 let folder_request = MockRequest::new(
1236 HttpMethod::POST,
1237 "/folder-test".to_string(),
1238 "Folder Request".to_string(),
1239 );
1240 folder.add_request(folder_request).unwrap();
1241 assert_eq!(folder.requests.len(), 1);
1242 }
1243
1244 #[test]
1245 fn test_workspace_registry() {
1246 let mut registry = WorkspaceRegistry::new();
1247
1248 let workspace = Workspace::new("Test Workspace".to_string());
1249 let workspace_id = registry.add_workspace(workspace).unwrap();
1250
1251 registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1253 assert!(registry.get_active_workspace().is_some());
1254
1255 let retrieved = registry.get_workspace(&workspace_id).unwrap();
1257 assert_eq!(retrieved.name, "Test Workspace");
1258
1259 registry.remove_workspace(&workspace_id).unwrap();
1261 assert!(registry.get_workspace(&workspace_id).is_none());
1262 }
1263
1264 #[test]
1265 fn test_inheritance_header_priority() {
1266 let mut workspace = Workspace::new("Test Workspace".to_string());
1267 workspace
1268 .config
1269 .default_headers
1270 .insert("X-Common".to_string(), "workspace-value".to_string());
1271 workspace
1272 .config
1273 .default_headers
1274 .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1275
1276 let mut folder = Folder::new("Test Folder".to_string());
1278 folder
1279 .inheritance
1280 .headers
1281 .insert("X-Common".to_string(), "folder-value".to_string());
1282 folder
1283 .inheritance
1284 .headers
1285 .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1286
1287 let folder_path = vec![&folder];
1289 let effective_headers = workspace.get_effective_headers(&folder_path);
1290
1291 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");
1294 }
1296
1297 #[test]
1298 fn test_inheritance_request_headers_override() {
1299 let mut workspace = Workspace::new("Test Workspace".to_string());
1300 workspace
1301 .config
1302 .default_headers
1303 .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1304
1305 let folder_path = vec![];
1306 let effective_headers = workspace.get_effective_headers(&folder_path);
1307 let mut request = MockRequest::new(
1308 crate::routing::HttpMethod::GET,
1309 "/test".to_string(),
1310 "Test Request".to_string(),
1311 );
1312
1313 request
1315 .headers
1316 .insert("Authorization".to_string(), "Bearer request-token".to_string());
1317
1318 request.apply_inheritance(effective_headers, None);
1320
1321 assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1322 }
1323
1324 #[test]
1325 fn test_inheritance_nested_folders() {
1326 let mut workspace = Workspace::new("Test Workspace".to_string());
1327 workspace
1328 .config
1329 .default_headers
1330 .insert("X-Level".to_string(), "workspace".to_string());
1331
1332 let mut parent_folder = Folder::new("Parent Folder".to_string());
1334 parent_folder
1335 .inheritance
1336 .headers
1337 .insert("X-Level".to_string(), "parent".to_string());
1338 parent_folder
1339 .inheritance
1340 .headers
1341 .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1342
1343 let mut child_folder = Folder::new("Child Folder".to_string());
1345 child_folder
1346 .inheritance
1347 .headers
1348 .insert("X-Level".to_string(), "child".to_string());
1349 child_folder
1350 .inheritance
1351 .headers
1352 .insert("X-Child-Only".to_string(), "child-value".to_string());
1353
1354 let folder_path = vec![&parent_folder, &child_folder];
1356 let effective_headers = workspace.get_effective_headers(&folder_path);
1357
1358 assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1360 assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1361 assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1362 }
1363
1364 #[test]
1365 fn test_inheritance_auth_from_folder() {
1366 let workspace = Workspace::new("Test Workspace".to_string());
1368
1369 let mut folder = Folder::new("Test Folder".to_string());
1371 let auth = AuthConfig {
1372 require_auth: true,
1373 api_key: Some(ApiKeyConfig {
1374 header_name: "X-API-Key".to_string(),
1375 query_name: Some("api_key".to_string()),
1376 keys: vec!["folder-key".to_string()],
1377 }),
1378 ..Default::default()
1379 };
1380 folder.inheritance.auth = Some(auth);
1381
1382 let folder_path = vec![&folder];
1383 let effective_auth = workspace.get_effective_auth(&folder_path);
1384
1385 assert!(effective_auth.is_some());
1386 let auth_config = effective_auth.unwrap();
1387 assert!(auth_config.require_auth);
1388 let api_key_config = auth_config.api_key.as_ref().unwrap();
1389 assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1390 }
1391}