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