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