data_modelling_sdk/model/
saver.rs1use crate::export::{cads::CADSExporter, odcs::ODCSExporter, odps::ODPSExporter};
15#[cfg(feature = "bpmn")]
16use crate::models::bpmn::BPMNModel;
17#[cfg(feature = "dmn")]
18use crate::models::dmn::DMNModel;
19#[cfg(feature = "openapi")]
20use crate::models::openapi::{OpenAPIFormat, OpenAPIModel};
21use crate::models::{cads::CADSAsset, domain::Domain, odps::ODPSDataProduct, table::Table};
22use crate::storage::{StorageBackend, StorageError};
23use anyhow::Result;
24use serde_yaml;
25use std::collections::HashMap;
26use tracing::info;
27use uuid::Uuid;
28
29pub struct ModelSaver<B: StorageBackend> {
31 storage: B,
32}
33
34impl<B: StorageBackend> ModelSaver<B> {
35 pub fn new(storage: B) -> Self {
37 Self { storage }
38 }
39
40 pub async fn save_table(
45 &self,
46 workspace_path: &str,
47 table: &TableData,
48 ) -> Result<(), StorageError> {
49 let tables_dir = format!("{}/tables", workspace_path);
50
51 if !self.storage.dir_exists(&tables_dir).await? {
53 self.storage.create_dir(&tables_dir).await?;
54 }
55
56 let file_path = if let Some(ref yaml_path) = table.yaml_file_path {
58 format!(
59 "{}/{}",
60 workspace_path,
61 yaml_path.strip_prefix('/').unwrap_or(yaml_path)
62 )
63 } else {
64 let sanitized_name = sanitize_filename(&table.name);
66 format!("{}/tables/{}.yaml", workspace_path, sanitized_name)
67 };
68
69 let yaml_content = serde_yaml::to_string(&table.yaml_value).map_err(|e| {
71 StorageError::SerializationError(format!("Failed to serialize table: {}", e))
72 })?;
73
74 self.storage
76 .write_file(&file_path, yaml_content.as_bytes())
77 .await?;
78
79 info!("Saved table '{}' to {}", table.name, file_path);
80 Ok(())
81 }
82
83 pub async fn save_relationships(
89 &self,
90 workspace_path: &str,
91 relationships: &[RelationshipData],
92 ) -> Result<(), StorageError> {
93 let file_path = format!("{}/relationships.yaml", workspace_path);
94
95 let mut yaml_map = serde_yaml::Mapping::new();
97 let mut rels_array = serde_yaml::Sequence::new();
98 for rel in relationships {
99 rels_array.push(rel.yaml_value.clone());
100 }
101 yaml_map.insert(
102 serde_yaml::Value::String("relationships".to_string()),
103 serde_yaml::Value::Sequence(rels_array),
104 );
105 let yaml_value = serde_yaml::Value::Mapping(yaml_map);
106
107 let yaml_content = serde_yaml::to_string(&yaml_value).map_err(|e| {
108 StorageError::SerializationError(format!("Failed to write YAML: {}", e))
109 })?;
110
111 self.storage
113 .write_file(&file_path, yaml_content.as_bytes())
114 .await?;
115
116 info!(
117 "Saved {} relationships to {}",
118 relationships.len(),
119 file_path
120 );
121 Ok(())
122 }
123
124 pub async fn save_domain(
129 &self,
130 workspace_path: &str,
131 domain: &Domain,
132 tables: &HashMap<Uuid, Table>,
133 odps_products: &HashMap<Uuid, ODPSDataProduct>,
134 cads_assets: &HashMap<Uuid, CADSAsset>,
135 ) -> Result<(), StorageError> {
136 let sanitized_domain_name = sanitize_filename(&domain.name);
137 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
138
139 if !self.storage.dir_exists(&domain_dir).await? {
141 self.storage.create_dir(&domain_dir).await?;
142 }
143
144 let domain_yaml = domain.to_yaml().map_err(|e| {
146 StorageError::SerializationError(format!("Failed to serialize domain: {}", e))
147 })?;
148 let domain_file_path = format!("{}/domain.yaml", domain_dir);
149 self.storage
150 .write_file(&domain_file_path, domain_yaml.as_bytes())
151 .await?;
152 info!("Saved domain '{}' to {}", domain.name, domain_file_path);
153
154 for odcs_node in &domain.odcs_nodes {
156 if let Some(table_id) = odcs_node.table_id
157 && let Some(table) = tables.get(&table_id)
158 {
159 let sanitized_table_name = sanitize_filename(&table.name);
160 let table_file_path = format!("{}/{}.odcs.yaml", domain_dir, sanitized_table_name);
161 let odcs_yaml = ODCSExporter::export_table(table, "odcs_v3_1_0");
162 self.storage
163 .write_file(&table_file_path, odcs_yaml.as_bytes())
164 .await?;
165 info!("Saved ODCS table '{}' to {}", table.name, table_file_path);
166 }
167 }
168
169 for product in odps_products.values() {
172 if let Some(product_domain) = &product.domain
173 && product_domain == &domain.name
174 {
175 let sanitized_product_name =
176 sanitize_filename(product.name.as_ref().unwrap_or(&product.id));
177 let product_file_path =
178 format!("{}/{}.odps.yaml", domain_dir, sanitized_product_name);
179 let odps_yaml = ODPSExporter::export_product(product);
180 self.storage
181 .write_file(&product_file_path, odps_yaml.as_bytes())
182 .await?;
183 info!(
184 "Saved ODPS product '{}' to {}",
185 product.id, product_file_path
186 );
187 }
188 }
189
190 for cads_node in &domain.cads_nodes {
192 if let Some(cads_asset_id) = cads_node.cads_asset_id
193 && let Some(asset) = cads_assets.get(&cads_asset_id)
194 {
195 let sanitized_asset_name = sanitize_filename(&asset.name);
196 let asset_file_path = format!("{}/{}.cads.yaml", domain_dir, sanitized_asset_name);
197 let cads_yaml = CADSExporter::export_asset(asset);
198 self.storage
199 .write_file(&asset_file_path, cads_yaml.as_bytes())
200 .await?;
201 info!("Saved CADS asset '{}' to {}", asset.name, asset_file_path);
202 }
203 }
204
205 Ok(())
206 }
207
208 pub async fn save_odps_product(
212 &self,
213 workspace_path: &str,
214 domain_name: &str,
215 product: &ODPSDataProduct,
216 ) -> Result<(), StorageError> {
217 let sanitized_domain_name = sanitize_filename(domain_name);
218 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
219
220 if !self.storage.dir_exists(&domain_dir).await? {
222 self.storage.create_dir(&domain_dir).await?;
223 }
224
225 let sanitized_product_name =
226 sanitize_filename(product.name.as_ref().unwrap_or(&product.id));
227 let product_file_path = format!("{}/{}.odps.yaml", domain_dir, sanitized_product_name);
228 let odps_yaml = ODPSExporter::export_product(product);
229 self.storage
230 .write_file(&product_file_path, odps_yaml.as_bytes())
231 .await?;
232
233 info!(
234 "Saved ODPS product '{}' to {}",
235 product.id, product_file_path
236 );
237 Ok(())
238 }
239
240 pub async fn save_cads_asset(
244 &self,
245 workspace_path: &str,
246 domain_name: &str,
247 asset: &CADSAsset,
248 ) -> Result<(), StorageError> {
249 let sanitized_domain_name = sanitize_filename(domain_name);
250 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
251
252 if !self.storage.dir_exists(&domain_dir).await? {
254 self.storage.create_dir(&domain_dir).await?;
255 }
256
257 let sanitized_asset_name = sanitize_filename(&asset.name);
258 let asset_file_path = format!("{}/{}.cads.yaml", domain_dir, sanitized_asset_name);
259 let cads_yaml = CADSExporter::export_asset(asset);
260 self.storage
261 .write_file(&asset_file_path, cads_yaml.as_bytes())
262 .await?;
263
264 info!("Saved CADS asset '{}' to {}", asset.name, asset_file_path);
265 Ok(())
266 }
267
268 #[cfg(feature = "bpmn")]
272 pub async fn save_bpmn_model(
273 &self,
274 workspace_path: &str,
275 domain_name: &str,
276 model: &BPMNModel,
277 xml_content: &str,
278 ) -> Result<(), StorageError> {
279 let sanitized_domain_name = sanitize_filename(domain_name);
280 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
281
282 if !self.storage.dir_exists(&domain_dir).await? {
284 self.storage.create_dir(&domain_dir).await?;
285 }
286
287 let sanitized_model_name = sanitize_filename(&model.name);
288 let model_file_path = format!("{}/{}.bpmn.xml", domain_dir, sanitized_model_name);
289 self.storage
290 .write_file(&model_file_path, xml_content.as_bytes())
291 .await?;
292
293 info!("Saved BPMN model '{}' to {}", model.name, model_file_path);
294 Ok(())
295 }
296
297 #[cfg(feature = "dmn")]
301 pub async fn save_dmn_model(
302 &self,
303 workspace_path: &str,
304 domain_name: &str,
305 model: &DMNModel,
306 xml_content: &str,
307 ) -> Result<(), StorageError> {
308 let sanitized_domain_name = sanitize_filename(domain_name);
309 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
310
311 if !self.storage.dir_exists(&domain_dir).await? {
313 self.storage.create_dir(&domain_dir).await?;
314 }
315
316 let sanitized_model_name = sanitize_filename(&model.name);
317 let model_file_path = format!("{}/{}.dmn.xml", domain_dir, sanitized_model_name);
318 self.storage
319 .write_file(&model_file_path, xml_content.as_bytes())
320 .await?;
321
322 info!("Saved DMN model '{}' to {}", model.name, model_file_path);
323 Ok(())
324 }
325
326 #[cfg(feature = "openapi")]
330 pub async fn save_openapi_model(
331 &self,
332 workspace_path: &str,
333 domain_name: &str,
334 model: &OpenAPIModel,
335 content: &str,
336 ) -> Result<(), StorageError> {
337 let sanitized_domain_name = sanitize_filename(domain_name);
338 let domain_dir = format!("{}/{}", workspace_path, sanitized_domain_name);
339
340 if !self.storage.dir_exists(&domain_dir).await? {
342 self.storage.create_dir(&domain_dir).await?;
343 }
344
345 let sanitized_api_name = sanitize_filename(&model.name);
346 let extension = match model.format {
347 OpenAPIFormat::Yaml => "yaml",
348 OpenAPIFormat::Json => "json",
349 };
350 let model_file_path = format!(
351 "{}/{}.openapi.{}",
352 domain_dir, sanitized_api_name, extension
353 );
354 self.storage
355 .write_file(&model_file_path, content.as_bytes())
356 .await?;
357
358 info!("Saved OpenAPI spec '{}' to {}", model.name, model_file_path);
359 Ok(())
360 }
361}
362
363#[derive(Debug, Clone)]
365pub struct TableData {
366 pub id: Uuid,
367 pub name: String,
368 pub yaml_file_path: Option<String>,
369 pub yaml_value: serde_yaml::Value,
370}
371
372#[derive(Debug, Clone)]
374pub struct RelationshipData {
375 pub id: Uuid,
376 pub source_table_id: Uuid,
377 pub target_table_id: Uuid,
378 pub yaml_value: serde_yaml::Value,
379}
380
381fn sanitize_filename(name: &str) -> String {
383 name.chars()
384 .map(|c| match c {
385 '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
386 _ => c,
387 })
388 .collect()
389}