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::routing::{HttpMethod, Route, RouteRegistry};
27use crate::{Error, Result};
28use chrono::{DateTime, Utc};
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31use uuid::Uuid;
32
33pub type EntityId = String;
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct Workspace {
39 pub id: EntityId,
41 pub name: String,
43 pub description: Option<String>,
45 pub created_at: DateTime<Utc>,
47 pub updated_at: DateTime<Utc>,
49 pub tags: Vec<String>,
51 pub config: WorkspaceConfig,
53 pub folders: Vec<Folder>,
55 pub requests: Vec<MockRequest>,
57 #[serde(default)]
59 pub order: i32,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, Default)]
64pub struct FolderInheritanceConfig {
65 #[serde(default)]
67 pub headers: HashMap<String, String>,
68 pub auth: Option<AuthConfig>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct Folder {
75 pub id: EntityId,
77 pub name: String,
79 pub description: Option<String>,
81 pub parent_id: Option<EntityId>,
83 pub created_at: DateTime<Utc>,
85 pub updated_at: DateTime<Utc>,
87 pub tags: Vec<String>,
89 #[serde(default)]
91 pub inheritance: FolderInheritanceConfig,
92 pub folders: Vec<Folder>,
94 pub requests: Vec<MockRequest>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
100pub struct MockRequest {
101 pub id: EntityId,
103 pub name: String,
105 pub description: Option<String>,
107 pub method: HttpMethod,
109 pub path: String,
111 pub headers: HashMap<String, String>,
113 pub query_params: HashMap<String, String>,
115 pub body: Option<String>,
117 pub response: MockResponse,
119 pub response_history: Vec<ResponseHistoryEntry>,
121 pub created_at: DateTime<Utc>,
123 pub updated_at: DateTime<Utc>,
125 pub tags: Vec<String>,
127 pub auth: Option<AuthConfig>,
129 pub priority: i32,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct MockResponse {
136 pub status_code: u16,
138 pub headers: HashMap<String, String>,
140 pub body: Option<String>,
142 pub content_type: Option<String>,
144 pub delay_ms: Option<u64>,
146}
147
148#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct ResponseHistoryEntry {
151 pub id: String,
153 pub executed_at: DateTime<Utc>,
155 pub request_method: HttpMethod,
157 pub request_path: String,
159 pub request_headers: HashMap<String, String>,
161 pub request_body: Option<String>,
163 pub response_status_code: u16,
165 pub response_headers: HashMap<String, String>,
167 pub response_body: Option<String>,
169 pub response_time_ms: u64,
171 pub response_size_bytes: u64,
173 pub error_message: Option<String>,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
179pub struct EnvironmentColor {
180 pub hex: String,
182 pub name: Option<String>,
184}
185
186#[derive(Debug, Clone, Serialize, Deserialize)]
188pub struct Environment {
189 pub id: EntityId,
191 pub name: String,
193 pub description: Option<String>,
195 pub color: Option<EnvironmentColor>,
197 pub variables: HashMap<String, String>,
199 pub created_at: DateTime<Utc>,
201 pub updated_at: DateTime<Utc>,
203 #[serde(default)]
205 pub order: i32,
206 #[serde(default)]
208 pub sharable: bool,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct SyncConfig {
214 pub enabled: bool,
216 pub target_directory: Option<String>,
218 pub directory_structure: SyncDirectoryStructure,
220 pub sync_direction: SyncDirection,
222 pub include_metadata: bool,
224 pub realtime_monitoring: bool,
226 pub filename_pattern: String,
228 pub exclude_pattern: Option<String>,
230 pub force_overwrite: bool,
232 pub last_sync: Option<DateTime<Utc>>,
234}
235
236#[derive(Debug, Clone, Serialize, Deserialize)]
238pub enum SyncDirectoryStructure {
239 Flat,
241 Nested,
243 Grouped,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249pub enum SyncDirection {
250 Manual,
252 WorkspaceToDirectory,
254 Bidirectional,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize)]
260pub struct WorkspaceConfig {
261 pub base_url: Option<String>,
263 pub default_headers: HashMap<String, String>,
265 pub auth: Option<AuthConfig>,
267 pub global_environment: Environment,
269 pub environments: Vec<Environment>,
271 pub active_environment_id: Option<EntityId>,
273 pub sync: SyncConfig,
275 #[serde(default)]
277 pub auto_encryption: AutoEncryptionConfig,
278}
279
280#[derive(Debug, Clone)]
282pub struct WorkspaceRegistry {
283 workspaces: HashMap<EntityId, Workspace>,
284 active_workspace: Option<EntityId>,
285}
286
287impl Workspace {
288 pub fn new(name: String) -> Self {
290 let now = Utc::now();
291 Self {
292 id: Uuid::new_v4().to_string(),
293 name,
294 description: None,
295 created_at: now,
296 updated_at: now,
297 tags: Vec::new(),
298 config: WorkspaceConfig::default(),
299 folders: Vec::new(),
300 requests: Vec::new(),
301 order: 0, }
303 }
304
305 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
307 let folder = Folder::new(name);
308 let id = folder.id.clone();
309 self.folders.push(folder);
310 self.updated_at = Utc::now();
311 Ok(id)
312 }
313
314 pub fn create_environment(
316 &mut self,
317 name: String,
318 description: Option<String>,
319 ) -> Result<EntityId> {
320 if self.config.environments.iter().any(|env| env.name == name) {
322 return Err(Error::generic(format!("Environment with name '{}' already exists", name)));
323 }
324
325 let mut environment = Environment::new(name);
326 environment.description = description;
327
328 environment.order = self.config.environments.len() as i32;
330
331 let id = environment.id.clone();
332
333 self.config.environments.push(environment);
334 self.updated_at = Utc::now();
335 Ok(id)
336 }
337
338 pub fn get_environments(&self) -> Vec<&Environment> {
340 let mut all_envs = vec![&self.config.global_environment];
341 all_envs.extend(self.config.environments.iter());
342 all_envs
343 }
344
345 pub fn get_environment(&self, id: &str) -> Option<&Environment> {
347 if self.config.global_environment.id == id {
348 Some(&self.config.global_environment)
349 } else {
350 self.config.environments.iter().find(|env| env.id == id)
351 }
352 }
353
354 pub fn get_environment_mut(&mut self, id: &str) -> Option<&mut Environment> {
356 if self.config.global_environment.id == id {
357 Some(&mut self.config.global_environment)
358 } else {
359 self.config.environments.iter_mut().find(|env| env.id == id)
360 }
361 }
362
363 pub fn set_active_environment(&mut self, environment_id: Option<String>) -> Result<()> {
365 if let Some(ref id) = environment_id {
366 if self.get_environment(id).is_none() {
367 return Err(Error::generic(format!("Environment with ID '{}' not found", id)));
368 }
369 }
370 self.config.active_environment_id = environment_id;
371 self.updated_at = Utc::now();
372 Ok(())
373 }
374
375 pub fn get_active_environment(&self) -> &Environment {
377 if let Some(ref active_id) = self.config.active_environment_id {
378 self.get_environment(active_id).unwrap_or(&self.config.global_environment)
379 } else {
380 &self.config.global_environment
381 }
382 }
383
384 pub fn get_active_environment_id(&self) -> Option<&str> {
386 self.config.active_environment_id.as_deref()
387 }
388
389 pub fn get_variable(&self, key: &str) -> Option<&String> {
391 let active_env = self.get_active_environment();
393 active_env.get_variable(key).or_else(|| {
394 if active_env.id != self.config.global_environment.id {
395 self.config.global_environment.get_variable(key)
396 } else {
397 None
398 }
399 })
400 }
401
402 pub fn get_all_variables(&self) -> HashMap<String, String> {
404 let mut variables = HashMap::new();
405
406 variables.extend(self.config.global_environment.variables.clone());
408
409 let active_env = self.get_active_environment();
411 if active_env.id != self.config.global_environment.id {
412 variables.extend(active_env.variables.clone());
413 }
414
415 variables
416 }
417
418 pub fn delete_environment(&mut self, id: &str) -> Result<()> {
420 if id == self.config.global_environment.id {
421 return Err(Error::generic("Cannot delete global environment".to_string()));
422 }
423
424 let position = self.config.environments.iter().position(|env| env.id == id);
425 if let Some(pos) = position {
426 self.config.environments.remove(pos);
427
428 if self.config.active_environment_id.as_deref() == Some(id) {
430 self.config.active_environment_id = None;
431 }
432
433 self.updated_at = Utc::now();
434 Ok(())
435 } else {
436 Err(Error::generic(format!("Environment with ID '{}' not found", id)))
437 }
438 }
439
440 pub fn update_environments_order(&mut self, environment_ids: Vec<String>) -> Result<()> {
442 for env_id in &environment_ids {
444 if !self.config.environments.iter().any(|env| env.id == *env_id) {
445 return Err(Error::generic(format!("Environment with ID '{}' not found", env_id)));
446 }
447 }
448
449 for (index, env_id) in environment_ids.iter().enumerate() {
451 if let Some(env) = self.config.environments.iter_mut().find(|env| env.id == *env_id) {
452 env.order = index as i32;
453 env.updated_at = Utc::now();
454 }
455 }
456
457 self.updated_at = Utc::now();
458 Ok(())
459 }
460
461 pub fn get_environments_ordered(&self) -> Vec<&Environment> {
463 let mut all_envs = vec![&self.config.global_environment];
464 all_envs.extend(self.config.environments.iter());
465 all_envs.sort_by_key(|env| env.order);
466 all_envs
467 }
468
469 pub fn configure_sync(&mut self, config: SyncConfig) -> Result<()> {
471 self.config.sync = config;
472 self.updated_at = Utc::now();
473 Ok(())
474 }
475
476 pub fn enable_sync(&mut self, target_directory: String) -> Result<()> {
478 self.config.sync.enabled = true;
479 self.config.sync.target_directory = Some(target_directory);
480 self.config.sync.realtime_monitoring = true; self.updated_at = Utc::now();
482 Ok(())
483 }
484
485 pub fn disable_sync(&mut self) -> Result<()> {
487 self.config.sync.enabled = false;
488 self.updated_at = Utc::now();
489 Ok(())
490 }
491
492 pub fn get_sync_config(&self) -> &SyncConfig {
494 &self.config.sync
495 }
496
497 pub fn is_sync_enabled(&self) -> bool {
499 self.config.sync.enabled
500 }
501
502 pub fn get_sync_directory(&self) -> Option<&str> {
504 self.config.sync.target_directory.as_deref()
505 }
506
507 pub fn set_sync_directory(&mut self, directory: Option<String>) -> Result<()> {
509 self.config.sync.target_directory = directory;
510 self.updated_at = Utc::now();
511 Ok(())
512 }
513
514 pub fn set_sync_direction(&mut self, direction: SyncDirection) -> Result<()> {
516 self.config.sync.sync_direction = direction;
517 self.updated_at = Utc::now();
518 Ok(())
519 }
520
521 pub fn get_sync_direction(&self) -> &SyncDirection {
523 &self.config.sync.sync_direction
524 }
525
526 pub fn set_realtime_monitoring(&mut self, enabled: bool) -> Result<()> {
528 self.config.sync.realtime_monitoring = enabled;
529 self.updated_at = Utc::now();
530 Ok(())
531 }
532
533 pub fn is_realtime_monitoring_enabled(&self) -> bool {
535 self.config.sync.realtime_monitoring && self.config.sync.enabled
536 }
537
538 pub fn to_filtered_for_sync(&self) -> Workspace {
540 let mut filtered = self.clone();
541
542 filtered.config.global_environment.variables =
545 self.filter_sensitive_variables(&self.config.global_environment.variables);
546
547 filtered.config.environments = filtered
549 .config
550 .environments
551 .into_iter()
552 .filter(|env| env.sharable)
553 .map(|mut env| {
554 env.variables = self.filter_sensitive_variables(&env.variables);
555 env
556 })
557 .collect();
558
559 filtered
560 }
561
562 fn filter_sensitive_variables(
564 &self,
565 variables: &HashMap<String, String>,
566 ) -> HashMap<String, String> {
567 let sensitive_keys = [
568 "password",
570 "secret",
571 "key",
572 "token",
573 "credential",
574 "api_key",
575 "apikey",
576 "api_secret",
577 "db_password",
578 "database_password",
579 "aws_secret_key",
580 "aws_session_token",
581 "private_key",
582 "authorization",
583 "auth_token",
584 "access_token",
585 "refresh_token",
586 "cookie",
587 "session",
588 "csrf",
589 "jwt",
590 "bearer",
591 ];
592
593 variables
594 .iter()
595 .filter(|(key, _)| {
596 let key_lower = key.to_lowercase();
597 !sensitive_keys.iter().any(|sensitive| key_lower.contains(sensitive))
598 })
599 .map(|(k, v)| (k.clone(), v.clone()))
600 .collect()
601 }
602
603 pub fn should_sync(&self) -> bool {
605 self.config.sync.enabled && self.config.sync.target_directory.is_some()
606 }
607
608 pub fn get_sync_filename(&self) -> String {
610 let pattern = &self.config.sync.filename_pattern;
612
613 let filename = pattern
615 .replace("{name}", &sanitize_filename(&self.name))
616 .replace("{id}", &self.id);
617
618 if filename.ends_with(".yaml") || filename.ends_with(".yml") {
619 filename
620 } else {
621 format!("{}.yaml", filename)
622 }
623 }
624}
625
626fn sanitize_filename(name: &str) -> String {
628 name.chars()
629 .map(|c| match c {
630 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
631 c if c.is_control() => '_',
632 c if c.is_whitespace() => '-',
633 c => c,
634 })
635 .collect::<String>()
636 .to_lowercase()
637}
638
639impl Folder {
640 pub fn new(name: String) -> Self {
642 let now = Utc::now();
643 Self {
644 id: Uuid::new_v4().to_string(),
645 name,
646 description: None,
647 parent_id: None,
648 created_at: now,
649 updated_at: now,
650 tags: Vec::new(),
651 inheritance: FolderInheritanceConfig::default(),
652 folders: Vec::new(),
653 requests: Vec::new(),
654 }
655 }
656
657 pub fn add_folder(&mut self, name: String) -> Result<EntityId> {
659 let mut folder = Folder::new(name);
660 folder.parent_id = Some(self.id.clone());
661 let id = folder.id.clone();
662 self.folders.push(folder);
663 self.updated_at = Utc::now();
664 Ok(id)
665 }
666
667 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
669 let id = request.id.clone();
670 self.requests.push(request);
671 self.updated_at = Utc::now();
672 Ok(id)
673 }
674
675 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
677 for folder in &self.folders {
678 if folder.id == id {
679 return Some(folder);
680 }
681 if let Some(found) = folder.find_folder(id) {
682 return Some(found);
683 }
684 }
685 None
686 }
687
688 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
690 for folder in &mut self.folders {
691 if folder.id == id {
692 return Some(folder);
693 }
694 if let Some(found) = folder.find_folder_mut(id) {
695 return Some(found);
696 }
697 }
698 None
699 }
700
701 pub fn find_request(&self, id: &str) -> Option<&MockRequest> {
703 for request in &self.requests {
705 if request.id == id {
706 return Some(request);
707 }
708 }
709
710 for folder in &self.folders {
712 if let Some(found) = folder.find_request(id) {
713 return Some(found);
714 }
715 }
716 None
717 }
718
719 pub fn get_routes(&self, workspace_id: &str) -> Vec<Route> {
721 let mut routes = Vec::new();
722
723 for request in &self.requests {
725 routes.push(
726 Route::new(request.method.clone(), request.path.clone())
727 .with_priority(request.priority)
728 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
729 .with_metadata("folder_id".to_string(), serde_json::json!(self.id))
730 .with_metadata("workspace_id".to_string(), serde_json::json!(workspace_id)),
731 );
732 }
733
734 for folder in &self.folders {
736 routes.extend(folder.get_routes(workspace_id));
737 }
738
739 routes
740 }
741}
742
743impl Workspace {
744 pub fn find_folder(&self, id: &str) -> Option<&Folder> {
746 for folder in &self.folders {
747 if folder.id == id {
748 return Some(folder);
749 }
750 if let Some(found) = folder.find_folder(id) {
751 return Some(found);
752 }
753 }
754 None
755 }
756
757 pub fn find_folder_mut(&mut self, id: &str) -> Option<&mut Folder> {
759 for folder in &mut self.folders {
760 if folder.id == id {
761 return Some(folder);
762 }
763 if let Some(found) = folder.find_folder_mut(id) {
764 return Some(found);
765 }
766 }
767 None
768 }
769
770 pub fn add_request(&mut self, request: MockRequest) -> Result<EntityId> {
772 let id = request.id.clone();
773 self.requests.push(request);
774 self.updated_at = Utc::now();
775 Ok(id)
776 }
777
778 pub fn get_routes(&self) -> Vec<Route> {
780 let mut routes = Vec::new();
781
782 for request in &self.requests {
784 routes.push(
785 Route::new(request.method.clone(), request.path.clone())
786 .with_priority(request.priority)
787 .with_metadata("request_id".to_string(), serde_json::json!(request.id))
788 .with_metadata("workspace_id".to_string(), serde_json::json!(self.id)),
789 );
790 }
791
792 for folder in &self.folders {
794 routes.extend(folder.get_routes(&self.id));
795 }
796
797 routes
798 }
799
800 pub fn get_effective_auth<'a>(&'a self, folder_path: &[&'a Folder]) -> Option<&'a AuthConfig> {
802 for folder in folder_path.iter().rev() {
804 if let Some(auth) = &folder.inheritance.auth {
805 return Some(auth);
806 }
807 }
808
809 self.config.auth.as_ref()
811 }
812
813 pub fn get_effective_headers(&self, folder_path: &[&Folder]) -> HashMap<String, String> {
815 let mut effective_headers = HashMap::new();
816
817 for (key, value) in &self.config.default_headers {
819 effective_headers.insert(key.clone(), value.clone());
820 }
821
822 for folder in folder_path {
824 for (key, value) in &folder.inheritance.headers {
825 effective_headers.insert(key.clone(), value.clone());
826 }
827 }
828
829 effective_headers
830 }
831}
832
833impl Folder {
834 pub fn get_inheritance_path<'a>(&'a self, workspace: &'a Workspace) -> Vec<&'a Folder> {
836 let mut path = Vec::new();
837 let mut current = Some(self);
838
839 while let Some(folder) = current {
840 path.push(folder);
841 current =
842 folder.parent_id.as_ref().and_then(|parent_id| workspace.find_folder(parent_id));
843 }
844
845 path.reverse(); path
847 }
848}
849
850impl MockRequest {
851 pub fn apply_inheritance(
853 &mut self,
854 effective_headers: HashMap<String, String>,
855 effective_auth: Option<&AuthConfig>,
856 ) {
857 for (key, value) in effective_headers {
859 self.headers.entry(key).or_insert(value);
860 }
861
862 if let Some(auth) = effective_auth {
865 self.auth = Some(auth.clone());
866 }
867 }
868
869 pub fn create_inherited_request(
871 mut self,
872 workspace: &Workspace,
873 folder_path: &[&Folder],
874 ) -> Self {
875 let effective_headers = workspace.get_effective_headers(folder_path);
876 let effective_auth = workspace.get_effective_auth(folder_path);
877
878 self.apply_inheritance(effective_headers, effective_auth);
879 self
880 }
881
882 pub fn new(method: HttpMethod, path: String, name: String) -> Self {
884 let now = Utc::now();
885 Self {
886 id: Uuid::new_v4().to_string(),
887 name,
888 description: None,
889 method,
890 path,
891 headers: HashMap::new(),
892 query_params: HashMap::new(),
893 body: None,
894 response: MockResponse::default(),
895 response_history: Vec::new(),
896 created_at: now,
897 updated_at: now,
898 tags: Vec::new(),
899 auth: None,
900 priority: 0,
901 }
902 }
903
904 pub fn with_response(mut self, response: MockResponse) -> Self {
906 self.response = response;
907 self
908 }
909
910 pub fn with_header(mut self, key: String, value: String) -> Self {
912 self.headers.insert(key, value);
913 self
914 }
915
916 pub fn with_query_param(mut self, key: String, value: String) -> Self {
918 self.query_params.insert(key, value);
919 self
920 }
921
922 pub fn with_body(mut self, body: String) -> Self {
924 self.body = Some(body);
925 self
926 }
927
928 pub fn with_tag(mut self, tag: String) -> Self {
930 self.tags.push(tag);
931 self
932 }
933
934 pub fn add_response_history(&mut self, entry: ResponseHistoryEntry) {
936 self.response_history.push(entry);
937 if self.response_history.len() > 100 {
939 self.response_history.remove(0);
940 }
941 self.response_history.sort_by(|a, b| b.executed_at.cmp(&a.executed_at));
943 }
944
945 pub fn get_response_history(&self) -> &[ResponseHistoryEntry] {
947 &self.response_history
948 }
949}
950
951impl Default for MockResponse {
952 fn default() -> Self {
953 Self {
954 status_code: 200,
955 headers: HashMap::new(),
956 body: Some("{}".to_string()),
957 content_type: Some("application/json".to_string()),
958 delay_ms: None,
959 }
960 }
961}
962
963impl Environment {
964 pub fn new(name: String) -> Self {
966 let now = Utc::now();
967 Self {
968 id: Uuid::new_v4().to_string(),
969 name,
970 description: None,
971 color: None,
972 variables: HashMap::new(),
973 created_at: now,
974 updated_at: now,
975 order: 0, sharable: false, }
978 }
979
980 pub fn new_global() -> Self {
982 let mut env = Self::new("Global".to_string());
983 env.description =
984 Some("Global environment variables available in all contexts".to_string());
985 env
986 }
987
988 pub fn set_variable(&mut self, key: String, value: String) {
990 self.variables.insert(key, value);
991 self.updated_at = Utc::now();
992 }
993
994 pub fn remove_variable(&mut self, key: &str) -> bool {
996 let removed = self.variables.remove(key).is_some();
997 if removed {
998 self.updated_at = Utc::now();
999 }
1000 removed
1001 }
1002
1003 pub fn get_variable(&self, key: &str) -> Option<&String> {
1005 self.variables.get(key)
1006 }
1007
1008 pub fn set_color(&mut self, color: EnvironmentColor) {
1010 self.color = Some(color);
1011 self.updated_at = Utc::now();
1012 }
1013}
1014
1015impl Default for SyncConfig {
1016 fn default() -> Self {
1017 Self {
1018 enabled: false,
1019 target_directory: None,
1020 directory_structure: SyncDirectoryStructure::Nested,
1021 sync_direction: SyncDirection::Manual,
1022 include_metadata: true,
1023 realtime_monitoring: false,
1024 filename_pattern: "{name}".to_string(),
1025 exclude_pattern: None,
1026 force_overwrite: false,
1027 last_sync: None,
1028 }
1029 }
1030}
1031
1032impl Default for WorkspaceConfig {
1033 fn default() -> Self {
1034 Self {
1035 base_url: None,
1036 default_headers: HashMap::new(),
1037 auth: None,
1038 global_environment: Environment::new_global(),
1039 environments: Vec::new(),
1040 active_environment_id: None,
1041 sync: SyncConfig::default(),
1042 auto_encryption: AutoEncryptionConfig::default(),
1043 }
1044 }
1045}
1046
1047impl WorkspaceRegistry {
1048 pub fn new() -> Self {
1050 Self {
1051 workspaces: HashMap::new(),
1052 active_workspace: None,
1053 }
1054 }
1055
1056 pub fn add_workspace(&mut self, mut workspace: Workspace) -> Result<EntityId> {
1058 let id = workspace.id.clone();
1059
1060 if workspace.order == 0 && !self.workspaces.is_empty() {
1062 workspace.order = self.workspaces.len() as i32;
1063 }
1064
1065 self.workspaces.insert(id.clone(), workspace);
1066 Ok(id)
1067 }
1068
1069 pub fn get_workspace(&self, id: &str) -> Option<&Workspace> {
1071 self.workspaces.get(id)
1072 }
1073
1074 pub fn get_workspace_mut(&mut self, id: &str) -> Option<&mut Workspace> {
1076 self.workspaces.get_mut(id)
1077 }
1078
1079 pub fn remove_workspace(&mut self, id: &str) -> Result<()> {
1081 if self.workspaces.remove(id).is_some() {
1082 if self.active_workspace.as_deref() == Some(id) {
1084 self.active_workspace = None;
1085 }
1086 Ok(())
1087 } else {
1088 Err(Error::generic(format!("Workspace with ID '{}' not found", id)))
1089 }
1090 }
1091
1092 pub fn set_active_workspace(&mut self, id: Option<String>) -> Result<()> {
1094 if let Some(ref workspace_id) = id {
1095 if !self.workspaces.contains_key(workspace_id) {
1096 return Err(Error::generic(format!(
1097 "Workspace with ID '{}' not found",
1098 workspace_id
1099 )));
1100 }
1101 }
1102 self.active_workspace = id;
1103 Ok(())
1104 }
1105
1106 pub fn get_active_workspace(&self) -> Option<&Workspace> {
1108 self.active_workspace.as_ref().and_then(|id| self.workspaces.get(id))
1109 }
1110
1111 pub fn get_active_workspace_id(&self) -> Option<&str> {
1113 self.active_workspace.as_deref()
1114 }
1115
1116 pub fn get_workspaces(&self) -> Vec<&Workspace> {
1118 self.workspaces.values().collect()
1119 }
1120
1121 pub fn get_workspaces_ordered(&self) -> Vec<&Workspace> {
1123 let mut workspaces: Vec<&Workspace> = self.workspaces.values().collect();
1124 workspaces.sort_by_key(|w| w.order);
1125 workspaces
1126 }
1127
1128 pub fn update_workspaces_order(&mut self, workspace_ids: Vec<String>) -> Result<()> {
1130 for workspace_id in &workspace_ids {
1132 if !self.workspaces.contains_key(workspace_id) {
1133 return Err(Error::generic(format!(
1134 "Workspace with ID '{}' not found",
1135 workspace_id
1136 )));
1137 }
1138 }
1139
1140 for (index, workspace_id) in workspace_ids.iter().enumerate() {
1142 if let Some(workspace) = self.workspaces.get_mut(workspace_id) {
1143 workspace.order = index as i32;
1144 workspace.updated_at = Utc::now();
1145 }
1146 }
1147
1148 Ok(())
1149 }
1150
1151 pub fn get_all_routes(&self) -> Vec<Route> {
1153 let mut all_routes = Vec::new();
1154 for workspace in self.workspaces.values() {
1155 all_routes.extend(workspace.get_routes());
1156 }
1157 all_routes
1158 }
1159
1160 pub fn create_route_registry(&self) -> Result<RouteRegistry> {
1162 let mut registry = RouteRegistry::new();
1163 let routes = self.get_all_routes();
1164
1165 for route in routes {
1166 registry.add_http_route(route)?;
1167 }
1168
1169 Ok(registry)
1170 }
1171}
1172
1173impl Default for WorkspaceRegistry {
1174 fn default() -> Self {
1175 Self::new()
1176 }
1177}
1178
1179#[cfg(test)]
1180mod tests {
1181 use super::*;
1182
1183 use crate::ApiKeyConfig;
1184
1185 #[test]
1186 fn test_workspace_creation() {
1187 let workspace = Workspace::new("Test Workspace".to_string());
1188 assert_eq!(workspace.name, "Test Workspace");
1189 assert!(!workspace.id.is_empty());
1190 assert!(workspace.folders.is_empty());
1191 assert!(workspace.requests.is_empty());
1192 }
1193
1194 #[test]
1195 fn test_folder_creation() {
1196 let folder = Folder::new("Test Folder".to_string());
1197 assert_eq!(folder.name, "Test Folder");
1198 assert!(!folder.id.is_empty());
1199 assert!(folder.folders.is_empty());
1200 assert!(folder.requests.is_empty());
1201 }
1202
1203 #[test]
1204 fn test_request_creation() {
1205 let request =
1206 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1207 assert_eq!(request.name, "Test Request");
1208 assert_eq!(request.method, HttpMethod::GET);
1209 assert_eq!(request.path, "/test");
1210 assert_eq!(request.response.status_code, 200);
1211 }
1212
1213 #[test]
1214 fn test_workspace_hierarchy() {
1215 let mut workspace = Workspace::new("Test Workspace".to_string());
1216
1217 let folder_id = workspace.add_folder("Test Folder".to_string()).unwrap();
1219 assert_eq!(workspace.folders.len(), 1);
1220
1221 let request =
1223 MockRequest::new(HttpMethod::GET, "/test".to_string(), "Test Request".to_string());
1224 workspace.add_request(request).unwrap();
1225 assert_eq!(workspace.requests.len(), 1);
1226
1227 let folder = workspace.find_folder_mut(&folder_id).unwrap();
1229 let folder_request = MockRequest::new(
1230 HttpMethod::POST,
1231 "/folder-test".to_string(),
1232 "Folder Request".to_string(),
1233 );
1234 folder.add_request(folder_request).unwrap();
1235 assert_eq!(folder.requests.len(), 1);
1236 }
1237
1238 #[test]
1239 fn test_workspace_registry() {
1240 let mut registry = WorkspaceRegistry::new();
1241
1242 let workspace = Workspace::new("Test Workspace".to_string());
1243 let workspace_id = registry.add_workspace(workspace).unwrap();
1244
1245 registry.set_active_workspace(Some(workspace_id.clone())).unwrap();
1247 assert!(registry.get_active_workspace().is_some());
1248
1249 let retrieved = registry.get_workspace(&workspace_id).unwrap();
1251 assert_eq!(retrieved.name, "Test Workspace");
1252
1253 registry.remove_workspace(&workspace_id).unwrap();
1255 assert!(registry.get_workspace(&workspace_id).is_none());
1256 }
1257
1258 #[test]
1259 fn test_inheritance_header_priority() {
1260 let mut workspace = Workspace::new("Test Workspace".to_string());
1261 workspace
1262 .config
1263 .default_headers
1264 .insert("X-Common".to_string(), "workspace-value".to_string());
1265 workspace
1266 .config
1267 .default_headers
1268 .insert("X-Workspace-Only".to_string(), "workspace-only-value".to_string());
1269
1270 let mut folder = Folder::new("Test Folder".to_string());
1272 folder
1273 .inheritance
1274 .headers
1275 .insert("X-Common".to_string(), "folder-value".to_string());
1276 folder
1277 .inheritance
1278 .headers
1279 .insert("X-Folder-Only".to_string(), "folder-only-value".to_string());
1280
1281 let folder_path = vec![&folder];
1283 let effective_headers = workspace.get_effective_headers(&folder_path);
1284
1285 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");
1288 }
1290
1291 #[test]
1292 fn test_inheritance_request_headers_override() {
1293 let mut workspace = Workspace::new("Test Workspace".to_string());
1294 workspace
1295 .config
1296 .default_headers
1297 .insert("Authorization".to_string(), "Bearer workspace-token".to_string());
1298
1299 let folder_path = vec![];
1300 let effective_headers = workspace.get_effective_headers(&folder_path);
1301 let mut request = MockRequest::new(
1302 crate::routing::HttpMethod::GET,
1303 "/test".to_string(),
1304 "Test Request".to_string(),
1305 );
1306
1307 request
1309 .headers
1310 .insert("Authorization".to_string(), "Bearer request-token".to_string());
1311
1312 request.apply_inheritance(effective_headers, None);
1314
1315 assert_eq!(request.headers.get("Authorization").unwrap(), "Bearer request-token");
1316 }
1317
1318 #[test]
1319 fn test_inheritance_nested_folders() {
1320 let mut workspace = Workspace::new("Test Workspace".to_string());
1321 workspace
1322 .config
1323 .default_headers
1324 .insert("X-Level".to_string(), "workspace".to_string());
1325
1326 let mut parent_folder = Folder::new("Parent Folder".to_string());
1328 parent_folder
1329 .inheritance
1330 .headers
1331 .insert("X-Level".to_string(), "parent".to_string());
1332 parent_folder
1333 .inheritance
1334 .headers
1335 .insert("X-Parent-Only".to_string(), "parent-value".to_string());
1336
1337 let mut child_folder = Folder::new("Child Folder".to_string());
1339 child_folder
1340 .inheritance
1341 .headers
1342 .insert("X-Level".to_string(), "child".to_string());
1343 child_folder
1344 .inheritance
1345 .headers
1346 .insert("X-Child-Only".to_string(), "child-value".to_string());
1347
1348 let folder_path = vec![&parent_folder, &child_folder];
1350 let effective_headers = workspace.get_effective_headers(&folder_path);
1351
1352 assert_eq!(effective_headers.get("X-Level").unwrap(), "child");
1354 assert_eq!(effective_headers.get("X-Parent-Only").unwrap(), "parent-value");
1355 assert_eq!(effective_headers.get("X-Child-Only").unwrap(), "child-value");
1356 }
1357
1358 #[test]
1359 fn test_inheritance_auth_from_folder() {
1360 let workspace = Workspace::new("Test Workspace".to_string());
1362
1363 let mut folder = Folder::new("Test Folder".to_string());
1365 let auth = AuthConfig {
1366 require_auth: true,
1367 api_key: Some(ApiKeyConfig {
1368 header_name: "X-API-Key".to_string(),
1369 query_name: Some("api_key".to_string()),
1370 keys: vec!["folder-key".to_string()],
1371 }),
1372 ..Default::default()
1373 };
1374 folder.inheritance.auth = Some(auth);
1375
1376 let folder_path = vec![&folder];
1377 let effective_auth = workspace.get_effective_auth(&folder_path);
1378
1379 assert!(effective_auth.is_some());
1380 let auth_config = effective_auth.unwrap();
1381 assert!(auth_config.require_auth);
1382 let api_key_config = auth_config.api_key.as_ref().unwrap();
1383 assert_eq!(api_key_config.keys, vec!["folder-key".to_string()]);
1384 }
1385}