Skip to main content

data_modelling_core/models/
workspace.rs

1//! Workspace model
2//!
3//! Defines the Workspace entity for the data modelling application.
4//! Workspaces are top-level containers that organize domains and their associated assets.
5//!
6//! ## File Naming Convention
7//!
8//! All files use a flat naming pattern:
9//! - `workspace.yaml` - workspace metadata with references to all assets and relationships
10//! - `{workspace}_{domain}_{system}_{resource}.odcs.yaml` - ODCS table files
11//! - `{workspace}_{domain}_{system}_{resource}.odps.yaml` - ODPS product files
12//! - `{workspace}_{domain}_{system}_{resource}.cads.yaml` - CADS asset files
13//!
14//! Where `{system}` is optional if the resource is at the domain level.
15
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use uuid::Uuid;
20
21use super::Relationship;
22use super::domain_config::ViewPosition;
23
24/// Asset reference within a workspace
25///
26/// Contains information about an asset file and its location in the domain/system hierarchy.
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub struct AssetReference {
30    /// Asset identifier (UUID)
31    pub id: Uuid,
32    /// Asset name
33    pub name: String,
34    /// Domain name this asset belongs to
35    pub domain: String,
36    /// Optional system name (if asset is within a system)
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub system: Option<String>,
39    /// Asset type (odcs, odps, cads)
40    #[serde(alias = "asset_type")]
41    pub asset_type: AssetType,
42    /// File path relative to workspace (generated from naming convention)
43    #[serde(skip_serializing_if = "Option::is_none", alias = "file_path")]
44    pub file_path: Option<String>,
45}
46
47/// Type of asset or file in the workspace
48#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(rename_all = "lowercase")]
50pub enum AssetType {
51    /// Workspace configuration file
52    Workspace,
53    /// Relationships file
54    Relationships,
55    /// ODCS table definition
56    Odcs,
57    /// ODPS data product
58    Odps,
59    /// CADS compute asset
60    Cads,
61    /// BPMN process model
62    Bpmn,
63    /// DMN decision model
64    Dmn,
65    /// OpenAPI specification
66    Openapi,
67    /// MADR decision record
68    Decision,
69    /// Knowledge base article
70    Knowledge,
71    /// Decision log index file
72    DecisionIndex,
73    /// Knowledge base index file
74    KnowledgeIndex,
75}
76
77impl AssetType {
78    /// Get file extension for this asset type
79    pub fn extension(&self) -> &'static str {
80        match self {
81            AssetType::Workspace => "yaml",
82            AssetType::Relationships => "yaml",
83            AssetType::Odcs => "odcs.yaml",
84            AssetType::Odps => "odps.yaml",
85            AssetType::Cads => "cads.yaml",
86            AssetType::Bpmn => "bpmn.xml",
87            AssetType::Dmn => "dmn.xml",
88            AssetType::Openapi => "openapi.yaml",
89            AssetType::Decision => "madr.yaml",
90            AssetType::Knowledge => "kb.yaml",
91            AssetType::DecisionIndex => "yaml",
92            AssetType::KnowledgeIndex => "yaml",
93        }
94    }
95
96    /// Get the full filename for workspace-level files
97    pub fn filename(&self) -> Option<&'static str> {
98        match self {
99            AssetType::Workspace => Some("workspace.yaml"),
100            AssetType::Relationships => Some("relationships.yaml"),
101            AssetType::DecisionIndex => Some("decisions.yaml"),
102            AssetType::KnowledgeIndex => Some("knowledge.yaml"),
103            _ => None,
104        }
105    }
106
107    /// Check if this is a workspace-level file (not a domain/system asset)
108    pub fn is_workspace_level(&self) -> bool {
109        matches!(
110            self,
111            AssetType::Workspace
112                | AssetType::Relationships
113                | AssetType::DecisionIndex
114                | AssetType::KnowledgeIndex
115        )
116    }
117
118    /// Detect asset type from filename
119    pub fn from_filename(filename: &str) -> Option<Self> {
120        if filename == "workspace.yaml" {
121            Some(AssetType::Workspace)
122        } else if filename == "relationships.yaml" {
123            Some(AssetType::Relationships)
124        } else if filename == "decisions.yaml" {
125            Some(AssetType::DecisionIndex)
126        } else if filename == "knowledge.yaml" {
127            Some(AssetType::KnowledgeIndex)
128        } else if filename.ends_with(".odcs.yaml") {
129            Some(AssetType::Odcs)
130        } else if filename.ends_with(".odps.yaml") {
131            Some(AssetType::Odps)
132        } else if filename.ends_with(".cads.yaml") {
133            Some(AssetType::Cads)
134        } else if filename.ends_with(".madr.yaml") {
135            Some(AssetType::Decision)
136        } else if filename.ends_with(".kb.yaml") {
137            Some(AssetType::Knowledge)
138        } else if filename.ends_with(".bpmn.xml") {
139            Some(AssetType::Bpmn)
140        } else if filename.ends_with(".dmn.xml") {
141            Some(AssetType::Dmn)
142        } else if filename.ends_with(".openapi.yaml") || filename.ends_with(".openapi.json") {
143            Some(AssetType::Openapi)
144        } else {
145            None
146        }
147    }
148
149    /// Get all supported file extensions
150    pub fn supported_extensions() -> &'static [&'static str] {
151        &[
152            "workspace.yaml",
153            "relationships.yaml",
154            "decisions.yaml",
155            "knowledge.yaml",
156            ".odcs.yaml",
157            ".odps.yaml",
158            ".cads.yaml",
159            ".madr.yaml",
160            ".kb.yaml",
161            ".bpmn.xml",
162            ".dmn.xml",
163            ".openapi.yaml",
164            ".openapi.json",
165        ]
166    }
167
168    /// Check if a filename is a supported asset type
169    pub fn is_supported_file(filename: &str) -> bool {
170        Self::from_filename(filename).is_some()
171    }
172}
173
174/// Domain reference within a workspace
175///
176/// Contains information about a domain and its systems.
177#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
178#[serde(rename_all = "camelCase")]
179pub struct DomainReference {
180    /// Domain identifier
181    pub id: Uuid,
182    /// Domain name
183    pub name: String,
184    /// Optional description
185    #[serde(skip_serializing_if = "Option::is_none")]
186    pub description: Option<String>,
187    /// Systems within this domain
188    #[serde(default, skip_serializing_if = "Vec::is_empty")]
189    pub systems: Vec<SystemReference>,
190    /// View positions for different view modes (operational, analytical, process, systems)
191    /// Key: view mode name, Value: Map of entity ID to position
192    #[serde(
193        default,
194        skip_serializing_if = "HashMap::is_empty",
195        alias = "view_positions"
196    )]
197    pub view_positions: HashMap<String, HashMap<String, ViewPosition>>,
198}
199
200/// System reference within a domain
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202#[serde(rename_all = "camelCase")]
203pub struct SystemReference {
204    /// System identifier
205    pub id: Uuid,
206    /// System name
207    pub name: String,
208    /// Optional description
209    #[serde(skip_serializing_if = "Option::is_none")]
210    pub description: Option<String>,
211    /// Optional array of table UUIDs that belong to this system.
212    /// When present, provides explicit table-to-system mapping without requiring parsing of individual ODCS files.
213    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "table_ids")]
214    pub table_ids: Vec<Uuid>,
215    /// Optional array of compute asset (CADS) UUIDs that belong to this system.
216    /// When present, provides explicit asset-to-system mapping.
217    #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "asset_ids")]
218    pub asset_ids: Vec<Uuid>,
219}
220
221/// Workspace - Top-level container for domains, assets, and relationships
222///
223/// Workspaces organize domains, systems, and their associated assets.
224/// All files use a flat naming convention: `{workspace}_{domain}_{system}_{resource}.xxx.yaml`
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226#[serde(rename_all = "camelCase")]
227pub struct Workspace {
228    /// Unique identifier for the workspace
229    pub id: Uuid,
230    /// Workspace name (used in file naming)
231    pub name: String,
232    /// Owner/creator user identifier
233    #[serde(alias = "owner_id")]
234    pub owner_id: Uuid,
235    /// Creation timestamp
236    #[serde(alias = "created_at")]
237    pub created_at: DateTime<Utc>,
238    /// Last modification timestamp
239    #[serde(alias = "last_modified_at")]
240    pub last_modified_at: DateTime<Utc>,
241    /// Optional workspace description (displayed in UI and README)
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub description: Option<String>,
244    /// Domain references with their systems
245    #[serde(default)]
246    pub domains: Vec<DomainReference>,
247    /// All asset references in this workspace
248    #[serde(default)]
249    pub assets: Vec<AssetReference>,
250    /// Relationships between assets in this workspace
251    #[serde(default, skip_serializing_if = "Vec::is_empty")]
252    pub relationships: Vec<Relationship>,
253}
254
255impl Workspace {
256    /// Create a new Workspace
257    pub fn new(name: String, owner_id: Uuid) -> Self {
258        let now = Utc::now();
259        Self {
260            id: Uuid::new_v4(),
261            name,
262            owner_id,
263            created_at: now,
264            last_modified_at: now,
265            description: None,
266            domains: Vec::new(),
267            assets: Vec::new(),
268            relationships: Vec::new(),
269        }
270    }
271
272    /// Create a workspace with a specific ID
273    pub fn with_id(id: Uuid, name: String, owner_id: Uuid) -> Self {
274        let now = Utc::now();
275        Self {
276            id,
277            name,
278            owner_id,
279            created_at: now,
280            last_modified_at: now,
281            description: None,
282            domains: Vec::new(),
283            assets: Vec::new(),
284            relationships: Vec::new(),
285        }
286    }
287
288    /// Add a relationship to the workspace
289    pub fn add_relationship(&mut self, relationship: Relationship) {
290        // Check if relationship already exists
291        if self.relationships.iter().any(|r| r.id == relationship.id) {
292            return;
293        }
294        self.relationships.push(relationship);
295        self.last_modified_at = Utc::now();
296    }
297
298    /// Remove a relationship by ID
299    pub fn remove_relationship(&mut self, relationship_id: Uuid) -> bool {
300        let initial_len = self.relationships.len();
301        self.relationships.retain(|r| r.id != relationship_id);
302        let removed = self.relationships.len() < initial_len;
303        if removed {
304            self.last_modified_at = Utc::now();
305        }
306        removed
307    }
308
309    /// Get relationships by source table ID
310    pub fn get_relationships_for_source(&self, source_table_id: Uuid) -> Vec<&Relationship> {
311        self.relationships
312            .iter()
313            .filter(|r| r.source_table_id == source_table_id)
314            .collect()
315    }
316
317    /// Get relationships by target table ID
318    pub fn get_relationships_for_target(&self, target_table_id: Uuid) -> Vec<&Relationship> {
319        self.relationships
320            .iter()
321            .filter(|r| r.target_table_id == target_table_id)
322            .collect()
323    }
324
325    /// Add a domain reference to the workspace
326    pub fn add_domain(&mut self, domain_id: Uuid, domain_name: String) {
327        // Check if domain already exists
328        if self.domains.iter().any(|d| d.id == domain_id) {
329            return;
330        }
331        self.domains.push(DomainReference {
332            id: domain_id,
333            name: domain_name,
334            description: None,
335            systems: Vec::new(),
336            view_positions: HashMap::new(),
337        });
338        self.last_modified_at = Utc::now();
339    }
340
341    /// Add a domain with description
342    pub fn add_domain_with_description(
343        &mut self,
344        domain_id: Uuid,
345        domain_name: String,
346        description: Option<String>,
347    ) {
348        if self.domains.iter().any(|d| d.id == domain_id) {
349            return;
350        }
351        self.domains.push(DomainReference {
352            id: domain_id,
353            name: domain_name,
354            description,
355            systems: Vec::new(),
356            view_positions: HashMap::new(),
357        });
358        self.last_modified_at = Utc::now();
359    }
360
361    /// Add a system to a domain
362    pub fn add_system_to_domain(
363        &mut self,
364        domain_name: &str,
365        system_id: Uuid,
366        system_name: String,
367        description: Option<String>,
368    ) -> bool {
369        if let Some(domain) = self.domains.iter_mut().find(|d| d.name == domain_name)
370            && !domain.systems.iter().any(|s| s.id == system_id)
371        {
372            domain.systems.push(SystemReference {
373                id: system_id,
374                name: system_name,
375                description,
376                table_ids: Vec::new(),
377                asset_ids: Vec::new(),
378            });
379            self.last_modified_at = Utc::now();
380            return true;
381        }
382        false
383    }
384
385    /// Remove a domain reference by ID
386    pub fn remove_domain(&mut self, domain_id: Uuid) -> bool {
387        let initial_len = self.domains.len();
388        self.domains.retain(|d| d.id != domain_id);
389        // Also remove assets belonging to this domain
390        if let Some(domain) = self.domains.iter().find(|d| d.id == domain_id) {
391            let domain_name = domain.name.clone();
392            self.assets.retain(|a| a.domain != domain_name);
393        }
394        if self.domains.len() != initial_len {
395            self.last_modified_at = Utc::now();
396            true
397        } else {
398            false
399        }
400    }
401
402    /// Get a domain reference by ID
403    pub fn get_domain(&self, domain_id: Uuid) -> Option<&DomainReference> {
404        self.domains.iter().find(|d| d.id == domain_id)
405    }
406
407    /// Get a domain reference by name
408    pub fn get_domain_by_name(&self, name: &str) -> Option<&DomainReference> {
409        self.domains.iter().find(|d| d.name == name)
410    }
411
412    /// Add an asset reference
413    pub fn add_asset(&mut self, asset: AssetReference) {
414        // Check if asset already exists
415        if self.assets.iter().any(|a| a.id == asset.id) {
416            return;
417        }
418        self.assets.push(asset);
419        self.last_modified_at = Utc::now();
420    }
421
422    /// Remove an asset by ID
423    pub fn remove_asset(&mut self, asset_id: Uuid) -> bool {
424        let initial_len = self.assets.len();
425        self.assets.retain(|a| a.id != asset_id);
426        if self.assets.len() != initial_len {
427            self.last_modified_at = Utc::now();
428            true
429        } else {
430            false
431        }
432    }
433
434    /// Get an asset by ID
435    pub fn get_asset(&self, asset_id: Uuid) -> Option<&AssetReference> {
436        self.assets.iter().find(|a| a.id == asset_id)
437    }
438
439    /// Get assets by domain
440    pub fn get_assets_by_domain(&self, domain_name: &str) -> Vec<&AssetReference> {
441        self.assets
442            .iter()
443            .filter(|a| a.domain == domain_name)
444            .collect()
445    }
446
447    /// Get assets by type
448    pub fn get_assets_by_type(&self, asset_type: &AssetType) -> Vec<&AssetReference> {
449        self.assets
450            .iter()
451            .filter(|a| &a.asset_type == asset_type)
452            .collect()
453    }
454
455    /// Generate filename for an asset using the naming convention
456    /// Format: {workspace}_{domain}_{system}_{resource}.{extension}
457    pub fn generate_asset_filename(&self, asset: &AssetReference) -> String {
458        let mut parts = vec![sanitize_name(&self.name), sanitize_name(&asset.domain)];
459
460        if let Some(ref system) = asset.system {
461            parts.push(sanitize_name(system));
462        }
463
464        parts.push(sanitize_name(&asset.name));
465
466        format!("{}.{}", parts.join("_"), asset.asset_type.extension())
467    }
468
469    /// Parse a filename to extract workspace, domain, system, and resource names
470    /// Returns (domain, system, resource_name) or None if parsing fails
471    pub fn parse_asset_filename(
472        filename: &str,
473    ) -> Option<(String, Option<String>, String, AssetType)> {
474        // Determine asset type from extension
475        let (base, asset_type) = if filename.ends_with(".odcs.yaml") {
476            (filename.strip_suffix(".odcs.yaml")?, AssetType::Odcs)
477        } else if filename.ends_with(".odps.yaml") {
478            (filename.strip_suffix(".odps.yaml")?, AssetType::Odps)
479        } else if filename.ends_with(".cads.yaml") {
480            (filename.strip_suffix(".cads.yaml")?, AssetType::Cads)
481        } else if filename.ends_with(".bpmn.xml") {
482            (filename.strip_suffix(".bpmn.xml")?, AssetType::Bpmn)
483        } else if filename.ends_with(".dmn.xml") {
484            (filename.strip_suffix(".dmn.xml")?, AssetType::Dmn)
485        } else if filename.ends_with(".openapi.yaml") {
486            (filename.strip_suffix(".openapi.yaml")?, AssetType::Openapi)
487        } else {
488            return None;
489        };
490
491        let parts: Vec<&str> = base.split('_').collect();
492
493        match parts.len() {
494            // workspace_domain_resource (no system)
495            3 => Some((parts[1].to_string(), None, parts[2].to_string(), asset_type)),
496            // workspace_domain_system_resource
497            4 => Some((
498                parts[1].to_string(),
499                Some(parts[2].to_string()),
500                parts[3].to_string(),
501                asset_type,
502            )),
503            // More than 4 parts - treat remaining as resource name with underscores
504            n if n > 4 => Some((
505                parts[1].to_string(),
506                Some(parts[2].to_string()),
507                parts[3..].join("_"),
508                asset_type,
509            )),
510            _ => None,
511        }
512    }
513
514    /// Import workspace from YAML
515    pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
516        serde_yaml::from_str(yaml_content)
517    }
518
519    /// Export workspace to YAML
520    pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
521        serde_yaml::to_string(self)
522    }
523
524    /// Import workspace from JSON
525    pub fn from_json(json_content: &str) -> Result<Self, serde_json::Error> {
526        serde_json::from_str(json_content)
527    }
528
529    /// Export workspace to JSON
530    pub fn to_json(&self) -> Result<String, serde_json::Error> {
531        serde_json::to_string(self)
532    }
533
534    /// Export workspace to pretty JSON
535    pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
536        serde_json::to_string_pretty(self)
537    }
538}
539
540/// Sanitize a name for use in filenames (replace spaces/special chars with hyphens)
541fn sanitize_name(name: &str) -> String {
542    name.chars()
543        .map(|c| match c {
544            ' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
545            _ => c,
546        })
547        .collect::<String>()
548        .to_lowercase()
549}
550
551impl Default for Workspace {
552    fn default() -> Self {
553        Self::new("Default Workspace".to_string(), Uuid::new_v4())
554    }
555}
556
557#[cfg(test)]
558mod tests {
559    use super::*;
560
561    #[test]
562    fn test_workspace_new() {
563        let owner_id = Uuid::new_v4();
564        let workspace = Workspace::new("Test Workspace".to_string(), owner_id);
565
566        assert_eq!(workspace.name, "Test Workspace");
567        assert_eq!(workspace.owner_id, owner_id);
568        assert!(workspace.domains.is_empty());
569        assert!(workspace.assets.is_empty());
570    }
571
572    #[test]
573    fn test_workspace_add_domain() {
574        let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
575        let domain_id = Uuid::new_v4();
576
577        workspace.add_domain(domain_id, "customer-management".to_string());
578
579        assert_eq!(workspace.domains.len(), 1);
580        assert_eq!(workspace.domains[0].id, domain_id);
581        assert_eq!(workspace.domains[0].name, "customer-management");
582
583        // Adding same domain again should not duplicate
584        workspace.add_domain(domain_id, "customer-management".to_string());
585        assert_eq!(workspace.domains.len(), 1);
586    }
587
588    #[test]
589    fn test_workspace_add_system_to_domain() {
590        let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
591        let domain_id = Uuid::new_v4();
592        let system_id = Uuid::new_v4();
593
594        workspace.add_domain(domain_id, "sales".to_string());
595        let result = workspace.add_system_to_domain(
596            "sales",
597            system_id,
598            "kafka".to_string(),
599            Some("Kafka streaming".to_string()),
600        );
601
602        assert!(result);
603        assert_eq!(workspace.domains[0].systems.len(), 1);
604        assert_eq!(workspace.domains[0].systems[0].name, "kafka");
605    }
606
607    #[test]
608    fn test_workspace_remove_domain() {
609        let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
610        let domain_id = Uuid::new_v4();
611        workspace.add_domain(domain_id, "test-domain".to_string());
612
613        assert!(workspace.remove_domain(domain_id));
614        assert!(workspace.domains.is_empty());
615        assert!(!workspace.remove_domain(domain_id)); // Already removed
616    }
617
618    #[test]
619    fn test_workspace_add_asset() {
620        let mut workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
621        let asset_id = Uuid::new_v4();
622
623        let asset = AssetReference {
624            id: asset_id,
625            name: "orders".to_string(),
626            domain: "sales".to_string(),
627            system: Some("kafka".to_string()),
628            asset_type: AssetType::Odcs,
629            file_path: None,
630        };
631
632        workspace.add_asset(asset);
633        assert_eq!(workspace.assets.len(), 1);
634        assert_eq!(workspace.assets[0].name, "orders");
635    }
636
637    #[test]
638    fn test_workspace_generate_asset_filename() {
639        let workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
640
641        // With system
642        let asset_with_system = AssetReference {
643            id: Uuid::new_v4(),
644            name: "orders".to_string(),
645            domain: "sales".to_string(),
646            system: Some("kafka".to_string()),
647            asset_type: AssetType::Odcs,
648            file_path: None,
649        };
650        assert_eq!(
651            workspace.generate_asset_filename(&asset_with_system),
652            "enterprise_sales_kafka_orders.odcs.yaml"
653        );
654
655        // Without system
656        let asset_no_system = AssetReference {
657            id: Uuid::new_v4(),
658            name: "customers".to_string(),
659            domain: "crm".to_string(),
660            system: None,
661            asset_type: AssetType::Odcs,
662            file_path: None,
663        };
664        assert_eq!(
665            workspace.generate_asset_filename(&asset_no_system),
666            "enterprise_crm_customers.odcs.yaml"
667        );
668
669        // ODPS product
670        let odps_asset = AssetReference {
671            id: Uuid::new_v4(),
672            name: "analytics".to_string(),
673            domain: "finance".to_string(),
674            system: None,
675            asset_type: AssetType::Odps,
676            file_path: None,
677        };
678        assert_eq!(
679            workspace.generate_asset_filename(&odps_asset),
680            "enterprise_finance_analytics.odps.yaml"
681        );
682    }
683
684    #[test]
685    fn test_workspace_parse_asset_filename() {
686        // With system
687        let result = Workspace::parse_asset_filename("enterprise_sales_kafka_orders.odcs.yaml");
688        assert!(result.is_some());
689        let (domain, system, name, asset_type) = result.unwrap();
690        assert_eq!(domain, "sales");
691        assert_eq!(system, Some("kafka".to_string()));
692        assert_eq!(name, "orders");
693        assert_eq!(asset_type, AssetType::Odcs);
694
695        // Without system (3 parts)
696        let result = Workspace::parse_asset_filename("enterprise_crm_customers.odcs.yaml");
697        assert!(result.is_some());
698        let (domain, system, name, asset_type) = result.unwrap();
699        assert_eq!(domain, "crm");
700        assert_eq!(system, None);
701        assert_eq!(name, "customers");
702        assert_eq!(asset_type, AssetType::Odcs);
703
704        // ODPS type
705        let result = Workspace::parse_asset_filename("workspace_domain_product.odps.yaml");
706        assert!(result.is_some());
707        let (_, _, _, asset_type) = result.unwrap();
708        assert_eq!(asset_type, AssetType::Odps);
709    }
710
711    #[test]
712    fn test_workspace_yaml_roundtrip() {
713        let mut workspace = Workspace::new("Enterprise Models".to_string(), Uuid::new_v4());
714        workspace.add_domain(Uuid::new_v4(), "finance".to_string());
715        workspace.add_domain(Uuid::new_v4(), "risk".to_string());
716        workspace.add_asset(AssetReference {
717            id: Uuid::new_v4(),
718            name: "accounts".to_string(),
719            domain: "finance".to_string(),
720            system: None,
721            asset_type: AssetType::Odcs,
722            file_path: None,
723        });
724
725        let yaml = workspace.to_yaml().unwrap();
726        let parsed = Workspace::from_yaml(&yaml).unwrap();
727
728        assert_eq!(workspace.id, parsed.id);
729        assert_eq!(workspace.name, parsed.name);
730        assert_eq!(workspace.domains.len(), parsed.domains.len());
731        assert_eq!(workspace.assets.len(), parsed.assets.len());
732    }
733
734    #[test]
735    fn test_workspace_json_roundtrip() {
736        let workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
737
738        let json = workspace.to_json().unwrap();
739        let parsed = Workspace::from_json(&json).unwrap();
740
741        assert_eq!(workspace.id, parsed.id);
742        assert_eq!(workspace.name, parsed.name);
743    }
744
745    #[test]
746    fn test_asset_type_extension() {
747        assert_eq!(AssetType::Workspace.extension(), "yaml");
748        assert_eq!(AssetType::Relationships.extension(), "yaml");
749        assert_eq!(AssetType::Odcs.extension(), "odcs.yaml");
750        assert_eq!(AssetType::Odps.extension(), "odps.yaml");
751        assert_eq!(AssetType::Cads.extension(), "cads.yaml");
752        assert_eq!(AssetType::Bpmn.extension(), "bpmn.xml");
753        assert_eq!(AssetType::Dmn.extension(), "dmn.xml");
754        assert_eq!(AssetType::Openapi.extension(), "openapi.yaml");
755    }
756
757    #[test]
758    fn test_asset_type_filename() {
759        assert_eq!(AssetType::Workspace.filename(), Some("workspace.yaml"));
760        assert_eq!(
761            AssetType::Relationships.filename(),
762            Some("relationships.yaml")
763        );
764        assert_eq!(AssetType::Odcs.filename(), None);
765    }
766
767    #[test]
768    fn test_asset_type_from_filename() {
769        assert_eq!(
770            AssetType::from_filename("workspace.yaml"),
771            Some(AssetType::Workspace)
772        );
773        assert_eq!(
774            AssetType::from_filename("relationships.yaml"),
775            Some(AssetType::Relationships)
776        );
777        assert_eq!(
778            AssetType::from_filename("test.odcs.yaml"),
779            Some(AssetType::Odcs)
780        );
781        assert_eq!(
782            AssetType::from_filename("test.odps.yaml"),
783            Some(AssetType::Odps)
784        );
785        assert_eq!(
786            AssetType::from_filename("test.cads.yaml"),
787            Some(AssetType::Cads)
788        );
789        assert_eq!(
790            AssetType::from_filename("test.bpmn.xml"),
791            Some(AssetType::Bpmn)
792        );
793        assert_eq!(
794            AssetType::from_filename("test.dmn.xml"),
795            Some(AssetType::Dmn)
796        );
797        assert_eq!(
798            AssetType::from_filename("test.openapi.yaml"),
799            Some(AssetType::Openapi)
800        );
801        assert_eq!(
802            AssetType::from_filename("test.openapi.json"),
803            Some(AssetType::Openapi)
804        );
805        assert_eq!(AssetType::from_filename("random.txt"), None);
806        assert_eq!(AssetType::from_filename("test.yaml"), None);
807    }
808
809    #[test]
810    fn test_asset_type_is_supported_file() {
811        assert!(AssetType::is_supported_file("workspace.yaml"));
812        assert!(AssetType::is_supported_file("relationships.yaml"));
813        assert!(AssetType::is_supported_file(
814            "enterprise_sales_orders.odcs.yaml"
815        ));
816        assert!(!AssetType::is_supported_file("readme.md"));
817        assert!(!AssetType::is_supported_file("config.json"));
818    }
819
820    #[test]
821    fn test_asset_type_is_workspace_level() {
822        assert!(AssetType::Workspace.is_workspace_level());
823        assert!(AssetType::Relationships.is_workspace_level());
824        assert!(!AssetType::Odcs.is_workspace_level());
825        assert!(!AssetType::Odps.is_workspace_level());
826    }
827
828    #[test]
829    fn test_sanitize_name() {
830        assert_eq!(sanitize_name("Hello World"), "hello-world");
831        assert_eq!(sanitize_name("Test/Path"), "test-path");
832        assert_eq!(sanitize_name("Normal"), "normal");
833    }
834}