1use crate::export::{
15 cads::CADSExporter, decision::DecisionExporter, knowledge::KnowledgeExporter,
16 markdown::MarkdownExporter, odcs::ODCSExporter, odps::ODPSExporter,
17};
18#[cfg(feature = "bpmn")]
19use crate::models::bpmn::BPMNModel;
20use crate::models::decision::{Decision, DecisionIndex};
21#[cfg(feature = "dmn")]
22use crate::models::dmn::DMNModel;
23use crate::models::knowledge::{KnowledgeArticle, KnowledgeIndex};
24#[cfg(feature = "openapi")]
25use crate::models::openapi::{OpenAPIFormat, OpenAPIModel};
26use crate::models::{cads::CADSAsset, domain::Domain, odps::ODPSDataProduct, table::Table};
27use crate::storage::{StorageBackend, StorageError};
28use anyhow::Result;
29use serde_yaml;
30use std::collections::HashMap;
31use tracing::info;
32use uuid::Uuid;
33
34pub struct ModelSaver<B: StorageBackend> {
36 storage: B,
37}
38
39impl<B: StorageBackend> ModelSaver<B> {
40 pub fn new(storage: B) -> Self {
42 Self { storage }
43 }
44
45 pub async fn save_table(
50 &self,
51 workspace_path: &str,
52 table: &TableData,
53 ) -> Result<(), StorageError> {
54 let tables_dir = format!("{}/tables", workspace_path);
55
56 if !self.storage.dir_exists(&tables_dir).await? {
58 self.storage.create_dir(&tables_dir).await?;
59 }
60
61 let file_path = if let Some(ref yaml_path) = table.yaml_file_path {
63 format!(
64 "{}/{}",
65 workspace_path,
66 yaml_path.strip_prefix('/').unwrap_or(yaml_path)
67 )
68 } else {
69 let sanitized_name = sanitize_filename(&table.name);
71 format!("{}/tables/{}.yaml", workspace_path, sanitized_name)
72 };
73
74 let yaml_content = serde_yaml::to_string(&table.yaml_value).map_err(|e| {
76 StorageError::SerializationError(format!("Failed to serialize table: {}", e))
77 })?;
78
79 self.storage
81 .write_file(&file_path, yaml_content.as_bytes())
82 .await?;
83
84 info!("Saved table '{}' to {}", table.name, file_path);
85 Ok(())
86 }
87
88 pub async fn save_relationships(
94 &self,
95 workspace_path: &str,
96 relationships: &[RelationshipData],
97 ) -> Result<(), StorageError> {
98 let file_path = format!("{}/relationships.yaml", workspace_path);
99
100 let mut yaml_map = serde_yaml::Mapping::new();
102 let mut rels_array = serde_yaml::Sequence::new();
103 for rel in relationships {
104 rels_array.push(rel.yaml_value.clone());
105 }
106 yaml_map.insert(
107 serde_yaml::Value::String("relationships".to_string()),
108 serde_yaml::Value::Sequence(rels_array),
109 );
110 let yaml_value = serde_yaml::Value::Mapping(yaml_map);
111
112 let yaml_content = serde_yaml::to_string(&yaml_value).map_err(|e| {
113 StorageError::SerializationError(format!("Failed to write YAML: {}", e))
114 })?;
115
116 self.storage
118 .write_file(&file_path, yaml_content.as_bytes())
119 .await?;
120
121 info!(
122 "Saved {} relationships to {}",
123 relationships.len(),
124 file_path
125 );
126 Ok(())
127 }
128
129 pub async fn save_domain(
134 &self,
135 workspace_path: &str,
136 domain: &Domain,
137 tables: &HashMap<Uuid, Table>,
138 odps_products: &HashMap<Uuid, ODPSDataProduct>,
139 cads_assets: &HashMap<Uuid, CADSAsset>,
140 ) -> Result<(), StorageError> {
141 let sanitized_domain_name = sanitize_filename(&domain.name);
142 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
143
144 if !self.storage.dir_exists(&domain_dir).await? {
146 self.storage.create_dir(&domain_dir).await?;
147 }
148
149 let domain_yaml = domain.to_yaml().map_err(|e| {
151 StorageError::SerializationError(format!("Failed to serialize domain: {}", e))
152 })?;
153 let domain_file_path = format!("{}/domain.yaml", domain_dir);
154 self.storage
155 .write_file(&domain_file_path, domain_yaml.as_bytes())
156 .await?;
157 info!("Saved domain '{}' to {}", domain.name, domain_file_path);
158
159 for odcs_node in &domain.odcs_nodes {
161 if let Some(table_id) = odcs_node.table_id
162 && let Some(table) = tables.get(&table_id)
163 {
164 let sanitized_table_name = sanitize_filename(&table.name);
165 let table_file_path = format!("{}/{}.odcs.yaml", domain_dir, sanitized_table_name);
166 let odcs_yaml = ODCSExporter::export_table(table, "odcs_v3_1_0");
167 self.storage
168 .write_file(&table_file_path, odcs_yaml.as_bytes())
169 .await?;
170 info!("Saved ODCS table '{}' to {}", table.name, table_file_path);
171 }
172 }
173
174 for product in odps_products.values() {
177 if let Some(product_domain) = &product.domain
178 && product_domain == &domain.name
179 {
180 let sanitized_product_name =
181 sanitize_filename(product.name.as_ref().unwrap_or(&product.id));
182 let product_file_path =
183 format!("{}/{}.odps.yaml", domain_dir, sanitized_product_name);
184 let odps_yaml = ODPSExporter::export_product(product);
185 self.storage
186 .write_file(&product_file_path, odps_yaml.as_bytes())
187 .await?;
188 info!(
189 "Saved ODPS product '{}' to {}",
190 product.id, product_file_path
191 );
192 }
193 }
194
195 for cads_node in &domain.cads_nodes {
197 if let Some(cads_asset_id) = cads_node.cads_asset_id
198 && let Some(asset) = cads_assets.get(&cads_asset_id)
199 {
200 let sanitized_asset_name = sanitize_filename(&asset.name);
201 let asset_file_path = format!("{}/{}.cads.yaml", domain_dir, sanitized_asset_name);
202 let cads_yaml = CADSExporter::export_asset(asset);
203 self.storage
204 .write_file(&asset_file_path, cads_yaml.as_bytes())
205 .await?;
206 info!("Saved CADS asset '{}' to {}", asset.name, asset_file_path);
207 }
208 }
209
210 Ok(())
211 }
212
213 pub async fn save_odps_product(
217 &self,
218 workspace_path: &str,
219 domain_name: &str,
220 product: &ODPSDataProduct,
221 ) -> Result<(), StorageError> {
222 let sanitized_domain_name = sanitize_filename(domain_name);
223 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
224
225 if !self.storage.dir_exists(&domain_dir).await? {
227 self.storage.create_dir(&domain_dir).await?;
228 }
229
230 let sanitized_product_name =
231 sanitize_filename(product.name.as_ref().unwrap_or(&product.id));
232 let product_file_path = format!("{}/{}.odps.yaml", domain_dir, sanitized_product_name);
233 let odps_yaml = ODPSExporter::export_product(product);
234 self.storage
235 .write_file(&product_file_path, odps_yaml.as_bytes())
236 .await?;
237
238 info!(
239 "Saved ODPS product '{}' to {}",
240 product.id, product_file_path
241 );
242 Ok(())
243 }
244
245 pub async fn save_cads_asset(
249 &self,
250 workspace_path: &str,
251 domain_name: &str,
252 asset: &CADSAsset,
253 ) -> Result<(), StorageError> {
254 let sanitized_domain_name = sanitize_filename(domain_name);
255 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
256
257 if !self.storage.dir_exists(&domain_dir).await? {
259 self.storage.create_dir(&domain_dir).await?;
260 }
261
262 let sanitized_asset_name = sanitize_filename(&asset.name);
263 let asset_file_path = format!("{}/{}.cads.yaml", domain_dir, sanitized_asset_name);
264 let cads_yaml = CADSExporter::export_asset(asset);
265 self.storage
266 .write_file(&asset_file_path, cads_yaml.as_bytes())
267 .await?;
268
269 info!("Saved CADS asset '{}' to {}", asset.name, asset_file_path);
270 Ok(())
271 }
272
273 #[cfg(feature = "bpmn")]
277 pub async fn save_bpmn_model(
278 &self,
279 workspace_path: &str,
280 domain_name: &str,
281 model: &BPMNModel,
282 xml_content: &str,
283 ) -> Result<(), StorageError> {
284 let sanitized_domain_name = sanitize_filename(domain_name);
285 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
286
287 if !self.storage.dir_exists(&domain_dir).await? {
289 self.storage.create_dir(&domain_dir).await?;
290 }
291
292 let sanitized_model_name = sanitize_filename(&model.name);
293 let model_file_path = format!("{}/{}.bpmn.xml", domain_dir, sanitized_model_name);
294 self.storage
295 .write_file(&model_file_path, xml_content.as_bytes())
296 .await?;
297
298 info!("Saved BPMN model '{}' to {}", model.name, model_file_path);
299 Ok(())
300 }
301
302 #[cfg(feature = "dmn")]
306 pub async fn save_dmn_model(
307 &self,
308 workspace_path: &str,
309 domain_name: &str,
310 model: &DMNModel,
311 xml_content: &str,
312 ) -> Result<(), StorageError> {
313 let sanitized_domain_name = sanitize_filename(domain_name);
314 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
315
316 if !self.storage.dir_exists(&domain_dir).await? {
318 self.storage.create_dir(&domain_dir).await?;
319 }
320
321 let sanitized_model_name = sanitize_filename(&model.name);
322 let model_file_path = format!("{}/{}.dmn.xml", domain_dir, sanitized_model_name);
323 self.storage
324 .write_file(&model_file_path, xml_content.as_bytes())
325 .await?;
326
327 info!("Saved DMN model '{}' to {}", model.name, model_file_path);
328 Ok(())
329 }
330
331 #[cfg(feature = "openapi")]
335 pub async fn save_openapi_model(
336 &self,
337 workspace_path: &str,
338 domain_name: &str,
339 model: &OpenAPIModel,
340 content: &str,
341 ) -> Result<(), StorageError> {
342 let sanitized_domain_name = sanitize_filename(domain_name);
343 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
344
345 if !self.storage.dir_exists(&domain_dir).await? {
347 self.storage.create_dir(&domain_dir).await?;
348 }
349
350 let sanitized_api_name = sanitize_filename(&model.name);
351 let extension = match model.format {
352 OpenAPIFormat::Yaml => "yaml",
353 OpenAPIFormat::Json => "json",
354 };
355 let model_file_path = format!(
356 "{}/{}.openapi.{}",
357 domain_dir, sanitized_api_name, extension
358 );
359 self.storage
360 .write_file(&model_file_path, content.as_bytes())
361 .await?;
362
363 info!("Saved OpenAPI spec '{}' to {}", model.name, model_file_path);
364 Ok(())
365 }
366
367 pub async fn save_decision(
381 &self,
382 workspace_path: &str,
383 workspace_name: &str,
384 decision: &Decision,
385 ) -> Result<String, StorageError> {
386 let sanitized_workspace = sanitize_filename(workspace_name);
387 let number_str = format!("{:04}", decision.number);
388
389 let file_name = if let Some(ref domain) = decision.domain {
390 let sanitized_domain = sanitize_filename(domain);
391 format!(
392 "{}_{}_adr-{}.madr.yaml",
393 sanitized_workspace, sanitized_domain, number_str
394 )
395 } else {
396 format!("{}_adr-{}.madr.yaml", sanitized_workspace, number_str)
397 };
398
399 let file_path = format!("{}/{}", workspace_path, file_name);
400
401 let exporter = DecisionExporter;
402 let yaml_content = exporter.export(decision).map_err(|e| {
403 StorageError::SerializationError(format!("Failed to export decision: {}", e))
404 })?;
405
406 self.storage
407 .write_file(&file_path, yaml_content.as_bytes())
408 .await?;
409
410 info!(
411 "Saved decision '{}' ({}) to {}",
412 decision.title, decision.number, file_path
413 );
414
415 Ok(file_path)
416 }
417
418 pub async fn save_decision_index(
425 &self,
426 workspace_path: &str,
427 index: &DecisionIndex,
428 ) -> Result<(), StorageError> {
429 let file_path = format!("{}/decisions.yaml", workspace_path);
430
431 let exporter = DecisionExporter;
432 let yaml_content = exporter.export_index(index).map_err(|e| {
433 StorageError::SerializationError(format!("Failed to export decision index: {}", e))
434 })?;
435
436 self.storage
437 .write_file(&file_path, yaml_content.as_bytes())
438 .await?;
439
440 info!(
441 "Saved decision index with {} entries to {}",
442 index.decisions.len(),
443 file_path
444 );
445
446 Ok(())
447 }
448
449 pub async fn save_knowledge(
461 &self,
462 workspace_path: &str,
463 workspace_name: &str,
464 article: &KnowledgeArticle,
465 ) -> Result<String, StorageError> {
466 let sanitized_workspace = sanitize_filename(workspace_name);
467 let number_str = if article.is_timestamp_number() {
469 format!("{}", article.number)
470 } else {
471 format!("{:04}", article.number)
472 };
473
474 let file_name = if let Some(ref domain) = article.domain {
475 let sanitized_domain = sanitize_filename(domain);
476 format!(
477 "{}_{}_kb-{}.kb.yaml",
478 sanitized_workspace, sanitized_domain, number_str
479 )
480 } else {
481 format!("{}_kb-{}.kb.yaml", sanitized_workspace, number_str)
482 };
483
484 let file_path = format!("{}/{}", workspace_path, file_name);
485
486 let exporter = KnowledgeExporter;
487 let yaml_content = exporter.export(article).map_err(|e| {
488 StorageError::SerializationError(format!("Failed to export knowledge article: {}", e))
489 })?;
490
491 self.storage
492 .write_file(&file_path, yaml_content.as_bytes())
493 .await?;
494
495 info!(
496 "Saved knowledge article '{}' ({}) to {}",
497 article.title, article.number, file_path
498 );
499
500 Ok(file_path)
501 }
502
503 pub async fn save_knowledge_index(
510 &self,
511 workspace_path: &str,
512 index: &KnowledgeIndex,
513 ) -> Result<(), StorageError> {
514 let file_path = format!("{}/knowledge.yaml", workspace_path);
515
516 let exporter = KnowledgeExporter;
517 let yaml_content = exporter.export_index(index).map_err(|e| {
518 StorageError::SerializationError(format!("Failed to export knowledge index: {}", e))
519 })?;
520
521 self.storage
522 .write_file(&file_path, yaml_content.as_bytes())
523 .await?;
524
525 info!(
526 "Saved knowledge index with {} entries to {}",
527 index.articles.len(),
528 file_path
529 );
530
531 Ok(())
532 }
533
534 pub async fn export_decision_markdown(
544 &self,
545 workspace_path: &str,
546 decision: &Decision,
547 ) -> Result<String, StorageError> {
548 let decisions_dir = format!("{}/decisions", workspace_path);
549
550 if !self.storage.dir_exists(&decisions_dir).await? {
552 self.storage.create_dir(&decisions_dir).await?;
553 }
554
555 let file_name = decision.markdown_filename();
557 let file_path = format!("{}/{}", decisions_dir, file_name);
558
559 let exporter = MarkdownExporter;
560 let markdown_content = exporter.export_decision(decision).map_err(|e| {
561 StorageError::SerializationError(format!(
562 "Failed to export decision to Markdown: {}",
563 e
564 ))
565 })?;
566
567 self.storage
568 .write_file(&file_path, markdown_content.as_bytes())
569 .await?;
570
571 info!(
572 "Exported decision '{}' to Markdown: {}",
573 decision.number, file_path
574 );
575
576 Ok(file_path)
577 }
578
579 pub async fn export_knowledge_markdown(
589 &self,
590 workspace_path: &str,
591 article: &KnowledgeArticle,
592 ) -> Result<String, StorageError> {
593 let knowledge_dir = format!("{}/knowledge", workspace_path);
594
595 if !self.storage.dir_exists(&knowledge_dir).await? {
597 self.storage.create_dir(&knowledge_dir).await?;
598 }
599
600 let file_name = article.markdown_filename();
602 let file_path = format!("{}/{}", knowledge_dir, file_name);
603
604 let exporter = MarkdownExporter;
605 let markdown_content = exporter.export_knowledge(article).map_err(|e| {
606 StorageError::SerializationError(format!(
607 "Failed to export knowledge article to Markdown: {}",
608 e
609 ))
610 })?;
611
612 self.storage
613 .write_file(&file_path, markdown_content.as_bytes())
614 .await?;
615
616 info!(
617 "Exported knowledge article '{}' to Markdown: {}",
618 article.number, file_path
619 );
620
621 Ok(file_path)
622 }
623
624 pub async fn export_all_decisions_markdown(
637 &self,
638 workspace_path: &str,
639 decisions: &[Decision],
640 ) -> Result<usize, StorageError> {
641 let mut count = 0;
642 for decision in decisions {
643 self.export_decision_markdown(workspace_path, decision)
644 .await?;
645 count += 1;
646 }
647 Ok(count)
648 }
649
650 pub async fn export_all_knowledge_markdown(
663 &self,
664 workspace_path: &str,
665 articles: &[KnowledgeArticle],
666 ) -> Result<usize, StorageError> {
667 let mut count = 0;
668 for article in articles {
669 self.export_knowledge_markdown(workspace_path, article)
670 .await?;
671 count += 1;
672 }
673 Ok(count)
674 }
675}
676
677#[derive(Debug, Clone)]
679pub struct TableData {
680 pub id: Uuid,
681 pub name: String,
682 pub yaml_file_path: Option<String>,
683 pub yaml_value: serde_yaml::Value,
684}
685
686#[derive(Debug, Clone)]
688pub struct RelationshipData {
689 pub id: Uuid,
690 pub source_table_id: Uuid,
691 pub target_table_id: Uuid,
692 pub yaml_value: serde_yaml::Value,
693}
694
695fn sanitize_filename(name: &str) -> String {
697 name.chars()
698 .map(|c| match c {
699 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
700 _ => c,
701 })
702 .collect()
703}