data_modelling_sdk/export/
odcs.rs

1//! ODCS exporter for generating ODCS v3.1.0 YAML from data models.
2//!
3//! This module exports data models to ODCS (Open Data Contract Standard) v3.1.0 format only.
4//! Legacy ODCL formats are no longer supported for export.
5
6use super::{ExportError, ExportResult};
7use crate::models::{DataModel, Table};
8use serde_yaml;
9use std::collections::HashMap;
10
11/// Exporter for ODCS (Open Data Contract Standard) v3.1.0 YAML format.
12pub struct ODCSExporter;
13
14impl ODCSExporter {
15    /// Export a table to ODCS v3.1.0 YAML format.
16    ///
17    /// Note: Only ODCS v3.1.0 format is supported. Legacy formats have been removed.
18    ///
19    /// # Arguments
20    ///
21    /// * `table` - The table to export
22    /// * `_format` - Format parameter (ignored, always uses ODCS v3.1.0)
23    ///
24    /// # Returns
25    ///
26    /// A YAML string in ODCS v3.1.0 format.
27    ///
28    /// # Example
29    ///
30    /// ```rust
31    /// use data_modelling_sdk::export::odcs::ODCSExporter;
32    /// use data_modelling_sdk::models::{Table, Column};
33    ///
34    /// let table = Table::new(
35    ///     "users".to_string(),
36    ///     vec![Column::new("id".to_string(), "BIGINT".to_string())],
37    /// );
38    ///
39    /// let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
40    /// assert!(yaml.contains("apiVersion: v3.1.0"));
41    /// assert!(yaml.contains("kind: DataContract"));
42    /// ```
43    pub fn export_table(table: &Table, _format: &str) -> String {
44        // All exports use ODCS v3.1.0 format
45        Self::export_odcs_v3_1_0_format(table)
46    }
47
48    /// Helper to convert serde_json::Value to serde_yaml::Value
49    fn json_to_yaml_value(json: &serde_json::Value) -> serde_yaml::Value {
50        match json {
51            serde_json::Value::Null => serde_yaml::Value::Null,
52            serde_json::Value::Bool(b) => serde_yaml::Value::Bool(*b),
53            serde_json::Value::Number(n) => {
54                if let Some(i) = n.as_i64() {
55                    serde_yaml::Value::Number(serde_yaml::Number::from(i))
56                } else if let Some(f) = n.as_f64() {
57                    serde_yaml::Value::Number(serde_yaml::Number::from(f))
58                } else {
59                    serde_yaml::Value::String(n.to_string())
60                }
61            }
62            serde_json::Value::String(s) => serde_yaml::Value::String(s.clone()),
63            serde_json::Value::Array(arr) => {
64                let yaml_arr: Vec<serde_yaml::Value> =
65                    arr.iter().map(Self::json_to_yaml_value).collect();
66                serde_yaml::Value::Sequence(yaml_arr)
67            }
68            serde_json::Value::Object(obj) => {
69                let mut yaml_map = serde_yaml::Mapping::new();
70                for (k, v) in obj {
71                    yaml_map.insert(
72                        serde_yaml::Value::String(k.clone()),
73                        Self::json_to_yaml_value(v),
74                    );
75                }
76                serde_yaml::Value::Mapping(yaml_map)
77            }
78        }
79    }
80
81    /// Export in ODCS v3.1.0 format (the only supported export format).
82    fn export_odcs_v3_1_0_format(table: &Table) -> String {
83        let mut yaml = serde_yaml::Mapping::new();
84
85        // Required ODCS v3.1.0 fields
86        yaml.insert(
87            serde_yaml::Value::String("apiVersion".to_string()),
88            serde_yaml::Value::String("v3.1.0".to_string()),
89        );
90        yaml.insert(
91            serde_yaml::Value::String("kind".to_string()),
92            serde_yaml::Value::String("DataContract".to_string()),
93        );
94
95        // ID - use table UUID (ODCS spec: "A unique identifier used to reduce the risk of dataset name collisions, such as a UUID.")
96        yaml.insert(
97            serde_yaml::Value::String("id".to_string()),
98            serde_yaml::Value::String(table.id.to_string()),
99        );
100
101        // Name
102        yaml.insert(
103            serde_yaml::Value::String("name".to_string()),
104            serde_yaml::Value::String(table.name.clone()),
105        );
106
107        // Version - from metadata or default
108        let version = table
109            .odcl_metadata
110            .get("version")
111            .and_then(|v| v.as_str())
112            .map(|s| s.to_string())
113            .unwrap_or_else(|| "1.0.0".to_string());
114        yaml.insert(
115            serde_yaml::Value::String("version".to_string()),
116            serde_yaml::Value::String(version),
117        );
118
119        // Status - from metadata or default
120        if let Some(status) = table.odcl_metadata.get("status")
121            && !status.is_null()
122        {
123            yaml.insert(
124                serde_yaml::Value::String("status".to_string()),
125                Self::json_to_yaml_value(status),
126            );
127        }
128
129        // Domain - from metadata
130        if let Some(domain) = table.odcl_metadata.get("domain")
131            && !domain.is_null()
132        {
133            yaml.insert(
134                serde_yaml::Value::String("domain".to_string()),
135                Self::json_to_yaml_value(domain),
136            );
137        }
138
139        // Data Product - from metadata
140        if let Some(data_product) = table.odcl_metadata.get("dataProduct")
141            && !data_product.is_null()
142        {
143            yaml.insert(
144                serde_yaml::Value::String("dataProduct".to_string()),
145                Self::json_to_yaml_value(data_product),
146            );
147        }
148
149        // Tenant - from metadata
150        if let Some(tenant) = table.odcl_metadata.get("tenant")
151            && !tenant.is_null()
152        {
153            yaml.insert(
154                serde_yaml::Value::String("tenant".to_string()),
155                Self::json_to_yaml_value(tenant),
156            );
157        }
158
159        // Description - from metadata (can be object or string)
160        if let Some(description) = table.odcl_metadata.get("description")
161            && !description.is_null()
162        {
163            yaml.insert(
164                serde_yaml::Value::String("description".to_string()),
165                Self::json_to_yaml_value(description),
166            );
167        }
168
169        // Tags
170        if !table.tags.is_empty() {
171            let tags_yaml: Vec<serde_yaml::Value> = table
172                .tags
173                .iter()
174                .map(|t| serde_yaml::Value::String(t.clone()))
175                .collect();
176            yaml.insert(
177                serde_yaml::Value::String("tags".to_string()),
178                serde_yaml::Value::Sequence(tags_yaml),
179            );
180        }
181
182        // Team - from metadata
183        if let Some(team) = table.odcl_metadata.get("team")
184            && !team.is_null()
185        {
186            yaml.insert(
187                serde_yaml::Value::String("team".to_string()),
188                Self::json_to_yaml_value(team),
189            );
190        }
191
192        // Roles - from metadata
193        if let Some(roles) = table.odcl_metadata.get("roles")
194            && !roles.is_null()
195        {
196            yaml.insert(
197                serde_yaml::Value::String("roles".to_string()),
198                Self::json_to_yaml_value(roles),
199            );
200        }
201
202        // Pricing - from metadata (ODCS uses "price")
203        if let Some(pricing) = table.odcl_metadata.get("pricing")
204            && !pricing.is_null()
205        {
206            yaml.insert(
207                serde_yaml::Value::String("price".to_string()),
208                Self::json_to_yaml_value(pricing),
209            );
210        }
211
212        // Terms - from metadata
213        if let Some(terms) = table.odcl_metadata.get("terms")
214            && !terms.is_null()
215        {
216            yaml.insert(
217                serde_yaml::Value::String("terms".to_string()),
218                Self::json_to_yaml_value(terms),
219            );
220        }
221
222        // Servers - from metadata
223        if let Some(servers) = table.odcl_metadata.get("servers")
224            && !servers.is_null()
225        {
226            yaml.insert(
227                serde_yaml::Value::String("servers".to_string()),
228                Self::json_to_yaml_value(servers),
229            );
230        }
231
232        // Service Levels - from metadata
233        if let Some(servicelevels) = table.odcl_metadata.get("servicelevels")
234            && !servicelevels.is_null()
235        {
236            yaml.insert(
237                serde_yaml::Value::String("servicelevels".to_string()),
238                Self::json_to_yaml_value(servicelevels),
239            );
240        }
241
242        // Links - from metadata
243        if let Some(links) = table.odcl_metadata.get("links")
244            && !links.is_null()
245        {
246            yaml.insert(
247                serde_yaml::Value::String("links".to_string()),
248                Self::json_to_yaml_value(links),
249            );
250        }
251
252        // Infrastructure - from metadata
253        if let Some(infrastructure) = table.odcl_metadata.get("infrastructure")
254            && !infrastructure.is_null()
255        {
256            yaml.insert(
257                serde_yaml::Value::String("infrastructure".to_string()),
258                Self::json_to_yaml_value(infrastructure),
259            );
260        }
261
262        // Schema array (ODCS v3.1.0 uses array of SchemaObject)
263        let mut schema_array = Vec::new();
264        let mut schema_obj = serde_yaml::Mapping::new();
265
266        schema_obj.insert(
267            serde_yaml::Value::String("name".to_string()),
268            serde_yaml::Value::String(table.name.clone()),
269        );
270
271        // Build properties from columns
272        let mut properties = serde_yaml::Mapping::new();
273
274        // Helper function to build nested properties structure
275        fn build_nested_properties(
276            parent_name: &str,
277            all_columns: &[crate::models::Column],
278            json_to_yaml_fn: &dyn Fn(&serde_json::Value) -> serde_yaml::Value,
279        ) -> Option<serde_yaml::Mapping> {
280            let parent_prefix = format!("{}.", parent_name);
281            let nested_columns: Vec<&crate::models::Column> = all_columns
282                .iter()
283                .filter(|col| col.name.starts_with(&parent_prefix))
284                .collect();
285
286            if nested_columns.is_empty() {
287                return None;
288            }
289
290            let mut nested_props = serde_yaml::Mapping::new();
291
292            // Group nested columns by their immediate child name (first level only)
293            let mut child_map: std::collections::HashMap<String, Vec<&crate::models::Column>> =
294                std::collections::HashMap::new();
295
296            for nested_col in &nested_columns {
297                // Safety: skip columns that don't start with the expected prefix
298                let Some(relative_name) = nested_col.name.strip_prefix(&parent_prefix) else {
299                    continue;
300                };
301                if let Some(dot_pos) = relative_name.find('.') {
302                    let child_name = &relative_name[..dot_pos];
303                    child_map
304                        .entry(child_name.to_string())
305                        .or_default()
306                        .push(nested_col);
307                } else {
308                    // Direct child - add to map
309                    child_map
310                        .entry(relative_name.to_string())
311                        .or_default()
312                        .push(nested_col);
313                }
314            }
315
316            // Build properties for each child
317            for (child_name, child_cols) in child_map {
318                // Find the direct child column (no dots in relative name)
319                let direct_child = child_cols.iter().find(|col| {
320                    col.name
321                        .strip_prefix(&parent_prefix)
322                        .map(|rel_name| !rel_name.contains('.'))
323                        .unwrap_or(false)
324                });
325
326                if let Some(child_col) = direct_child {
327                    let mut child_prop = serde_yaml::Mapping::new();
328
329                    // Check if this child has nested children
330                    let child_has_nested = child_cols.iter().any(|col| {
331                        col.name
332                            .strip_prefix(&parent_prefix)
333                            .map(|rel| {
334                                rel.starts_with(&format!("{}.", child_name)) && rel != child_name
335                            })
336                            .unwrap_or(false)
337                    });
338
339                    // Handle ARRAY<OBJECT> or ARRAY<STRUCT> types
340                    let data_type_upper = child_col.data_type.to_uppercase();
341                    let is_array_object = data_type_upper.starts_with("ARRAY<")
342                        && (data_type_upper.contains("OBJECT")
343                            || data_type_upper.contains("STRUCT"));
344                    let is_struct_or_object = data_type_upper == "STRUCT"
345                        || data_type_upper == "OBJECT"
346                        || data_type_upper.starts_with("STRUCT<");
347
348                    // Try to build nested properties first (regardless of type)
349                    let nested_props_map =
350                        build_nested_properties(&child_col.name, all_columns, json_to_yaml_fn);
351
352                    if is_array_object && (child_has_nested || nested_props_map.is_some()) {
353                        // ARRAY<OBJECT> with nested fields
354                        child_prop.insert(
355                            serde_yaml::Value::String("type".to_string()),
356                            serde_yaml::Value::String("array".to_string()),
357                        );
358
359                        let mut items = serde_yaml::Mapping::new();
360                        items.insert(
361                            serde_yaml::Value::String("type".to_string()),
362                            serde_yaml::Value::String("object".to_string()),
363                        );
364
365                        // Add nested properties if they exist
366                        if let Some(nested_props) = nested_props_map {
367                            items.insert(
368                                serde_yaml::Value::String("properties".to_string()),
369                                serde_yaml::Value::Mapping(nested_props),
370                            );
371                        }
372
373                        child_prop.insert(
374                            serde_yaml::Value::String("items".to_string()),
375                            serde_yaml::Value::Mapping(items),
376                        );
377                    } else if is_struct_or_object || child_has_nested || nested_props_map.is_some()
378                    {
379                        // OBJECT/STRUCT with nested properties, or any column with nested children
380                        child_prop.insert(
381                            serde_yaml::Value::String("type".to_string()),
382                            serde_yaml::Value::String("object".to_string()),
383                        );
384
385                        // Add nested properties if they exist
386                        if let Some(nested_props) = nested_props_map {
387                            child_prop.insert(
388                                serde_yaml::Value::String("properties".to_string()),
389                                serde_yaml::Value::Mapping(nested_props),
390                            );
391                        }
392                    } else {
393                        // Simple field
394                        child_prop.insert(
395                            serde_yaml::Value::String("type".to_string()),
396                            serde_yaml::Value::String(child_col.data_type.clone().to_lowercase()),
397                        );
398                    }
399
400                    if !child_col.nullable {
401                        child_prop.insert(
402                            serde_yaml::Value::String("required".to_string()),
403                            serde_yaml::Value::Bool(true),
404                        );
405                    }
406
407                    if !child_col.description.is_empty() {
408                        child_prop.insert(
409                            serde_yaml::Value::String("description".to_string()),
410                            serde_yaml::Value::String(child_col.description.clone()),
411                        );
412                    }
413
414                    // Export column-level quality rules for nested columns
415                    if !child_col.quality.is_empty() {
416                        let quality_array: Vec<serde_yaml::Value> = child_col
417                            .quality
418                            .iter()
419                            .map(|rule| {
420                                let mut rule_map = serde_yaml::Mapping::new();
421                                for (k, v) in rule {
422                                    rule_map.insert(
423                                        serde_yaml::Value::String(k.clone()),
424                                        json_to_yaml_fn(v),
425                                    );
426                                }
427                                serde_yaml::Value::Mapping(rule_map)
428                            })
429                            .collect();
430                        child_prop.insert(
431                            serde_yaml::Value::String("quality".to_string()),
432                            serde_yaml::Value::Sequence(quality_array),
433                        );
434                    }
435
436                    // Export enum values for nested columns
437                    if !child_col.enum_values.is_empty() {
438                        let enum_yaml: Vec<serde_yaml::Value> = child_col
439                            .enum_values
440                            .iter()
441                            .map(|e| serde_yaml::Value::String(e.clone()))
442                            .collect();
443                        child_prop.insert(
444                            serde_yaml::Value::String("enum".to_string()),
445                            serde_yaml::Value::Sequence(enum_yaml),
446                        );
447                    }
448
449                    // Export constraints for nested columns
450                    if !child_col.constraints.is_empty() {
451                        let constraints_yaml: Vec<serde_yaml::Value> = child_col
452                            .constraints
453                            .iter()
454                            .map(|c| serde_yaml::Value::String(c.clone()))
455                            .collect();
456                        child_prop.insert(
457                            serde_yaml::Value::String("constraints".to_string()),
458                            serde_yaml::Value::Sequence(constraints_yaml),
459                        );
460                    }
461
462                    // Export foreign key for nested columns
463                    if let Some(ref fk) = child_col.foreign_key {
464                        let mut fk_map = serde_yaml::Mapping::new();
465                        fk_map.insert(
466                            serde_yaml::Value::String("table".to_string()),
467                            serde_yaml::Value::String(fk.table_id.clone()),
468                        );
469                        fk_map.insert(
470                            serde_yaml::Value::String("column".to_string()),
471                            serde_yaml::Value::String(fk.column_name.clone()),
472                        );
473                        child_prop.insert(
474                            serde_yaml::Value::String("foreignKey".to_string()),
475                            serde_yaml::Value::Mapping(fk_map),
476                        );
477                    }
478
479                    nested_props.insert(
480                        serde_yaml::Value::String(child_name),
481                        serde_yaml::Value::Mapping(child_prop),
482                    );
483                }
484            }
485
486            if nested_props.is_empty() {
487                None
488            } else {
489                Some(nested_props)
490            }
491        }
492
493        for column in &table.columns {
494            // Skip nested columns (they're handled as part of parent columns)
495            if column.name.contains('.') {
496                continue;
497            }
498
499            let mut prop = serde_yaml::Mapping::new();
500
501            // Check if this column has nested columns
502            let has_nested = table.columns.iter().any(|col| {
503                col.name.starts_with(&format!("{}.", column.name)) && col.name != column.name
504            });
505
506            // Determine the type - handle ARRAY<OBJECT>, STRUCT, OBJECT, etc.
507            let data_type_upper = column.data_type.to_uppercase();
508            let is_array_object = data_type_upper.starts_with("ARRAY<")
509                && (data_type_upper.contains("OBJECT") || data_type_upper.contains("STRUCT"));
510            let is_struct_or_object = data_type_upper == "STRUCT"
511                || data_type_upper == "OBJECT"
512                || data_type_upper.starts_with("STRUCT<");
513
514            // Always check for nested properties if nested columns exist
515            if has_nested {
516                // Try to build nested properties first
517                let nested_props = build_nested_properties(
518                    &column.name,
519                    &table.columns,
520                    &Self::json_to_yaml_value,
521                );
522
523                if is_array_object {
524                    // ARRAY<OBJECT> with nested fields
525                    prop.insert(
526                        serde_yaml::Value::String("type".to_string()),
527                        serde_yaml::Value::String("array".to_string()),
528                    );
529
530                    let mut items = serde_yaml::Mapping::new();
531                    items.insert(
532                        serde_yaml::Value::String("type".to_string()),
533                        serde_yaml::Value::String("object".to_string()),
534                    );
535
536                    // Add nested properties if they exist
537                    if let Some(nested_props_map) = nested_props {
538                        items.insert(
539                            serde_yaml::Value::String("properties".to_string()),
540                            serde_yaml::Value::Mapping(nested_props_map),
541                        );
542                    }
543
544                    prop.insert(
545                        serde_yaml::Value::String("items".to_string()),
546                        serde_yaml::Value::Mapping(items),
547                    );
548                } else if is_struct_or_object || nested_props.is_some() {
549                    // OBJECT/STRUCT with nested fields, or any column with nested columns
550                    prop.insert(
551                        serde_yaml::Value::String("type".to_string()),
552                        serde_yaml::Value::String("object".to_string()),
553                    );
554
555                    // Add nested properties if they exist
556                    if let Some(nested_props_map) = nested_props {
557                        prop.insert(
558                            serde_yaml::Value::String("properties".to_string()),
559                            serde_yaml::Value::Mapping(nested_props_map),
560                        );
561                    }
562                } else {
563                    // Has nested columns but couldn't build structure - use simple type
564                    prop.insert(
565                        serde_yaml::Value::String("type".to_string()),
566                        serde_yaml::Value::String(column.data_type.clone().to_lowercase()),
567                    );
568                }
569            } else {
570                // No nested columns - use simple type
571                prop.insert(
572                    serde_yaml::Value::String("type".to_string()),
573                    serde_yaml::Value::String(column.data_type.clone().to_lowercase()),
574                );
575            }
576
577            if !column.nullable {
578                prop.insert(
579                    serde_yaml::Value::String("required".to_string()),
580                    serde_yaml::Value::Bool(true),
581                );
582            }
583
584            if column.primary_key {
585                prop.insert(
586                    serde_yaml::Value::String("primaryKey".to_string()),
587                    serde_yaml::Value::Bool(true),
588                );
589            }
590
591            if column.secondary_key {
592                prop.insert(
593                    serde_yaml::Value::String("businessKey".to_string()),
594                    serde_yaml::Value::Bool(true),
595                );
596            }
597
598            if !column.description.is_empty() {
599                prop.insert(
600                    serde_yaml::Value::String("description".to_string()),
601                    serde_yaml::Value::String(column.description.clone()),
602                );
603            }
604
605            // Export column-level quality rules
606            if !column.quality.is_empty() {
607                let quality_array: Vec<serde_yaml::Value> = column
608                    .quality
609                    .iter()
610                    .map(|rule| {
611                        let mut rule_map = serde_yaml::Mapping::new();
612                        for (k, v) in rule {
613                            rule_map.insert(
614                                serde_yaml::Value::String(k.clone()),
615                                Self::json_to_yaml_value(v),
616                            );
617                        }
618                        serde_yaml::Value::Mapping(rule_map)
619                    })
620                    .collect();
621                prop.insert(
622                    serde_yaml::Value::String("quality".to_string()),
623                    serde_yaml::Value::Sequence(quality_array),
624                );
625            }
626
627            // Export enum values
628            if !column.enum_values.is_empty() {
629                let enum_yaml: Vec<serde_yaml::Value> = column
630                    .enum_values
631                    .iter()
632                    .map(|e| serde_yaml::Value::String(e.clone()))
633                    .collect();
634                prop.insert(
635                    serde_yaml::Value::String("enum".to_string()),
636                    serde_yaml::Value::Sequence(enum_yaml),
637                );
638            }
639
640            // Export constraints
641            if !column.constraints.is_empty() {
642                let constraints_yaml: Vec<serde_yaml::Value> = column
643                    .constraints
644                    .iter()
645                    .map(|c| serde_yaml::Value::String(c.clone()))
646                    .collect();
647                prop.insert(
648                    serde_yaml::Value::String("constraints".to_string()),
649                    serde_yaml::Value::Sequence(constraints_yaml),
650                );
651            }
652
653            // Export foreign key
654            if let Some(ref fk) = column.foreign_key {
655                let mut fk_map = serde_yaml::Mapping::new();
656                fk_map.insert(
657                    serde_yaml::Value::String("table".to_string()),
658                    serde_yaml::Value::String(fk.table_id.clone()),
659                );
660                fk_map.insert(
661                    serde_yaml::Value::String("column".to_string()),
662                    serde_yaml::Value::String(fk.column_name.clone()),
663                );
664                prop.insert(
665                    serde_yaml::Value::String("foreignKey".to_string()),
666                    serde_yaml::Value::Mapping(fk_map),
667                );
668            }
669
670            properties.insert(
671                serde_yaml::Value::String(column.name.clone()),
672                serde_yaml::Value::Mapping(prop),
673            );
674        }
675
676        schema_obj.insert(
677            serde_yaml::Value::String("properties".to_string()),
678            serde_yaml::Value::Mapping(properties),
679        );
680
681        schema_array.push(serde_yaml::Value::Mapping(schema_obj));
682        yaml.insert(
683            serde_yaml::Value::String("schema".to_string()),
684            serde_yaml::Value::Sequence(schema_array),
685        );
686
687        // Table-level quality rules
688        if !table.quality.is_empty() {
689            let quality_array: Vec<serde_yaml::Value> = table
690                .quality
691                .iter()
692                .map(|rule| {
693                    let mut rule_map = serde_yaml::Mapping::new();
694                    for (k, v) in rule {
695                        rule_map.insert(
696                            serde_yaml::Value::String(k.clone()),
697                            Self::json_to_yaml_value(v),
698                        );
699                    }
700                    serde_yaml::Value::Mapping(rule_map)
701                })
702                .collect();
703            yaml.insert(
704                serde_yaml::Value::String("quality".to_string()),
705                serde_yaml::Value::Sequence(quality_array),
706            );
707        }
708
709        // Custom Properties from metadata (excluding already exported fields)
710        let excluded_keys = [
711            "id",
712            "version",
713            "status",
714            "domain",
715            "dataProduct",
716            "tenant",
717            "description",
718            "team",
719            "roles",
720            "pricing",
721            "terms",
722            "servers",
723            "servicelevels",
724            "links",
725            "apiVersion",
726            "kind",
727            "info",
728            "dataContractSpecification",
729        ];
730
731        let mut custom_props = Vec::new();
732        for (key, value) in &table.odcl_metadata {
733            if !excluded_keys.contains(&key.as_str()) && !value.is_null() {
734                let mut prop = serde_yaml::Mapping::new();
735                prop.insert(
736                    serde_yaml::Value::String("property".to_string()),
737                    serde_yaml::Value::String(key.clone()),
738                );
739                prop.insert(
740                    serde_yaml::Value::String("value".to_string()),
741                    Self::json_to_yaml_value(value),
742                );
743                custom_props.push(serde_yaml::Value::Mapping(prop));
744            }
745        }
746
747        // Add database type as custom property if present
748        if let Some(ref db_type) = table.database_type {
749            let mut prop = serde_yaml::Mapping::new();
750            prop.insert(
751                serde_yaml::Value::String("property".to_string()),
752                serde_yaml::Value::String("databaseType".to_string()),
753            );
754            prop.insert(
755                serde_yaml::Value::String("value".to_string()),
756                serde_yaml::Value::String(format!("{:?}", db_type)),
757            );
758            custom_props.push(serde_yaml::Value::Mapping(prop));
759        }
760
761        // Add medallion layers as custom property if present
762        if !table.medallion_layers.is_empty() {
763            let layers: Vec<serde_yaml::Value> = table
764                .medallion_layers
765                .iter()
766                .map(|l| serde_yaml::Value::String(format!("{:?}", l)))
767                .collect();
768            let mut prop = serde_yaml::Mapping::new();
769            prop.insert(
770                serde_yaml::Value::String("property".to_string()),
771                serde_yaml::Value::String("medallionLayers".to_string()),
772            );
773            prop.insert(
774                serde_yaml::Value::String("value".to_string()),
775                serde_yaml::Value::Sequence(layers),
776            );
777            custom_props.push(serde_yaml::Value::Mapping(prop));
778        }
779
780        // Add SCD pattern as custom property if present
781        if let Some(ref scd_pattern) = table.scd_pattern {
782            let mut prop = serde_yaml::Mapping::new();
783            prop.insert(
784                serde_yaml::Value::String("property".to_string()),
785                serde_yaml::Value::String("scdPattern".to_string()),
786            );
787            prop.insert(
788                serde_yaml::Value::String("value".to_string()),
789                serde_yaml::Value::String(format!("{:?}", scd_pattern)),
790            );
791            custom_props.push(serde_yaml::Value::Mapping(prop));
792        }
793
794        // Add Data Vault classification as custom property if present
795        if let Some(ref dv_class) = table.data_vault_classification {
796            let mut prop = serde_yaml::Mapping::new();
797            prop.insert(
798                serde_yaml::Value::String("property".to_string()),
799                serde_yaml::Value::String("dataVaultClassification".to_string()),
800            );
801            prop.insert(
802                serde_yaml::Value::String("value".to_string()),
803                serde_yaml::Value::String(format!("{:?}", dv_class)),
804            );
805            custom_props.push(serde_yaml::Value::Mapping(prop));
806        }
807
808        // Add catalog/schema names as custom properties if present
809        if let Some(ref catalog) = table.catalog_name {
810            let mut prop = serde_yaml::Mapping::new();
811            prop.insert(
812                serde_yaml::Value::String("property".to_string()),
813                serde_yaml::Value::String("catalogName".to_string()),
814            );
815            prop.insert(
816                serde_yaml::Value::String("value".to_string()),
817                serde_yaml::Value::String(catalog.clone()),
818            );
819            custom_props.push(serde_yaml::Value::Mapping(prop));
820        }
821
822        if let Some(ref schema) = table.schema_name {
823            let mut prop = serde_yaml::Mapping::new();
824            prop.insert(
825                serde_yaml::Value::String("property".to_string()),
826                serde_yaml::Value::String("schemaName".to_string()),
827            );
828            prop.insert(
829                serde_yaml::Value::String("value".to_string()),
830                serde_yaml::Value::String(schema.clone()),
831            );
832            custom_props.push(serde_yaml::Value::Mapping(prop));
833        }
834
835        if !custom_props.is_empty() {
836            yaml.insert(
837                serde_yaml::Value::String("customProperties".to_string()),
838                serde_yaml::Value::Sequence(custom_props),
839            );
840        }
841
842        // Contract created timestamp
843        yaml.insert(
844            serde_yaml::Value::String("contractCreatedTs".to_string()),
845            serde_yaml::Value::String(table.created_at.to_rfc3339()),
846        );
847
848        serde_yaml::to_string(&yaml).unwrap_or_default()
849    }
850
851    /// Export tables to ODCS v3.1.0 YAML format (SDK interface).
852    pub fn export(
853        &self,
854        tables: &[Table],
855        _format: &str,
856    ) -> Result<HashMap<String, ExportResult>, ExportError> {
857        let mut exports = HashMap::new();
858        for table in tables {
859            // All exports use ODCS v3.1.0 format
860            let yaml = Self::export_odcs_v3_1_0_format(table);
861            exports.insert(
862                table.name.clone(),
863                ExportResult {
864                    content: yaml,
865                    format: "odcs_v3_1_0".to_string(),
866                },
867            );
868        }
869        Ok(exports)
870    }
871
872    /// Export a data model to ODCS v3.1.0 YAML format (legacy method for compatibility).
873    pub fn export_model(
874        model: &DataModel,
875        table_ids: Option<&[uuid::Uuid]>,
876        _format: &str,
877    ) -> HashMap<String, String> {
878        let tables_to_export: Vec<&Table> = if let Some(ids) = table_ids {
879            model
880                .tables
881                .iter()
882                .filter(|t| ids.contains(&t.id))
883                .collect()
884        } else {
885            model.tables.iter().collect()
886        };
887
888        let mut exports = HashMap::new();
889        for table in tables_to_export {
890            // All exports use ODCS v3.1.0 format
891            let yaml = Self::export_odcs_v3_1_0_format(table);
892            exports.insert(table.name.clone(), yaml);
893        }
894
895        exports
896    }
897}
898
899#[cfg(test)]
900mod tests {
901    use super::*;
902    use crate::models::Column;
903
904    #[test]
905    fn test_export_odcs_v3_1_0_basic() {
906        let table = Table {
907            id: Table::generate_id("test_table", None, None, None),
908            name: "test_table".to_string(),
909            columns: vec![Column {
910                name: "id".to_string(),
911                data_type: "BIGINT".to_string(),
912                nullable: false,
913                primary_key: true,
914                secondary_key: false,
915                composite_key: None,
916                foreign_key: None,
917                constraints: Vec::new(),
918                description: "Primary key".to_string(),
919                errors: Vec::new(),
920                quality: Vec::new(),
921                enum_values: Vec::new(),
922                column_order: 0,
923            }],
924            database_type: None,
925            catalog_name: None,
926            schema_name: None,
927            medallion_layers: Vec::new(),
928            scd_pattern: None,
929            data_vault_classification: None,
930            modeling_level: None,
931            tags: vec!["test".to_string()],
932            odcl_metadata: HashMap::new(),
933            owner: None,
934            sla: None,
935            contact_details: None,
936            infrastructure_type: None,
937            notes: None,
938            position: None,
939            yaml_file_path: None,
940            drawio_cell_id: None,
941            quality: Vec::new(),
942            errors: Vec::new(),
943            created_at: chrono::Utc::now(),
944            updated_at: chrono::Utc::now(),
945        };
946
947        let yaml = ODCSExporter::export_table(&table, "odcs_v3_1_0");
948
949        assert!(yaml.contains("apiVersion: v3.1.0"));
950        assert!(yaml.contains("kind: DataContract"));
951        assert!(yaml.contains("name: test_table"));
952        assert!(yaml.contains("tags:"));
953        assert!(yaml.contains("- test"));
954    }
955}