1use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use uuid::Uuid;
20
21use super::Relationship;
22use super::domain_config::ViewPosition;
23use super::enums::{AuthMethod, EnvironmentStatus, InfrastructureType};
24use super::table::{ContactDetails, SlaProperty};
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
30#[serde(rename_all = "camelCase")]
31pub struct AssetReference {
32 pub id: Uuid,
34 pub name: String,
36 pub domain: String,
38 #[serde(skip_serializing_if = "Option::is_none")]
40 pub system: Option<String>,
41 #[serde(alias = "asset_type")]
43 pub asset_type: AssetType,
44 #[serde(skip_serializing_if = "Option::is_none", alias = "file_path")]
46 pub file_path: Option<String>,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
51#[serde(rename_all = "lowercase")]
52pub enum AssetType {
53 Workspace,
55 Relationships,
57 Odcs,
59 Odps,
61 Cads,
63 Bpmn,
65 Dmn,
67 Openapi,
69 Decision,
71 Knowledge,
73 DecisionIndex,
75 KnowledgeIndex,
77 Sketch,
79 SketchIndex,
81 Dbmv,
83}
84
85impl AssetType {
86 pub fn extension(&self) -> &'static str {
88 match self {
89 AssetType::Workspace => "yaml",
90 AssetType::Relationships => "yaml",
91 AssetType::Odcs => "odcs.yaml",
92 AssetType::Odps => "odps.yaml",
93 AssetType::Cads => "cads.yaml",
94 AssetType::Bpmn => "bpmn.xml",
95 AssetType::Dmn => "dmn.xml",
96 AssetType::Openapi => "openapi.yaml",
97 AssetType::Decision => "madr.yaml",
98 AssetType::Knowledge => "kb.yaml",
99 AssetType::DecisionIndex => "yaml",
100 AssetType::KnowledgeIndex => "yaml",
101 AssetType::Sketch => "sketch.yaml",
102 AssetType::SketchIndex => "yaml",
103 AssetType::Dbmv => "dbmv.yaml",
104 }
105 }
106
107 pub fn filename(&self) -> Option<&'static str> {
109 match self {
110 AssetType::Workspace => Some("workspace.yaml"),
111 AssetType::Relationships => Some("relationships.yaml"),
112 AssetType::DecisionIndex => Some("decisions.yaml"),
113 AssetType::KnowledgeIndex => Some("knowledge.yaml"),
114 AssetType::SketchIndex => Some("sketches.yaml"),
115 _ => None,
116 }
117 }
118
119 pub fn is_workspace_level(&self) -> bool {
121 matches!(
122 self,
123 AssetType::Workspace
124 | AssetType::Relationships
125 | AssetType::DecisionIndex
126 | AssetType::KnowledgeIndex
127 | AssetType::SketchIndex
128 )
129 }
130
131 pub fn from_filename(filename: &str) -> Option<Self> {
133 if filename == "workspace.yaml" {
134 Some(AssetType::Workspace)
135 } else if filename == "relationships.yaml" {
136 Some(AssetType::Relationships)
137 } else if filename == "decisions.yaml" {
138 Some(AssetType::DecisionIndex)
139 } else if filename == "knowledge.yaml" {
140 Some(AssetType::KnowledgeIndex)
141 } else if filename == "sketches.yaml" {
142 Some(AssetType::SketchIndex)
143 } else if filename.ends_with(".odcs.yaml") {
144 Some(AssetType::Odcs)
145 } else if filename.ends_with(".odps.yaml") {
146 Some(AssetType::Odps)
147 } else if filename.ends_with(".cads.yaml") {
148 Some(AssetType::Cads)
149 } else if filename.ends_with(".madr.yaml") {
150 Some(AssetType::Decision)
151 } else if filename.ends_with(".kb.yaml") {
152 Some(AssetType::Knowledge)
153 } else if filename.ends_with(".sketch.yaml") {
154 Some(AssetType::Sketch)
155 } else if filename.ends_with(".dbmv.yaml") {
156 Some(AssetType::Dbmv)
157 } else if filename.ends_with(".bpmn.xml") {
158 Some(AssetType::Bpmn)
159 } else if filename.ends_with(".dmn.xml") {
160 Some(AssetType::Dmn)
161 } else if filename.ends_with(".openapi.yaml") || filename.ends_with(".openapi.json") {
162 Some(AssetType::Openapi)
163 } else {
164 None
165 }
166 }
167
168 pub fn supported_extensions() -> &'static [&'static str] {
170 &[
171 "workspace.yaml",
172 "relationships.yaml",
173 "decisions.yaml",
174 "knowledge.yaml",
175 "sketches.yaml",
176 ".odcs.yaml",
177 ".odps.yaml",
178 ".cads.yaml",
179 ".madr.yaml",
180 ".kb.yaml",
181 ".sketch.yaml",
182 ".dbmv.yaml",
183 ".bpmn.xml",
184 ".dmn.xml",
185 ".openapi.yaml",
186 ".openapi.json",
187 ]
188 }
189
190 pub fn is_supported_file(filename: &str) -> bool {
192 Self::from_filename(filename).is_some()
193 }
194}
195
196#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, Default)]
198#[serde(rename_all = "camelCase")]
199pub enum TableVisibility {
200 #[default]
202 Public,
203 DomainOnly,
205 Hidden,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
211#[serde(rename_all = "camelCase")]
212pub struct TransformationLink {
213 pub id: Uuid,
215 pub name: String,
217 #[serde(skip_serializing_if = "Option::is_none", alias = "transformation_type")]
219 pub transformation_type: Option<String>,
220 #[serde(skip_serializing_if = "Option::is_none")]
222 pub url: Option<String>,
223 #[serde(skip_serializing_if = "Option::is_none")]
225 pub description: Option<String>,
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
230#[serde(rename_all = "camelCase")]
231pub struct SharedResource {
232 pub id: Uuid,
234 pub name: String,
236 #[serde(alias = "resource_type")]
238 pub resource_type: String,
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub url: Option<String>,
242 #[serde(skip_serializing_if = "Option::is_none")]
244 pub description: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
251#[serde(rename_all = "camelCase")]
252pub struct DomainReference {
253 pub id: Uuid,
255 pub name: String,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub description: Option<String>,
260 #[serde(default, skip_serializing_if = "Vec::is_empty")]
262 pub systems: Vec<SystemReference>,
263 #[serde(
265 default,
266 skip_serializing_if = "Vec::is_empty",
267 alias = "shared_resources"
268 )]
269 pub shared_resources: Vec<SharedResource>,
270 #[serde(
272 default,
273 skip_serializing_if = "Vec::is_empty",
274 alias = "transformation_links"
275 )]
276 pub transformation_links: Vec<TransformationLink>,
277 #[serde(
279 default,
280 skip_serializing_if = "Option::is_none",
281 alias = "table_visibility"
282 )]
283 pub table_visibility: Option<TableVisibility>,
284 #[serde(
287 default,
288 skip_serializing_if = "HashMap::is_empty",
289 alias = "view_positions"
290 )]
291 pub view_positions: HashMap<String, HashMap<String, ViewPosition>>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
301#[serde(rename_all = "camelCase")]
302pub struct EnvironmentConnection {
303 pub environment: String,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub owner: Option<String>,
309
310 #[serde(skip_serializing_if = "Option::is_none", alias = "contact_details")]
312 pub contact_details: Option<ContactDetails>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub sla: Option<Vec<SlaProperty>>,
317
318 #[serde(skip_serializing_if = "Option::is_none", alias = "auth_method")]
320 pub auth_method: Option<AuthMethod>,
321
322 #[serde(skip_serializing_if = "Option::is_none", alias = "support_team")]
324 pub support_team: Option<String>,
325
326 #[serde(skip_serializing_if = "Option::is_none", alias = "connection_string")]
328 pub connection_string: Option<String>,
329
330 #[serde(skip_serializing_if = "Option::is_none", alias = "secret_link")]
332 pub secret_link: Option<String>,
333
334 #[serde(skip_serializing_if = "Option::is_none")]
336 pub endpoint: Option<String>,
337
338 #[serde(skip_serializing_if = "Option::is_none")]
340 pub port: Option<u16>,
341
342 #[serde(skip_serializing_if = "Option::is_none")]
344 pub region: Option<String>,
345
346 #[serde(skip_serializing_if = "Option::is_none")]
348 pub status: Option<EnvironmentStatus>,
349
350 #[serde(skip_serializing_if = "Option::is_none")]
352 pub notes: Option<String>,
353
354 #[serde(
356 default,
357 skip_serializing_if = "HashMap::is_empty",
358 alias = "custom_properties"
359 )]
360 pub custom_properties: HashMap<String, serde_json::Value>,
361}
362
363#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
369#[serde(rename_all = "camelCase")]
370pub struct SystemReference {
371 pub id: Uuid,
373 pub name: String,
375 #[serde(skip_serializing_if = "Option::is_none")]
377 pub description: Option<String>,
378 #[serde(skip_serializing_if = "Option::is_none", alias = "system_type")]
380 pub system_type: Option<InfrastructureType>,
381 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "table_ids")]
384 pub table_ids: Vec<Uuid>,
385 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "asset_ids")]
388 pub asset_ids: Vec<Uuid>,
389 #[serde(default, skip_serializing_if = "Vec::is_empty")]
391 pub environments: Vec<EnvironmentConnection>,
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
399#[serde(rename_all = "camelCase")]
400pub struct Workspace {
401 pub id: Uuid,
403 pub name: String,
405 #[serde(alias = "owner_id")]
407 pub owner_id: Uuid,
408 #[serde(alias = "created_at")]
410 pub created_at: DateTime<Utc>,
411 #[serde(alias = "last_modified_at")]
413 pub last_modified_at: DateTime<Utc>,
414 #[serde(skip_serializing_if = "Option::is_none")]
416 pub description: Option<String>,
417 #[serde(default)]
419 pub domains: Vec<DomainReference>,
420 #[serde(default)]
422 pub assets: Vec<AssetReference>,
423 #[serde(default, skip_serializing_if = "Vec::is_empty")]
425 pub relationships: Vec<Relationship>,
426}
427
428impl Workspace {
429 pub fn new(name: String, owner_id: Uuid) -> Self {
431 let now = Utc::now();
432 Self {
433 id: Uuid::new_v4(),
434 name,
435 owner_id,
436 created_at: now,
437 last_modified_at: now,
438 description: None,
439 domains: Vec::new(),
440 assets: Vec::new(),
441 relationships: Vec::new(),
442 }
443 }
444
445 pub fn with_id(id: Uuid, name: String, owner_id: Uuid) -> Self {
447 let now = Utc::now();
448 Self {
449 id,
450 name,
451 owner_id,
452 created_at: now,
453 last_modified_at: now,
454 description: None,
455 domains: Vec::new(),
456 assets: Vec::new(),
457 relationships: Vec::new(),
458 }
459 }
460
461 pub fn add_relationship(&mut self, relationship: Relationship) {
463 if self.relationships.iter().any(|r| r.id == relationship.id) {
465 return;
466 }
467 self.relationships.push(relationship);
468 self.last_modified_at = Utc::now();
469 }
470
471 pub fn remove_relationship(&mut self, relationship_id: Uuid) -> bool {
473 let initial_len = self.relationships.len();
474 self.relationships.retain(|r| r.id != relationship_id);
475 let removed = self.relationships.len() < initial_len;
476 if removed {
477 self.last_modified_at = Utc::now();
478 }
479 removed
480 }
481
482 pub fn get_relationships_for_source(&self, source_table_id: Uuid) -> Vec<&Relationship> {
484 self.relationships
485 .iter()
486 .filter(|r| r.source_table_id == source_table_id)
487 .collect()
488 }
489
490 pub fn get_relationships_for_target(&self, target_table_id: Uuid) -> Vec<&Relationship> {
492 self.relationships
493 .iter()
494 .filter(|r| r.target_table_id == target_table_id)
495 .collect()
496 }
497
498 pub fn add_domain(&mut self, domain_id: Uuid, domain_name: String) {
500 if self.domains.iter().any(|d| d.id == domain_id) {
502 return;
503 }
504 self.domains.push(DomainReference {
505 id: domain_id,
506 name: domain_name,
507 description: None,
508 systems: Vec::new(),
509 shared_resources: Vec::new(),
510 transformation_links: Vec::new(),
511 table_visibility: None,
512 view_positions: HashMap::new(),
513 });
514 self.last_modified_at = Utc::now();
515 }
516
517 pub fn add_domain_with_description(
519 &mut self,
520 domain_id: Uuid,
521 domain_name: String,
522 description: Option<String>,
523 ) {
524 if self.domains.iter().any(|d| d.id == domain_id) {
525 return;
526 }
527 self.domains.push(DomainReference {
528 id: domain_id,
529 name: domain_name,
530 description,
531 systems: Vec::new(),
532 shared_resources: Vec::new(),
533 transformation_links: Vec::new(),
534 table_visibility: None,
535 view_positions: HashMap::new(),
536 });
537 self.last_modified_at = Utc::now();
538 }
539
540 pub fn add_system_to_domain(
542 &mut self,
543 domain_name: &str,
544 system_id: Uuid,
545 system_name: String,
546 description: Option<String>,
547 ) -> bool {
548 if let Some(domain) = self.domains.iter_mut().find(|d| d.name == domain_name)
549 && !domain.systems.iter().any(|s| s.id == system_id)
550 {
551 domain.systems.push(SystemReference {
552 id: system_id,
553 name: system_name,
554 description,
555 system_type: None,
556 table_ids: Vec::new(),
557 asset_ids: Vec::new(),
558 environments: Vec::new(),
559 });
560 self.last_modified_at = Utc::now();
561 return true;
562 }
563 false
564 }
565
566 pub fn remove_domain(&mut self, domain_id: Uuid) -> bool {
568 let initial_len = self.domains.len();
569 self.domains.retain(|d| d.id != domain_id);
570 if let Some(domain) = self.domains.iter().find(|d| d.id == domain_id) {
572 let domain_name = domain.name.clone();
573 self.assets.retain(|a| a.domain != domain_name);
574 }
575 if self.domains.len() != initial_len {
576 self.last_modified_at = Utc::now();
577 true
578 } else {
579 false
580 }
581 }
582
583 pub fn get_domain(&self, domain_id: Uuid) -> Option<&DomainReference> {
585 self.domains.iter().find(|d| d.id == domain_id)
586 }
587
588 pub fn get_domain_by_name(&self, name: &str) -> Option<&DomainReference> {
590 self.domains.iter().find(|d| d.name == name)
591 }
592
593 pub fn add_asset(&mut self, asset: AssetReference) {
595 if self.assets.iter().any(|a| a.id == asset.id) {
597 return;
598 }
599 self.assets.push(asset);
600 self.last_modified_at = Utc::now();
601 }
602
603 pub fn remove_asset(&mut self, asset_id: Uuid) -> bool {
605 let initial_len = self.assets.len();
606 self.assets.retain(|a| a.id != asset_id);
607 if self.assets.len() != initial_len {
608 self.last_modified_at = Utc::now();
609 true
610 } else {
611 false
612 }
613 }
614
615 pub fn get_asset(&self, asset_id: Uuid) -> Option<&AssetReference> {
617 self.assets.iter().find(|a| a.id == asset_id)
618 }
619
620 pub fn get_assets_by_domain(&self, domain_name: &str) -> Vec<&AssetReference> {
622 self.assets
623 .iter()
624 .filter(|a| a.domain == domain_name)
625 .collect()
626 }
627
628 pub fn get_assets_by_type(&self, asset_type: &AssetType) -> Vec<&AssetReference> {
630 self.assets
631 .iter()
632 .filter(|a| &a.asset_type == asset_type)
633 .collect()
634 }
635
636 pub fn generate_asset_filename(&self, asset: &AssetReference) -> String {
639 let mut parts = vec![sanitize_name(&self.name), sanitize_name(&asset.domain)];
640
641 if let Some(ref system) = asset.system {
642 parts.push(sanitize_name(system));
643 }
644
645 parts.push(sanitize_name(&asset.name));
646
647 format!("{}.{}", parts.join("_"), asset.asset_type.extension())
648 }
649
650 pub fn parse_asset_filename(
653 filename: &str,
654 ) -> Option<(String, Option<String>, String, AssetType)> {
655 let (base, asset_type) = if filename.ends_with(".odcs.yaml") {
657 (filename.strip_suffix(".odcs.yaml")?, AssetType::Odcs)
658 } else if filename.ends_with(".odps.yaml") {
659 (filename.strip_suffix(".odps.yaml")?, AssetType::Odps)
660 } else if filename.ends_with(".cads.yaml") {
661 (filename.strip_suffix(".cads.yaml")?, AssetType::Cads)
662 } else if filename.ends_with(".bpmn.xml") {
663 (filename.strip_suffix(".bpmn.xml")?, AssetType::Bpmn)
664 } else if filename.ends_with(".dmn.xml") {
665 (filename.strip_suffix(".dmn.xml")?, AssetType::Dmn)
666 } else if filename.ends_with(".openapi.yaml") {
667 (filename.strip_suffix(".openapi.yaml")?, AssetType::Openapi)
668 } else if filename.ends_with(".dbmv.yaml") {
669 (filename.strip_suffix(".dbmv.yaml")?, AssetType::Dbmv)
670 } else {
671 return None;
672 };
673
674 let parts: Vec<&str> = base.split('_').collect();
675
676 match parts.len() {
677 3 => Some((parts[1].to_string(), None, parts[2].to_string(), asset_type)),
679 4 => Some((
681 parts[1].to_string(),
682 Some(parts[2].to_string()),
683 parts[3].to_string(),
684 asset_type,
685 )),
686 n if n > 4 => Some((
688 parts[1].to_string(),
689 Some(parts[2].to_string()),
690 parts[3..].join("_"),
691 asset_type,
692 )),
693 _ => None,
694 }
695 }
696
697 pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
699 serde_yaml::from_str(yaml_content)
700 }
701
702 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
704 serde_yaml::to_string(self)
705 }
706
707 pub fn from_json(json_content: &str) -> Result<Self, serde_json::Error> {
709 serde_json::from_str(json_content)
710 }
711
712 pub fn to_json(&self) -> Result<String, serde_json::Error> {
714 serde_json::to_string(self)
715 }
716
717 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
719 serde_json::to_string_pretty(self)
720 }
721}
722
723fn sanitize_name(name: &str) -> String {
725 name.chars()
726 .map(|c| match c {
727 ' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
728 _ => c,
729 })
730 .collect::<String>()
731 .to_lowercase()
732}
733
734impl Default for Workspace {
735 fn default() -> Self {
736 Self::new("Default Workspace".to_string(), Uuid::new_v4())
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743
744 #[test]
745 fn test_workspace_new() {
746 let owner_id = Uuid::new_v4();
747 let workspace = Workspace::new("Test Workspace".to_string(), owner_id);
748
749 assert_eq!(workspace.name, "Test Workspace");
750 assert_eq!(workspace.owner_id, owner_id);
751 assert!(workspace.domains.is_empty());
752 assert!(workspace.assets.is_empty());
753 }
754
755 #[test]
756 fn test_workspace_add_domain() {
757 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
758 let domain_id = Uuid::new_v4();
759
760 workspace.add_domain(domain_id, "customer-management".to_string());
761
762 assert_eq!(workspace.domains.len(), 1);
763 assert_eq!(workspace.domains[0].id, domain_id);
764 assert_eq!(workspace.domains[0].name, "customer-management");
765
766 workspace.add_domain(domain_id, "customer-management".to_string());
768 assert_eq!(workspace.domains.len(), 1);
769 }
770
771 #[test]
772 fn test_workspace_add_system_to_domain() {
773 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
774 let domain_id = Uuid::new_v4();
775 let system_id = Uuid::new_v4();
776
777 workspace.add_domain(domain_id, "sales".to_string());
778 let result = workspace.add_system_to_domain(
779 "sales",
780 system_id,
781 "kafka".to_string(),
782 Some("Kafka streaming".to_string()),
783 );
784
785 assert!(result);
786 assert_eq!(workspace.domains[0].systems.len(), 1);
787 assert_eq!(workspace.domains[0].systems[0].name, "kafka");
788 }
789
790 #[test]
791 fn test_workspace_remove_domain() {
792 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
793 let domain_id = Uuid::new_v4();
794 workspace.add_domain(domain_id, "test-domain".to_string());
795
796 assert!(workspace.remove_domain(domain_id));
797 assert!(workspace.domains.is_empty());
798 assert!(!workspace.remove_domain(domain_id)); }
800
801 #[test]
802 fn test_workspace_add_asset() {
803 let mut workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
804 let asset_id = Uuid::new_v4();
805
806 let asset = AssetReference {
807 id: asset_id,
808 name: "orders".to_string(),
809 domain: "sales".to_string(),
810 system: Some("kafka".to_string()),
811 asset_type: AssetType::Odcs,
812 file_path: None,
813 };
814
815 workspace.add_asset(asset);
816 assert_eq!(workspace.assets.len(), 1);
817 assert_eq!(workspace.assets[0].name, "orders");
818 }
819
820 #[test]
821 fn test_workspace_generate_asset_filename() {
822 let workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
823
824 let asset_with_system = AssetReference {
826 id: Uuid::new_v4(),
827 name: "orders".to_string(),
828 domain: "sales".to_string(),
829 system: Some("kafka".to_string()),
830 asset_type: AssetType::Odcs,
831 file_path: None,
832 };
833 assert_eq!(
834 workspace.generate_asset_filename(&asset_with_system),
835 "enterprise_sales_kafka_orders.odcs.yaml"
836 );
837
838 let asset_no_system = AssetReference {
840 id: Uuid::new_v4(),
841 name: "customers".to_string(),
842 domain: "crm".to_string(),
843 system: None,
844 asset_type: AssetType::Odcs,
845 file_path: None,
846 };
847 assert_eq!(
848 workspace.generate_asset_filename(&asset_no_system),
849 "enterprise_crm_customers.odcs.yaml"
850 );
851
852 let odps_asset = AssetReference {
854 id: Uuid::new_v4(),
855 name: "analytics".to_string(),
856 domain: "finance".to_string(),
857 system: None,
858 asset_type: AssetType::Odps,
859 file_path: None,
860 };
861 assert_eq!(
862 workspace.generate_asset_filename(&odps_asset),
863 "enterprise_finance_analytics.odps.yaml"
864 );
865 }
866
867 #[test]
868 fn test_workspace_parse_asset_filename() {
869 let result = Workspace::parse_asset_filename("enterprise_sales_kafka_orders.odcs.yaml");
871 assert!(result.is_some());
872 let (domain, system, name, asset_type) = result.unwrap();
873 assert_eq!(domain, "sales");
874 assert_eq!(system, Some("kafka".to_string()));
875 assert_eq!(name, "orders");
876 assert_eq!(asset_type, AssetType::Odcs);
877
878 let result = Workspace::parse_asset_filename("enterprise_crm_customers.odcs.yaml");
880 assert!(result.is_some());
881 let (domain, system, name, asset_type) = result.unwrap();
882 assert_eq!(domain, "crm");
883 assert_eq!(system, None);
884 assert_eq!(name, "customers");
885 assert_eq!(asset_type, AssetType::Odcs);
886
887 let result = Workspace::parse_asset_filename("workspace_domain_product.odps.yaml");
889 assert!(result.is_some());
890 let (_, _, _, asset_type) = result.unwrap();
891 assert_eq!(asset_type, AssetType::Odps);
892
893 let result =
895 Workspace::parse_asset_filename("enterprise_sales_databricks_metrics.dbmv.yaml");
896 assert!(result.is_some());
897 let (domain, system, name, asset_type) = result.unwrap();
898 assert_eq!(domain, "sales");
899 assert_eq!(system, Some("databricks".to_string()));
900 assert_eq!(name, "metrics");
901 assert_eq!(asset_type, AssetType::Dbmv);
902 }
903
904 #[test]
905 fn test_workspace_yaml_roundtrip() {
906 let mut workspace = Workspace::new("Enterprise Models".to_string(), Uuid::new_v4());
907 workspace.add_domain(Uuid::new_v4(), "finance".to_string());
908 workspace.add_domain(Uuid::new_v4(), "risk".to_string());
909 workspace.add_asset(AssetReference {
910 id: Uuid::new_v4(),
911 name: "accounts".to_string(),
912 domain: "finance".to_string(),
913 system: None,
914 asset_type: AssetType::Odcs,
915 file_path: None,
916 });
917
918 let yaml = workspace.to_yaml().unwrap();
919 let parsed = Workspace::from_yaml(&yaml).unwrap();
920
921 assert_eq!(workspace.id, parsed.id);
922 assert_eq!(workspace.name, parsed.name);
923 assert_eq!(workspace.domains.len(), parsed.domains.len());
924 assert_eq!(workspace.assets.len(), parsed.assets.len());
925 }
926
927 #[test]
928 fn test_workspace_json_roundtrip() {
929 let workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
930
931 let json = workspace.to_json().unwrap();
932 let parsed = Workspace::from_json(&json).unwrap();
933
934 assert_eq!(workspace.id, parsed.id);
935 assert_eq!(workspace.name, parsed.name);
936 }
937
938 #[test]
939 fn test_asset_type_extension() {
940 assert_eq!(AssetType::Workspace.extension(), "yaml");
941 assert_eq!(AssetType::Relationships.extension(), "yaml");
942 assert_eq!(AssetType::Odcs.extension(), "odcs.yaml");
943 assert_eq!(AssetType::Odps.extension(), "odps.yaml");
944 assert_eq!(AssetType::Cads.extension(), "cads.yaml");
945 assert_eq!(AssetType::Bpmn.extension(), "bpmn.xml");
946 assert_eq!(AssetType::Dmn.extension(), "dmn.xml");
947 assert_eq!(AssetType::Openapi.extension(), "openapi.yaml");
948 assert_eq!(AssetType::Sketch.extension(), "sketch.yaml");
949 assert_eq!(AssetType::SketchIndex.extension(), "yaml");
950 assert_eq!(AssetType::Dbmv.extension(), "dbmv.yaml");
951 }
952
953 #[test]
954 fn test_asset_type_filename() {
955 assert_eq!(AssetType::Workspace.filename(), Some("workspace.yaml"));
956 assert_eq!(
957 AssetType::Relationships.filename(),
958 Some("relationships.yaml")
959 );
960 assert_eq!(AssetType::Odcs.filename(), None);
961 assert_eq!(AssetType::Dbmv.filename(), None);
962 assert_eq!(AssetType::Sketch.filename(), None);
963 assert_eq!(AssetType::SketchIndex.filename(), Some("sketches.yaml"));
964 }
965
966 #[test]
967 fn test_asset_type_from_filename() {
968 assert_eq!(
969 AssetType::from_filename("workspace.yaml"),
970 Some(AssetType::Workspace)
971 );
972 assert_eq!(
973 AssetType::from_filename("relationships.yaml"),
974 Some(AssetType::Relationships)
975 );
976 assert_eq!(
977 AssetType::from_filename("test.odcs.yaml"),
978 Some(AssetType::Odcs)
979 );
980 assert_eq!(
981 AssetType::from_filename("test.odps.yaml"),
982 Some(AssetType::Odps)
983 );
984 assert_eq!(
985 AssetType::from_filename("test.cads.yaml"),
986 Some(AssetType::Cads)
987 );
988 assert_eq!(
989 AssetType::from_filename("test.bpmn.xml"),
990 Some(AssetType::Bpmn)
991 );
992 assert_eq!(
993 AssetType::from_filename("test.dmn.xml"),
994 Some(AssetType::Dmn)
995 );
996 assert_eq!(
997 AssetType::from_filename("test.openapi.yaml"),
998 Some(AssetType::Openapi)
999 );
1000 assert_eq!(
1001 AssetType::from_filename("test.openapi.json"),
1002 Some(AssetType::Openapi)
1003 );
1004 assert_eq!(
1005 AssetType::from_filename("test.sketch.yaml"),
1006 Some(AssetType::Sketch)
1007 );
1008 assert_eq!(
1009 AssetType::from_filename("sketches.yaml"),
1010 Some(AssetType::SketchIndex)
1011 );
1012 assert_eq!(
1013 AssetType::from_filename("test.dbmv.yaml"),
1014 Some(AssetType::Dbmv)
1015 );
1016 assert_eq!(AssetType::from_filename("random.txt"), None);
1017 assert_eq!(AssetType::from_filename("test.yaml"), None);
1018 }
1019
1020 #[test]
1021 fn test_asset_type_is_supported_file() {
1022 assert!(AssetType::is_supported_file("workspace.yaml"));
1023 assert!(AssetType::is_supported_file("relationships.yaml"));
1024 assert!(AssetType::is_supported_file(
1025 "enterprise_sales_orders.odcs.yaml"
1026 ));
1027 assert!(AssetType::is_supported_file(
1028 "enterprise_sales_metrics.dbmv.yaml"
1029 ));
1030 assert!(AssetType::is_supported_file("test.sketch.yaml"));
1031 assert!(AssetType::is_supported_file("sketches.yaml"));
1032 assert!(!AssetType::is_supported_file("readme.md"));
1033 assert!(!AssetType::is_supported_file("config.json"));
1034 }
1035
1036 #[test]
1037 fn test_asset_type_is_workspace_level() {
1038 assert!(AssetType::Workspace.is_workspace_level());
1039 assert!(AssetType::Relationships.is_workspace_level());
1040 assert!(!AssetType::Odcs.is_workspace_level());
1041 assert!(!AssetType::Odps.is_workspace_level());
1042 assert!(!AssetType::Dbmv.is_workspace_level());
1043 assert!(!AssetType::Sketch.is_workspace_level());
1044 }
1045
1046 #[test]
1047 fn test_sanitize_name() {
1048 assert_eq!(sanitize_name("Hello World"), "hello-world");
1049 assert_eq!(sanitize_name("Test/Path"), "test-path");
1050 assert_eq!(sanitize_name("Normal"), "normal");
1051 }
1052
1053 #[test]
1054 fn test_environment_connection_serialization() {
1055 let env = EnvironmentConnection {
1056 environment: "production".to_string(),
1057 owner: Some("Platform Team".to_string()),
1058 contact_details: Some(ContactDetails {
1059 email: Some("platform@example.com".to_string()),
1060 phone: None,
1061 name: Some("Platform Team".to_string()),
1062 role: Some("Data Owner".to_string()),
1063 other: None,
1064 }),
1065 sla: Some(vec![SlaProperty {
1066 property: "availability".to_string(),
1067 value: serde_json::json!(99.9),
1068 unit: "percent".to_string(),
1069 element: None,
1070 driver: Some("operational".to_string()),
1071 description: Some("99.9% uptime SLA".to_string()),
1072 scheduler: None,
1073 schedule: None,
1074 }]),
1075 auth_method: Some(AuthMethod::IamRole),
1076 support_team: Some("platform-oncall".to_string()),
1077 connection_string: None,
1078 secret_link: Some("https://vault.example.com/secrets/db-prod".to_string()),
1079 endpoint: Some("db-prod.example.com".to_string()),
1080 port: Some(5432),
1081 region: Some("us-east-1".to_string()),
1082 status: Some(EnvironmentStatus::Active),
1083 notes: Some("Primary production database".to_string()),
1084 custom_properties: HashMap::new(),
1085 };
1086
1087 let json = serde_json::to_string(&env).unwrap();
1088 let parsed: EnvironmentConnection = serde_json::from_str(&json).unwrap();
1089
1090 assert_eq!(env.environment, parsed.environment);
1091 assert_eq!(env.owner, parsed.owner);
1092 assert_eq!(env.auth_method, parsed.auth_method);
1093 assert_eq!(env.endpoint, parsed.endpoint);
1094 assert_eq!(env.port, parsed.port);
1095 assert_eq!(env.status, parsed.status);
1096 }
1097
1098 #[test]
1099 fn test_system_reference_with_environments() {
1100 let system = SystemReference {
1101 id: Uuid::new_v4(),
1102 name: "postgres-main".to_string(),
1103 description: Some("Main PostgreSQL cluster".to_string()),
1104 system_type: Some(InfrastructureType::PostgreSQL),
1105 table_ids: vec![],
1106 asset_ids: vec![],
1107 environments: vec![
1108 EnvironmentConnection {
1109 environment: "production".to_string(),
1110 owner: Some("Database Team".to_string()),
1111 contact_details: None,
1112 sla: None,
1113 auth_method: Some(AuthMethod::IamRole),
1114 support_team: Some("dba-oncall".to_string()),
1115 connection_string: None,
1116 secret_link: Some("https://vault.example.com/secrets/pg-prod".to_string()),
1117 endpoint: Some("postgres-prod.example.com".to_string()),
1118 port: Some(5432),
1119 region: Some("us-east-1".to_string()),
1120 status: Some(EnvironmentStatus::Active),
1121 notes: None,
1122 custom_properties: HashMap::new(),
1123 },
1124 EnvironmentConnection {
1125 environment: "staging".to_string(),
1126 owner: Some("Database Team".to_string()),
1127 contact_details: None,
1128 sla: None,
1129 auth_method: Some(AuthMethod::BasicAuth),
1130 support_team: None,
1131 connection_string: None,
1132 secret_link: None,
1133 endpoint: Some("postgres-staging.example.com".to_string()),
1134 port: Some(5432),
1135 region: Some("us-east-1".to_string()),
1136 status: Some(EnvironmentStatus::Active),
1137 notes: None,
1138 custom_properties: HashMap::new(),
1139 },
1140 ],
1141 };
1142
1143 let json = serde_json::to_string(&system).unwrap();
1145 let parsed: SystemReference = serde_json::from_str(&json).unwrap();
1146
1147 assert_eq!(system.id, parsed.id);
1148 assert_eq!(system.name, parsed.name);
1149 assert_eq!(system.system_type, parsed.system_type);
1150 assert_eq!(system.environments.len(), 2);
1151 assert_eq!(parsed.environments[0].environment, "production");
1152 assert_eq!(parsed.environments[1].environment, "staging");
1153
1154 let yaml = serde_yaml::to_string(&system).unwrap();
1156 let parsed_yaml: SystemReference = serde_yaml::from_str(&yaml).unwrap();
1157
1158 assert_eq!(system.id, parsed_yaml.id);
1159 assert_eq!(system.environments.len(), parsed_yaml.environments.len());
1160 }
1161
1162 #[test]
1163 fn test_backward_compatibility_no_environments() {
1164 let yaml = r#"
1166id: 550e8400-e29b-41d4-a716-446655440000
1167name: legacy-system
1168description: A legacy system without environments
1169"#;
1170 let parsed: SystemReference = serde_yaml::from_str(yaml).unwrap();
1171 assert!(parsed.environments.is_empty());
1172 assert!(parsed.system_type.is_none());
1173 assert_eq!(parsed.name, "legacy-system");
1174 }
1175
1176 #[test]
1177 fn test_backward_compatibility_no_system_type() {
1178 let json = r#"{
1180 "id": "550e8400-e29b-41d4-a716-446655440000",
1181 "name": "old-system",
1182 "tableIds": ["660e8400-e29b-41d4-a716-446655440001"]
1183 }"#;
1184 let parsed: SystemReference = serde_json::from_str(json).unwrap();
1185 assert!(parsed.system_type.is_none());
1186 assert!(parsed.environments.is_empty());
1187 assert_eq!(parsed.table_ids.len(), 1);
1188 }
1189
1190 #[test]
1191 fn test_auth_method_serialization() {
1192 let env = EnvironmentConnection {
1194 environment: "test".to_string(),
1195 owner: None,
1196 contact_details: None,
1197 sla: None,
1198 auth_method: Some(AuthMethod::AwsSignatureV4),
1199 support_team: None,
1200 connection_string: None,
1201 secret_link: None,
1202 endpoint: None,
1203 port: None,
1204 region: None,
1205 status: None,
1206 notes: None,
1207 custom_properties: HashMap::new(),
1208 };
1209
1210 let json = serde_json::to_string(&env).unwrap();
1211 assert!(json.contains("awsSignatureV4"));
1212
1213 let parsed: EnvironmentConnection = serde_json::from_str(&json).unwrap();
1215 assert_eq!(parsed.auth_method, Some(AuthMethod::AwsSignatureV4));
1216 }
1217
1218 #[test]
1219 fn test_environment_status_default() {
1220 let status: EnvironmentStatus = Default::default();
1222 assert_eq!(status, EnvironmentStatus::Active);
1223 }
1224
1225 #[test]
1226 fn test_system_type_serialization() {
1227 let system = SystemReference {
1228 id: Uuid::new_v4(),
1229 name: "kafka-cluster".to_string(),
1230 description: None,
1231 system_type: Some(InfrastructureType::Kafka),
1232 table_ids: vec![],
1233 asset_ids: vec![],
1234 environments: vec![],
1235 };
1236
1237 let json = serde_json::to_string(&system).unwrap();
1238 assert!(json.contains("\"systemType\":\"Kafka\""));
1239
1240 let parsed: SystemReference = serde_json::from_str(&json).unwrap();
1241 assert_eq!(parsed.system_type, Some(InfrastructureType::Kafka));
1242 }
1243
1244 #[test]
1245 fn test_domain_with_shared_resources() {
1246 let domain = DomainReference {
1247 id: Uuid::new_v4(),
1248 name: "sales".to_string(),
1249 description: Some("Sales domain".to_string()),
1250 systems: vec![],
1251 shared_resources: vec![
1252 SharedResource {
1253 id: Uuid::new_v4(),
1254 name: "common-schema".to_string(),
1255 resource_type: "schema".to_string(),
1256 url: Some("https://github.com/org/schemas/common".to_string()),
1257 description: Some("Common schema definitions".to_string()),
1258 },
1259 SharedResource {
1260 id: Uuid::new_v4(),
1261 name: "validation-library".to_string(),
1262 resource_type: "library".to_string(),
1263 url: None,
1264 description: None,
1265 },
1266 ],
1267 transformation_links: vec![],
1268 table_visibility: None,
1269 view_positions: HashMap::new(),
1270 };
1271
1272 let json = serde_json::to_string(&domain).unwrap();
1273 let parsed: DomainReference = serde_json::from_str(&json).unwrap();
1274
1275 assert_eq!(parsed.shared_resources.len(), 2);
1276 assert_eq!(parsed.shared_resources[0].name, "common-schema");
1277 assert_eq!(parsed.shared_resources[0].resource_type, "schema");
1278 }
1279
1280 #[test]
1281 fn test_domain_with_transformation_links() {
1282 let domain = DomainReference {
1283 id: Uuid::new_v4(),
1284 name: "analytics".to_string(),
1285 description: None,
1286 systems: vec![],
1287 shared_resources: vec![],
1288 transformation_links: vec![
1289 TransformationLink {
1290 id: Uuid::new_v4(),
1291 name: "sales-etl".to_string(),
1292 transformation_type: Some("dbt".to_string()),
1293 url: Some("https://github.com/org/dbt-models/sales".to_string()),
1294 description: Some("Sales data transformation".to_string()),
1295 },
1296 TransformationLink {
1297 id: Uuid::new_v4(),
1298 name: "aggregation-pipeline".to_string(),
1299 transformation_type: Some("spark".to_string()),
1300 url: None,
1301 description: None,
1302 },
1303 ],
1304 table_visibility: Some(TableVisibility::DomainOnly),
1305 view_positions: HashMap::new(),
1306 };
1307
1308 let yaml = serde_yaml::to_string(&domain).unwrap();
1309 let parsed: DomainReference = serde_yaml::from_str(&yaml).unwrap();
1310
1311 assert_eq!(parsed.transformation_links.len(), 2);
1312 assert_eq!(parsed.transformation_links[0].name, "sales-etl");
1313 assert_eq!(
1314 parsed.transformation_links[0].transformation_type,
1315 Some("dbt".to_string())
1316 );
1317 assert_eq!(parsed.table_visibility, Some(TableVisibility::DomainOnly));
1318 }
1319
1320 #[test]
1321 fn test_table_visibility_default() {
1322 let visibility: TableVisibility = Default::default();
1323 assert_eq!(visibility, TableVisibility::Public);
1324 }
1325
1326 #[test]
1327 fn test_table_visibility_serialization() {
1328 let domain = DomainReference {
1329 id: Uuid::new_v4(),
1330 name: "private-domain".to_string(),
1331 description: None,
1332 systems: vec![],
1333 shared_resources: vec![],
1334 transformation_links: vec![],
1335 table_visibility: Some(TableVisibility::Hidden),
1336 view_positions: HashMap::new(),
1337 };
1338
1339 let json = serde_json::to_string(&domain).unwrap();
1340 assert!(json.contains("\"tableVisibility\":\"hidden\""));
1341
1342 let parsed: DomainReference = serde_json::from_str(&json).unwrap();
1343 assert_eq!(parsed.table_visibility, Some(TableVisibility::Hidden));
1344 }
1345
1346 #[test]
1347 fn test_domain_backward_compatibility_no_new_fields() {
1348 let yaml = r#"
1350id: 550e8400-e29b-41d4-a716-446655440000
1351name: legacy-domain
1352description: A legacy domain
1353systems: []
1354"#;
1355 let parsed: DomainReference = serde_yaml::from_str(yaml).unwrap();
1356 assert!(parsed.shared_resources.is_empty());
1357 assert!(parsed.transformation_links.is_empty());
1358 assert!(parsed.table_visibility.is_none());
1359 assert_eq!(parsed.name, "legacy-domain");
1360 }
1361}