1use super::{ExportError, ExportResult};
7use crate::models::{Column, DataModel, Table};
8use serde_yaml;
9use std::collections::HashMap;
10
11fn get_physical_type(column: &Column) -> String {
14 column
15 .physical_type
16 .clone()
17 .unwrap_or_else(|| column.data_type.clone())
18}
19
20pub struct ODCSExporter;
22
23impl ODCSExporter {
24 pub fn export_table(table: &Table, _format: &str) -> String {
53 Self::export_odcs_v3_1_0_format(table)
55 }
56
57 fn parse_struct_properties_from_data_type(
60 parent_name: &str,
61 data_type: &str,
62 map_data_type_fn: &dyn Fn(&str) -> (String, bool),
63 ) -> Option<Vec<serde_yaml::Value>> {
64 use crate::import::odcs::ODCSImporter;
65
66 let importer = ODCSImporter::new();
67 let field_data = serde_json::Map::new();
68
69 let struct_type = if data_type.to_uppercase().starts_with("ARRAY<STRUCT<") {
71 if let Some(start) = data_type.find("STRUCT<") {
73 &data_type[start..]
74 } else {
75 data_type
76 }
77 } else {
78 data_type
79 };
80
81 if let Ok(nested_cols) =
83 importer.parse_struct_type_from_string(parent_name, struct_type, &field_data)
84 && !nested_cols.is_empty()
85 {
86 use std::collections::HashMap;
90 let mut props_map: HashMap<String, Vec<&crate::models::Column>> = HashMap::new();
91
92 for nested_col in &nested_cols {
93 let name_after_prefix = nested_col
95 .name
96 .strip_prefix(&format!("{}.[]", parent_name))
97 .or_else(|| nested_col.name.strip_prefix(&format!("{}.", parent_name)))
98 .unwrap_or(&nested_col.name);
99
100 let immediate_child = name_after_prefix
102 .split('.')
103 .next()
104 .unwrap_or(name_after_prefix)
105 .to_string();
106
107 props_map
108 .entry(immediate_child)
109 .or_default()
110 .push(nested_col);
111 }
112
113 let mut props_array = Vec::new();
115 for (immediate_child, child_cols) in props_map {
116 if immediate_child.is_empty() {
118 continue;
119 }
120
121 let has_nested_struct = child_cols.iter().any(|col| {
123 let name_after_prefix = col
124 .name
125 .strip_prefix(&format!("{}.[]", parent_name))
126 .or_else(|| col.name.strip_prefix(&format!("{}.", parent_name)))
127 .unwrap_or(&col.name);
128 name_after_prefix.contains('.')
129 });
130
131 if has_nested_struct {
132 let mut prop_map = serde_yaml::Mapping::new();
134 let id = immediate_child
135 .chars()
136 .map(|c| {
137 if c.is_alphanumeric() {
138 c.to_lowercase().to_string()
139 } else {
140 "_".to_string()
141 }
142 })
143 .collect::<String>()
144 .replace("__", "_");
145
146 prop_map.insert(
147 serde_yaml::Value::String("id".to_string()),
148 serde_yaml::Value::String(format!("{}_field", id)),
149 );
150 prop_map.insert(
151 serde_yaml::Value::String("name".to_string()),
152 serde_yaml::Value::String(immediate_child.clone()),
153 );
154 prop_map.insert(
155 serde_yaml::Value::String("logicalType".to_string()),
156 serde_yaml::Value::String("object".to_string()),
157 );
158
159 let mut nested_props = Vec::new();
161 for nested_col in child_cols {
162 let name_after_prefix = nested_col
164 .name
165 .strip_prefix(&format!("{}.[]", parent_name))
166 .or_else(|| nested_col.name.strip_prefix(&format!("{}.", parent_name)))
167 .unwrap_or(&nested_col.name);
168
169 let nested_name = name_after_prefix
170 .strip_prefix(&format!("{}.", immediate_child))
171 .unwrap_or(name_after_prefix)
172 .to_string();
173
174 if !nested_name.is_empty() && nested_name != immediate_child {
175 let (logical_type, _) = map_data_type_fn(&nested_col.data_type);
176 let nested_id = nested_name
177 .chars()
178 .map(|c| {
179 if c.is_alphanumeric() {
180 c.to_lowercase().to_string()
181 } else {
182 "_".to_string()
183 }
184 })
185 .collect::<String>()
186 .replace("__", "_");
187
188 let mut nested_prop = serde_yaml::Mapping::new();
189 nested_prop.insert(
190 serde_yaml::Value::String("id".to_string()),
191 serde_yaml::Value::String(format!("{}_field", nested_id)),
192 );
193 nested_prop.insert(
194 serde_yaml::Value::String("name".to_string()),
195 serde_yaml::Value::String(nested_name),
196 );
197 nested_prop.insert(
198 serde_yaml::Value::String("logicalType".to_string()),
199 serde_yaml::Value::String(logical_type),
200 );
201 nested_prop.insert(
202 serde_yaml::Value::String("physicalType".to_string()),
203 serde_yaml::Value::String(get_physical_type(nested_col)),
204 );
205
206 if !nested_col.nullable {
207 nested_prop.insert(
208 serde_yaml::Value::String("required".to_string()),
209 serde_yaml::Value::Bool(true),
210 );
211 }
212
213 nested_props.push(serde_yaml::Value::Mapping(nested_prop));
214 }
215 }
216
217 if !nested_props.is_empty() {
218 prop_map.insert(
219 serde_yaml::Value::String("properties".to_string()),
220 serde_yaml::Value::Sequence(nested_props),
221 );
222 }
223
224 props_array.push(serde_yaml::Value::Mapping(prop_map));
225 } else {
226 let nested_col = child_cols[0];
228 let mut prop_map = serde_yaml::Mapping::new();
229 let (logical_type, _) = map_data_type_fn(&nested_col.data_type);
230
231 let id = immediate_child
232 .chars()
233 .map(|c| {
234 if c.is_alphanumeric() {
235 c.to_lowercase().to_string()
236 } else {
237 "_".to_string()
238 }
239 })
240 .collect::<String>()
241 .replace("__", "_");
242
243 prop_map.insert(
244 serde_yaml::Value::String("id".to_string()),
245 serde_yaml::Value::String(format!("{}_field", id)),
246 );
247 prop_map.insert(
248 serde_yaml::Value::String("name".to_string()),
249 serde_yaml::Value::String(immediate_child),
250 );
251 prop_map.insert(
252 serde_yaml::Value::String("logicalType".to_string()),
253 serde_yaml::Value::String(logical_type),
254 );
255 prop_map.insert(
256 serde_yaml::Value::String("physicalType".to_string()),
257 serde_yaml::Value::String(get_physical_type(nested_col)),
258 );
259
260 if !nested_col.nullable {
261 prop_map.insert(
262 serde_yaml::Value::String("required".to_string()),
263 serde_yaml::Value::Bool(true),
264 );
265 }
266
267 if !nested_col.description.is_empty() {
268 prop_map.insert(
269 serde_yaml::Value::String("description".to_string()),
270 serde_yaml::Value::String(nested_col.description.clone()),
271 );
272 }
273
274 props_array.push(serde_yaml::Value::Mapping(prop_map));
275 }
276 }
277 return Some(props_array);
278 }
279 None
280 }
281
282 fn map_data_type_to_logical_type(data_type: &str) -> (String, bool) {
285 let upper = data_type.to_uppercase();
286
287 if upper.starts_with("ARRAY<") {
289 return ("array".to_string(), true);
290 }
291
292 if upper.contains("INT") || upper == "BIGINT" || upper == "SMALLINT" || upper == "TINYINT" {
294 ("integer".to_string(), false)
295 } else if upper.contains("DECIMAL")
296 || upper.contains("DOUBLE")
297 || upper.contains("FLOAT")
298 || upper.contains("NUMERIC")
299 || upper == "NUMBER"
300 {
301 ("number".to_string(), false)
302 } else if upper == "BOOLEAN" || upper == "BOOL" {
303 ("boolean".to_string(), false)
304 } else if upper == "DATE" {
305 ("date".to_string(), false)
306 } else if upper.contains("TIMESTAMP") {
307 ("timestamp".to_string(), false)
308 } else if upper == "TIME" {
309 ("time".to_string(), false)
310 } else if upper == "STRUCT" || upper == "OBJECT" || upper.starts_with("STRUCT<") {
311 ("object".to_string(), false)
312 } else {
313 ("string".to_string(), false)
315 }
316 }
317
318 fn json_to_yaml_value(json: &serde_json::Value) -> serde_yaml::Value {
320 match json {
321 serde_json::Value::Null => serde_yaml::Value::Null,
322 serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
323 serde_json::Value::Number(n) => {
324 if let Some(i) = n.as_i64() {
325 serde_yaml::Value::Number(serde_yaml::Number::from(i))
326 } else if let Some(f) = n.as_f64() {
327 serde_yaml::Value::Number(serde_yaml::Number::from(f))
328 } else {
329 serde_yaml::Value::String(n.to_string())
330 }
331 }
332 serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
333 serde_json::Value::Array(arr) => {
334 let yaml_arr: Vec<serde_yaml::Value> =
335 arr.iter().map(Self::json_to_yaml_value).collect();
336 serde_yaml::Value::Sequence(yaml_arr)
337 }
338 serde_json::Value::Object(obj) => {
339 let mut yaml_map = serde_yaml::Mapping::new();
340 for (k, v) in obj {
341 yaml_map.insert(
342 serde_yaml::Value::String(k.clone()),
343 Self::json_to_yaml_value(v),
344 );
345 }
346 serde_yaml::Value::Mapping(yaml_map)
347 }
348 }
349 }
350
351 fn export_odcs_v3_1_0_format(table: &Table) -> String {
353 let mut yaml = serde_yaml::Mapping::new();
354
355 yaml.insert(
357 serde_yaml::Value::String("apiVersion".to_string()),
358 serde_yaml::Value::String("v3.1.0".to_string()),
359 );
360 yaml.insert(
361 serde_yaml::Value::String("kind".to_string()),
362 serde_yaml::Value::String("DataContract".to_string()),
363 );
364
365 yaml.insert(
367 serde_yaml::Value::String("id".to_string()),
368 serde_yaml::Value::String(table.id.to_string()),
369 );
370
371 yaml.insert(
373 serde_yaml::Value::String("name".to_string()),
374 serde_yaml::Value::String(table.name.clone()),
375 );
376
377 let version = table
379 .odcl_metadata
380 .get("version")
381 .and_then(|v| v.as_str())
382 .map(|s| s.to_string())
383 .unwrap_or_else(|| "1.0.0".to_string());
384 yaml.insert(
385 serde_yaml::Value::String("version".to_string()),
386 serde_yaml::Value::String(version),
387 );
388
389 let status_value = table
391 .odcl_metadata
392 .get("status")
393 .and_then(|v| {
394 if v.is_null() {
395 None
396 } else {
397 Some(Self::json_to_yaml_value(v))
398 }
399 })
400 .unwrap_or_else(|| serde_yaml::Value::String("draft".to_string()));
401 yaml.insert(
402 serde_yaml::Value::String("status".to_string()),
403 status_value,
404 );
405
406 if let Some(domain) = table.odcl_metadata.get("domain")
408 && !domain.is_null()
409 {
410 yaml.insert(
411 serde_yaml::Value::String("domain".to_string()),
412 Self::json_to_yaml_value(domain),
413 );
414 }
415
416 if let Some(data_product) = table.odcl_metadata.get("dataProduct")
418 && !data_product.is_null()
419 {
420 yaml.insert(
421 serde_yaml::Value::String("dataProduct".to_string()),
422 Self::json_to_yaml_value(data_product),
423 );
424 }
425
426 if let Some(tenant) = table.odcl_metadata.get("tenant")
428 && !tenant.is_null()
429 {
430 yaml.insert(
431 serde_yaml::Value::String("tenant".to_string()),
432 Self::json_to_yaml_value(tenant),
433 );
434 }
435
436 if let Some(description) = table.odcl_metadata.get("description")
438 && !description.is_null()
439 {
440 yaml.insert(
441 serde_yaml::Value::String("description".to_string()),
442 Self::json_to_yaml_value(description),
443 );
444 }
445
446 if !table.tags.is_empty() {
448 let tags_yaml: Vec<serde_yaml::Value> = table
449 .tags
450 .iter()
451 .map(|t| serde_yaml::Value::String(t.to_string()))
452 .collect();
453 yaml.insert(
454 serde_yaml::Value::String("tags".to_string()),
455 serde_yaml::Value::Sequence(tags_yaml),
456 );
457 }
458
459 if let Some(team) = table.odcl_metadata.get("team")
461 && !team.is_null()
462 {
463 yaml.insert(
464 serde_yaml::Value::String("team".to_string()),
465 Self::json_to_yaml_value(team),
466 );
467 }
468
469 if let Some(roles) = table.odcl_metadata.get("roles")
471 && !roles.is_null()
472 {
473 yaml.insert(
474 serde_yaml::Value::String("roles".to_string()),
475 Self::json_to_yaml_value(roles),
476 );
477 }
478
479 if let Some(pricing) = table.odcl_metadata.get("pricing")
481 && !pricing.is_null()
482 {
483 yaml.insert(
484 serde_yaml::Value::String("price".to_string()),
485 Self::json_to_yaml_value(pricing),
486 );
487 }
488
489 if let Some(terms) = table.odcl_metadata.get("terms")
491 && !terms.is_null()
492 {
493 yaml.insert(
494 serde_yaml::Value::String("terms".to_string()),
495 Self::json_to_yaml_value(terms),
496 );
497 }
498
499 if let Some(servers) = table.odcl_metadata.get("servers")
501 && !servers.is_null()
502 {
503 yaml.insert(
504 serde_yaml::Value::String("servers".to_string()),
505 Self::json_to_yaml_value(servers),
506 );
507 }
508
509 if let Some(servicelevels) = table.odcl_metadata.get("servicelevels")
511 && !servicelevels.is_null()
512 {
513 yaml.insert(
514 serde_yaml::Value::String("servicelevels".to_string()),
515 Self::json_to_yaml_value(servicelevels),
516 );
517 }
518
519 if let Some(links) = table.odcl_metadata.get("links")
521 && !links.is_null()
522 {
523 yaml.insert(
524 serde_yaml::Value::String("links".to_string()),
525 Self::json_to_yaml_value(links),
526 );
527 }
528
529 if let Some(infrastructure) = table.odcl_metadata.get("infrastructure")
531 && !infrastructure.is_null()
532 {
533 yaml.insert(
534 serde_yaml::Value::String("infrastructure".to_string()),
535 Self::json_to_yaml_value(infrastructure),
536 );
537 }
538
539 let mut schema_array = Vec::new();
541 let mut schema_obj = serde_yaml::Mapping::new();
542
543 schema_obj.insert(
544 serde_yaml::Value::String("name".to_string()),
545 serde_yaml::Value::String(table.name.clone()),
546 );
547
548 let mut properties = Vec::new();
550
551 fn mapping_to_properties_array(props_map: serde_yaml::Mapping) -> Vec<serde_yaml::Value> {
553 let mut props_array = Vec::new();
554 for (key, value) in props_map {
555 if let serde_yaml::Value::String(name) = key
556 && let serde_yaml::Value::Mapping(mut prop_map) = value
557 {
558 prop_map.insert(
560 serde_yaml::Value::String("name".to_string()),
561 serde_yaml::Value::String(name.clone()),
562 );
563 props_array.push(serde_yaml::Value::Mapping(prop_map));
564 }
565 }
566 props_array
567 }
568
569 fn build_nested_properties(
571 parent_name: &str,
572 _table_name: &str,
573 all_columns: &[crate::models::Column],
574 json_to_yaml_fn: &dyn Fn(&serde_json::Value) -> serde_yaml::Value,
575 map_data_type_fn: &dyn Fn(&str) -> (String, bool),
576 ) -> Option<Vec<serde_yaml::Value>> {
577 let parent_prefix_dot = format!("{}.", parent_name);
579 let parent_prefix_array = format!("{}.[].", parent_name); let parent_prefix_array_no_dot = format!("{}.[]", parent_name); let nested_columns: Vec<&crate::models::Column> = all_columns
582 .iter()
583 .filter(|col| {
584 col.name.starts_with(&parent_prefix_dot)
585 || col.name.starts_with(&parent_prefix_array_no_dot)
586 })
587 .collect();
588
589 if nested_columns.is_empty() {
590 return None;
591 }
592
593 let mut nested_props_map = serde_yaml::Mapping::new();
594
595 let mut child_map: std::collections::HashMap<String, Vec<&crate::models::Column>> =
597 std::collections::HashMap::new();
598
599 for nested_col in &nested_columns {
600 let relative_name = if nested_col.name.starts_with(&parent_prefix_array) {
603 nested_col
606 .name
607 .strip_prefix(&parent_prefix_array)
608 .unwrap_or("")
609 } else if nested_col.name.starts_with(&parent_prefix_dot) {
610 nested_col
613 .name
614 .strip_prefix(&parent_prefix_dot)
615 .unwrap_or("")
616 } else {
617 continue;
618 };
619
620 if relative_name.is_empty() {
621 continue;
622 }
623
624 let child_name = if let Some(dot_pos) = relative_name.find('.') {
629 if relative_name.starts_with(".[]") {
631 continue;
634 } else {
635 &relative_name[..dot_pos]
637 }
638 } else {
639 relative_name
641 };
642
643 child_map
644 .entry(child_name.to_string())
645 .or_default()
646 .push(nested_col);
647 }
648
649 for (child_name, child_cols) in child_map {
651 if child_name.is_empty() {
653 continue;
654 }
655 let direct_child = child_cols.iter().find(|col| {
659 let rel_name = if col.name.starts_with(&parent_prefix_array) {
660 col.name.strip_prefix(&parent_prefix_array)
661 } else if col.name.starts_with(&parent_prefix_dot) {
662 col.name.strip_prefix(&parent_prefix_dot)
663 } else {
664 None
665 };
666 rel_name
668 .map(|rel| {
669 rel == child_name
670 || rel.starts_with(&format!("{}.", child_name))
671 || rel.starts_with(&format!("{}.[]", child_name))
672 })
673 .unwrap_or(false)
674 });
675
676 let child_has_nested = child_cols.iter().any(|col| {
678 let rel_name = if col.name.starts_with(&parent_prefix_array) {
679 col.name.strip_prefix(&parent_prefix_array)
680 } else if col.name.starts_with(&parent_prefix_dot) {
681 col.name.strip_prefix(&parent_prefix_dot)
682 } else {
683 None
684 };
685 rel_name
686 .map(|rel| {
687 rel.starts_with(&format!("{}.", child_name)) && rel != child_name
688 })
689 .unwrap_or(false)
690 });
691
692 let child_col = if let Some(col) = direct_child {
697 Some(*col)
698 } else {
699 child_cols
701 .iter()
702 .find(|col| {
703 let rel_name = if col.name.starts_with(&parent_prefix_array) {
704 col.name.strip_prefix(&parent_prefix_array)
705 } else if col.name.starts_with(&parent_prefix_dot) {
706 col.name.strip_prefix(&parent_prefix_dot)
707 } else {
708 None
709 };
710 rel_name.map(|rel| rel == child_name).unwrap_or(false)
711 })
712 .copied()
713 };
714
715 if let Some(child_col) = child_col {
716 let mut child_prop = serde_yaml::Mapping::new();
717
718 child_prop.insert(
720 serde_yaml::Value::String("name".to_string()),
721 serde_yaml::Value::String(child_name.clone()),
722 );
723
724 let data_type_upper = child_col.data_type.to_uppercase();
726 let is_array_object = data_type_upper.starts_with("ARRAY<")
727 && (data_type_upper.contains("OBJECT")
728 || data_type_upper.contains("STRUCT"));
729 let is_struct_or_object = data_type_upper == "STRUCT"
730 || data_type_upper == "OBJECT"
731 || data_type_upper.starts_with("STRUCT<");
732
733 let nested_props_array = build_nested_properties(
735 &child_col.name,
736 _table_name,
737 all_columns,
738 json_to_yaml_fn,
739 map_data_type_fn,
740 );
741
742 if is_array_object && (child_has_nested || nested_props_array.is_some()) {
743 child_prop.insert(
745 serde_yaml::Value::String("logicalType".to_string()),
746 serde_yaml::Value::String("array".to_string()),
747 );
748 child_prop.insert(
749 serde_yaml::Value::String("physicalType".to_string()),
750 serde_yaml::Value::String(get_physical_type(child_col)),
751 );
752
753 let mut items = serde_yaml::Mapping::new();
754 items.insert(
755 serde_yaml::Value::String("logicalType".to_string()),
756 serde_yaml::Value::String("object".to_string()),
757 );
758
759 if let Some(nested_props) = nested_props_array {
761 items.insert(
762 serde_yaml::Value::String("properties".to_string()),
763 serde_yaml::Value::Sequence(nested_props),
764 );
765 }
766
767 child_prop.insert(
768 serde_yaml::Value::String("items".to_string()),
769 serde_yaml::Value::Mapping(items),
770 );
771 } else if is_struct_or_object
772 || child_has_nested
773 || nested_props_array.is_some()
774 {
775 child_prop.insert(
777 serde_yaml::Value::String("logicalType".to_string()),
778 serde_yaml::Value::String("object".to_string()),
779 );
780 child_prop.insert(
781 serde_yaml::Value::String("physicalType".to_string()),
782 serde_yaml::Value::String(get_physical_type(child_col)),
783 );
784
785 if let Some(nested_props) = nested_props_array {
787 child_prop.insert(
788 serde_yaml::Value::String("properties".to_string()),
789 serde_yaml::Value::Sequence(nested_props),
790 );
791 }
792 } else {
793 let (logical_type, _) = map_data_type_fn(&child_col.data_type);
795 child_prop.insert(
796 serde_yaml::Value::String("logicalType".to_string()),
797 serde_yaml::Value::String(logical_type),
798 );
799 child_prop.insert(
800 serde_yaml::Value::String("physicalType".to_string()),
801 serde_yaml::Value::String(get_physical_type(child_col)),
802 );
803 }
804
805 if !child_col.nullable {
806 child_prop.insert(
807 serde_yaml::Value::String("required".to_string()),
808 serde_yaml::Value::Bool(true),
809 );
810 }
811
812 if !child_col.description.is_empty() {
813 child_prop.insert(
814 serde_yaml::Value::String("description".to_string()),
815 serde_yaml::Value::String(child_col.description.clone()),
816 );
817 }
818
819 if !child_col.quality.is_empty() {
821 let quality_array: Vec<serde_yaml::Value> = child_col
822 .quality
823 .iter()
824 .map(|rule| {
825 let mut rule_map = serde_yaml::Mapping::new();
826 for (k, v) in rule {
827 rule_map.insert(
828 serde_yaml::Value::String(k.clone()),
829 json_to_yaml_fn(v),
830 );
831 }
832 serde_yaml::Value::Mapping(rule_map)
833 })
834 .collect();
835 child_prop.insert(
836 serde_yaml::Value::String("quality".to_string()),
837 serde_yaml::Value::Sequence(quality_array),
838 );
839 }
840
841 if !child_col.relationships.is_empty() {
843 let rels_yaml: Vec<serde_yaml::Value> = child_col
844 .relationships
845 .iter()
846 .map(|rel| {
847 let mut rel_map = serde_yaml::Mapping::new();
848 rel_map.insert(
849 serde_yaml::Value::String("type".to_string()),
850 serde_yaml::Value::String(rel.relationship_type.clone()),
851 );
852 rel_map.insert(
853 serde_yaml::Value::String("to".to_string()),
854 serde_yaml::Value::String(rel.to.clone()),
855 );
856 serde_yaml::Value::Mapping(rel_map)
857 })
858 .collect();
859 child_prop.insert(
860 serde_yaml::Value::String("relationships".to_string()),
861 serde_yaml::Value::Sequence(rels_yaml),
862 );
863 }
864
865 if !child_col.enum_values.is_empty() {
868 let quality = child_prop
869 .entry(serde_yaml::Value::String("quality".to_string()))
870 .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
871
872 if let serde_yaml::Value::Sequence(quality_rules) = quality {
873 let mut enum_rule = serde_yaml::Mapping::new();
874 enum_rule.insert(
875 serde_yaml::Value::String("type".to_string()),
876 serde_yaml::Value::String("sql".to_string()),
877 );
878
879 let enum_list: String = child_col
880 .enum_values
881 .iter()
882 .map(|e| format!("'{}'", e.replace('\'', "''")))
883 .collect::<Vec<_>>()
884 .join(", ");
885 let query = format!(
886 "SELECT COUNT(*) FROM ${{table}} WHERE ${{column}} NOT IN ({})",
887 enum_list
888 );
889
890 enum_rule.insert(
891 serde_yaml::Value::String("query".to_string()),
892 serde_yaml::Value::String(query),
893 );
894
895 enum_rule.insert(
896 serde_yaml::Value::String("mustBe".to_string()),
897 serde_yaml::Value::Number(serde_yaml::Number::from(0)),
898 );
899
900 enum_rule.insert(
901 serde_yaml::Value::String("description".to_string()),
902 serde_yaml::Value::String(format!(
903 "Value must be one of: {}",
904 child_col.enum_values.join(", ")
905 )),
906 );
907
908 quality_rules.push(serde_yaml::Value::Mapping(enum_rule));
909 }
910 }
911
912 if !child_col.constraints.is_empty() {
914 let constraints_yaml: Vec<serde_yaml::Value> = child_col
915 .constraints
916 .iter()
917 .map(|c| serde_yaml::Value::String(c.clone()))
918 .collect();
919 child_prop.insert(
920 serde_yaml::Value::String("constraints".to_string()),
921 serde_yaml::Value::Sequence(constraints_yaml),
922 );
923 }
924
925 if let Some(ref fk) = child_col.foreign_key {
927 let mut fk_map = serde_yaml::Mapping::new();
928 fk_map.insert(
929 serde_yaml::Value::String("table".to_string()),
930 serde_yaml::Value::String(fk.table_id.clone()),
931 );
932 fk_map.insert(
933 serde_yaml::Value::String("column".to_string()),
934 serde_yaml::Value::String(fk.column_name.clone()),
935 );
936 child_prop.insert(
937 serde_yaml::Value::String("foreignKey".to_string()),
938 serde_yaml::Value::Mapping(fk_map),
939 );
940 }
941
942 nested_props_map.insert(
943 serde_yaml::Value::String(child_name.clone()),
944 serde_yaml::Value::Mapping(child_prop),
945 );
946 } else if !child_cols.is_empty() {
947 if let Some(first_col) = child_cols.iter().find(|col| {
950 let rel_name = if col.name.starts_with(&parent_prefix_array) {
951 col.name.strip_prefix(&parent_prefix_array)
952 } else if col.name.starts_with(&parent_prefix_dot) {
953 col.name.strip_prefix(&parent_prefix_dot)
954 } else {
955 None
956 };
957 rel_name.map(|rel| rel == child_name).unwrap_or(false)
958 }) {
959 let mut child_prop = serde_yaml::Mapping::new();
960
961 child_prop.insert(
963 serde_yaml::Value::String("name".to_string()),
964 serde_yaml::Value::String(child_name.clone()),
965 );
966
967 let child_has_nested = child_cols.iter().any(|col| {
969 let rel_name = if col.name.starts_with(&parent_prefix_array) {
970 col.name.strip_prefix(&parent_prefix_array)
971 } else if col.name.starts_with(&parent_prefix_dot) {
972 col.name.strip_prefix(&parent_prefix_dot)
973 } else {
974 None
975 };
976 rel_name
977 .map(|rel| {
978 rel.starts_with(&format!("{}.", child_name))
979 && rel != child_name
980 })
981 .unwrap_or(false)
982 });
983
984 let nested_props_array = if child_has_nested {
986 build_nested_properties(
987 &first_col.name,
988 _table_name,
989 all_columns,
990 json_to_yaml_fn,
991 map_data_type_fn,
992 )
993 } else {
994 None
995 };
996
997 let data_type_upper = first_col.data_type.to_uppercase();
998 let is_array_object = data_type_upper.starts_with("ARRAY<")
999 && (data_type_upper.contains("OBJECT")
1000 || data_type_upper.contains("STRUCT"));
1001 let is_struct_or_object = data_type_upper == "STRUCT"
1002 || data_type_upper == "OBJECT"
1003 || data_type_upper.starts_with("STRUCT<");
1004
1005 if is_array_object && (child_has_nested || nested_props_array.is_some()) {
1006 child_prop.insert(
1007 serde_yaml::Value::String("logicalType".to_string()),
1008 serde_yaml::Value::String("array".to_string()),
1009 );
1010 child_prop.insert(
1011 serde_yaml::Value::String("physicalType".to_string()),
1012 serde_yaml::Value::String(get_physical_type(first_col)),
1013 );
1014
1015 let mut items = serde_yaml::Mapping::new();
1016 items.insert(
1017 serde_yaml::Value::String("logicalType".to_string()),
1018 serde_yaml::Value::String("object".to_string()),
1019 );
1020
1021 if let Some(nested_props) = nested_props_array {
1022 items.insert(
1023 serde_yaml::Value::String("properties".to_string()),
1024 serde_yaml::Value::Sequence(nested_props),
1025 );
1026 }
1027
1028 child_prop.insert(
1029 serde_yaml::Value::String("items".to_string()),
1030 serde_yaml::Value::Mapping(items),
1031 );
1032 } else if is_struct_or_object
1033 || child_has_nested
1034 || nested_props_array.is_some()
1035 {
1036 child_prop.insert(
1037 serde_yaml::Value::String("logicalType".to_string()),
1038 serde_yaml::Value::String("object".to_string()),
1039 );
1040 child_prop.insert(
1041 serde_yaml::Value::String("physicalType".to_string()),
1042 serde_yaml::Value::String(get_physical_type(first_col)),
1043 );
1044
1045 if let Some(nested_props) = nested_props_array {
1046 child_prop.insert(
1047 serde_yaml::Value::String("properties".to_string()),
1048 serde_yaml::Value::Sequence(nested_props),
1049 );
1050 }
1051 } else {
1052 let (logical_type, _) = map_data_type_fn(&first_col.data_type);
1053 child_prop.insert(
1054 serde_yaml::Value::String("logicalType".to_string()),
1055 serde_yaml::Value::String(logical_type),
1056 );
1057 child_prop.insert(
1058 serde_yaml::Value::String("physicalType".to_string()),
1059 serde_yaml::Value::String(get_physical_type(first_col)),
1060 );
1061 }
1062
1063 if !first_col.nullable {
1064 child_prop.insert(
1065 serde_yaml::Value::String("required".to_string()),
1066 serde_yaml::Value::Bool(true),
1067 );
1068 }
1069
1070 if !first_col.description.is_empty() {
1071 child_prop.insert(
1072 serde_yaml::Value::String("description".to_string()),
1073 serde_yaml::Value::String(first_col.description.clone()),
1074 );
1075 }
1076
1077 nested_props_map.insert(
1078 serde_yaml::Value::String(child_name.clone()),
1079 serde_yaml::Value::Mapping(child_prop),
1080 );
1081 }
1082 } else {
1083 continue;
1085 }
1086 }
1087
1088 if nested_props_map.is_empty() {
1089 None
1090 } else {
1091 Some(mapping_to_properties_array(nested_props_map))
1093 }
1094 }
1095
1096 for column in &table.columns {
1097 if column.name.contains('.') {
1099 continue;
1100 }
1101
1102 let mut prop = serde_yaml::Mapping::new();
1103
1104 let column_prefix_dot = format!("{}.", column.name);
1108 let column_prefix_array = format!("{}.[]", column.name);
1109 let has_nested = table.columns.iter().any(|col| {
1110 (col.name.starts_with(&column_prefix_dot)
1111 || col.name.starts_with(&column_prefix_array))
1112 && col.name != column.name
1113 });
1114
1115 let data_type_upper = column.data_type.to_uppercase();
1117
1118 let is_array_struct_from_desc =
1122 data_type_upper == "ARRAY" && column.description.contains("|| ARRAY<STRUCT<");
1123 let is_array_struct_from_nested = (data_type_upper == "ARRAY"
1124 || data_type_upper == "ARRAY<OBJECT>")
1125 && has_nested
1126 && table
1127 .columns
1128 .iter()
1129 .any(|col| col.name.starts_with(&format!("{}.[]", column.name)));
1130
1131 let is_array_struct_from_string_type = data_type_upper == "ARRAY<STRING>"
1133 && column.description.contains("|| ARRAY<STRUCT<");
1134
1135 let is_array_struct = is_array_struct_from_desc
1136 || is_array_struct_from_nested
1137 || is_array_struct_from_string_type;
1138
1139 let full_array_struct_type =
1141 if is_array_struct_from_desc || is_array_struct_from_string_type {
1142 column
1143 .description
1144 .split("|| ")
1145 .nth(1)
1146 .map(|s| s.trim().to_string())
1147 } else if is_array_struct_from_nested {
1148 None } else {
1152 None
1153 };
1154
1155 let is_array_object = data_type_upper.starts_with("ARRAY<")
1156 && (data_type_upper.contains("OBJECT") || data_type_upper.contains("STRUCT"));
1157 let is_struct_or_object = data_type_upper == "STRUCT"
1158 || data_type_upper == "OBJECT"
1159 || data_type_upper.starts_with("STRUCT<");
1160
1161 if is_array_struct {
1163 let struct_props = if let Some(ref full_type) = full_array_struct_type {
1164 Self::parse_struct_properties_from_data_type(
1166 &column.name,
1167 full_type,
1168 &Self::map_data_type_to_logical_type,
1169 )
1170 } else if is_array_struct_from_nested {
1171 build_nested_properties(
1173 &column.name,
1174 &table.name,
1175 &table.columns,
1176 &Self::json_to_yaml_value,
1177 &Self::map_data_type_to_logical_type,
1178 )
1179 } else {
1180 None
1181 };
1182
1183 if let Some(props) = struct_props {
1184 prop.insert(
1186 serde_yaml::Value::String("logicalType".to_string()),
1187 serde_yaml::Value::String("array".to_string()),
1188 );
1189 prop.insert(
1190 serde_yaml::Value::String("physicalType".to_string()),
1191 serde_yaml::Value::String("ARRAY".to_string()),
1192 );
1193
1194 let mut items = serde_yaml::Mapping::new();
1195 items.insert(
1196 serde_yaml::Value::String("logicalType".to_string()),
1197 serde_yaml::Value::String("object".to_string()),
1198 );
1199 items.insert(
1200 serde_yaml::Value::String("properties".to_string()),
1201 serde_yaml::Value::Sequence(props),
1202 );
1203
1204 prop.insert(
1205 serde_yaml::Value::String("items".to_string()),
1206 serde_yaml::Value::Mapping(items),
1207 );
1208
1209 if is_array_struct_from_desc {
1211 let base_description =
1212 column.description.split("||").next().unwrap_or("").trim();
1213 if !base_description.is_empty() {
1214 prop.insert(
1215 serde_yaml::Value::String("description".to_string()),
1216 serde_yaml::Value::String(base_description.to_string()),
1217 );
1218 }
1219 } else if !column.description.is_empty() {
1220 prop.insert(
1222 serde_yaml::Value::String("description".to_string()),
1223 serde_yaml::Value::String(column.description.clone()),
1224 );
1225 }
1226 } else {
1227 prop.insert(
1229 serde_yaml::Value::String("logicalType".to_string()),
1230 serde_yaml::Value::String("array".to_string()),
1231 );
1232 prop.insert(
1233 serde_yaml::Value::String("physicalType".to_string()),
1234 serde_yaml::Value::String("ARRAY".to_string()),
1235 );
1236 }
1237 }
1238 else if has_nested {
1241 let nested_props = build_nested_properties(
1243 &column.name,
1244 &table.name,
1245 &table.columns,
1246 &Self::json_to_yaml_value,
1247 &Self::map_data_type_to_logical_type,
1248 );
1249
1250 if is_array_object {
1251 prop.insert(
1253 serde_yaml::Value::String("logicalType".to_string()),
1254 serde_yaml::Value::String("array".to_string()),
1255 );
1256 prop.insert(
1257 serde_yaml::Value::String("physicalType".to_string()),
1258 serde_yaml::Value::String(get_physical_type(column)),
1259 );
1260
1261 let mut items = serde_yaml::Mapping::new();
1262 items.insert(
1263 serde_yaml::Value::String("logicalType".to_string()),
1264 serde_yaml::Value::String("object".to_string()),
1265 );
1266
1267 if let Some(nested_props_array) = nested_props {
1269 items.insert(
1270 serde_yaml::Value::String("properties".to_string()),
1271 serde_yaml::Value::Sequence(nested_props_array),
1272 );
1273 }
1274
1275 prop.insert(
1276 serde_yaml::Value::String("items".to_string()),
1277 serde_yaml::Value::Mapping(items),
1278 );
1279 } else if is_struct_or_object || nested_props.is_some() {
1280 prop.insert(
1282 serde_yaml::Value::String("logicalType".to_string()),
1283 serde_yaml::Value::String("object".to_string()),
1284 );
1285 prop.insert(
1286 serde_yaml::Value::String("physicalType".to_string()),
1287 serde_yaml::Value::String(get_physical_type(column)),
1288 );
1289
1290 if let Some(nested_props_array) = nested_props {
1292 prop.insert(
1293 serde_yaml::Value::String("properties".to_string()),
1294 serde_yaml::Value::Sequence(nested_props_array),
1295 );
1296 }
1297 } else {
1298 if is_array_object && has_nested {
1301 prop.insert(
1303 serde_yaml::Value::String("logicalType".to_string()),
1304 serde_yaml::Value::String("array".to_string()),
1305 );
1306 prop.insert(
1307 serde_yaml::Value::String("physicalType".to_string()),
1308 serde_yaml::Value::String("ARRAY".to_string()),
1309 );
1310
1311 let mut items = serde_yaml::Mapping::new();
1312 items.insert(
1313 serde_yaml::Value::String("logicalType".to_string()),
1314 serde_yaml::Value::String("object".to_string()),
1315 );
1316
1317 if let Some(nested_props_array) = build_nested_properties(
1319 &column.name,
1320 &table.name,
1321 &table.columns,
1322 &Self::json_to_yaml_value,
1323 &Self::map_data_type_to_logical_type,
1324 ) {
1325 items.insert(
1326 serde_yaml::Value::String("properties".to_string()),
1327 serde_yaml::Value::Sequence(nested_props_array),
1328 );
1329 }
1330
1331 prop.insert(
1332 serde_yaml::Value::String("items".to_string()),
1333 serde_yaml::Value::Mapping(items),
1334 );
1335 } else {
1336 let (logical_type, _) =
1337 Self::map_data_type_to_logical_type(&column.data_type);
1338 prop.insert(
1339 serde_yaml::Value::String("logicalType".to_string()),
1340 serde_yaml::Value::String(logical_type),
1341 );
1342 prop.insert(
1343 serde_yaml::Value::String("physicalType".to_string()),
1344 serde_yaml::Value::String(get_physical_type(column)),
1345 );
1346 }
1347 }
1348 } else if is_struct_or_object {
1349 let parsed_props = Self::parse_struct_properties_from_data_type(
1352 &column.name,
1353 &column.data_type,
1354 &Self::map_data_type_to_logical_type,
1355 );
1356
1357 prop.insert(
1358 serde_yaml::Value::String("logicalType".to_string()),
1359 serde_yaml::Value::String("object".to_string()),
1360 );
1361 prop.insert(
1362 serde_yaml::Value::String("physicalType".to_string()),
1363 serde_yaml::Value::String(get_physical_type(column)),
1364 );
1365
1366 if let Some(nested_props_array) = parsed_props {
1368 prop.insert(
1369 serde_yaml::Value::String("properties".to_string()),
1370 serde_yaml::Value::Sequence(nested_props_array),
1371 );
1372 }
1373 } else if prop.is_empty() {
1374 let (logical_type, _) = Self::map_data_type_to_logical_type(&column.data_type);
1376 prop.insert(
1377 serde_yaml::Value::String("logicalType".to_string()),
1378 serde_yaml::Value::String(logical_type),
1379 );
1380 prop.insert(
1381 serde_yaml::Value::String("physicalType".to_string()),
1382 serde_yaml::Value::String(get_physical_type(column)),
1383 );
1384 }
1385 if !column.nullable {
1388 prop.insert(
1389 serde_yaml::Value::String("required".to_string()),
1390 serde_yaml::Value::Bool(true),
1391 );
1392 }
1393
1394 if column.primary_key {
1395 prop.insert(
1396 serde_yaml::Value::String("primaryKey".to_string()),
1397 serde_yaml::Value::Bool(true),
1398 );
1399 }
1400
1401 if column.secondary_key {
1402 prop.insert(
1403 serde_yaml::Value::String("businessKey".to_string()),
1404 serde_yaml::Value::Bool(true),
1405 );
1406 }
1407
1408 if !column.description.is_empty() {
1409 prop.insert(
1410 serde_yaml::Value::String("description".to_string()),
1411 serde_yaml::Value::String(column.description.clone()),
1412 );
1413 }
1414
1415 if !column.quality.is_empty() {
1417 let quality_array: Vec<serde_yaml::Value> = column
1418 .quality
1419 .iter()
1420 .map(|rule| {
1421 let mut rule_map = serde_yaml::Mapping::new();
1422 for (k, v) in rule {
1423 rule_map.insert(
1424 serde_yaml::Value::String(k.clone()),
1425 Self::json_to_yaml_value(v),
1426 );
1427 }
1428 serde_yaml::Value::Mapping(rule_map)
1429 })
1430 .collect();
1431 prop.insert(
1432 serde_yaml::Value::String("quality".to_string()),
1433 serde_yaml::Value::Sequence(quality_array),
1434 );
1435 }
1436
1437 if !column.relationships.is_empty() {
1439 let rels_yaml: Vec<serde_yaml::Value> = column
1440 .relationships
1441 .iter()
1442 .map(|rel| {
1443 let mut rel_map = serde_yaml::Mapping::new();
1444 rel_map.insert(
1445 serde_yaml::Value::String("type".to_string()),
1446 serde_yaml::Value::String(rel.relationship_type.clone()),
1447 );
1448 rel_map.insert(
1449 serde_yaml::Value::String("to".to_string()),
1450 serde_yaml::Value::String(rel.to.clone()),
1451 );
1452 serde_yaml::Value::Mapping(rel_map)
1453 })
1454 .collect();
1455 prop.insert(
1456 serde_yaml::Value::String("relationships".to_string()),
1457 serde_yaml::Value::Sequence(rels_yaml),
1458 );
1459 }
1460
1461 if !column.enum_values.is_empty() {
1465 let quality = prop
1467 .entry(serde_yaml::Value::String("quality".to_string()))
1468 .or_insert_with(|| serde_yaml::Value::Sequence(Vec::new()));
1469
1470 if let serde_yaml::Value::Sequence(quality_rules) = quality {
1471 let mut enum_rule = serde_yaml::Mapping::new();
1474 enum_rule.insert(
1475 serde_yaml::Value::String("type".to_string()),
1476 serde_yaml::Value::String("sql".to_string()),
1477 );
1478
1479 let enum_list: String = column
1481 .enum_values
1482 .iter()
1483 .map(|e| format!("'{}'", e.replace('\'', "''"))) .collect::<Vec<_>>()
1485 .join(", ");
1486 let query = format!(
1487 "SELECT COUNT(*) FROM ${{table}} WHERE ${{column}} NOT IN ({})",
1488 enum_list
1489 );
1490
1491 enum_rule.insert(
1492 serde_yaml::Value::String("query".to_string()),
1493 serde_yaml::Value::String(query),
1494 );
1495
1496 enum_rule.insert(
1497 serde_yaml::Value::String("mustBe".to_string()),
1498 serde_yaml::Value::Number(serde_yaml::Number::from(0)),
1499 );
1500
1501 enum_rule.insert(
1502 serde_yaml::Value::String("description".to_string()),
1503 serde_yaml::Value::String(format!(
1504 "Value must be one of: {}",
1505 column.enum_values.join(", ")
1506 )),
1507 );
1508
1509 quality_rules.push(serde_yaml::Value::Mapping(enum_rule));
1510 }
1511 }
1512
1513 if !column.constraints.is_empty() {
1515 let constraints_yaml: Vec<serde_yaml::Value> = column
1516 .constraints
1517 .iter()
1518 .map(|c| serde_yaml::Value::String(c.clone()))
1519 .collect();
1520 prop.insert(
1521 serde_yaml::Value::String("constraints".to_string()),
1522 serde_yaml::Value::Sequence(constraints_yaml),
1523 );
1524 }
1525
1526 if let Some(ref fk) = column.foreign_key {
1528 let mut fk_map = serde_yaml::Mapping::new();
1529 fk_map.insert(
1530 serde_yaml::Value::String("table".to_string()),
1531 serde_yaml::Value::String(fk.table_id.clone()),
1532 );
1533 fk_map.insert(
1534 serde_yaml::Value::String("column".to_string()),
1535 serde_yaml::Value::String(fk.column_name.clone()),
1536 );
1537 prop.insert(
1538 serde_yaml::Value::String("foreignKey".to_string()),
1539 serde_yaml::Value::Mapping(fk_map),
1540 );
1541 }
1542
1543 let id = column
1546 .name
1547 .chars()
1548 .map(|c| {
1549 if c.is_alphanumeric() {
1550 c.to_lowercase().to_string()
1551 } else {
1552 "_".to_string()
1553 }
1554 })
1555 .collect::<String>()
1556 .replace("__", "_");
1557
1558 prop.insert(
1559 serde_yaml::Value::String("id".to_string()),
1560 serde_yaml::Value::String(format!("{}_obj", id)),
1561 );
1562 prop.insert(
1563 serde_yaml::Value::String("name".to_string()),
1564 serde_yaml::Value::String(column.name.clone()),
1565 );
1566
1567 properties.push(serde_yaml::Value::Mapping(prop));
1568 }
1569
1570 schema_obj.insert(
1571 serde_yaml::Value::String("properties".to_string()),
1572 serde_yaml::Value::Sequence(properties),
1573 );
1574
1575 schema_array.push(serde_yaml::Value::Mapping(schema_obj));
1576 yaml.insert(
1577 serde_yaml::Value::String("schema".to_string()),
1578 serde_yaml::Value::Sequence(schema_array),
1579 );
1580
1581 if !table.quality.is_empty() {
1583 let quality_array: Vec<serde_yaml::Value> = table
1584 .quality
1585 .iter()
1586 .map(|rule| {
1587 let mut rule_map = serde_yaml::Mapping::new();
1588 for (k, v) in rule {
1589 rule_map.insert(
1590 serde_yaml::Value::String(k.clone()),
1591 Self::json_to_yaml_value(v),
1592 );
1593 }
1594 serde_yaml::Value::Mapping(rule_map)
1595 })
1596 .collect();
1597 yaml.insert(
1598 serde_yaml::Value::String("quality".to_string()),
1599 serde_yaml::Value::Sequence(quality_array),
1600 );
1601 }
1602
1603 let excluded_keys = [
1605 "id",
1606 "version",
1607 "status",
1608 "domain",
1609 "dataProduct",
1610 "tenant",
1611 "description",
1612 "team",
1613 "roles",
1614 "pricing",
1615 "terms",
1616 "servers",
1617 "servicelevels",
1618 "links",
1619 "apiVersion",
1620 "kind",
1621 "info",
1622 "dataContractSpecification",
1623 ];
1624
1625 let mut custom_props = Vec::new();
1626 for (key, value) in &table.odcl_metadata {
1627 if !excluded_keys.contains(&key.as_str()) && !value.is_null() {
1628 let mut prop = serde_yaml::Mapping::new();
1629 prop.insert(
1630 serde_yaml::Value::String("property".to_string()),
1631 serde_yaml::Value::String(key.clone()),
1632 );
1633 prop.insert(
1634 serde_yaml::Value::String("value".to_string()),
1635 Self::json_to_yaml_value(value),
1636 );
1637 custom_props.push(serde_yaml::Value::Mapping(prop));
1638 }
1639 }
1640
1641 if let Some(ref db_type) = table.database_type {
1643 let mut prop = serde_yaml::Mapping::new();
1644 prop.insert(
1645 serde_yaml::Value::String("property".to_string()),
1646 serde_yaml::Value::String("databaseType".to_string()),
1647 );
1648 prop.insert(
1649 serde_yaml::Value::String("value".to_string()),
1650 serde_yaml::Value::String(format!("{:?}", db_type)),
1651 );
1652 custom_props.push(serde_yaml::Value::Mapping(prop));
1653 }
1654
1655 if !table.medallion_layers.is_empty() {
1657 let layers: Vec<serde_yaml::Value> = table
1658 .medallion_layers
1659 .iter()
1660 .map(|l| serde_yaml::Value::String(format!("{:?}", l)))
1661 .collect();
1662 let mut prop = serde_yaml::Mapping::new();
1663 prop.insert(
1664 serde_yaml::Value::String("property".to_string()),
1665 serde_yaml::Value::String("medallionLayers".to_string()),
1666 );
1667 prop.insert(
1668 serde_yaml::Value::String("value".to_string()),
1669 serde_yaml::Value::Sequence(layers),
1670 );
1671 custom_props.push(serde_yaml::Value::Mapping(prop));
1672 }
1673
1674 if let Some(ref scd_pattern) = table.scd_pattern {
1676 let mut prop = serde_yaml::Mapping::new();
1677 prop.insert(
1678 serde_yaml::Value::String("property".to_string()),
1679 serde_yaml::Value::String("scdPattern".to_string()),
1680 );
1681 prop.insert(
1682 serde_yaml::Value::String("value".to_string()),
1683 serde_yaml::Value::String(format!("{:?}", scd_pattern)),
1684 );
1685 custom_props.push(serde_yaml::Value::Mapping(prop));
1686 }
1687
1688 if let Some(ref dv_class) = table.data_vault_classification {
1690 let mut prop = serde_yaml::Mapping::new();
1691 prop.insert(
1692 serde_yaml::Value::String("property".to_string()),
1693 serde_yaml::Value::String("dataVaultClassification".to_string()),
1694 );
1695 prop.insert(
1696 serde_yaml::Value::String("value".to_string()),
1697 serde_yaml::Value::String(format!("{:?}", dv_class)),
1698 );
1699 custom_props.push(serde_yaml::Value::Mapping(prop));
1700 }
1701
1702 if let Some(ref catalog) = table.catalog_name {
1704 let mut prop = serde_yaml::Mapping::new();
1705 prop.insert(
1706 serde_yaml::Value::String("property".to_string()),
1707 serde_yaml::Value::String("catalogName".to_string()),
1708 );
1709 prop.insert(
1710 serde_yaml::Value::String("value".to_string()),
1711 serde_yaml::Value::String(catalog.clone()),
1712 );
1713 custom_props.push(serde_yaml::Value::Mapping(prop));
1714 }
1715
1716 if let Some(ref schema) = table.schema_name {
1717 let mut prop = serde_yaml::Mapping::new();
1718 prop.insert(
1719 serde_yaml::Value::String("property".to_string()),
1720 serde_yaml::Value::String("schemaName".to_string()),
1721 );
1722 prop.insert(
1723 serde_yaml::Value::String("value".to_string()),
1724 serde_yaml::Value::String(schema.clone()),
1725 );
1726 custom_props.push(serde_yaml::Value::Mapping(prop));
1727 }
1728
1729 if !custom_props.is_empty() {
1730 yaml.insert(
1731 serde_yaml::Value::String("customProperties".to_string()),
1732 serde_yaml::Value::Sequence(custom_props),
1733 );
1734 }
1735
1736 yaml.insert(
1738 serde_yaml::Value::String("contractCreatedTs".to_string()),
1739 serde_yaml::Value::String(table.created_at.to_rfc3339()),
1740 );
1741
1742 serde_yaml::to_string(&yaml).unwrap_or_default()
1743 }
1744
1745 pub fn export(
1747 &self,
1748 tables: &[Table],
1749 _format: &str,
1750 ) -> Result<HashMap<String, ExportResult>, ExportError> {
1751 let mut exports = HashMap::new();
1752 for table in tables {
1753 let yaml = Self::export_odcs_v3_1_0_format(table);
1755
1756 #[cfg(feature = "schema-validation")]
1758 {
1759 use crate::validation::schema::validate_odcs_internal;
1760 validate_odcs_internal(&yaml).map_err(|e| {
1761 ExportError::ValidationError(format!("ODCS validation failed: {}", e))
1762 })?;
1763 }
1764
1765 exports.insert(
1766 table.name.clone(),
1767 ExportResult {
1768 content: yaml,
1769 format: "odcs_v3_1_0".to_string(),
1770 },
1771 );
1772 }
1773 Ok(exports)
1774 }
1775
1776 pub fn export_model(
1778 model: &DataModel,
1779 table_ids: Option<&[uuid::Uuid]>,
1780 _format: &str,
1781 ) -> HashMap<String, String> {
1782 let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
1783 model
1784 .tables
1785 .iter()
1786 .filter(|t| ids.contains(&t.id))
1787 .collect()
1788 } else {
1789 model.tables.iter().collect()
1790 };
1791
1792 let mut exports = HashMap::new();
1793 for table in tables_to_export {
1794 let yaml = Self::export_odcs_v3_1_0_format(table);
1796 exports.insert(table.name.clone(), yaml);
1797 }
1798
1799 exports
1800 }
1801}
1802
1803#[cfg(test)]
1804mod tests {
1805 use super::*;
1806 use crate::models::{Column, Tag};
1807
1808 #[test]
1809 fn test_export_odcs_v3_1_0_basic() {
1810 let table = Table {
1811 id: Table::generate_id("test_table", None, None, None),
1812 name: "test_table".to_string(),
1813 columns: vec![Column {
1814 name: "id".to_string(),
1815 data_type: "BIGINT".to_string(),
1816 nullable: false,
1817 primary_key: true,
1818 description: "Primary key".to_string(),
1819 ..Default::default()
1820 }],
1821 database_type: None,
1822 catalog_name: None,
1823 schema_name: None,
1824 medallion_layers: Vec::new(),
1825 scd_pattern: None,
1826 data_vault_classification: None,
1827 modeling_level: None,
1828 tags: vec![Tag::Simple("test".to_string())],
1829 odcl_metadata: HashMap::new(),
1830 owner: None,
1831 sla: None,
1832 contact_details: None,
1833 infrastructure_type: None,
1834 notes: None,
1835 position: None,
1836 yaml_file_path: None,
1837 drawio_cell_id: None,
1838 quality: Vec::new(),
1839 errors: Vec::new(),
1840 created_at: chrono::Utc::now(),
1841 updated_at: chrono::Utc::now(),
1842 };
1843
1844 let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
1845
1846 assert!(yaml.contains("apiVersion: v3.1.0"));
1847 assert!(yaml.contains("kind: DataContract"));
1848 assert!(yaml.contains("name: test_table"));
1849 assert!(yaml.contains("tags:"));
1850 assert!(yaml.contains("- test"));
1851 }
1852}