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