1#[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
43pub struct ModelLoader<B: StorageBackend> {
45 storage: B,
46}
47
48impl<B: StorageBackend> ModelLoader<B> {
49 pub fn new(storage: B) -> Self {
51 Self { storage }
52 }
53
54 pub async fn load_model(&self, workspace_path: &str) -> Result<ModelLoadResult, StorageError> {
65 self.load_model_from_files(workspace_path).await
67 }
68
69 async fn load_model_from_files(
71 &self,
72 workspace_path: &str,
73 ) -> Result<ModelLoadResult, StorageError> {
74 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 if let Some(asset_type) = AssetType::from_filename(&file_name) {
82 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 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 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 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 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 if !parse_errors.is_empty() {
176 warn!(
177 "Table '{}' parsed with {} warnings/errors",
178 table.name,
179 parse_errors.len()
180 );
181 }
182
183 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 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 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 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 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 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 let workspace = self.load_workspace(workspace_path).await?;
289
290 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 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 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 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 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 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 }
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 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 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 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 #[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 #[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 self.load_domains(workspace_path).await
466 }
467
468 #[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 #[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 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 #[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 #[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 #[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 #[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 let model_name = file_name
634 .strip_suffix(".bpmn.xml")
635 .unwrap_or(file_name)
636 .to_string();
637
638 let domain_id = Uuid::new_v4();
640
641 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 #[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 #[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 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 #[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 #[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 let model_name = file_name
734 .strip_suffix(".dmn.xml")
735 .unwrap_or(file_name)
736 .to_string();
737
738 let domain_id = Uuid::new_v4();
740
741 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 #[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 #[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 #[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 #[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 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 let domain_id = Uuid::new_v4();
847
848 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 #[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 #[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 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 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 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 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 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 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 #[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 #[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 Ok(Vec::new())
1084 }
1085}
1086
1087fn sanitize_filename(name: &str) -> String {
1089 name.chars()
1090 .map(|c| match c {
1091 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1092 _ => c,
1093 })
1094 .collect()
1095}
1096
1097#[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#[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#[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#[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}