1#[cfg(feature = "bpmn")]
20use crate::import::bpmn::BPMNImporter;
21use crate::import::decision::DecisionImporter;
22#[cfg(feature = "dmn")]
23use crate::import::dmn::DMNImporter;
24use crate::import::knowledge::KnowledgeImporter;
25#[cfg(feature = "openapi")]
26use crate::import::openapi::OpenAPIImporter;
27use crate::import::{cads::CADSImporter, odcs::ODCSImporter, odps::ODPSImporter};
28#[cfg(feature = "bpmn")]
29use crate::models::bpmn::BPMNModel;
30use crate::models::decision::{Decision, DecisionIndex};
31#[cfg(feature = "dmn")]
32use crate::models::dmn::DMNModel;
33use crate::models::domain_config::DomainConfig;
34use crate::models::knowledge::{KnowledgeArticle, KnowledgeIndex};
35#[cfg(feature = "openapi")]
36use crate::models::openapi::{OpenAPIFormat, OpenAPIModel};
37use crate::models::workspace::{AssetType, Workspace};
38use crate::models::{cads::CADSAsset, domain::Domain, odps::ODPSDataProduct, table::Table};
39use crate::storage::{StorageBackend, StorageError};
40use anyhow::Result;
41use serde::{Deserialize, Serialize};
42use serde_yaml;
43use std::collections::HashMap;
44use tracing::{info, warn};
45use uuid::Uuid;
46
47pub struct ModelLoader<B: StorageBackend> {
49 storage: B,
50}
51
52impl<B: StorageBackend> ModelLoader<B> {
53 pub fn new(storage: B) -> Self {
55 Self { storage }
56 }
57
58 pub async fn load_model(&self, workspace_path: &str) -> Result<ModelLoadResult, StorageError> {
69 self.load_model_from_files(workspace_path).await
71 }
72
73 async fn load_model_from_files(
75 &self,
76 workspace_path: &str,
77 ) -> Result<ModelLoadResult, StorageError> {
78 let mut tables = Vec::new();
80 let mut table_ids: HashMap<Uuid, String> = HashMap::new();
81
82 let files = self.storage.list_files(workspace_path).await?;
83 for file_name in files {
84 if let Some(asset_type) = AssetType::from_filename(&file_name) {
86 if asset_type == AssetType::Odcs {
88 let file_path = format!("{}/{}", workspace_path, file_name);
89 match self.load_table_from_yaml(&file_path, workspace_path).await {
90 Ok(table_data) => {
91 table_ids.insert(table_data.id, table_data.name.clone());
92 tables.push(table_data);
93 }
94 Err(e) => {
95 warn!("Failed to load table from {}: {}", file_path, e);
96 }
97 }
98 }
99 }
100 }
101
102 info!(
103 "Loaded {} tables from workspace {}",
104 tables.len(),
105 workspace_path
106 );
107
108 let relationships_file = format!("{}/relationships.yaml", workspace_path);
110 let mut relationships = Vec::new();
111 let mut orphaned_relationships = Vec::new();
112
113 if self.storage.file_exists(&relationships_file).await? {
114 match self.load_relationships_from_yaml(&relationships_file).await {
115 Ok(loaded_rels) => {
116 for rel in loaded_rels {
118 let source_exists = table_ids.contains_key(&rel.source_table_id);
119 let target_exists = table_ids.contains_key(&rel.target_table_id);
120
121 if source_exists && target_exists {
122 relationships.push(rel.clone());
123 } else {
124 orphaned_relationships.push(rel.clone());
125 warn!(
126 "Orphaned relationship {}: source={} (exists: {}), target={} (exists: {})",
127 rel.id,
128 rel.source_table_id,
129 source_exists,
130 rel.target_table_id,
131 target_exists
132 );
133 }
134 }
135 }
136 Err(e) => {
137 warn!(
138 "Failed to load relationships from {}: {}",
139 relationships_file, e
140 );
141 }
142 }
143 }
144
145 info!(
146 "Loaded {} relationships ({} orphaned) from workspace {}",
147 relationships.len(),
148 orphaned_relationships.len(),
149 workspace_path
150 );
151
152 Ok(ModelLoadResult {
153 tables,
154 relationships,
155 orphaned_relationships,
156 })
157 }
158
159 async fn load_table_from_yaml(
164 &self,
165 yaml_path: &str,
166 workspace_path: &str,
167 ) -> Result<TableData, StorageError> {
168 let content = self.storage.read_file(yaml_path).await?;
169 let yaml_content = String::from_utf8(content)
170 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
171
172 let mut importer = crate::import::odcs::ODCSImporter::new();
174 let (table, parse_errors) = importer.parse_table(&yaml_content).map_err(|e| {
175 StorageError::SerializationError(format!("Failed to parse ODCS YAML: {}", e))
176 })?;
177
178 if !parse_errors.is_empty() {
180 warn!(
181 "Table '{}' parsed with {} warnings/errors",
182 table.name,
183 parse_errors.len()
184 );
185 }
186
187 let relative_path = yaml_path
189 .strip_prefix(workspace_path)
190 .map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
191 .unwrap_or_else(|| yaml_path.to_string());
192
193 Ok(TableData {
194 id: table.id,
195 name: table.name,
196 yaml_file_path: Some(relative_path),
197 yaml_content,
198 })
199 }
200
201 async fn load_relationships_from_yaml(
203 &self,
204 yaml_path: &str,
205 ) -> Result<Vec<RelationshipData>, StorageError> {
206 let content = self.storage.read_file(yaml_path).await?;
207 let yaml_content = String::from_utf8(content)
208 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
209
210 let data: serde_yaml::Value = serde_yaml::from_str(&yaml_content).map_err(|e| {
211 StorageError::SerializationError(format!("Failed to parse YAML: {}", e))
212 })?;
213
214 let mut relationships = Vec::new();
215
216 let rels_array = data
218 .get("relationships")
219 .and_then(|v| v.as_sequence())
220 .or_else(|| data.as_sequence());
221
222 if let Some(rels_array) = rels_array {
223 for rel_data in rels_array {
224 match self.parse_relationship(rel_data) {
225 Ok(rel) => relationships.push(rel),
226 Err(e) => {
227 warn!("Failed to parse relationship: {}", e);
228 }
229 }
230 }
231 }
232
233 Ok(relationships)
234 }
235
236 fn parse_relationship(
238 &self,
239 data: &serde_yaml::Value,
240 ) -> Result<RelationshipData, StorageError> {
241 let source_table_id = data
242 .get("source_table_id")
243 .and_then(|v| v.as_str())
244 .and_then(|s| Uuid::parse_str(s).ok())
245 .ok_or_else(|| {
246 StorageError::SerializationError("Missing source_table_id".to_string())
247 })?;
248
249 let target_table_id = data
250 .get("target_table_id")
251 .and_then(|v| v.as_str())
252 .and_then(|s| Uuid::parse_str(s).ok())
253 .ok_or_else(|| {
254 StorageError::SerializationError("Missing target_table_id".to_string())
255 })?;
256
257 let id = data
259 .get("id")
260 .and_then(|v| v.as_str())
261 .and_then(|s| Uuid::parse_str(s).ok())
262 .unwrap_or_else(|| {
263 crate::models::relationship::Relationship::generate_id(
264 source_table_id,
265 target_table_id,
266 )
267 });
268
269 Ok(RelationshipData {
270 id,
271 source_table_id,
272 target_table_id,
273 })
274 }
275
276 pub async fn load_domains(
283 &self,
284 workspace_path: &str,
285 ) -> Result<DomainLoadResult, StorageError> {
286 let mut domains = Vec::new();
287 let mut tables = HashMap::new();
288 let mut odps_products = HashMap::new();
289 let mut cads_assets = HashMap::new();
290
291 let workspace = self.load_workspace(workspace_path).await?;
293
294 if let Some(ws) = &workspace {
296 for domain_ref in &ws.domains {
297 domains.push(Domain::new(domain_ref.name.clone()));
298 }
299 }
300
301 let files = self.storage.list_files(workspace_path).await?;
303
304 for file_name in files {
305 let Some(asset_type) = AssetType::from_filename(&file_name) else {
306 continue;
307 };
308
309 if asset_type.is_workspace_level() {
311 continue;
312 }
313
314 let file_path = format!("{}/{}", workspace_path, file_name);
315
316 match asset_type {
317 AssetType::Odcs => {
318 match self.load_odcs_table_from_file(&file_path).await {
320 Ok(table) => {
321 tables.insert(table.id, table);
322 }
323 Err(e) => {
324 warn!("Failed to load ODCS table from {}: {}", file_path, e);
325 }
326 }
327 }
328 AssetType::Odps => {
329 match self.load_odps_product_from_file(&file_path).await {
331 Ok(product) => {
332 odps_products.insert(
333 Uuid::parse_str(&product.id).unwrap_or_else(|_| Uuid::new_v4()),
334 product,
335 );
336 }
337 Err(e) => {
338 warn!("Failed to load ODPS product from {}: {}", file_path, e);
339 }
340 }
341 }
342 AssetType::Cads => {
343 match self.load_cads_asset_from_file(&file_path).await {
345 Ok(asset) => {
346 cads_assets.insert(
347 Uuid::parse_str(&asset.id).unwrap_or_else(|_| Uuid::new_v4()),
348 asset,
349 );
350 }
351 Err(e) => {
352 warn!("Failed to load CADS asset from {}: {}", file_path, e);
353 }
354 }
355 }
356 _ => {
357 }
359 }
360 }
361
362 info!(
363 "Loaded {} domains, {} tables, {} ODPS products, {} CADS assets from workspace {}",
364 domains.len(),
365 tables.len(),
366 odps_products.len(),
367 cads_assets.len(),
368 workspace_path
369 );
370
371 Ok(DomainLoadResult {
372 domains,
373 tables,
374 odps_products,
375 cads_assets,
376 })
377 }
378
379 async fn load_odcs_table_from_file(&self, file_path: &str) -> Result<Table, StorageError> {
381 let content = self.storage.read_file(file_path).await?;
382 let yaml_content = String::from_utf8(content)
383 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
384
385 let mut importer = ODCSImporter::new();
386 let (table, _parse_errors) = importer.parse_table(&yaml_content).map_err(|e| {
387 StorageError::SerializationError(format!("Failed to parse ODCS table: {}", e))
388 })?;
389
390 Ok(table)
391 }
392
393 async fn load_odps_product_from_file(
395 &self,
396 file_path: &str,
397 ) -> Result<ODPSDataProduct, StorageError> {
398 let content = self.storage.read_file(file_path).await?;
399 let yaml_content = String::from_utf8(content)
400 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
401
402 let importer = ODPSImporter::new();
403 importer
404 .import(&yaml_content)
405 .map_err(|e| StorageError::SerializationError(format!("Failed to parse ODPS: {}", e)))
406 }
407
408 async fn load_cads_asset_from_file(&self, file_path: &str) -> Result<CADSAsset, StorageError> {
410 let content = self.storage.read_file(file_path).await?;
411 let yaml_content = String::from_utf8(content)
412 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
413
414 let importer = CADSImporter::new();
415 importer.import(&yaml_content).map_err(|e| {
416 StorageError::SerializationError(format!("Failed to parse CADS asset: {}", e))
417 })
418 }
419
420 #[deprecated(
424 since = "2.0.0",
425 note = "Use load_domains() with flat file structure instead"
426 )]
427 #[allow(dead_code)]
428 async fn load_domains_legacy(
429 &self,
430 workspace_path: &str,
431 ) -> Result<DomainLoadResult, StorageError> {
432 let domains = Vec::new();
433 let tables = HashMap::new();
434 let odps_products = HashMap::new();
435 let cads_assets = HashMap::new();
436
437 info!(
438 "Legacy domain loading is deprecated. Use flat file structure instead. Workspace: {}",
439 workspace_path
440 );
441
442 Ok(DomainLoadResult {
443 domains,
444 tables,
445 odps_products,
446 cads_assets,
447 })
448 }
449
450 #[deprecated(
454 since = "2.0.0",
455 note = "Use load_domains() with flat file structure instead. Domain directories are no longer supported."
456 )]
457 #[allow(dead_code)]
458 pub async fn load_domains_from_list(
459 &self,
460 workspace_path: &str,
461 _domain_directory_names: &[String],
462 ) -> Result<DomainLoadResult, StorageError> {
463 warn!(
464 "load_domains_from_list is deprecated. Using flat file structure for workspace: {}",
465 workspace_path
466 );
467
468 self.load_domains(workspace_path).await
470 }
471
472 #[deprecated(
474 since = "2.0.0",
475 note = "Domain directories are no longer supported. Use flat file structure."
476 )]
477 #[allow(dead_code)]
478 async fn load_domain_legacy(&self, domain_dir: &str) -> Result<Domain, StorageError> {
479 let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
480 let content = self.storage.read_file(&domain_yaml_path).await?;
481 let yaml_content = String::from_utf8(content)
482 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
483
484 Domain::from_yaml(&yaml_content).map_err(|e| {
485 StorageError::SerializationError(format!("Failed to parse domain YAML: {}", e))
486 })
487 }
488
489 #[deprecated(
491 since = "2.0.0",
492 note = "Domain directories are no longer supported. Use flat file structure."
493 )]
494 #[allow(dead_code)]
495 async fn load_domain_odcs_tables_legacy(
496 &self,
497 domain_dir: &str,
498 ) -> Result<Vec<Table>, StorageError> {
499 let mut tables = Vec::new();
500 let files = self.storage.list_files(domain_dir).await?;
501
502 for file_name in files {
503 if file_name.ends_with(".odcs.yaml") || file_name.ends_with(".odcs.yml") {
504 let file_path = format!("{}/{}", domain_dir, file_name);
505 match self.load_table_from_yaml(&file_path, domain_dir).await {
506 Ok(table_data) => {
507 let mut importer = ODCSImporter::new();
509 match importer.parse_table(&table_data.yaml_content) {
510 Ok((table, _parse_errors)) => {
511 tables.push(table);
512 }
513 Err(e) => {
514 warn!("Failed to parse ODCS table from {}: {}", file_path, e);
515 }
516 }
517 }
518 Err(e) => {
519 warn!("Failed to load ODCS table from {}: {}", file_path, e);
520 }
521 }
522 }
523 }
524
525 Ok(tables)
526 }
527
528 #[deprecated(
530 since = "2.0.0",
531 note = "Domain directories are no longer supported. Use flat file structure."
532 )]
533 #[allow(dead_code)]
534 async fn load_domain_odps_products_legacy(
535 &self,
536 domain_dir: &str,
537 ) -> Result<Vec<ODPSDataProduct>, StorageError> {
538 let mut products = Vec::new();
539 let files = self.storage.list_files(domain_dir).await?;
540
541 for file_name in files {
542 if file_name.ends_with(".odps.yaml") || file_name.ends_with(".odps.yml") {
543 let file_path = format!("{}/{}", domain_dir, file_name);
544 let content = self.storage.read_file(&file_path).await?;
545 let yaml_content = String::from_utf8(content).map_err(|e| {
546 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
547 })?;
548
549 let importer = ODPSImporter::new();
550 match importer.import(&yaml_content) {
551 Ok(product) => {
552 products.push(product);
553 }
554 Err(e) => {
555 warn!("Failed to parse ODPS product from {}: {}", file_path, e);
556 }
557 }
558 }
559 }
560
561 Ok(products)
562 }
563
564 #[deprecated(
566 since = "2.0.0",
567 note = "Domain directories are no longer supported. Use flat file structure."
568 )]
569 #[allow(dead_code)]
570 async fn load_domain_cads_assets_legacy(
571 &self,
572 domain_dir: &str,
573 ) -> Result<Vec<CADSAsset>, StorageError> {
574 let mut assets = 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(".cads.yaml") || file_name.ends_with(".cads.yml") {
579 let file_path = format!("{}/{}", domain_dir, file_name);
580 let content = self.storage.read_file(&file_path).await?;
581 let yaml_content = String::from_utf8(content).map_err(|e| {
582 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
583 })?;
584
585 let importer = CADSImporter::new();
586 match importer.import(&yaml_content) {
587 Ok(asset) => {
588 assets.push(asset);
589 }
590 Err(e) => {
591 warn!("Failed to parse CADS asset from {}: {}", file_path, e);
592 }
593 }
594 }
595 }
596
597 Ok(assets)
598 }
599
600 #[cfg(feature = "bpmn")]
602 pub async fn load_bpmn_models(
603 &self,
604 workspace_path: &str,
605 _domain_name: &str,
606 ) -> Result<Vec<BPMNModel>, StorageError> {
607 let mut models = Vec::new();
608 let files = self.storage.list_files(workspace_path).await?;
609
610 for file_name in files {
611 if file_name.ends_with(".bpmn.xml") {
612 let file_path = format!("{}/{}", workspace_path, file_name);
613 match self.load_bpmn_model_from_file(&file_path, &file_name).await {
614 Ok(model) => models.push(model),
615 Err(e) => {
616 warn!("Failed to load BPMN model from {}: {}", file_path, e);
617 }
618 }
619 }
620 }
621
622 Ok(models)
623 }
624
625 #[cfg(feature = "bpmn")]
627 async fn load_bpmn_model_from_file(
628 &self,
629 file_path: &str,
630 file_name: &str,
631 ) -> Result<BPMNModel, StorageError> {
632 let content = self.storage.read_file(file_path).await?;
633 let xml_content = String::from_utf8(content)
634 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
635
636 let model_name = file_name
638 .strip_suffix(".bpmn.xml")
639 .unwrap_or(file_name)
640 .to_string();
641
642 let domain_id = Uuid::new_v4();
644
645 let mut importer = BPMNImporter::new();
647 let model = importer
648 .import(&xml_content, domain_id, Some(&model_name))
649 .map_err(|e| {
650 StorageError::SerializationError(format!("Failed to import BPMN model: {}", e))
651 })?;
652
653 Ok(model)
654 }
655
656 #[cfg(feature = "bpmn")]
658 #[deprecated(
659 since = "2.0.0",
660 note = "Use load_bpmn_model_from_file with flat file structure instead"
661 )]
662 #[allow(dead_code)]
663 pub async fn load_bpmn_model(
664 &self,
665 domain_dir: &str,
666 file_name: &str,
667 ) -> Result<BPMNModel, StorageError> {
668 let file_path = format!("{}/{}", domain_dir, file_name);
669 self.load_bpmn_model_from_file(&file_path, file_name).await
670 }
671
672 #[cfg(feature = "bpmn")]
674 pub async fn load_bpmn_xml(
675 &self,
676 workspace_path: &str,
677 _domain_name: &str,
678 model_name: &str,
679 ) -> Result<String, StorageError> {
680 let sanitized_model_name = sanitize_filename(model_name);
681 let files = self.storage.list_files(workspace_path).await?;
683
684 for file_name in files {
685 if file_name.ends_with(".bpmn.xml") && file_name.contains(&sanitized_model_name) {
686 let file_path = format!("{}/{}", workspace_path, file_name);
687 let content = self.storage.read_file(&file_path).await?;
688 return String::from_utf8(content).map_err(|e| {
689 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
690 });
691 }
692 }
693
694 Err(StorageError::IoError(format!(
695 "BPMN model '{}' not found in workspace",
696 model_name
697 )))
698 }
699
700 #[cfg(feature = "dmn")]
702 pub async fn load_dmn_models(
703 &self,
704 workspace_path: &str,
705 _domain_name: &str,
706 ) -> Result<Vec<DMNModel>, StorageError> {
707 let mut models = Vec::new();
708 let files = self.storage.list_files(workspace_path).await?;
709
710 for file_name in files {
711 if file_name.ends_with(".dmn.xml") {
712 let file_path = format!("{}/{}", workspace_path, file_name);
713 match self.load_dmn_model_from_file(&file_path, &file_name).await {
714 Ok(model) => models.push(model),
715 Err(e) => {
716 warn!("Failed to load DMN model from {}: {}", file_path, e);
717 }
718 }
719 }
720 }
721
722 Ok(models)
723 }
724
725 #[cfg(feature = "dmn")]
727 async fn load_dmn_model_from_file(
728 &self,
729 file_path: &str,
730 file_name: &str,
731 ) -> Result<DMNModel, StorageError> {
732 let content = self.storage.read_file(file_path).await?;
733 let xml_content = String::from_utf8(content)
734 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
735
736 let model_name = file_name
738 .strip_suffix(".dmn.xml")
739 .unwrap_or(file_name)
740 .to_string();
741
742 let domain_id = Uuid::new_v4();
744
745 let mut importer = DMNImporter::new();
747 let model = importer
748 .import(&xml_content, domain_id, Some(&model_name))
749 .map_err(|e| {
750 StorageError::SerializationError(format!("Failed to import DMN model: {}", e))
751 })?;
752
753 Ok(model)
754 }
755
756 #[cfg(feature = "dmn")]
758 #[deprecated(
759 since = "2.0.0",
760 note = "Use load_dmn_model_from_file with flat file structure instead"
761 )]
762 #[allow(dead_code)]
763 pub async fn load_dmn_model(
764 &self,
765 domain_dir: &str,
766 file_name: &str,
767 ) -> Result<DMNModel, StorageError> {
768 let file_path = format!("{}/{}", domain_dir, file_name);
769 self.load_dmn_model_from_file(&file_path, file_name).await
770 }
771
772 #[cfg(feature = "dmn")]
774 pub async fn load_dmn_xml(
775 &self,
776 workspace_path: &str,
777 _domain_name: &str,
778 model_name: &str,
779 ) -> Result<String, StorageError> {
780 let sanitized_model_name = sanitize_filename(model_name);
781 let files = self.storage.list_files(workspace_path).await?;
782
783 for file_name in files {
784 if file_name.ends_with(".dmn.xml") && file_name.contains(&sanitized_model_name) {
785 let file_path = format!("{}/{}", workspace_path, file_name);
786 let content = self.storage.read_file(&file_path).await?;
787 return String::from_utf8(content).map_err(|e| {
788 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
789 });
790 }
791 }
792
793 Err(StorageError::IoError(format!(
794 "DMN model '{}' not found in workspace",
795 model_name
796 )))
797 }
798
799 #[cfg(feature = "openapi")]
801 pub async fn load_openapi_models(
802 &self,
803 workspace_path: &str,
804 _domain_name: &str,
805 ) -> Result<Vec<OpenAPIModel>, StorageError> {
806 let mut models = Vec::new();
807 let files = self.storage.list_files(workspace_path).await?;
808
809 for file_name in files {
810 if file_name.ends_with(".openapi.yaml")
811 || file_name.ends_with(".openapi.yml")
812 || file_name.ends_with(".openapi.json")
813 {
814 let file_path = format!("{}/{}", workspace_path, file_name);
815 match self
816 .load_openapi_model_from_file(&file_path, &file_name)
817 .await
818 {
819 Ok(model) => models.push(model),
820 Err(e) => {
821 warn!("Failed to load OpenAPI spec from {}: {}", file_path, e);
822 }
823 }
824 }
825 }
826
827 Ok(models)
828 }
829
830 #[cfg(feature = "openapi")]
832 async fn load_openapi_model_from_file(
833 &self,
834 file_path: &str,
835 file_name: &str,
836 ) -> Result<OpenAPIModel, StorageError> {
837 let content = self.storage.read_file(file_path).await?;
838 let spec_content = String::from_utf8(content)
839 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
840
841 let api_name = file_name
843 .strip_suffix(".openapi.yaml")
844 .or_else(|| file_name.strip_suffix(".openapi.yml"))
845 .or_else(|| file_name.strip_suffix(".openapi.json"))
846 .unwrap_or(file_name)
847 .to_string();
848
849 let domain_id = Uuid::new_v4();
851
852 let mut importer = OpenAPIImporter::new();
854 let model = importer
855 .import(&spec_content, domain_id, Some(&api_name))
856 .map_err(|e| {
857 StorageError::SerializationError(format!("Failed to import OpenAPI spec: {}", e))
858 })?;
859
860 Ok(model)
861 }
862
863 #[cfg(feature = "openapi")]
865 #[deprecated(
866 since = "2.0.0",
867 note = "Use load_openapi_model_from_file with flat file structure instead"
868 )]
869 #[allow(dead_code)]
870 pub async fn load_openapi_model(
871 &self,
872 domain_dir: &str,
873 file_name: &str,
874 ) -> Result<OpenAPIModel, StorageError> {
875 let file_path = format!("{}/{}", domain_dir, file_name);
876 self.load_openapi_model_from_file(&file_path, file_name)
877 .await
878 }
879
880 #[cfg(feature = "openapi")]
882 pub async fn load_openapi_content(
883 &self,
884 workspace_path: &str,
885 _domain_name: &str,
886 api_name: &str,
887 format: Option<OpenAPIFormat>,
888 ) -> Result<String, StorageError> {
889 let sanitized_api_name = sanitize_filename(api_name);
890
891 let extensions: Vec<&str> = if let Some(fmt) = format {
893 match fmt {
894 OpenAPIFormat::Yaml => vec!["yaml", "yml"],
895 OpenAPIFormat::Json => vec!["json"],
896 }
897 } else {
898 vec!["yaml", "yml", "json"]
899 };
900
901 let files = self.storage.list_files(workspace_path).await?;
902
903 for file_name in files {
904 for ext in &extensions {
905 let suffix = format!(".openapi.{}", ext);
906 if file_name.ends_with(&suffix) && file_name.contains(&sanitized_api_name) {
907 let file_path = format!("{}/{}", workspace_path, file_name);
908 let content = self.storage.read_file(&file_path).await?;
909 return String::from_utf8(content).map_err(|e| {
910 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
911 });
912 }
913 }
914 }
915
916 Err(StorageError::IoError(format!(
917 "OpenAPI spec '{}' not found in workspace",
918 api_name
919 )))
920 }
921
922 pub async fn load_decisions(
937 &self,
938 workspace_path: &str,
939 ) -> Result<DecisionLoadResult, StorageError> {
940 let mut decisions = Vec::new();
941 let mut load_errors = Vec::new();
942
943 let files = self.storage.list_files(workspace_path).await?;
944 let importer = DecisionImporter;
945
946 for file_name in files {
947 if let Some(AssetType::Decision) = AssetType::from_filename(&file_name) {
948 let file_path = format!("{}/{}", workspace_path, file_name);
949 match self.storage.read_file(&file_path).await {
950 Ok(content) => {
951 let yaml_content = match String::from_utf8(content) {
952 Ok(s) => s,
953 Err(e) => {
954 load_errors.push(DecisionLoadError {
955 file_path: file_path.clone(),
956 error: format!("Invalid UTF-8: {}", e),
957 });
958 continue;
959 }
960 };
961
962 match importer.import(&yaml_content) {
963 Ok(decision) => {
964 decisions.push(decision);
965 }
966 Err(e) => {
967 load_errors.push(DecisionLoadError {
968 file_path: file_path.clone(),
969 error: format!("Failed to import decision: {}", e),
970 });
971 }
972 }
973 }
974 Err(e) => {
975 load_errors.push(DecisionLoadError {
976 file_path: file_path.clone(),
977 error: format!("Failed to read file: {}", e),
978 });
979 }
980 }
981 }
982 }
983
984 info!(
985 "Loaded {} decisions ({} errors) from workspace {}",
986 decisions.len(),
987 load_errors.len(),
988 workspace_path
989 );
990
991 Ok(DecisionLoadResult {
992 decisions,
993 errors: load_errors,
994 })
995 }
996
997 pub async fn load_decision_index(
1007 &self,
1008 workspace_path: &str,
1009 ) -> Result<Option<DecisionIndex>, StorageError> {
1010 let index_file = format!("{}/decisions.yaml", workspace_path);
1011
1012 if !self.storage.file_exists(&index_file).await? {
1013 return Ok(None);
1014 }
1015
1016 let content = self.storage.read_file(&index_file).await?;
1017 let yaml_content = String::from_utf8(content)
1018 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
1019
1020 let importer = DecisionImporter;
1021 let index = importer.import_index(&yaml_content).map_err(|e| {
1022 StorageError::SerializationError(format!("Failed to parse decisions.yaml: {}", e))
1023 })?;
1024
1025 Ok(Some(index))
1026 }
1027
1028 pub async fn load_knowledge(
1041 &self,
1042 workspace_path: &str,
1043 ) -> Result<KnowledgeLoadResult, StorageError> {
1044 let mut articles = Vec::new();
1045 let mut load_errors = Vec::new();
1046
1047 let files = self.storage.list_files(workspace_path).await?;
1048 let importer = KnowledgeImporter;
1049
1050 for file_name in files {
1051 if let Some(AssetType::Knowledge) = AssetType::from_filename(&file_name) {
1052 let file_path = format!("{}/{}", workspace_path, file_name);
1053 match self.storage.read_file(&file_path).await {
1054 Ok(content) => {
1055 let yaml_content = match String::from_utf8(content) {
1056 Ok(s) => s,
1057 Err(e) => {
1058 load_errors.push(KnowledgeLoadError {
1059 file_path: file_path.clone(),
1060 error: format!("Invalid UTF-8: {}", e),
1061 });
1062 continue;
1063 }
1064 };
1065
1066 match importer.import(&yaml_content) {
1067 Ok(article) => {
1068 articles.push(article);
1069 }
1070 Err(e) => {
1071 load_errors.push(KnowledgeLoadError {
1072 file_path: file_path.clone(),
1073 error: format!("Failed to import knowledge article: {}", e),
1074 });
1075 }
1076 }
1077 }
1078 Err(e) => {
1079 load_errors.push(KnowledgeLoadError {
1080 file_path: file_path.clone(),
1081 error: format!("Failed to read file: {}", e),
1082 });
1083 }
1084 }
1085 }
1086 }
1087
1088 info!(
1089 "Loaded {} knowledge articles ({} errors) from workspace {}",
1090 articles.len(),
1091 load_errors.len(),
1092 workspace_path
1093 );
1094
1095 Ok(KnowledgeLoadResult {
1096 articles,
1097 errors: load_errors,
1098 })
1099 }
1100
1101 pub async fn load_knowledge_index(
1111 &self,
1112 workspace_path: &str,
1113 ) -> Result<Option<KnowledgeIndex>, StorageError> {
1114 let index_file = format!("{}/knowledge.yaml", workspace_path);
1115
1116 if !self.storage.file_exists(&index_file).await? {
1117 return Ok(None);
1118 }
1119
1120 let content = self.storage.read_file(&index_file).await?;
1121 let yaml_content = String::from_utf8(content)
1122 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
1123
1124 let importer = KnowledgeImporter;
1125 let index = importer.import_index(&yaml_content).map_err(|e| {
1126 StorageError::SerializationError(format!("Failed to parse knowledge.yaml: {}", e))
1127 })?;
1128
1129 Ok(Some(index))
1130 }
1131
1132 pub async fn load_knowledge_by_domain(
1145 &self,
1146 workspace_path: &str,
1147 domain: &str,
1148 ) -> Result<KnowledgeLoadResult, StorageError> {
1149 let result = self.load_knowledge(workspace_path).await?;
1150
1151 let filtered_articles: Vec<_> = result
1152 .articles
1153 .into_iter()
1154 .filter(|article| article.domain.as_deref() == Some(domain))
1155 .collect();
1156
1157 Ok(KnowledgeLoadResult {
1158 articles: filtered_articles,
1159 errors: result.errors,
1160 })
1161 }
1162
1163 pub async fn load_decisions_by_domain(
1176 &self,
1177 workspace_path: &str,
1178 domain: &str,
1179 ) -> Result<DecisionLoadResult, StorageError> {
1180 let result = self.load_decisions(workspace_path).await?;
1181
1182 let filtered_decisions: Vec<_> = result
1183 .decisions
1184 .into_iter()
1185 .filter(|decision| decision.domain.as_deref() == Some(domain))
1186 .collect();
1187
1188 Ok(DecisionLoadResult {
1189 decisions: filtered_decisions,
1190 errors: result.errors,
1191 })
1192 }
1193
1194 pub async fn load_workspace(
1206 &self,
1207 workspace_path: &str,
1208 ) -> Result<Option<Workspace>, StorageError> {
1209 let workspace_file = format!("{}/workspace.yaml", workspace_path);
1210
1211 if !self.storage.file_exists(&workspace_file).await? {
1212 return Ok(None);
1213 }
1214
1215 let content = self.storage.read_file(&workspace_file).await?;
1216 let yaml_content = String::from_utf8(content)
1217 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
1218
1219 let workspace: Workspace = serde_yaml::from_str(&yaml_content).map_err(|e| {
1220 StorageError::SerializationError(format!("Failed to parse workspace.yaml: {}", e))
1221 })?;
1222
1223 Ok(Some(workspace))
1224 }
1225
1226 pub async fn save_workspace(
1233 &self,
1234 workspace_path: &str,
1235 workspace: &Workspace,
1236 ) -> Result<(), StorageError> {
1237 let workspace_file = format!("{}/workspace.yaml", workspace_path);
1238
1239 let yaml_content = serde_yaml::to_string(workspace).map_err(|e| {
1240 StorageError::SerializationError(format!("Failed to serialize workspace: {}", e))
1241 })?;
1242
1243 self.storage
1244 .write_file(&workspace_file, yaml_content.as_bytes())
1245 .await?;
1246
1247 Ok(())
1248 }
1249
1250 pub async fn load_domain_config(
1260 &self,
1261 domain_dir: &str,
1262 ) -> Result<Option<DomainConfig>, StorageError> {
1263 let domain_file = format!("{}/domain.yaml", domain_dir);
1264
1265 if !self.storage.file_exists(&domain_file).await? {
1266 return Ok(None);
1267 }
1268
1269 let content = self.storage.read_file(&domain_file).await?;
1270 let yaml_content = String::from_utf8(content)
1271 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
1272
1273 let config: DomainConfig = serde_yaml::from_str(&yaml_content).map_err(|e| {
1274 StorageError::SerializationError(format!("Failed to parse domain.yaml: {}", e))
1275 })?;
1276
1277 Ok(Some(config))
1278 }
1279
1280 pub async fn save_domain_config(
1287 &self,
1288 domain_dir: &str,
1289 config: &DomainConfig,
1290 ) -> Result<(), StorageError> {
1291 let domain_file = format!("{}/domain.yaml", domain_dir);
1292
1293 let yaml_content = serde_yaml::to_string(config).map_err(|e| {
1294 StorageError::SerializationError(format!("Failed to serialize domain config: {}", e))
1295 })?;
1296
1297 self.storage
1298 .write_file(&domain_file, yaml_content.as_bytes())
1299 .await?;
1300
1301 Ok(())
1302 }
1303
1304 pub async fn load_domain_config_by_name(
1315 &self,
1316 workspace_path: &str,
1317 domain_name: &str,
1318 ) -> Result<Option<DomainConfig>, StorageError> {
1319 let sanitized_domain_name = sanitize_filename(domain_name);
1320 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
1321 self.load_domain_config(&domain_dir).await
1322 }
1323
1324 #[deprecated(
1330 since = "2.0.0",
1331 note = "Domain directories are no longer supported. Domain info is in workspace.yaml"
1332 )]
1333 #[allow(dead_code)]
1334 pub async fn get_domain_id(&self, domain_dir: &str) -> Result<Option<Uuid>, StorageError> {
1335 match self.load_domain_config(domain_dir).await? {
1336 Some(config) => Ok(Some(config.id)),
1337 None => Ok(None),
1338 }
1339 }
1340
1341 #[deprecated(
1345 since = "2.0.0",
1346 note = "Domain directories are no longer supported. Use load_workspace() instead"
1347 )]
1348 #[allow(dead_code)]
1349 pub async fn load_all_domain_configs(
1350 &self,
1351 workspace_path: &str,
1352 ) -> Result<Vec<DomainConfig>, StorageError> {
1353 warn!(
1354 "load_all_domain_configs is deprecated. Use load_workspace() for workspace: {}",
1355 workspace_path
1356 );
1357
1358 Ok(Vec::new())
1360 }
1361}
1362
1363fn sanitize_filename(name: &str) -> String {
1365 name.chars()
1366 .map(|c| match c {
1367 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1368 _ => c,
1369 })
1370 .collect()
1371}
1372
1373#[derive(Debug, Serialize, Deserialize)]
1375pub struct ModelLoadResult {
1376 pub tables: Vec<TableData>,
1377 pub relationships: Vec<RelationshipData>,
1378 pub orphaned_relationships: Vec<RelationshipData>,
1379}
1380
1381#[derive(Debug, Clone, Serialize, Deserialize)]
1383pub struct TableData {
1384 pub id: Uuid,
1385 pub name: String,
1386 pub yaml_file_path: Option<String>,
1387 pub yaml_content: String,
1388}
1389
1390#[derive(Debug, Clone, Serialize, Deserialize)]
1392pub struct RelationshipData {
1393 pub id: Uuid,
1394 pub source_table_id: Uuid,
1395 pub target_table_id: Uuid,
1396}
1397
1398#[derive(Debug)]
1400pub struct DomainLoadResult {
1401 pub domains: Vec<Domain>,
1402 pub tables: HashMap<Uuid, Table>,
1403 pub odps_products: HashMap<Uuid, ODPSDataProduct>,
1404 pub cads_assets: HashMap<Uuid, CADSAsset>,
1405}
1406
1407#[derive(Debug)]
1409pub struct DecisionLoadResult {
1410 pub decisions: Vec<Decision>,
1412 pub errors: Vec<DecisionLoadError>,
1414}
1415
1416#[derive(Debug, Clone)]
1418pub struct DecisionLoadError {
1419 pub file_path: String,
1421 pub error: String,
1423}
1424
1425#[derive(Debug)]
1427pub struct KnowledgeLoadResult {
1428 pub articles: Vec<KnowledgeArticle>,
1430 pub errors: Vec<KnowledgeLoadError>,
1432}
1433
1434#[derive(Debug, Clone)]
1436pub struct KnowledgeLoadError {
1437 pub file_path: String,
1439 pub error: String,
1441}