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 biz_name) = column.business_name {
1530 prop.insert(
1531 serde_yaml::Value::String("businessName".to_string()),
1532 serde_yaml::Value::String(biz_name.clone()),
1533 );
1534 }
1535
1536 if let Some(ref phys_name) = column.physical_name {
1538 prop.insert(
1539 serde_yaml::Value::String("physicalName".to_string()),
1540 serde_yaml::Value::String(phys_name.clone()),
1541 );
1542 }
1543
1544 if let Some(ref opts) = column.logical_type_options
1546 && !opts.is_empty()
1547 {
1548 let mut opts_map = serde_yaml::Mapping::new();
1549 if let Some(min_len) = opts.min_length {
1550 opts_map.insert(
1551 serde_yaml::Value::String("minLength".to_string()),
1552 serde_yaml::Value::Number(serde_yaml::Number::from(min_len)),
1553 );
1554 }
1555 if let Some(max_len) = opts.max_length {
1556 opts_map.insert(
1557 serde_yaml::Value::String("maxLength".to_string()),
1558 serde_yaml::Value::Number(serde_yaml::Number::from(max_len)),
1559 );
1560 }
1561 if let Some(ref pattern) = opts.pattern {
1562 opts_map.insert(
1563 serde_yaml::Value::String("pattern".to_string()),
1564 serde_yaml::Value::String(pattern.clone()),
1565 );
1566 }
1567 if let Some(ref format) = opts.format {
1568 opts_map.insert(
1569 serde_yaml::Value::String("format".to_string()),
1570 serde_yaml::Value::String(format.clone()),
1571 );
1572 }
1573 if let Some(ref minimum) = opts.minimum {
1574 opts_map.insert(
1575 serde_yaml::Value::String("minimum".to_string()),
1576 Self::json_to_yaml_value(minimum),
1577 );
1578 }
1579 if let Some(ref maximum) = opts.maximum {
1580 opts_map.insert(
1581 serde_yaml::Value::String("maximum".to_string()),
1582 Self::json_to_yaml_value(maximum),
1583 );
1584 }
1585 if let Some(ref exc_min) = opts.exclusive_minimum {
1586 opts_map.insert(
1587 serde_yaml::Value::String("exclusiveMinimum".to_string()),
1588 Self::json_to_yaml_value(exc_min),
1589 );
1590 }
1591 if let Some(ref exc_max) = opts.exclusive_maximum {
1592 opts_map.insert(
1593 serde_yaml::Value::String("exclusiveMaximum".to_string()),
1594 Self::json_to_yaml_value(exc_max),
1595 );
1596 }
1597 if let Some(precision) = opts.precision {
1598 opts_map.insert(
1599 serde_yaml::Value::String("precision".to_string()),
1600 serde_yaml::Value::Number(serde_yaml::Number::from(precision)),
1601 );
1602 }
1603 if let Some(scale) = opts.scale {
1604 opts_map.insert(
1605 serde_yaml::Value::String("scale".to_string()),
1606 serde_yaml::Value::Number(serde_yaml::Number::from(scale)),
1607 );
1608 }
1609 if !opts_map.is_empty() {
1610 prop.insert(
1611 serde_yaml::Value::String("logicalTypeOptions".to_string()),
1612 serde_yaml::Value::Mapping(opts_map),
1613 );
1614 }
1615 }
1616
1617 if let Some(pk_pos) = column.primary_key_position {
1619 prop.insert(
1620 serde_yaml::Value::String("primaryKeyPosition".to_string()),
1621 serde_yaml::Value::Number(serde_yaml::Number::from(pk_pos)),
1622 );
1623 }
1624
1625 if column.unique {
1627 prop.insert(
1628 serde_yaml::Value::String("unique".to_string()),
1629 serde_yaml::Value::Bool(true),
1630 );
1631 }
1632
1633 if column.partitioned {
1635 prop.insert(
1636 serde_yaml::Value::String("partitioned".to_string()),
1637 serde_yaml::Value::Bool(true),
1638 );
1639 }
1640
1641 if let Some(part_pos) = column.partition_key_position {
1643 prop.insert(
1644 serde_yaml::Value::String("partitionKeyPosition".to_string()),
1645 serde_yaml::Value::Number(serde_yaml::Number::from(part_pos)),
1646 );
1647 }
1648
1649 if let Some(ref class) = column.classification {
1651 prop.insert(
1652 serde_yaml::Value::String("classification".to_string()),
1653 serde_yaml::Value::String(class.clone()),
1654 );
1655 }
1656
1657 if column.critical_data_element {
1659 prop.insert(
1660 serde_yaml::Value::String("criticalDataElement".to_string()),
1661 serde_yaml::Value::Bool(true),
1662 );
1663 }
1664
1665 if let Some(ref enc_name) = column.encrypted_name {
1667 prop.insert(
1668 serde_yaml::Value::String("encryptedName".to_string()),
1669 serde_yaml::Value::String(enc_name.clone()),
1670 );
1671 }
1672
1673 if !column.transform_source_objects.is_empty() {
1675 let sources: Vec<serde_yaml::Value> = column
1676 .transform_source_objects
1677 .iter()
1678 .map(|s| serde_yaml::Value::String(s.clone()))
1679 .collect();
1680 prop.insert(
1681 serde_yaml::Value::String("transformSourceObjects".to_string()),
1682 serde_yaml::Value::Sequence(sources),
1683 );
1684 }
1685
1686 if let Some(ref logic) = column.transform_logic {
1688 prop.insert(
1689 serde_yaml::Value::String("transformLogic".to_string()),
1690 serde_yaml::Value::String(logic.clone()),
1691 );
1692 }
1693
1694 if let Some(ref desc) = column.transform_description {
1696 prop.insert(
1697 serde_yaml::Value::String("transformDescription".to_string()),
1698 serde_yaml::Value::String(desc.clone()),
1699 );
1700 }
1701
1702 if !column.examples.is_empty() {
1704 let examples: Vec<serde_yaml::Value> = column
1705 .examples
1706 .iter()
1707 .map(Self::json_to_yaml_value)
1708 .collect();
1709 prop.insert(
1710 serde_yaml::Value::String("examples".to_string()),
1711 serde_yaml::Value::Sequence(examples),
1712 );
1713 }
1714
1715 if !column.authoritative_definitions.is_empty() {
1717 let defs: Vec<serde_yaml::Value> = column
1718 .authoritative_definitions
1719 .iter()
1720 .map(|d| {
1721 let mut def_map = serde_yaml::Mapping::new();
1722 def_map.insert(
1723 serde_yaml::Value::String("type".to_string()),
1724 serde_yaml::Value::String(d.definition_type.clone()),
1725 );
1726 def_map.insert(
1727 serde_yaml::Value::String("url".to_string()),
1728 serde_yaml::Value::String(d.url.clone()),
1729 );
1730 serde_yaml::Value::Mapping(def_map)
1731 })
1732 .collect();
1733 prop.insert(
1734 serde_yaml::Value::String("authoritativeDefinitions".to_string()),
1735 serde_yaml::Value::Sequence(defs),
1736 );
1737 }
1738
1739 if !column.tags.is_empty() {
1741 let tags: Vec<serde_yaml::Value> = column
1742 .tags
1743 .iter()
1744 .map(|t| serde_yaml::Value::String(t.clone()))
1745 .collect();
1746 prop.insert(
1747 serde_yaml::Value::String("tags".to_string()),
1748 serde_yaml::Value::Sequence(tags),
1749 );
1750 }
1751
1752 if !column.custom_properties.is_empty() {
1754 let custom_props: Vec<serde_yaml::Value> = column
1755 .custom_properties
1756 .iter()
1757 .map(|(key, value)| {
1758 let mut prop_map = serde_yaml::Mapping::new();
1759 prop_map.insert(
1760 serde_yaml::Value::String("property".to_string()),
1761 serde_yaml::Value::String(key.clone()),
1762 );
1763 prop_map.insert(
1764 serde_yaml::Value::String("value".to_string()),
1765 Self::json_to_yaml_value(value),
1766 );
1767 serde_yaml::Value::Mapping(prop_map)
1768 })
1769 .collect();
1770 prop.insert(
1771 serde_yaml::Value::String("customProperties".to_string()),
1772 serde_yaml::Value::Sequence(custom_props),
1773 );
1774 }
1775
1776 if let Some(ref fk) = column.foreign_key {
1778 let mut fk_map = serde_yaml::Mapping::new();
1779 fk_map.insert(
1780 serde_yaml::Value::String("table".to_string()),
1781 serde_yaml::Value::String(fk.table_id.clone()),
1782 );
1783 fk_map.insert(
1784 serde_yaml::Value::String("column".to_string()),
1785 serde_yaml::Value::String(fk.column_name.clone()),
1786 );
1787 prop.insert(
1788 serde_yaml::Value::String("foreignKey".to_string()),
1789 serde_yaml::Value::Mapping(fk_map),
1790 );
1791 }
1792
1793 let id = column
1796 .name
1797 .chars()
1798 .map(|c| {
1799 if c.is_alphanumeric() {
1800 c.to_lowercase().to_string()
1801 } else {
1802 "_".to_string()
1803 }
1804 })
1805 .collect::<String>()
1806 .replace("__", "_");
1807
1808 prop.insert(
1809 serde_yaml::Value::String("id".to_string()),
1810 serde_yaml::Value::String(format!("{}_obj", id)),
1811 );
1812 prop.insert(
1813 serde_yaml::Value::String("name".to_string()),
1814 serde_yaml::Value::String(column.name.clone()),
1815 );
1816
1817 properties.push(serde_yaml::Value::Mapping(prop));
1818 }
1819
1820 schema_obj.insert(
1821 serde_yaml::Value::String("properties".to_string()),
1822 serde_yaml::Value::Sequence(properties),
1823 );
1824
1825 schema_array.push(serde_yaml::Value::Mapping(schema_obj));
1826 yaml.insert(
1827 serde_yaml::Value::String("schema".to_string()),
1828 serde_yaml::Value::Sequence(schema_array),
1829 );
1830
1831 if !table.quality.is_empty() {
1833 let quality_array: Vec<serde_yaml::Value> = table
1834 .quality
1835 .iter()
1836 .map(|rule| {
1837 let mut rule_map = serde_yaml::Mapping::new();
1838 for (k, v) in rule {
1839 rule_map.insert(
1840 serde_yaml::Value::String(k.clone()),
1841 Self::json_to_yaml_value(v),
1842 );
1843 }
1844 serde_yaml::Value::Mapping(rule_map)
1845 })
1846 .collect();
1847 yaml.insert(
1848 serde_yaml::Value::String("quality".to_string()),
1849 serde_yaml::Value::Sequence(quality_array),
1850 );
1851 }
1852
1853 let excluded_keys = [
1855 "id",
1856 "version",
1857 "status",
1858 "domain",
1859 "dataProduct",
1860 "tenant",
1861 "description",
1862 "team",
1863 "roles",
1864 "pricing",
1865 "terms",
1866 "servers",
1867 "servicelevels",
1868 "links",
1869 "apiVersion",
1870 "kind",
1871 "info",
1872 "dataContractSpecification",
1873 ];
1874
1875 let mut custom_props = Vec::new();
1876 for (key, value) in &table.odcl_metadata {
1877 if !excluded_keys.contains(&key.as_str()) && !value.is_null() {
1878 let mut prop = serde_yaml::Mapping::new();
1879 prop.insert(
1880 serde_yaml::Value::String("property".to_string()),
1881 serde_yaml::Value::String(key.clone()),
1882 );
1883 prop.insert(
1884 serde_yaml::Value::String("value".to_string()),
1885 Self::json_to_yaml_value(value),
1886 );
1887 custom_props.push(serde_yaml::Value::Mapping(prop));
1888 }
1889 }
1890
1891 if let Some(ref db_type) = table.database_type {
1893 let mut prop = serde_yaml::Mapping::new();
1894 prop.insert(
1895 serde_yaml::Value::String("property".to_string()),
1896 serde_yaml::Value::String("databaseType".to_string()),
1897 );
1898 prop.insert(
1899 serde_yaml::Value::String("value".to_string()),
1900 serde_yaml::Value::String(format!("{:?}", db_type)),
1901 );
1902 custom_props.push(serde_yaml::Value::Mapping(prop));
1903 }
1904
1905 if !table.medallion_layers.is_empty() {
1907 let layers: Vec<serde_yaml::Value> = table
1908 .medallion_layers
1909 .iter()
1910 .map(|l| serde_yaml::Value::String(format!("{:?}", l)))
1911 .collect();
1912 let mut prop = serde_yaml::Mapping::new();
1913 prop.insert(
1914 serde_yaml::Value::String("property".to_string()),
1915 serde_yaml::Value::String("medallionLayers".to_string()),
1916 );
1917 prop.insert(
1918 serde_yaml::Value::String("value".to_string()),
1919 serde_yaml::Value::Sequence(layers),
1920 );
1921 custom_props.push(serde_yaml::Value::Mapping(prop));
1922 }
1923
1924 if let Some(ref scd_pattern) = table.scd_pattern {
1926 let mut prop = serde_yaml::Mapping::new();
1927 prop.insert(
1928 serde_yaml::Value::String("property".to_string()),
1929 serde_yaml::Value::String("scdPattern".to_string()),
1930 );
1931 prop.insert(
1932 serde_yaml::Value::String("value".to_string()),
1933 serde_yaml::Value::String(format!("{:?}", scd_pattern)),
1934 );
1935 custom_props.push(serde_yaml::Value::Mapping(prop));
1936 }
1937
1938 if let Some(ref dv_class) = table.data_vault_classification {
1940 let mut prop = serde_yaml::Mapping::new();
1941 prop.insert(
1942 serde_yaml::Value::String("property".to_string()),
1943 serde_yaml::Value::String("dataVaultClassification".to_string()),
1944 );
1945 prop.insert(
1946 serde_yaml::Value::String("value".to_string()),
1947 serde_yaml::Value::String(format!("{:?}", dv_class)),
1948 );
1949 custom_props.push(serde_yaml::Value::Mapping(prop));
1950 }
1951
1952 if let Some(ref catalog) = table.catalog_name {
1954 let mut prop = serde_yaml::Mapping::new();
1955 prop.insert(
1956 serde_yaml::Value::String("property".to_string()),
1957 serde_yaml::Value::String("catalogName".to_string()),
1958 );
1959 prop.insert(
1960 serde_yaml::Value::String("value".to_string()),
1961 serde_yaml::Value::String(catalog.clone()),
1962 );
1963 custom_props.push(serde_yaml::Value::Mapping(prop));
1964 }
1965
1966 if let Some(ref schema) = table.schema_name {
1967 let mut prop = serde_yaml::Mapping::new();
1968 prop.insert(
1969 serde_yaml::Value::String("property".to_string()),
1970 serde_yaml::Value::String("schemaName".to_string()),
1971 );
1972 prop.insert(
1973 serde_yaml::Value::String("value".to_string()),
1974 serde_yaml::Value::String(schema.clone()),
1975 );
1976 custom_props.push(serde_yaml::Value::Mapping(prop));
1977 }
1978
1979 if !custom_props.is_empty() {
1980 yaml.insert(
1981 serde_yaml::Value::String("customProperties".to_string()),
1982 serde_yaml::Value::Sequence(custom_props),
1983 );
1984 }
1985
1986 yaml.insert(
1988 serde_yaml::Value::String("contractCreatedTs".to_string()),
1989 serde_yaml::Value::String(table.created_at.to_rfc3339()),
1990 );
1991
1992 serde_yaml::to_string(&yaml).unwrap_or_default()
1993 }
1994
1995 pub fn export(
1997 &self,
1998 tables: &[Table],
1999 _format: &str,
2000 ) -> Result<HashMap<String, ExportResult>, ExportError> {
2001 let mut exports = HashMap::new();
2002 for table in tables {
2003 let yaml = Self::export_odcs_v3_1_0_format(table);
2005
2006 #[cfg(feature = "schema-validation")]
2008 {
2009 use crate::validation::schema::validate_odcs_internal;
2010 validate_odcs_internal(&yaml).map_err(|e| {
2011 ExportError::ValidationError(format!("ODCS validation failed: {}", e))
2012 })?;
2013 }
2014
2015 exports.insert(
2016 table.name.clone(),
2017 ExportResult {
2018 content: yaml,
2019 format: "odcs_v3_1_0".to_string(),
2020 },
2021 );
2022 }
2023 Ok(exports)
2024 }
2025
2026 pub fn export_model(
2028 model: &DataModel,
2029 table_ids: Option<&[uuid::Uuid]>,
2030 _format: &str,
2031 ) -> HashMap<String, String> {
2032 let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
2033 model
2034 .tables
2035 .iter()
2036 .filter(|t| ids.contains(&t.id))
2037 .collect()
2038 } else {
2039 model.tables.iter().collect()
2040 };
2041
2042 let mut exports = HashMap::new();
2043 for table in tables_to_export {
2044 let yaml = Self::export_odcs_v3_1_0_format(table);
2046 exports.insert(table.name.clone(), yaml);
2047 }
2048
2049 exports
2050 }
2051}
2052
2053#[cfg(test)]
2054mod tests {
2055 use super::*;
2056 use crate::models::{Column, Tag};
2057
2058 #[test]
2059 fn test_export_odcs_v3_1_0_basic() {
2060 let table = Table {
2061 id: Table::generate_id("test_table", None, None, None),
2062 name: "test_table".to_string(),
2063 columns: vec![Column {
2064 name: "id".to_string(),
2065 data_type: "BIGINT".to_string(),
2066 nullable: false,
2067 primary_key: true,
2068 description: "Primary key".to_string(),
2069 ..Default::default()
2070 }],
2071 database_type: None,
2072 catalog_name: None,
2073 schema_name: None,
2074 medallion_layers: Vec::new(),
2075 scd_pattern: None,
2076 data_vault_classification: None,
2077 modeling_level: None,
2078 tags: vec![Tag::Simple("test".to_string())],
2079 odcl_metadata: HashMap::new(),
2080 owner: None,
2081 sla: None,
2082 contact_details: None,
2083 infrastructure_type: None,
2084 notes: None,
2085 position: None,
2086 yaml_file_path: None,
2087 drawio_cell_id: None,
2088 quality: Vec::new(),
2089 errors: Vec::new(),
2090 created_at: chrono::Utc::now(),
2091 updated_at: chrono::Utc::now(),
2092 };
2093
2094 let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
2095
2096 assert!(yaml.contains("apiVersion: v3.1.0"));
2097 assert!(yaml.contains("kind: DataContract"));
2098 assert!(yaml.contains("name: test_table"));
2099 assert!(yaml.contains("tags:"));
2100 assert!(yaml.contains("- test"));
2101 }
2102}