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