data_modelling_sdk/model/
loader.rs

1//! Model loading functionality
2//!
3//! Loads models from storage backends, handling YAML parsing and validation.
4//!
5//! Supports both file-based loading (FileSystemStorageBackend, BrowserStorageBackend)
6//! and API-based loading (ApiStorageBackend).
7//!
8//! File structure:
9//! - Base directory (workspace_path)
10//!   - Domain directories (e.g., `domain1/`, `domain2/`)
11//!     - `domain.yaml` - Domain definition
12//!     - `{name}.odcs.yaml` - ODCS table files
13//!     - `{name}.odps.yaml` - ODPS product files
14//!     - `{name}.cads.yaml` - CADS asset files
15//!   - `tables/` - Legacy: tables not in any domain (backward compatibility)
16
17#[cfg(feature = "bpmn")]
18use crate::import::bpmn::BPMNImporter;
19#[cfg(feature = "dmn")]
20use crate::import::dmn::DMNImporter;
21#[cfg(feature = "openapi")]
22use crate::import::openapi::OpenAPIImporter;
23use crate::import::{cads::CADSImporter, odcs::ODCSImporter, odps::ODPSImporter};
24#[cfg(feature = "bpmn")]
25use crate::models::bpmn::BPMNModel;
26#[cfg(feature = "dmn")]
27use crate::models::dmn::DMNModel;
28use crate::models::domain_config::DomainConfig;
29#[cfg(feature = "openapi")]
30use crate::models::openapi::{OpenAPIFormat, OpenAPIModel};
31use crate::models::workspace::Workspace;
32use crate::models::{cads::CADSAsset, domain::Domain, odps::ODPSDataProduct, table::Table};
33use crate::storage::{StorageBackend, StorageError};
34use anyhow::Result;
35use serde::{Deserialize, Serialize};
36use serde_yaml;
37use std::collections::HashMap;
38use tracing::{info, warn};
39use uuid::Uuid;
40
41/// Model loader that uses a storage backend
42pub struct ModelLoader<B: StorageBackend> {
43    storage: B,
44}
45
46impl<B: StorageBackend> ModelLoader<B> {
47    /// Create a new model loader with the given storage backend
48    pub fn new(storage: B) -> Self {
49        Self { storage }
50    }
51
52    /// Load a model from storage
53    ///
54    /// For file-based backends (FileSystemStorageBackend, BrowserStorageBackend):
55    /// - Loads from `tables/` subdirectory with YAML files
56    /// - Loads from `relationships.yaml` file
57    ///
58    /// For API backend (ApiStorageBackend), use `load_model_from_api()` instead.
59    ///
60    /// Returns the loaded model data and a list of orphaned relationships
61    /// (relationships that reference non-existent tables).
62    pub async fn load_model(&self, workspace_path: &str) -> Result<ModelLoadResult, StorageError> {
63        // File-based loading implementation
64        self.load_model_from_files(workspace_path).await
65    }
66
67    /// Load model from file-based storage
68    async fn load_model_from_files(
69        &self,
70        workspace_path: &str,
71    ) -> Result<ModelLoadResult, StorageError> {
72        let tables_dir = format!("{}/tables", workspace_path);
73
74        // Ensure tables directory exists
75        if !self.storage.dir_exists(&tables_dir).await? {
76            self.storage.create_dir(&tables_dir).await?;
77        }
78
79        // Load tables from individual YAML files
80        let mut tables = Vec::new();
81        let mut table_ids: HashMap<Uuid, String> = HashMap::new();
82
83        let files = self.storage.list_files(&tables_dir).await?;
84        for file_name in files {
85            if file_name.ends_with(".yaml") || file_name.ends_with(".yml") {
86                let file_path = format!("{}/{}", tables_dir, file_name);
87                match self.load_table_from_yaml(&file_path, workspace_path).await {
88                    Ok(table_data) => {
89                        table_ids.insert(table_data.id, table_data.name.clone());
90                        tables.push(table_data);
91                    }
92                    Err(e) => {
93                        warn!("Failed to load table from {}: {}", file_path, e);
94                    }
95                }
96            }
97        }
98
99        info!(
100            "Loaded {} tables from workspace {}",
101            tables.len(),
102            workspace_path
103        );
104
105        // Load relationships from control file
106        let relationships_file = format!("{}/relationships.yaml", workspace_path);
107        let mut relationships = Vec::new();
108        let mut orphaned_relationships = Vec::new();
109
110        if self.storage.file_exists(&relationships_file).await? {
111            match self.load_relationships_from_yaml(&relationships_file).await {
112                Ok(loaded_rels) => {
113                    // Separate valid and orphaned relationships
114                    for rel in loaded_rels {
115                        let source_exists = table_ids.contains_key(&rel.source_table_id);
116                        let target_exists = table_ids.contains_key(&rel.target_table_id);
117
118                        if source_exists && target_exists {
119                            relationships.push(rel.clone());
120                        } else {
121                            orphaned_relationships.push(rel.clone());
122                            warn!(
123                                "Orphaned relationship {}: source={} (exists: {}), target={} (exists: {})",
124                                rel.id,
125                                rel.source_table_id,
126                                source_exists,
127                                rel.target_table_id,
128                                target_exists
129                            );
130                        }
131                    }
132                }
133                Err(e) => {
134                    warn!(
135                        "Failed to load relationships from {}: {}",
136                        relationships_file, e
137                    );
138                }
139            }
140        }
141
142        info!(
143            "Loaded {} relationships ({} orphaned) from workspace {}",
144            relationships.len(),
145            orphaned_relationships.len(),
146            workspace_path
147        );
148
149        Ok(ModelLoadResult {
150            tables,
151            relationships,
152            orphaned_relationships,
153        })
154    }
155
156    /// Load a table from a YAML file
157    ///
158    /// Uses ODCSImporter to fully parse the table structure, including all columns,
159    /// metadata, and nested properties. This ensures complete table data is loaded.
160    async fn load_table_from_yaml(
161        &self,
162        yaml_path: &str,
163        workspace_path: &str,
164    ) -> Result<TableData, StorageError> {
165        let content = self.storage.read_file(yaml_path).await?;
166        let yaml_content = String::from_utf8(content)
167            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
168
169        // Use ODCSImporter to fully parse the table structure
170        let mut importer = crate::import::odcs::ODCSImporter::new();
171        let (table, parse_errors) = importer.parse_table(&yaml_content).map_err(|e| {
172            StorageError::SerializationError(format!("Failed to parse ODCS YAML: {}", e))
173        })?;
174
175        // Log any parse warnings/errors but don't fail if table was successfully parsed
176        if !parse_errors.is_empty() {
177            warn!(
178                "Table '{}' parsed with {} warnings/errors",
179                table.name,
180                parse_errors.len()
181            );
182        }
183
184        // Calculate relative path
185        let relative_path = yaml_path
186            .strip_prefix(workspace_path)
187            .map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
188            .unwrap_or_else(|| yaml_path.to_string());
189
190        Ok(TableData {
191            id: table.id,
192            name: table.name,
193            yaml_file_path: Some(relative_path),
194            yaml_content,
195        })
196    }
197
198    /// Load relationships from YAML file
199    async fn load_relationships_from_yaml(
200        &self,
201        yaml_path: &str,
202    ) -> Result<Vec<RelationshipData>, StorageError> {
203        let content = self.storage.read_file(yaml_path).await?;
204        let yaml_content = String::from_utf8(content)
205            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
206
207        let data: serde_yaml::Value = serde_yaml::from_str(&yaml_content).map_err(|e| {
208            StorageError::SerializationError(format!("Failed to parse YAML: {}", e))
209        })?;
210
211        let mut relationships = Vec::new();
212
213        // Handle both formats: direct array or object with "relationships" key
214        let rels_array = data
215            .get("relationships")
216            .and_then(|v| v.as_sequence())
217            .or_else(|| data.as_sequence());
218
219        if let Some(rels_array) = rels_array {
220            for rel_data in rels_array {
221                match self.parse_relationship(rel_data) {
222                    Ok(rel) => relationships.push(rel),
223                    Err(e) => {
224                        warn!("Failed to parse relationship: {}", e);
225                    }
226                }
227            }
228        }
229
230        Ok(relationships)
231    }
232
233    /// Parse a relationship from YAML value
234    fn parse_relationship(
235        &self,
236        data: &serde_yaml::Value,
237    ) -> Result<RelationshipData, StorageError> {
238        let source_table_id = data
239            .get("source_table_id")
240            .and_then(|v| v.as_str())
241            .and_then(|s| Uuid::parse_str(s).ok())
242            .ok_or_else(|| {
243                StorageError::SerializationError("Missing source_table_id".to_string())
244            })?;
245
246        let target_table_id = data
247            .get("target_table_id")
248            .and_then(|v| v.as_str())
249            .and_then(|s| Uuid::parse_str(s).ok())
250            .ok_or_else(|| {
251                StorageError::SerializationError("Missing target_table_id".to_string())
252            })?;
253
254        // Parse existing UUID or generate deterministic one based on source and target table IDs
255        let id = data
256            .get("id")
257            .and_then(|v| v.as_str())
258            .and_then(|s| Uuid::parse_str(s).ok())
259            .unwrap_or_else(|| {
260                crate::models::relationship::Relationship::generate_id(
261                    source_table_id,
262                    target_table_id,
263                )
264            });
265
266        Ok(RelationshipData {
267            id,
268            source_table_id,
269            target_table_id,
270        })
271    }
272
273    /// Load all domains from storage
274    ///
275    /// Scans the workspace for domain directories and loads each domain along with
276    /// its associated ODCS tables, ODPS products, and CADS assets.
277    ///
278    /// Domain directories are identified by the presence of a `domain.yaml` file.
279    pub async fn load_domains(
280        &self,
281        workspace_path: &str,
282    ) -> Result<DomainLoadResult, StorageError> {
283        let mut domains = Vec::new();
284        let mut tables = HashMap::new();
285        let mut odps_products = HashMap::new();
286        let mut cads_assets = HashMap::new();
287
288        // Try to discover domain directories by checking for domain.yaml files
289        // Since list_files only returns files, we need to check potential directories
290        // by looking for domain.yaml files. We'll check common patterns or use
291        // a recursive approach if the storage backend supports it.
292
293        // For now, we'll check if entries that look like directories contain domain.yaml
294        // This is a limitation - ideally we'd have a list_directories method
295        let entries = self.storage.list_files(workspace_path).await?;
296
297        // Collect potential domain directories (entries that don't look like files)
298        let mut potential_domains = Vec::new();
299        for entry in entries {
300            // If entry doesn't end with .yaml/.yml and doesn't contain a dot, it might be a directory
301            if !entry.ends_with(".yaml") && !entry.ends_with(".yml") && !entry.contains('.') {
302                potential_domains.push(entry);
303            }
304        }
305
306        // Also check for domain.yaml files directly in subdirectories
307        // We'll try common domain directory patterns or scan recursively
308        // For a more robust solution, we'd need storage backend support for listing directories
309
310        // Check each potential domain directory
311        for entry in potential_domains {
312            let domain_dir = format!("{}/{}", workspace_path, entry);
313            let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
314
315            if self.storage.file_exists(&domain_yaml_path).await? {
316                // Load domain
317                match self.load_domain(&domain_dir).await {
318                    Ok(domain) => {
319                        let domain_name = domain.name.clone();
320                        domains.push(domain);
321
322                        // Load ODCS tables from this domain directory
323                        let domain_tables = self.load_domain_odcs_tables(&domain_dir).await?;
324                        let table_count = domain_tables.len();
325                        for table in domain_tables {
326                            tables.insert(table.id, table);
327                        }
328
329                        // Load ODPS products from this domain directory
330                        let domain_odps = self.load_domain_odps_products(&domain_dir).await?;
331                        let odps_count = domain_odps.len();
332                        for product in domain_odps {
333                            odps_products.insert(
334                                Uuid::parse_str(&product.id).unwrap_or_else(|_| Uuid::new_v4()),
335                                product,
336                            );
337                        }
338
339                        // Load CADS assets from this domain directory
340                        let domain_cads = self.load_domain_cads_assets(&domain_dir).await?;
341                        let cads_count = domain_cads.len();
342                        for asset in domain_cads {
343                            cads_assets.insert(
344                                Uuid::parse_str(&asset.id).unwrap_or_else(|_| Uuid::new_v4()),
345                                asset,
346                            );
347                        }
348
349                        info!(
350                            "Loaded domain '{}' with {} tables, {} ODPS products, {} CADS assets",
351                            domain_name, table_count, odps_count, cads_count
352                        );
353                    }
354                    Err(e) => {
355                        warn!("Failed to load domain from {}: {}", domain_dir, e);
356                    }
357                }
358            }
359        }
360
361        info!(
362            "Loaded {} domains from workspace {}",
363            domains.len(),
364            workspace_path
365        );
366
367        Ok(DomainLoadResult {
368            domains,
369            tables,
370            odps_products,
371            cads_assets,
372        })
373    }
374
375    /// Load domains from explicit domain directory names
376    ///
377    /// This is more reliable than `load_domains()` when you know the domain directory names,
378    /// as it doesn't rely on directory discovery which may be limited by the storage backend.
379    pub async fn load_domains_from_list(
380        &self,
381        workspace_path: &str,
382        domain_directory_names: &[String],
383    ) -> Result<DomainLoadResult, StorageError> {
384        let mut domains = Vec::new();
385        let mut tables = HashMap::new();
386        let mut odps_products = HashMap::new();
387        let mut cads_assets = HashMap::new();
388
389        for domain_dir_name in domain_directory_names {
390            let domain_dir = format!("{}/{}", workspace_path, domain_dir_name);
391            let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
392
393            if self.storage.file_exists(&domain_yaml_path).await? {
394                match self.load_domain(&domain_dir).await {
395                    Ok(domain) => {
396                        let domain_name = domain.name.clone();
397                        domains.push(domain);
398
399                        // Load ODCS tables from this domain directory
400                        let domain_tables = self.load_domain_odcs_tables(&domain_dir).await?;
401                        let table_count = domain_tables.len();
402                        for table in domain_tables {
403                            tables.insert(table.id, table);
404                        }
405
406                        // Load ODPS products from this domain directory
407                        let domain_odps = self.load_domain_odps_products(&domain_dir).await?;
408                        let odps_count = domain_odps.len();
409                        for product in domain_odps {
410                            odps_products.insert(
411                                Uuid::parse_str(&product.id).unwrap_or_else(|_| Uuid::new_v4()),
412                                product,
413                            );
414                        }
415
416                        // Load CADS assets from this domain directory
417                        let domain_cads = self.load_domain_cads_assets(&domain_dir).await?;
418                        let cads_count = domain_cads.len();
419                        for asset in domain_cads {
420                            cads_assets.insert(
421                                Uuid::parse_str(&asset.id).unwrap_or_else(|_| Uuid::new_v4()),
422                                asset,
423                            );
424                        }
425
426                        info!(
427                            "Loaded domain '{}' with {} tables, {} ODPS products, {} CADS assets",
428                            domain_name, table_count, odps_count, cads_count
429                        );
430                    }
431                    Err(e) => {
432                        warn!("Failed to load domain from {}: {}", domain_dir, e);
433                    }
434                }
435            } else {
436                warn!(
437                    "Domain directory '{}' does not contain domain.yaml",
438                    domain_dir
439                );
440            }
441        }
442
443        info!(
444            "Loaded {} domains from workspace {}",
445            domains.len(),
446            workspace_path
447        );
448
449        Ok(DomainLoadResult {
450            domains,
451            tables,
452            odps_products,
453            cads_assets,
454        })
455    }
456
457    /// Load a single domain from a domain directory
458    async fn load_domain(&self, domain_dir: &str) -> Result<Domain, StorageError> {
459        let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
460        let content = self.storage.read_file(&domain_yaml_path).await?;
461        let yaml_content = String::from_utf8(content)
462            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
463
464        Domain::from_yaml(&yaml_content).map_err(|e| {
465            StorageError::SerializationError(format!("Failed to parse domain YAML: {}", e))
466        })
467    }
468
469    /// Load ODCS tables from a domain directory
470    async fn load_domain_odcs_tables(&self, domain_dir: &str) -> Result<Vec<Table>, StorageError> {
471        let mut tables = Vec::new();
472        let files = self.storage.list_files(domain_dir).await?;
473
474        for file_name in files {
475            if file_name.ends_with(".odcs.yaml") || file_name.ends_with(".odcs.yml") {
476                let file_path = format!("{}/{}", domain_dir, file_name);
477                match self.load_table_from_yaml(&file_path, domain_dir).await {
478                    Ok(table_data) => {
479                        // Parse the table from ODCS YAML
480                        let mut importer = ODCSImporter::new();
481                        match importer.parse_table(&table_data.yaml_content) {
482                            Ok((table, _parse_errors)) => {
483                                tables.push(table);
484                            }
485                            Err(e) => {
486                                warn!("Failed to parse ODCS table from {}: {}", file_path, e);
487                            }
488                        }
489                    }
490                    Err(e) => {
491                        warn!("Failed to load ODCS table from {}: {}", file_path, e);
492                    }
493                }
494            }
495        }
496
497        Ok(tables)
498    }
499
500    /// Load ODPS products from a domain directory
501    async fn load_domain_odps_products(
502        &self,
503        domain_dir: &str,
504    ) -> Result<Vec<ODPSDataProduct>, StorageError> {
505        let mut products = Vec::new();
506        let files = self.storage.list_files(domain_dir).await?;
507
508        for file_name in files {
509            if file_name.ends_with(".odps.yaml") || file_name.ends_with(".odps.yml") {
510                let file_path = format!("{}/{}", domain_dir, file_name);
511                let content = self.storage.read_file(&file_path).await?;
512                let yaml_content = String::from_utf8(content).map_err(|e| {
513                    StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
514                })?;
515
516                let importer = ODPSImporter::new();
517                match importer.import(&yaml_content) {
518                    Ok(product) => {
519                        products.push(product);
520                    }
521                    Err(e) => {
522                        warn!("Failed to parse ODPS product from {}: {}", file_path, e);
523                    }
524                }
525            }
526        }
527
528        Ok(products)
529    }
530
531    /// Load CADS assets from a domain directory
532    async fn load_domain_cads_assets(
533        &self,
534        domain_dir: &str,
535    ) -> Result<Vec<CADSAsset>, StorageError> {
536        let mut assets = Vec::new();
537        let files = self.storage.list_files(domain_dir).await?;
538
539        for file_name in files {
540            if file_name.ends_with(".cads.yaml") || file_name.ends_with(".cads.yml") {
541                let file_path = format!("{}/{}", domain_dir, file_name);
542                let content = self.storage.read_file(&file_path).await?;
543                let yaml_content = String::from_utf8(content).map_err(|e| {
544                    StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
545                })?;
546
547                let importer = CADSImporter::new();
548                match importer.import(&yaml_content) {
549                    Ok(asset) => {
550                        assets.push(asset);
551                    }
552                    Err(e) => {
553                        warn!("Failed to parse CADS asset from {}: {}", file_path, e);
554                    }
555                }
556            }
557        }
558
559        Ok(assets)
560    }
561
562    /// Load all BPMN models from a domain directory
563    #[cfg(feature = "bpmn")]
564    pub async fn load_bpmn_models(
565        &self,
566        workspace_path: &str,
567        domain_name: &str,
568    ) -> Result<Vec<BPMNModel>, StorageError> {
569        let sanitized_domain_name = sanitize_filename(domain_name);
570        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
571
572        if !self.storage.dir_exists(&domain_dir).await? {
573            return Ok(Vec::new());
574        }
575
576        let mut models = Vec::new();
577        let files = self.storage.list_files(&domain_dir).await?;
578
579        for file_name in files {
580            if file_name.ends_with(".bpmn.xml") {
581                let file_path = format!("{}/{}", domain_dir, file_name);
582                match self.load_bpmn_model(&domain_dir, &file_name).await {
583                    Ok(model) => models.push(model),
584                    Err(e) => {
585                        warn!("Failed to load BPMN model from {}: {}", file_path, e);
586                    }
587                }
588            }
589        }
590
591        Ok(models)
592    }
593
594    /// Load a specific BPMN model by name from a domain directory
595    #[cfg(feature = "bpmn")]
596    pub async fn load_bpmn_model(
597        &self,
598        domain_dir: &str,
599        file_name: &str,
600    ) -> Result<BPMNModel, StorageError> {
601        let file_path = format!("{}/{}", domain_dir, file_name);
602        let content = self.storage.read_file(&file_path).await?;
603        let xml_content = String::from_utf8(content)
604            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
605
606        // Extract model name from filename (remove .bpmn.xml extension)
607        let model_name = file_name
608            .strip_suffix(".bpmn.xml")
609            .unwrap_or(file_name)
610            .to_string();
611
612        // Get domain ID from domain.yaml, or generate a new one if not found
613        let domain_id = self
614            .get_domain_id(domain_dir)
615            .await?
616            .unwrap_or_else(Uuid::new_v4);
617
618        // Import using BPMNImporter
619        let mut importer = BPMNImporter::new();
620        let model = importer
621            .import(&xml_content, domain_id, Some(&model_name))
622            .map_err(|e| {
623                StorageError::SerializationError(format!("Failed to import BPMN model: {}", e))
624            })?;
625
626        Ok(model)
627    }
628
629    /// Load BPMN XML content from a domain directory
630    #[cfg(feature = "bpmn")]
631    pub async fn load_bpmn_xml(
632        &self,
633        workspace_path: &str,
634        domain_name: &str,
635        model_name: &str,
636    ) -> Result<String, StorageError> {
637        let sanitized_domain_name = sanitize_filename(domain_name);
638        let sanitized_model_name = sanitize_filename(model_name);
639        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
640        let file_path = format!("{}/{}.bpmn.xml", domain_dir, sanitized_model_name);
641
642        let content = self.storage.read_file(&file_path).await?;
643        String::from_utf8(content)
644            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))
645    }
646
647    /// Load all DMN models from a domain directory
648    #[cfg(feature = "dmn")]
649    pub async fn load_dmn_models(
650        &self,
651        workspace_path: &str,
652        domain_name: &str,
653    ) -> Result<Vec<DMNModel>, StorageError> {
654        let sanitized_domain_name = sanitize_filename(domain_name);
655        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
656
657        if !self.storage.dir_exists(&domain_dir).await? {
658            return Ok(Vec::new());
659        }
660
661        let mut models = Vec::new();
662        let files = self.storage.list_files(&domain_dir).await?;
663
664        for file_name in files {
665            if file_name.ends_with(".dmn.xml") {
666                let file_path = format!("{}/{}", domain_dir, file_name);
667                match self.load_dmn_model(&domain_dir, &file_name).await {
668                    Ok(model) => models.push(model),
669                    Err(e) => {
670                        warn!("Failed to load DMN model from {}: {}", file_path, e);
671                    }
672                }
673            }
674        }
675
676        Ok(models)
677    }
678
679    /// Load a specific DMN model by name from a domain directory
680    #[cfg(feature = "dmn")]
681    pub async fn load_dmn_model(
682        &self,
683        domain_dir: &str,
684        file_name: &str,
685    ) -> Result<DMNModel, StorageError> {
686        let file_path = format!("{}/{}", domain_dir, file_name);
687        let content = self.storage.read_file(&file_path).await?;
688        let xml_content = String::from_utf8(content)
689            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
690
691        // Extract model name from filename (remove .dmn.xml extension)
692        let model_name = file_name
693            .strip_suffix(".dmn.xml")
694            .unwrap_or(file_name)
695            .to_string();
696
697        // Get domain ID from domain.yaml, or generate a new one if not found
698        let domain_id = self
699            .get_domain_id(domain_dir)
700            .await?
701            .unwrap_or_else(Uuid::new_v4);
702
703        // Import using DMNImporter
704        let mut importer = DMNImporter::new();
705        let model = importer
706            .import(&xml_content, domain_id, Some(&model_name))
707            .map_err(|e| {
708                StorageError::SerializationError(format!("Failed to import DMN model: {}", e))
709            })?;
710
711        Ok(model)
712    }
713
714    /// Load DMN XML content from a domain directory
715    #[cfg(feature = "dmn")]
716    pub async fn load_dmn_xml(
717        &self,
718        workspace_path: &str,
719        domain_name: &str,
720        model_name: &str,
721    ) -> Result<String, StorageError> {
722        let sanitized_domain_name = sanitize_filename(domain_name);
723        let sanitized_model_name = sanitize_filename(model_name);
724        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
725        let file_path = format!("{}/{}.dmn.xml", domain_dir, sanitized_model_name);
726
727        let content = self.storage.read_file(&file_path).await?;
728        String::from_utf8(content)
729            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))
730    }
731
732    /// Load all OpenAPI specifications from a domain directory
733    #[cfg(feature = "openapi")]
734    pub async fn load_openapi_models(
735        &self,
736        workspace_path: &str,
737        domain_name: &str,
738    ) -> Result<Vec<OpenAPIModel>, StorageError> {
739        let sanitized_domain_name = sanitize_filename(domain_name);
740        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
741
742        if !self.storage.dir_exists(&domain_dir).await? {
743            return Ok(Vec::new());
744        }
745
746        let mut models = Vec::new();
747        let files = self.storage.list_files(&domain_dir).await?;
748
749        for file_name in files {
750            if file_name.ends_with(".openapi.yaml")
751                || file_name.ends_with(".openapi.yml")
752                || file_name.ends_with(".openapi.json")
753            {
754                let file_path = format!("{}/{}", domain_dir, file_name);
755                match self.load_openapi_model(&domain_dir, &file_name).await {
756                    Ok(model) => models.push(model),
757                    Err(e) => {
758                        warn!("Failed to load OpenAPI spec from {}: {}", file_path, e);
759                    }
760                }
761            }
762        }
763
764        Ok(models)
765    }
766
767    /// Load a specific OpenAPI model by name from a domain directory
768    #[cfg(feature = "openapi")]
769    pub async fn load_openapi_model(
770        &self,
771        domain_dir: &str,
772        file_name: &str,
773    ) -> Result<OpenAPIModel, StorageError> {
774        let file_path = format!("{}/{}", domain_dir, file_name);
775        let content = self.storage.read_file(&file_path).await?;
776        let spec_content = String::from_utf8(content)
777            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
778
779        // Format is detected by OpenAPIImporter, no need to detect here
780
781        // Extract API name from filename (remove .openapi.yaml/.openapi.json extension)
782        let api_name = file_name
783            .strip_suffix(".openapi.yaml")
784            .or_else(|| file_name.strip_suffix(".openapi.yml"))
785            .or_else(|| file_name.strip_suffix(".openapi.json"))
786            .unwrap_or(file_name)
787            .to_string();
788
789        // Get domain ID from domain.yaml, or generate a new one if not found
790        let domain_id = self
791            .get_domain_id(domain_dir)
792            .await?
793            .unwrap_or_else(Uuid::new_v4);
794
795        // Import using OpenAPIImporter
796        let mut importer = OpenAPIImporter::new();
797        let model = importer
798            .import(&spec_content, domain_id, Some(&api_name))
799            .map_err(|e| {
800                StorageError::SerializationError(format!("Failed to import OpenAPI spec: {}", e))
801            })?;
802
803        Ok(model)
804    }
805
806    /// Load OpenAPI content from a domain directory
807    #[cfg(feature = "openapi")]
808    pub async fn load_openapi_content(
809        &self,
810        workspace_path: &str,
811        domain_name: &str,
812        api_name: &str,
813        format: Option<OpenAPIFormat>,
814    ) -> Result<String, StorageError> {
815        let sanitized_domain_name = sanitize_filename(domain_name);
816        let sanitized_api_name = sanitize_filename(api_name);
817        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
818
819        // Try to find the file with the requested format, or any format
820        let extensions: Vec<&str> = if let Some(fmt) = format {
821            match fmt {
822                OpenAPIFormat::Yaml => vec!["yaml", "yml"],
823                OpenAPIFormat::Json => vec!["json"],
824            }
825        } else {
826            vec!["yaml", "yml", "json"]
827        };
828
829        for ext in extensions {
830            let file_path = format!("{}/{}.openapi.{}", domain_dir, sanitized_api_name, ext);
831            if self.storage.file_exists(&file_path).await? {
832                let content = self.storage.read_file(&file_path).await?;
833                return String::from_utf8(content).map_err(|e| {
834                    StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
835                });
836            }
837        }
838
839        Err(StorageError::IoError(format!(
840            "OpenAPI spec '{}.openapi.{{yaml,json}}' not found in domain '{}'",
841            api_name, domain_name
842        )))
843    }
844
845    // ==================== Workspace and Domain Config Loading ====================
846
847    /// Load workspace configuration from workspace.yaml
848    ///
849    /// # Arguments
850    ///
851    /// * `workspace_path` - Path to the workspace directory
852    ///
853    /// # Returns
854    ///
855    /// The Workspace configuration if found, or None if workspace.yaml doesn't exist
856    pub async fn load_workspace(
857        &self,
858        workspace_path: &str,
859    ) -> Result<Option<Workspace>, StorageError> {
860        let workspace_file = format!("{}/workspace.yaml", workspace_path);
861
862        if !self.storage.file_exists(&workspace_file).await? {
863            return Ok(None);
864        }
865
866        let content = self.storage.read_file(&workspace_file).await?;
867        let yaml_content = String::from_utf8(content)
868            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
869
870        let workspace: Workspace = serde_yaml::from_str(&yaml_content).map_err(|e| {
871            StorageError::SerializationError(format!("Failed to parse workspace.yaml: {}", e))
872        })?;
873
874        Ok(Some(workspace))
875    }
876
877    /// Save workspace configuration to workspace.yaml
878    ///
879    /// # Arguments
880    ///
881    /// * `workspace_path` - Path to the workspace directory
882    /// * `workspace` - The Workspace configuration to save
883    pub async fn save_workspace(
884        &self,
885        workspace_path: &str,
886        workspace: &Workspace,
887    ) -> Result<(), StorageError> {
888        let workspace_file = format!("{}/workspace.yaml", workspace_path);
889
890        let yaml_content = serde_yaml::to_string(workspace).map_err(|e| {
891            StorageError::SerializationError(format!("Failed to serialize workspace: {}", e))
892        })?;
893
894        self.storage
895            .write_file(&workspace_file, yaml_content.as_bytes())
896            .await?;
897
898        Ok(())
899    }
900
901    /// Load domain configuration from domain.yaml
902    ///
903    /// # Arguments
904    ///
905    /// * `domain_dir` - Path to the domain directory
906    ///
907    /// # Returns
908    ///
909    /// The DomainConfig if found, or None if domain.yaml doesn't exist
910    pub async fn load_domain_config(
911        &self,
912        domain_dir: &str,
913    ) -> Result<Option<DomainConfig>, StorageError> {
914        let domain_file = format!("{}/domain.yaml", domain_dir);
915
916        if !self.storage.file_exists(&domain_file).await? {
917            return Ok(None);
918        }
919
920        let content = self.storage.read_file(&domain_file).await?;
921        let yaml_content = String::from_utf8(content)
922            .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
923
924        let config: DomainConfig = serde_yaml::from_str(&yaml_content).map_err(|e| {
925            StorageError::SerializationError(format!("Failed to parse domain.yaml: {}", e))
926        })?;
927
928        Ok(Some(config))
929    }
930
931    /// Save domain configuration to domain.yaml
932    ///
933    /// # Arguments
934    ///
935    /// * `domain_dir` - Path to the domain directory
936    /// * `config` - The DomainConfig to save
937    pub async fn save_domain_config(
938        &self,
939        domain_dir: &str,
940        config: &DomainConfig,
941    ) -> Result<(), StorageError> {
942        let domain_file = format!("{}/domain.yaml", domain_dir);
943
944        let yaml_content = serde_yaml::to_string(config).map_err(|e| {
945            StorageError::SerializationError(format!("Failed to serialize domain config: {}", e))
946        })?;
947
948        self.storage
949            .write_file(&domain_file, yaml_content.as_bytes())
950            .await?;
951
952        Ok(())
953    }
954
955    /// Load domain configuration by name from a workspace
956    ///
957    /// # Arguments
958    ///
959    /// * `workspace_path` - Path to the workspace directory
960    /// * `domain_name` - Name of the domain (folder name)
961    ///
962    /// # Returns
963    ///
964    /// The DomainConfig if found
965    pub async fn load_domain_config_by_name(
966        &self,
967        workspace_path: &str,
968        domain_name: &str,
969    ) -> Result<Option<DomainConfig>, StorageError> {
970        let sanitized_domain_name = sanitize_filename(domain_name);
971        let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
972        self.load_domain_config(&domain_dir).await
973    }
974
975    /// Get domain ID from domain.yaml, or None if not found
976    ///
977    /// This is a helper method used by BPMN/DMN/OpenAPI loaders to get the domain ID.
978    ///
979    /// # Arguments
980    ///
981    /// * `domain_dir` - Path to the domain directory
982    ///
983    /// # Returns
984    ///
985    /// The domain UUID if found in domain.yaml, or None
986    pub async fn get_domain_id(&self, domain_dir: &str) -> Result<Option<Uuid>, StorageError> {
987        match self.load_domain_config(domain_dir).await? {
988            Some(config) => Ok(Some(config.id)),
989            None => Ok(None),
990        }
991    }
992
993    /// Load all domain configurations from a workspace
994    ///
995    /// # Arguments
996    ///
997    /// * `workspace_path` - Path to the workspace directory
998    ///
999    /// # Returns
1000    ///
1001    /// A vector of all DomainConfig found in the workspace
1002    pub async fn load_all_domain_configs(
1003        &self,
1004        workspace_path: &str,
1005    ) -> Result<Vec<DomainConfig>, StorageError> {
1006        let mut configs = Vec::new();
1007
1008        // List all directories in the workspace
1009        let entries = self.storage.list_files(workspace_path).await?;
1010
1011        for entry in entries {
1012            // Skip files, only process directories
1013            let entry_path = format!("{}/{}", workspace_path, entry);
1014            if self.storage.dir_exists(&entry_path).await? {
1015                // Try to load domain.yaml from each directory
1016                if let Ok(Some(config)) = self.load_domain_config(&entry_path).await {
1017                    configs.push(config);
1018                }
1019            }
1020        }
1021
1022        Ok(configs)
1023    }
1024}
1025
1026/// Sanitize a filename by removing invalid characters
1027fn sanitize_filename(name: &str) -> String {
1028    name.chars()
1029        .map(|c| match c {
1030            '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1031            _ => c,
1032        })
1033        .collect()
1034}
1035
1036/// Result of loading a model
1037#[derive(Debug, Serialize, Deserialize)]
1038pub struct ModelLoadResult {
1039    pub tables: Vec<TableData>,
1040    pub relationships: Vec<RelationshipData>,
1041    pub orphaned_relationships: Vec<RelationshipData>,
1042}
1043
1044/// Table data loaded from storage
1045#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct TableData {
1047    pub id: Uuid,
1048    pub name: String,
1049    pub yaml_file_path: Option<String>,
1050    pub yaml_content: String,
1051}
1052
1053/// Relationship data loaded from storage
1054#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct RelationshipData {
1056    pub id: Uuid,
1057    pub source_table_id: Uuid,
1058    pub target_table_id: Uuid,
1059}
1060
1061/// Result of loading domains
1062#[derive(Debug)]
1063pub struct DomainLoadResult {
1064    pub domains: Vec<Domain>,
1065    pub tables: HashMap<Uuid, Table>,
1066    pub odps_products: HashMap<Uuid, ODPSDataProduct>,
1067    pub cads_assets: HashMap<Uuid, CADSAsset>,
1068}