1#[cfg(feature = "bpmn")]
18use crate::import::bpmn::BPMNImporter;
19#[cfg(feature = "dmn")]
20use crate::import::dmn::DMNImporter;
21#[cfg(feature = "openapi")]
22use crate::import::openapi::OpenAPIImporter;
23use crate::import::{cads::CADSImporter, odcs::ODCSImporter, odps::ODPSImporter};
24#[cfg(feature = "bpmn")]
25use crate::models::bpmn::BPMNModel;
26#[cfg(feature = "dmn")]
27use crate::models::dmn::DMNModel;
28use crate::models::domain_config::DomainConfig;
29#[cfg(feature = "openapi")]
30use crate::models::openapi::{OpenAPIFormat, OpenAPIModel};
31use crate::models::workspace::Workspace;
32use crate::models::{cads::CADSAsset, domain::Domain, odps::ODPSDataProduct, table::Table};
33use crate::storage::{StorageBackend, StorageError};
34use anyhow::Result;
35use serde::{Deserialize, Serialize};
36use serde_yaml;
37use std::collections::HashMap;
38use tracing::{info, warn};
39use uuid::Uuid;
40
41pub struct ModelLoader<B: StorageBackend> {
43 storage: B,
44}
45
46impl<B: StorageBackend> ModelLoader<B> {
47 pub fn new(storage: B) -> Self {
49 Self { storage }
50 }
51
52 pub async fn load_model(&self, workspace_path: &str) -> Result<ModelLoadResult, StorageError> {
63 self.load_model_from_files(workspace_path).await
65 }
66
67 async fn load_model_from_files(
69 &self,
70 workspace_path: &str,
71 ) -> Result<ModelLoadResult, StorageError> {
72 let tables_dir = format!("{}/tables", workspace_path);
73
74 if !self.storage.dir_exists(&tables_dir).await? {
76 self.storage.create_dir(&tables_dir).await?;
77 }
78
79 let mut tables = Vec::new();
81 let mut table_ids: HashMap<Uuid, String> = HashMap::new();
82
83 let files = self.storage.list_files(&tables_dir).await?;
84 for file_name in files {
85 if file_name.ends_with(".yaml") || file_name.ends_with(".yml") {
86 let file_path = format!("{}/{}", tables_dir, file_name);
87 match self.load_table_from_yaml(&file_path, workspace_path).await {
88 Ok(table_data) => {
89 table_ids.insert(table_data.id, table_data.name.clone());
90 tables.push(table_data);
91 }
92 Err(e) => {
93 warn!("Failed to load table from {}: {}", file_path, e);
94 }
95 }
96 }
97 }
98
99 info!(
100 "Loaded {} tables from workspace {}",
101 tables.len(),
102 workspace_path
103 );
104
105 let relationships_file = format!("{}/relationships.yaml", workspace_path);
107 let mut relationships = Vec::new();
108 let mut orphaned_relationships = Vec::new();
109
110 if self.storage.file_exists(&relationships_file).await? {
111 match self.load_relationships_from_yaml(&relationships_file).await {
112 Ok(loaded_rels) => {
113 for rel in loaded_rels {
115 let source_exists = table_ids.contains_key(&rel.source_table_id);
116 let target_exists = table_ids.contains_key(&rel.target_table_id);
117
118 if source_exists && target_exists {
119 relationships.push(rel.clone());
120 } else {
121 orphaned_relationships.push(rel.clone());
122 warn!(
123 "Orphaned relationship {}: source={} (exists: {}), target={} (exists: {})",
124 rel.id,
125 rel.source_table_id,
126 source_exists,
127 rel.target_table_id,
128 target_exists
129 );
130 }
131 }
132 }
133 Err(e) => {
134 warn!(
135 "Failed to load relationships from {}: {}",
136 relationships_file, e
137 );
138 }
139 }
140 }
141
142 info!(
143 "Loaded {} relationships ({} orphaned) from workspace {}",
144 relationships.len(),
145 orphaned_relationships.len(),
146 workspace_path
147 );
148
149 Ok(ModelLoadResult {
150 tables,
151 relationships,
152 orphaned_relationships,
153 })
154 }
155
156 async fn load_table_from_yaml(
161 &self,
162 yaml_path: &str,
163 workspace_path: &str,
164 ) -> Result<TableData, StorageError> {
165 let content = self.storage.read_file(yaml_path).await?;
166 let yaml_content = String::from_utf8(content)
167 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
168
169 let mut importer = crate::import::odcs::ODCSImporter::new();
171 let (table, parse_errors) = importer.parse_table(&yaml_content).map_err(|e| {
172 StorageError::SerializationError(format!("Failed to parse ODCS YAML: {}", e))
173 })?;
174
175 if !parse_errors.is_empty() {
177 warn!(
178 "Table '{}' parsed with {} warnings/errors",
179 table.name,
180 parse_errors.len()
181 );
182 }
183
184 let relative_path = yaml_path
186 .strip_prefix(workspace_path)
187 .map(|s| s.strip_prefix('/').unwrap_or(s).to_string())
188 .unwrap_or_else(|| yaml_path.to_string());
189
190 Ok(TableData {
191 id: table.id,
192 name: table.name,
193 yaml_file_path: Some(relative_path),
194 yaml_content,
195 })
196 }
197
198 async fn load_relationships_from_yaml(
200 &self,
201 yaml_path: &str,
202 ) -> Result<Vec<RelationshipData>, StorageError> {
203 let content = self.storage.read_file(yaml_path).await?;
204 let yaml_content = String::from_utf8(content)
205 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
206
207 let data: serde_yaml::Value = serde_yaml::from_str(&yaml_content).map_err(|e| {
208 StorageError::SerializationError(format!("Failed to parse YAML: {}", e))
209 })?;
210
211 let mut relationships = Vec::new();
212
213 let rels_array = data
215 .get("relationships")
216 .and_then(|v| v.as_sequence())
217 .or_else(|| data.as_sequence());
218
219 if let Some(rels_array) = rels_array {
220 for rel_data in rels_array {
221 match self.parse_relationship(rel_data) {
222 Ok(rel) => relationships.push(rel),
223 Err(e) => {
224 warn!("Failed to parse relationship: {}", e);
225 }
226 }
227 }
228 }
229
230 Ok(relationships)
231 }
232
233 fn parse_relationship(
235 &self,
236 data: &serde_yaml::Value,
237 ) -> Result<RelationshipData, StorageError> {
238 let source_table_id = data
239 .get("source_table_id")
240 .and_then(|v| v.as_str())
241 .and_then(|s| Uuid::parse_str(s).ok())
242 .ok_or_else(|| {
243 StorageError::SerializationError("Missing source_table_id".to_string())
244 })?;
245
246 let target_table_id = data
247 .get("target_table_id")
248 .and_then(|v| v.as_str())
249 .and_then(|s| Uuid::parse_str(s).ok())
250 .ok_or_else(|| {
251 StorageError::SerializationError("Missing target_table_id".to_string())
252 })?;
253
254 let id = data
256 .get("id")
257 .and_then(|v| v.as_str())
258 .and_then(|s| Uuid::parse_str(s).ok())
259 .unwrap_or_else(|| {
260 crate::models::relationship::Relationship::generate_id(
261 source_table_id,
262 target_table_id,
263 )
264 });
265
266 Ok(RelationshipData {
267 id,
268 source_table_id,
269 target_table_id,
270 })
271 }
272
273 pub async fn load_domains(
280 &self,
281 workspace_path: &str,
282 ) -> Result<DomainLoadResult, StorageError> {
283 let mut domains = Vec::new();
284 let mut tables = HashMap::new();
285 let mut odps_products = HashMap::new();
286 let mut cads_assets = HashMap::new();
287
288 let entries = self.storage.list_files(workspace_path).await?;
296
297 let mut potential_domains = Vec::new();
299 for entry in entries {
300 if !entry.ends_with(".yaml") && !entry.ends_with(".yml") && !entry.contains('.') {
302 potential_domains.push(entry);
303 }
304 }
305
306 for entry in potential_domains {
312 let domain_dir = format!("{}/{}", workspace_path, entry);
313 let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
314
315 if self.storage.file_exists(&domain_yaml_path).await? {
316 match self.load_domain(&domain_dir).await {
318 Ok(domain) => {
319 let domain_name = domain.name.clone();
320 domains.push(domain);
321
322 let domain_tables = self.load_domain_odcs_tables(&domain_dir).await?;
324 let table_count = domain_tables.len();
325 for table in domain_tables {
326 tables.insert(table.id, table);
327 }
328
329 let domain_odps = self.load_domain_odps_products(&domain_dir).await?;
331 let odps_count = domain_odps.len();
332 for product in domain_odps {
333 odps_products.insert(
334 Uuid::parse_str(&product.id).unwrap_or_else(|_| Uuid::new_v4()),
335 product,
336 );
337 }
338
339 let domain_cads = self.load_domain_cads_assets(&domain_dir).await?;
341 let cads_count = domain_cads.len();
342 for asset in domain_cads {
343 cads_assets.insert(
344 Uuid::parse_str(&asset.id).unwrap_or_else(|_| Uuid::new_v4()),
345 asset,
346 );
347 }
348
349 info!(
350 "Loaded domain '{}' with {} tables, {} ODPS products, {} CADS assets",
351 domain_name, table_count, odps_count, cads_count
352 );
353 }
354 Err(e) => {
355 warn!("Failed to load domain from {}: {}", domain_dir, e);
356 }
357 }
358 }
359 }
360
361 info!(
362 "Loaded {} domains from workspace {}",
363 domains.len(),
364 workspace_path
365 );
366
367 Ok(DomainLoadResult {
368 domains,
369 tables,
370 odps_products,
371 cads_assets,
372 })
373 }
374
375 pub async fn load_domains_from_list(
380 &self,
381 workspace_path: &str,
382 domain_directory_names: &[String],
383 ) -> Result<DomainLoadResult, StorageError> {
384 let mut domains = Vec::new();
385 let mut tables = HashMap::new();
386 let mut odps_products = HashMap::new();
387 let mut cads_assets = HashMap::new();
388
389 for domain_dir_name in domain_directory_names {
390 let domain_dir = format!("{}/{}", workspace_path, domain_dir_name);
391 let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
392
393 if self.storage.file_exists(&domain_yaml_path).await? {
394 match self.load_domain(&domain_dir).await {
395 Ok(domain) => {
396 let domain_name = domain.name.clone();
397 domains.push(domain);
398
399 let domain_tables = self.load_domain_odcs_tables(&domain_dir).await?;
401 let table_count = domain_tables.len();
402 for table in domain_tables {
403 tables.insert(table.id, table);
404 }
405
406 let domain_odps = self.load_domain_odps_products(&domain_dir).await?;
408 let odps_count = domain_odps.len();
409 for product in domain_odps {
410 odps_products.insert(
411 Uuid::parse_str(&product.id).unwrap_or_else(|_| Uuid::new_v4()),
412 product,
413 );
414 }
415
416 let domain_cads = self.load_domain_cads_assets(&domain_dir).await?;
418 let cads_count = domain_cads.len();
419 for asset in domain_cads {
420 cads_assets.insert(
421 Uuid::parse_str(&asset.id).unwrap_or_else(|_| Uuid::new_v4()),
422 asset,
423 );
424 }
425
426 info!(
427 "Loaded domain '{}' with {} tables, {} ODPS products, {} CADS assets",
428 domain_name, table_count, odps_count, cads_count
429 );
430 }
431 Err(e) => {
432 warn!("Failed to load domain from {}: {}", domain_dir, e);
433 }
434 }
435 } else {
436 warn!(
437 "Domain directory '{}' does not contain domain.yaml",
438 domain_dir
439 );
440 }
441 }
442
443 info!(
444 "Loaded {} domains from workspace {}",
445 domains.len(),
446 workspace_path
447 );
448
449 Ok(DomainLoadResult {
450 domains,
451 tables,
452 odps_products,
453 cads_assets,
454 })
455 }
456
457 async fn load_domain(&self, domain_dir: &str) -> Result<Domain, StorageError> {
459 let domain_yaml_path = format!("{}/domain.yaml", domain_dir);
460 let content = self.storage.read_file(&domain_yaml_path).await?;
461 let yaml_content = String::from_utf8(content)
462 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
463
464 Domain::from_yaml(&yaml_content).map_err(|e| {
465 StorageError::SerializationError(format!("Failed to parse domain YAML: {}", e))
466 })
467 }
468
469 async fn load_domain_odcs_tables(&self, domain_dir: &str) -> Result<Vec<Table>, StorageError> {
471 let mut tables = Vec::new();
472 let files = self.storage.list_files(domain_dir).await?;
473
474 for file_name in files {
475 if file_name.ends_with(".odcs.yaml") || file_name.ends_with(".odcs.yml") {
476 let file_path = format!("{}/{}", domain_dir, file_name);
477 match self.load_table_from_yaml(&file_path, domain_dir).await {
478 Ok(table_data) => {
479 let mut importer = ODCSImporter::new();
481 match importer.parse_table(&table_data.yaml_content) {
482 Ok((table, _parse_errors)) => {
483 tables.push(table);
484 }
485 Err(e) => {
486 warn!("Failed to parse ODCS table from {}: {}", file_path, e);
487 }
488 }
489 }
490 Err(e) => {
491 warn!("Failed to load ODCS table from {}: {}", file_path, e);
492 }
493 }
494 }
495 }
496
497 Ok(tables)
498 }
499
500 async fn load_domain_odps_products(
502 &self,
503 domain_dir: &str,
504 ) -> Result<Vec<ODPSDataProduct>, StorageError> {
505 let mut products = Vec::new();
506 let files = self.storage.list_files(domain_dir).await?;
507
508 for file_name in files {
509 if file_name.ends_with(".odps.yaml") || file_name.ends_with(".odps.yml") {
510 let file_path = format!("{}/{}", domain_dir, file_name);
511 let content = self.storage.read_file(&file_path).await?;
512 let yaml_content = String::from_utf8(content).map_err(|e| {
513 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
514 })?;
515
516 let importer = ODPSImporter::new();
517 match importer.import(&yaml_content) {
518 Ok(product) => {
519 products.push(product);
520 }
521 Err(e) => {
522 warn!("Failed to parse ODPS product from {}: {}", file_path, e);
523 }
524 }
525 }
526 }
527
528 Ok(products)
529 }
530
531 async fn load_domain_cads_assets(
533 &self,
534 domain_dir: &str,
535 ) -> Result<Vec<CADSAsset>, StorageError> {
536 let mut assets = Vec::new();
537 let files = self.storage.list_files(domain_dir).await?;
538
539 for file_name in files {
540 if file_name.ends_with(".cads.yaml") || file_name.ends_with(".cads.yml") {
541 let file_path = format!("{}/{}", domain_dir, file_name);
542 let content = self.storage.read_file(&file_path).await?;
543 let yaml_content = String::from_utf8(content).map_err(|e| {
544 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
545 })?;
546
547 let importer = CADSImporter::new();
548 match importer.import(&yaml_content) {
549 Ok(asset) => {
550 assets.push(asset);
551 }
552 Err(e) => {
553 warn!("Failed to parse CADS asset from {}: {}", file_path, e);
554 }
555 }
556 }
557 }
558
559 Ok(assets)
560 }
561
562 #[cfg(feature = "bpmn")]
564 pub async fn load_bpmn_models(
565 &self,
566 workspace_path: &str,
567 domain_name: &str,
568 ) -> Result<Vec<BPMNModel>, StorageError> {
569 let sanitized_domain_name = sanitize_filename(domain_name);
570 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
571
572 if !self.storage.dir_exists(&domain_dir).await? {
573 return Ok(Vec::new());
574 }
575
576 let mut models = Vec::new();
577 let files = self.storage.list_files(&domain_dir).await?;
578
579 for file_name in files {
580 if file_name.ends_with(".bpmn.xml") {
581 let file_path = format!("{}/{}", domain_dir, file_name);
582 match self.load_bpmn_model(&domain_dir, &file_name).await {
583 Ok(model) => models.push(model),
584 Err(e) => {
585 warn!("Failed to load BPMN model from {}: {}", file_path, e);
586 }
587 }
588 }
589 }
590
591 Ok(models)
592 }
593
594 #[cfg(feature = "bpmn")]
596 pub async fn load_bpmn_model(
597 &self,
598 domain_dir: &str,
599 file_name: &str,
600 ) -> Result<BPMNModel, StorageError> {
601 let file_path = format!("{}/{}", domain_dir, file_name);
602 let content = self.storage.read_file(&file_path).await?;
603 let xml_content = String::from_utf8(content)
604 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
605
606 let model_name = file_name
608 .strip_suffix(".bpmn.xml")
609 .unwrap_or(file_name)
610 .to_string();
611
612 let domain_id = self
614 .get_domain_id(domain_dir)
615 .await?
616 .unwrap_or_else(Uuid::new_v4);
617
618 let mut importer = BPMNImporter::new();
620 let model = importer
621 .import(&xml_content, domain_id, Some(&model_name))
622 .map_err(|e| {
623 StorageError::SerializationError(format!("Failed to import BPMN model: {}", e))
624 })?;
625
626 Ok(model)
627 }
628
629 #[cfg(feature = "bpmn")]
631 pub async fn load_bpmn_xml(
632 &self,
633 workspace_path: &str,
634 domain_name: &str,
635 model_name: &str,
636 ) -> Result<String, StorageError> {
637 let sanitized_domain_name = sanitize_filename(domain_name);
638 let sanitized_model_name = sanitize_filename(model_name);
639 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
640 let file_path = format!("{}/{}.bpmn.xml", domain_dir, sanitized_model_name);
641
642 let content = self.storage.read_file(&file_path).await?;
643 String::from_utf8(content)
644 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))
645 }
646
647 #[cfg(feature = "dmn")]
649 pub async fn load_dmn_models(
650 &self,
651 workspace_path: &str,
652 domain_name: &str,
653 ) -> Result<Vec<DMNModel>, StorageError> {
654 let sanitized_domain_name = sanitize_filename(domain_name);
655 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
656
657 if !self.storage.dir_exists(&domain_dir).await? {
658 return Ok(Vec::new());
659 }
660
661 let mut models = Vec::new();
662 let files = self.storage.list_files(&domain_dir).await?;
663
664 for file_name in files {
665 if file_name.ends_with(".dmn.xml") {
666 let file_path = format!("{}/{}", domain_dir, file_name);
667 match self.load_dmn_model(&domain_dir, &file_name).await {
668 Ok(model) => models.push(model),
669 Err(e) => {
670 warn!("Failed to load DMN model from {}: {}", file_path, e);
671 }
672 }
673 }
674 }
675
676 Ok(models)
677 }
678
679 #[cfg(feature = "dmn")]
681 pub async fn load_dmn_model(
682 &self,
683 domain_dir: &str,
684 file_name: &str,
685 ) -> Result<DMNModel, StorageError> {
686 let file_path = format!("{}/{}", domain_dir, file_name);
687 let content = self.storage.read_file(&file_path).await?;
688 let xml_content = String::from_utf8(content)
689 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
690
691 let model_name = file_name
693 .strip_suffix(".dmn.xml")
694 .unwrap_or(file_name)
695 .to_string();
696
697 let domain_id = self
699 .get_domain_id(domain_dir)
700 .await?
701 .unwrap_or_else(Uuid::new_v4);
702
703 let mut importer = DMNImporter::new();
705 let model = importer
706 .import(&xml_content, domain_id, Some(&model_name))
707 .map_err(|e| {
708 StorageError::SerializationError(format!("Failed to import DMN model: {}", e))
709 })?;
710
711 Ok(model)
712 }
713
714 #[cfg(feature = "dmn")]
716 pub async fn load_dmn_xml(
717 &self,
718 workspace_path: &str,
719 domain_name: &str,
720 model_name: &str,
721 ) -> Result<String, StorageError> {
722 let sanitized_domain_name = sanitize_filename(domain_name);
723 let sanitized_model_name = sanitize_filename(model_name);
724 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
725 let file_path = format!("{}/{}.dmn.xml", domain_dir, sanitized_model_name);
726
727 let content = self.storage.read_file(&file_path).await?;
728 String::from_utf8(content)
729 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))
730 }
731
732 #[cfg(feature = "openapi")]
734 pub async fn load_openapi_models(
735 &self,
736 workspace_path: &str,
737 domain_name: &str,
738 ) -> Result<Vec<OpenAPIModel>, StorageError> {
739 let sanitized_domain_name = sanitize_filename(domain_name);
740 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
741
742 if !self.storage.dir_exists(&domain_dir).await? {
743 return Ok(Vec::new());
744 }
745
746 let mut models = Vec::new();
747 let files = self.storage.list_files(&domain_dir).await?;
748
749 for file_name in files {
750 if file_name.ends_with(".openapi.yaml")
751 || file_name.ends_with(".openapi.yml")
752 || file_name.ends_with(".openapi.json")
753 {
754 let file_path = format!("{}/{}", domain_dir, file_name);
755 match self.load_openapi_model(&domain_dir, &file_name).await {
756 Ok(model) => models.push(model),
757 Err(e) => {
758 warn!("Failed to load OpenAPI spec from {}: {}", file_path, e);
759 }
760 }
761 }
762 }
763
764 Ok(models)
765 }
766
767 #[cfg(feature = "openapi")]
769 pub async fn load_openapi_model(
770 &self,
771 domain_dir: &str,
772 file_name: &str,
773 ) -> Result<OpenAPIModel, StorageError> {
774 let file_path = format!("{}/{}", domain_dir, file_name);
775 let content = self.storage.read_file(&file_path).await?;
776 let spec_content = String::from_utf8(content)
777 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
778
779 let api_name = file_name
783 .strip_suffix(".openapi.yaml")
784 .or_else(|| file_name.strip_suffix(".openapi.yml"))
785 .or_else(|| file_name.strip_suffix(".openapi.json"))
786 .unwrap_or(file_name)
787 .to_string();
788
789 let domain_id = self
791 .get_domain_id(domain_dir)
792 .await?
793 .unwrap_or_else(Uuid::new_v4);
794
795 let mut importer = OpenAPIImporter::new();
797 let model = importer
798 .import(&spec_content, domain_id, Some(&api_name))
799 .map_err(|e| {
800 StorageError::SerializationError(format!("Failed to import OpenAPI spec: {}", e))
801 })?;
802
803 Ok(model)
804 }
805
806 #[cfg(feature = "openapi")]
808 pub async fn load_openapi_content(
809 &self,
810 workspace_path: &str,
811 domain_name: &str,
812 api_name: &str,
813 format: Option<OpenAPIFormat>,
814 ) -> Result<String, StorageError> {
815 let sanitized_domain_name = sanitize_filename(domain_name);
816 let sanitized_api_name = sanitize_filename(api_name);
817 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
818
819 let extensions: Vec<&str> = if let Some(fmt) = format {
821 match fmt {
822 OpenAPIFormat::Yaml => vec!["yaml", "yml"],
823 OpenAPIFormat::Json => vec!["json"],
824 }
825 } else {
826 vec!["yaml", "yml", "json"]
827 };
828
829 for ext in extensions {
830 let file_path = format!("{}/{}.openapi.{}", domain_dir, sanitized_api_name, ext);
831 if self.storage.file_exists(&file_path).await? {
832 let content = self.storage.read_file(&file_path).await?;
833 return String::from_utf8(content).map_err(|e| {
834 StorageError::SerializationError(format!("Invalid UTF-8: {}", e))
835 });
836 }
837 }
838
839 Err(StorageError::IoError(format!(
840 "OpenAPI spec '{}.openapi.{{yaml,json}}' not found in domain '{}'",
841 api_name, domain_name
842 )))
843 }
844
845 pub async fn load_workspace(
857 &self,
858 workspace_path: &str,
859 ) -> Result<Option<Workspace>, StorageError> {
860 let workspace_file = format!("{}/workspace.yaml", workspace_path);
861
862 if !self.storage.file_exists(&workspace_file).await? {
863 return Ok(None);
864 }
865
866 let content = self.storage.read_file(&workspace_file).await?;
867 let yaml_content = String::from_utf8(content)
868 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
869
870 let workspace: Workspace = serde_yaml::from_str(&yaml_content).map_err(|e| {
871 StorageError::SerializationError(format!("Failed to parse workspace.yaml: {}", e))
872 })?;
873
874 Ok(Some(workspace))
875 }
876
877 pub async fn save_workspace(
884 &self,
885 workspace_path: &str,
886 workspace: &Workspace,
887 ) -> Result<(), StorageError> {
888 let workspace_file = format!("{}/workspace.yaml", workspace_path);
889
890 let yaml_content = serde_yaml::to_string(workspace).map_err(|e| {
891 StorageError::SerializationError(format!("Failed to serialize workspace: {}", e))
892 })?;
893
894 self.storage
895 .write_file(&workspace_file, yaml_content.as_bytes())
896 .await?;
897
898 Ok(())
899 }
900
901 pub async fn load_domain_config(
911 &self,
912 domain_dir: &str,
913 ) -> Result<Option<DomainConfig>, StorageError> {
914 let domain_file = format!("{}/domain.yaml", domain_dir);
915
916 if !self.storage.file_exists(&domain_file).await? {
917 return Ok(None);
918 }
919
920 let content = self.storage.read_file(&domain_file).await?;
921 let yaml_content = String::from_utf8(content)
922 .map_err(|e| StorageError::SerializationError(format!("Invalid UTF-8: {}", e)))?;
923
924 let config: DomainConfig = serde_yaml::from_str(&yaml_content).map_err(|e| {
925 StorageError::SerializationError(format!("Failed to parse domain.yaml: {}", e))
926 })?;
927
928 Ok(Some(config))
929 }
930
931 pub async fn save_domain_config(
938 &self,
939 domain_dir: &str,
940 config: &DomainConfig,
941 ) -> Result<(), StorageError> {
942 let domain_file = format!("{}/domain.yaml", domain_dir);
943
944 let yaml_content = serde_yaml::to_string(config).map_err(|e| {
945 StorageError::SerializationError(format!("Failed to serialize domain config: {}", e))
946 })?;
947
948 self.storage
949 .write_file(&domain_file, yaml_content.as_bytes())
950 .await?;
951
952 Ok(())
953 }
954
955 pub async fn load_domain_config_by_name(
966 &self,
967 workspace_path: &str,
968 domain_name: &str,
969 ) -> Result<Option<DomainConfig>, StorageError> {
970 let sanitized_domain_name = sanitize_filename(domain_name);
971 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
972 self.load_domain_config(&domain_dir).await
973 }
974
975 pub async fn get_domain_id(&self, domain_dir: &str) -> Result<Option<Uuid>, StorageError> {
987 match self.load_domain_config(domain_dir).await? {
988 Some(config) => Ok(Some(config.id)),
989 None => Ok(None),
990 }
991 }
992
993 pub async fn load_all_domain_configs(
1003 &self,
1004 workspace_path: &str,
1005 ) -> Result<Vec<DomainConfig>, StorageError> {
1006 let mut configs = Vec::new();
1007
1008 let entries = self.storage.list_files(workspace_path).await?;
1010
1011 for entry in entries {
1012 let entry_path = format!("{}/{}", workspace_path, entry);
1014 if self.storage.dir_exists(&entry_path).await? {
1015 if let Ok(Some(config)) = self.load_domain_config(&entry_path).await {
1017 configs.push(config);
1018 }
1019 }
1020 }
1021
1022 Ok(configs)
1023 }
1024}
1025
1026fn sanitize_filename(name: &str) -> String {
1028 name.chars()
1029 .map(|c| match c {
1030 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
1031 _ => c,
1032 })
1033 .collect()
1034}
1035
1036#[derive(Debug, Serialize, Deserialize)]
1038pub struct ModelLoadResult {
1039 pub tables: Vec<TableData>,
1040 pub relationships: Vec<RelationshipData>,
1041 pub orphaned_relationships: Vec<RelationshipData>,
1042}
1043
1044#[derive(Debug, Clone, Serialize, Deserialize)]
1046pub struct TableData {
1047 pub id: Uuid,
1048 pub name: String,
1049 pub yaml_file_path: Option<String>,
1050 pub yaml_content: String,
1051}
1052
1053#[derive(Debug, Clone, Serialize, Deserialize)]
1055pub struct RelationshipData {
1056 pub id: Uuid,
1057 pub source_table_id: Uuid,
1058 pub target_table_id: Uuid,
1059}
1060
1061#[derive(Debug)]
1063pub struct DomainLoadResult {
1064 pub domains: Vec<Domain>,
1065 pub tables: HashMap<Uuid, Table>,
1066 pub odps_products: HashMap<Uuid, ODPSDataProduct>,
1067 pub cads_assets: HashMap<Uuid, CADSAsset>,
1068}