1use 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#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
28#[serde(rename_all = "camelCase")]
29pub struct AssetReference {
30 pub id: Uuid,
32 pub name: String,
34 pub domain: String,
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub system: Option<String>,
39 #[serde(alias = "asset_type")]
41 pub asset_type: AssetType,
42 #[serde(skip_serializing_if = "Option::is_none", alias = "file_path")]
44 pub file_path: Option<String>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
49#[serde(rename_all = "lowercase")]
50pub enum AssetType {
51 Workspace,
53 Relationships,
55 Odcs,
57 Odps,
59 Cads,
61 Bpmn,
63 Dmn,
65 Openapi,
67 Decision,
69 Knowledge,
71 DecisionIndex,
73 KnowledgeIndex,
75 Sketch,
77 SketchIndex,
79}
80
81impl AssetType {
82 pub fn extension(&self) -> &'static str {
84 match self {
85 AssetType::Workspace => "yaml",
86 AssetType::Relationships => "yaml",
87 AssetType::Odcs => "odcs.yaml",
88 AssetType::Odps => "odps.yaml",
89 AssetType::Cads => "cads.yaml",
90 AssetType::Bpmn => "bpmn.xml",
91 AssetType::Dmn => "dmn.xml",
92 AssetType::Openapi => "openapi.yaml",
93 AssetType::Decision => "madr.yaml",
94 AssetType::Knowledge => "kb.yaml",
95 AssetType::DecisionIndex => "yaml",
96 AssetType::KnowledgeIndex => "yaml",
97 AssetType::Sketch => "sketch.yaml",
98 AssetType::SketchIndex => "yaml",
99 }
100 }
101
102 pub fn filename(&self) -> Option<&'static str> {
104 match self {
105 AssetType::Workspace => Some("workspace.yaml"),
106 AssetType::Relationships => Some("relationships.yaml"),
107 AssetType::DecisionIndex => Some("decisions.yaml"),
108 AssetType::KnowledgeIndex => Some("knowledge.yaml"),
109 AssetType::SketchIndex => Some("sketches.yaml"),
110 _ => None,
111 }
112 }
113
114 pub fn is_workspace_level(&self) -> bool {
116 matches!(
117 self,
118 AssetType::Workspace
119 | AssetType::Relationships
120 | AssetType::DecisionIndex
121 | AssetType::KnowledgeIndex
122 | AssetType::SketchIndex
123 )
124 }
125
126 pub fn from_filename(filename: &str) -> Option<Self> {
128 if filename == "workspace.yaml" {
129 Some(AssetType::Workspace)
130 } else if filename == "relationships.yaml" {
131 Some(AssetType::Relationships)
132 } else if filename == "decisions.yaml" {
133 Some(AssetType::DecisionIndex)
134 } else if filename == "knowledge.yaml" {
135 Some(AssetType::KnowledgeIndex)
136 } else if filename == "sketches.yaml" {
137 Some(AssetType::SketchIndex)
138 } else if filename.ends_with(".odcs.yaml") {
139 Some(AssetType::Odcs)
140 } else if filename.ends_with(".odps.yaml") {
141 Some(AssetType::Odps)
142 } else if filename.ends_with(".cads.yaml") {
143 Some(AssetType::Cads)
144 } else if filename.ends_with(".madr.yaml") {
145 Some(AssetType::Decision)
146 } else if filename.ends_with(".kb.yaml") {
147 Some(AssetType::Knowledge)
148 } else if filename.ends_with(".sketch.yaml") {
149 Some(AssetType::Sketch)
150 } else if filename.ends_with(".bpmn.xml") {
151 Some(AssetType::Bpmn)
152 } else if filename.ends_with(".dmn.xml") {
153 Some(AssetType::Dmn)
154 } else if filename.ends_with(".openapi.yaml") || filename.ends_with(".openapi.json") {
155 Some(AssetType::Openapi)
156 } else {
157 None
158 }
159 }
160
161 pub fn supported_extensions() -> &'static [&'static str] {
163 &[
164 "workspace.yaml",
165 "relationships.yaml",
166 "decisions.yaml",
167 "knowledge.yaml",
168 "sketches.yaml",
169 ".odcs.yaml",
170 ".odps.yaml",
171 ".cads.yaml",
172 ".madr.yaml",
173 ".kb.yaml",
174 ".sketch.yaml",
175 ".bpmn.xml",
176 ".dmn.xml",
177 ".openapi.yaml",
178 ".openapi.json",
179 ]
180 }
181
182 pub fn is_supported_file(filename: &str) -> bool {
184 Self::from_filename(filename).is_some()
185 }
186}
187
188#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
192#[serde(rename_all = "camelCase")]
193pub struct DomainReference {
194 pub id: Uuid,
196 pub name: String,
198 #[serde(skip_serializing_if = "Option::is_none")]
200 pub description: Option<String>,
201 #[serde(default, skip_serializing_if = "Vec::is_empty")]
203 pub systems: Vec<SystemReference>,
204 #[serde(
207 default,
208 skip_serializing_if = "HashMap::is_empty",
209 alias = "view_positions"
210 )]
211 pub view_positions: HashMap<String, HashMap<String, ViewPosition>>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
216#[serde(rename_all = "camelCase")]
217pub struct SystemReference {
218 pub id: Uuid,
220 pub name: String,
222 #[serde(skip_serializing_if = "Option::is_none")]
224 pub description: Option<String>,
225 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "table_ids")]
228 pub table_ids: Vec<Uuid>,
229 #[serde(default, skip_serializing_if = "Vec::is_empty", alias = "asset_ids")]
232 pub asset_ids: Vec<Uuid>,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
240#[serde(rename_all = "camelCase")]
241pub struct Workspace {
242 pub id: Uuid,
244 pub name: String,
246 #[serde(alias = "owner_id")]
248 pub owner_id: Uuid,
249 #[serde(alias = "created_at")]
251 pub created_at: DateTime<Utc>,
252 #[serde(alias = "last_modified_at")]
254 pub last_modified_at: DateTime<Utc>,
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub description: Option<String>,
258 #[serde(default)]
260 pub domains: Vec<DomainReference>,
261 #[serde(default)]
263 pub assets: Vec<AssetReference>,
264 #[serde(default, skip_serializing_if = "Vec::is_empty")]
266 pub relationships: Vec<Relationship>,
267}
268
269impl Workspace {
270 pub fn new(name: String, owner_id: Uuid) -> Self {
272 let now = Utc::now();
273 Self {
274 id: Uuid::new_v4(),
275 name,
276 owner_id,
277 created_at: now,
278 last_modified_at: now,
279 description: None,
280 domains: Vec::new(),
281 assets: Vec::new(),
282 relationships: Vec::new(),
283 }
284 }
285
286 pub fn with_id(id: Uuid, name: String, owner_id: Uuid) -> Self {
288 let now = Utc::now();
289 Self {
290 id,
291 name,
292 owner_id,
293 created_at: now,
294 last_modified_at: now,
295 description: None,
296 domains: Vec::new(),
297 assets: Vec::new(),
298 relationships: Vec::new(),
299 }
300 }
301
302 pub fn add_relationship(&mut self, relationship: Relationship) {
304 if self.relationships.iter().any(|r| r.id == relationship.id) {
306 return;
307 }
308 self.relationships.push(relationship);
309 self.last_modified_at = Utc::now();
310 }
311
312 pub fn remove_relationship(&mut self, relationship_id: Uuid) -> bool {
314 let initial_len = self.relationships.len();
315 self.relationships.retain(|r| r.id != relationship_id);
316 let removed = self.relationships.len() < initial_len;
317 if removed {
318 self.last_modified_at = Utc::now();
319 }
320 removed
321 }
322
323 pub fn get_relationships_for_source(&self, source_table_id: Uuid) -> Vec<&Relationship> {
325 self.relationships
326 .iter()
327 .filter(|r| r.source_table_id == source_table_id)
328 .collect()
329 }
330
331 pub fn get_relationships_for_target(&self, target_table_id: Uuid) -> Vec<&Relationship> {
333 self.relationships
334 .iter()
335 .filter(|r| r.target_table_id == target_table_id)
336 .collect()
337 }
338
339 pub fn add_domain(&mut self, domain_id: Uuid, domain_name: String) {
341 if self.domains.iter().any(|d| d.id == domain_id) {
343 return;
344 }
345 self.domains.push(DomainReference {
346 id: domain_id,
347 name: domain_name,
348 description: None,
349 systems: Vec::new(),
350 view_positions: HashMap::new(),
351 });
352 self.last_modified_at = Utc::now();
353 }
354
355 pub fn add_domain_with_description(
357 &mut self,
358 domain_id: Uuid,
359 domain_name: String,
360 description: Option<String>,
361 ) {
362 if self.domains.iter().any(|d| d.id == domain_id) {
363 return;
364 }
365 self.domains.push(DomainReference {
366 id: domain_id,
367 name: domain_name,
368 description,
369 systems: Vec::new(),
370 view_positions: HashMap::new(),
371 });
372 self.last_modified_at = Utc::now();
373 }
374
375 pub fn add_system_to_domain(
377 &mut self,
378 domain_name: &str,
379 system_id: Uuid,
380 system_name: String,
381 description: Option<String>,
382 ) -> bool {
383 if let Some(domain) = self.domains.iter_mut().find(|d| d.name == domain_name)
384 && !domain.systems.iter().any(|s| s.id == system_id)
385 {
386 domain.systems.push(SystemReference {
387 id: system_id,
388 name: system_name,
389 description,
390 table_ids: Vec::new(),
391 asset_ids: Vec::new(),
392 });
393 self.last_modified_at = Utc::now();
394 return true;
395 }
396 false
397 }
398
399 pub fn remove_domain(&mut self, domain_id: Uuid) -> bool {
401 let initial_len = self.domains.len();
402 self.domains.retain(|d| d.id != domain_id);
403 if let Some(domain) = self.domains.iter().find(|d| d.id == domain_id) {
405 let domain_name = domain.name.clone();
406 self.assets.retain(|a| a.domain != domain_name);
407 }
408 if self.domains.len() != initial_len {
409 self.last_modified_at = Utc::now();
410 true
411 } else {
412 false
413 }
414 }
415
416 pub fn get_domain(&self, domain_id: Uuid) -> Option<&DomainReference> {
418 self.domains.iter().find(|d| d.id == domain_id)
419 }
420
421 pub fn get_domain_by_name(&self, name: &str) -> Option<&DomainReference> {
423 self.domains.iter().find(|d| d.name == name)
424 }
425
426 pub fn add_asset(&mut self, asset: AssetReference) {
428 if self.assets.iter().any(|a| a.id == asset.id) {
430 return;
431 }
432 self.assets.push(asset);
433 self.last_modified_at = Utc::now();
434 }
435
436 pub fn remove_asset(&mut self, asset_id: Uuid) -> bool {
438 let initial_len = self.assets.len();
439 self.assets.retain(|a| a.id != asset_id);
440 if self.assets.len() != initial_len {
441 self.last_modified_at = Utc::now();
442 true
443 } else {
444 false
445 }
446 }
447
448 pub fn get_asset(&self, asset_id: Uuid) -> Option<&AssetReference> {
450 self.assets.iter().find(|a| a.id == asset_id)
451 }
452
453 pub fn get_assets_by_domain(&self, domain_name: &str) -> Vec<&AssetReference> {
455 self.assets
456 .iter()
457 .filter(|a| a.domain == domain_name)
458 .collect()
459 }
460
461 pub fn get_assets_by_type(&self, asset_type: &AssetType) -> Vec<&AssetReference> {
463 self.assets
464 .iter()
465 .filter(|a| &a.asset_type == asset_type)
466 .collect()
467 }
468
469 pub fn generate_asset_filename(&self, asset: &AssetReference) -> String {
472 let mut parts = vec![sanitize_name(&self.name), sanitize_name(&asset.domain)];
473
474 if let Some(ref system) = asset.system {
475 parts.push(sanitize_name(system));
476 }
477
478 parts.push(sanitize_name(&asset.name));
479
480 format!("{}.{}", parts.join("_"), asset.asset_type.extension())
481 }
482
483 pub fn parse_asset_filename(
486 filename: &str,
487 ) -> Option<(String, Option<String>, String, AssetType)> {
488 let (base, asset_type) = if filename.ends_with(".odcs.yaml") {
490 (filename.strip_suffix(".odcs.yaml")?, AssetType::Odcs)
491 } else if filename.ends_with(".odps.yaml") {
492 (filename.strip_suffix(".odps.yaml")?, AssetType::Odps)
493 } else if filename.ends_with(".cads.yaml") {
494 (filename.strip_suffix(".cads.yaml")?, AssetType::Cads)
495 } else if filename.ends_with(".bpmn.xml") {
496 (filename.strip_suffix(".bpmn.xml")?, AssetType::Bpmn)
497 } else if filename.ends_with(".dmn.xml") {
498 (filename.strip_suffix(".dmn.xml")?, AssetType::Dmn)
499 } else if filename.ends_with(".openapi.yaml") {
500 (filename.strip_suffix(".openapi.yaml")?, AssetType::Openapi)
501 } else {
502 return None;
503 };
504
505 let parts: Vec<&str> = base.split('_').collect();
506
507 match parts.len() {
508 3 => Some((parts[1].to_string(), None, parts[2].to_string(), asset_type)),
510 4 => Some((
512 parts[1].to_string(),
513 Some(parts[2].to_string()),
514 parts[3].to_string(),
515 asset_type,
516 )),
517 n if n > 4 => Some((
519 parts[1].to_string(),
520 Some(parts[2].to_string()),
521 parts[3..].join("_"),
522 asset_type,
523 )),
524 _ => None,
525 }
526 }
527
528 pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
530 serde_yaml::from_str(yaml_content)
531 }
532
533 pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
535 serde_yaml::to_string(self)
536 }
537
538 pub fn from_json(json_content: &str) -> Result<Self, serde_json::Error> {
540 serde_json::from_str(json_content)
541 }
542
543 pub fn to_json(&self) -> Result<String, serde_json::Error> {
545 serde_json::to_string(self)
546 }
547
548 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
550 serde_json::to_string_pretty(self)
551 }
552}
553
554fn sanitize_name(name: &str) -> String {
556 name.chars()
557 .map(|c| match c {
558 ' ' | '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '-',
559 _ => c,
560 })
561 .collect::<String>()
562 .to_lowercase()
563}
564
565impl Default for Workspace {
566 fn default() -> Self {
567 Self::new("Default Workspace".to_string(), Uuid::new_v4())
568 }
569}
570
571#[cfg(test)]
572mod tests {
573 use super::*;
574
575 #[test]
576 fn test_workspace_new() {
577 let owner_id = Uuid::new_v4();
578 let workspace = Workspace::new("Test Workspace".to_string(), owner_id);
579
580 assert_eq!(workspace.name, "Test Workspace");
581 assert_eq!(workspace.owner_id, owner_id);
582 assert!(workspace.domains.is_empty());
583 assert!(workspace.assets.is_empty());
584 }
585
586 #[test]
587 fn test_workspace_add_domain() {
588 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
589 let domain_id = Uuid::new_v4();
590
591 workspace.add_domain(domain_id, "customer-management".to_string());
592
593 assert_eq!(workspace.domains.len(), 1);
594 assert_eq!(workspace.domains[0].id, domain_id);
595 assert_eq!(workspace.domains[0].name, "customer-management");
596
597 workspace.add_domain(domain_id, "customer-management".to_string());
599 assert_eq!(workspace.domains.len(), 1);
600 }
601
602 #[test]
603 fn test_workspace_add_system_to_domain() {
604 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
605 let domain_id = Uuid::new_v4();
606 let system_id = Uuid::new_v4();
607
608 workspace.add_domain(domain_id, "sales".to_string());
609 let result = workspace.add_system_to_domain(
610 "sales",
611 system_id,
612 "kafka".to_string(),
613 Some("Kafka streaming".to_string()),
614 );
615
616 assert!(result);
617 assert_eq!(workspace.domains[0].systems.len(), 1);
618 assert_eq!(workspace.domains[0].systems[0].name, "kafka");
619 }
620
621 #[test]
622 fn test_workspace_remove_domain() {
623 let mut workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
624 let domain_id = Uuid::new_v4();
625 workspace.add_domain(domain_id, "test-domain".to_string());
626
627 assert!(workspace.remove_domain(domain_id));
628 assert!(workspace.domains.is_empty());
629 assert!(!workspace.remove_domain(domain_id)); }
631
632 #[test]
633 fn test_workspace_add_asset() {
634 let mut workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
635 let asset_id = Uuid::new_v4();
636
637 let asset = AssetReference {
638 id: asset_id,
639 name: "orders".to_string(),
640 domain: "sales".to_string(),
641 system: Some("kafka".to_string()),
642 asset_type: AssetType::Odcs,
643 file_path: None,
644 };
645
646 workspace.add_asset(asset);
647 assert_eq!(workspace.assets.len(), 1);
648 assert_eq!(workspace.assets[0].name, "orders");
649 }
650
651 #[test]
652 fn test_workspace_generate_asset_filename() {
653 let workspace = Workspace::new("enterprise".to_string(), Uuid::new_v4());
654
655 let asset_with_system = AssetReference {
657 id: Uuid::new_v4(),
658 name: "orders".to_string(),
659 domain: "sales".to_string(),
660 system: Some("kafka".to_string()),
661 asset_type: AssetType::Odcs,
662 file_path: None,
663 };
664 assert_eq!(
665 workspace.generate_asset_filename(&asset_with_system),
666 "enterprise_sales_kafka_orders.odcs.yaml"
667 );
668
669 let asset_no_system = AssetReference {
671 id: Uuid::new_v4(),
672 name: "customers".to_string(),
673 domain: "crm".to_string(),
674 system: None,
675 asset_type: AssetType::Odcs,
676 file_path: None,
677 };
678 assert_eq!(
679 workspace.generate_asset_filename(&asset_no_system),
680 "enterprise_crm_customers.odcs.yaml"
681 );
682
683 let odps_asset = AssetReference {
685 id: Uuid::new_v4(),
686 name: "analytics".to_string(),
687 domain: "finance".to_string(),
688 system: None,
689 asset_type: AssetType::Odps,
690 file_path: None,
691 };
692 assert_eq!(
693 workspace.generate_asset_filename(&odps_asset),
694 "enterprise_finance_analytics.odps.yaml"
695 );
696 }
697
698 #[test]
699 fn test_workspace_parse_asset_filename() {
700 let result = Workspace::parse_asset_filename("enterprise_sales_kafka_orders.odcs.yaml");
702 assert!(result.is_some());
703 let (domain, system, name, asset_type) = result.unwrap();
704 assert_eq!(domain, "sales");
705 assert_eq!(system, Some("kafka".to_string()));
706 assert_eq!(name, "orders");
707 assert_eq!(asset_type, AssetType::Odcs);
708
709 let result = Workspace::parse_asset_filename("enterprise_crm_customers.odcs.yaml");
711 assert!(result.is_some());
712 let (domain, system, name, asset_type) = result.unwrap();
713 assert_eq!(domain, "crm");
714 assert_eq!(system, None);
715 assert_eq!(name, "customers");
716 assert_eq!(asset_type, AssetType::Odcs);
717
718 let result = Workspace::parse_asset_filename("workspace_domain_product.odps.yaml");
720 assert!(result.is_some());
721 let (_, _, _, asset_type) = result.unwrap();
722 assert_eq!(asset_type, AssetType::Odps);
723 }
724
725 #[test]
726 fn test_workspace_yaml_roundtrip() {
727 let mut workspace = Workspace::new("Enterprise Models".to_string(), Uuid::new_v4());
728 workspace.add_domain(Uuid::new_v4(), "finance".to_string());
729 workspace.add_domain(Uuid::new_v4(), "risk".to_string());
730 workspace.add_asset(AssetReference {
731 id: Uuid::new_v4(),
732 name: "accounts".to_string(),
733 domain: "finance".to_string(),
734 system: None,
735 asset_type: AssetType::Odcs,
736 file_path: None,
737 });
738
739 let yaml = workspace.to_yaml().unwrap();
740 let parsed = Workspace::from_yaml(&yaml).unwrap();
741
742 assert_eq!(workspace.id, parsed.id);
743 assert_eq!(workspace.name, parsed.name);
744 assert_eq!(workspace.domains.len(), parsed.domains.len());
745 assert_eq!(workspace.assets.len(), parsed.assets.len());
746 }
747
748 #[test]
749 fn test_workspace_json_roundtrip() {
750 let workspace = Workspace::new("Test".to_string(), Uuid::new_v4());
751
752 let json = workspace.to_json().unwrap();
753 let parsed = Workspace::from_json(&json).unwrap();
754
755 assert_eq!(workspace.id, parsed.id);
756 assert_eq!(workspace.name, parsed.name);
757 }
758
759 #[test]
760 fn test_asset_type_extension() {
761 assert_eq!(AssetType::Workspace.extension(), "yaml");
762 assert_eq!(AssetType::Relationships.extension(), "yaml");
763 assert_eq!(AssetType::Odcs.extension(), "odcs.yaml");
764 assert_eq!(AssetType::Odps.extension(), "odps.yaml");
765 assert_eq!(AssetType::Cads.extension(), "cads.yaml");
766 assert_eq!(AssetType::Bpmn.extension(), "bpmn.xml");
767 assert_eq!(AssetType::Dmn.extension(), "dmn.xml");
768 assert_eq!(AssetType::Openapi.extension(), "openapi.yaml");
769 }
770
771 #[test]
772 fn test_asset_type_filename() {
773 assert_eq!(AssetType::Workspace.filename(), Some("workspace.yaml"));
774 assert_eq!(
775 AssetType::Relationships.filename(),
776 Some("relationships.yaml")
777 );
778 assert_eq!(AssetType::Odcs.filename(), None);
779 }
780
781 #[test]
782 fn test_asset_type_from_filename() {
783 assert_eq!(
784 AssetType::from_filename("workspace.yaml"),
785 Some(AssetType::Workspace)
786 );
787 assert_eq!(
788 AssetType::from_filename("relationships.yaml"),
789 Some(AssetType::Relationships)
790 );
791 assert_eq!(
792 AssetType::from_filename("test.odcs.yaml"),
793 Some(AssetType::Odcs)
794 );
795 assert_eq!(
796 AssetType::from_filename("test.odps.yaml"),
797 Some(AssetType::Odps)
798 );
799 assert_eq!(
800 AssetType::from_filename("test.cads.yaml"),
801 Some(AssetType::Cads)
802 );
803 assert_eq!(
804 AssetType::from_filename("test.bpmn.xml"),
805 Some(AssetType::Bpmn)
806 );
807 assert_eq!(
808 AssetType::from_filename("test.dmn.xml"),
809 Some(AssetType::Dmn)
810 );
811 assert_eq!(
812 AssetType::from_filename("test.openapi.yaml"),
813 Some(AssetType::Openapi)
814 );
815 assert_eq!(
816 AssetType::from_filename("test.openapi.json"),
817 Some(AssetType::Openapi)
818 );
819 assert_eq!(AssetType::from_filename("random.txt"), None);
820 assert_eq!(AssetType::from_filename("test.yaml"), None);
821 }
822
823 #[test]
824 fn test_asset_type_is_supported_file() {
825 assert!(AssetType::is_supported_file("workspace.yaml"));
826 assert!(AssetType::is_supported_file("relationships.yaml"));
827 assert!(AssetType::is_supported_file(
828 "enterprise_sales_orders.odcs.yaml"
829 ));
830 assert!(!AssetType::is_supported_file("readme.md"));
831 assert!(!AssetType::is_supported_file("config.json"));
832 }
833
834 #[test]
835 fn test_asset_type_is_workspace_level() {
836 assert!(AssetType::Workspace.is_workspace_level());
837 assert!(AssetType::Relationships.is_workspace_level());
838 assert!(!AssetType::Odcs.is_workspace_level());
839 assert!(!AssetType::Odps.is_workspace_level());
840 }
841
842 #[test]
843 fn test_sanitize_name() {
844 assert_eq!(sanitize_name("Hello World"), "hello-world");
845 assert_eq!(sanitize_name("Test/Path"), "test-path");
846 assert_eq!(sanitize_name("Normal"), "normal");
847 }
848}