data_modelling_core/models/odcs/
converters.rs

1//! Converters between ODCS native types and legacy Table/Column types
2//!
3//! These converters enable backwards compatibility with existing APIs while
4//! allowing the new ODCS-native types to be used internally.
5
6use super::contract::ODCSContract;
7use super::property::Property;
8use super::schema::SchemaObject;
9use super::supporting::{
10    AuthoritativeDefinition as OdcsAuthDef, CustomProperty,
11    LogicalTypeOptions as OdcsLogicalTypeOptions, PropertyRelationship as OdcsPropertyRelationship,
12};
13use crate::import::{ColumnData, TableData};
14use crate::models::column::{
15    AuthoritativeDefinition as ColumnAuthDef, Column,
16    LogicalTypeOptions as ColumnLogicalTypeOptions,
17    PropertyRelationship as ColumnPropertyRelationship,
18};
19use crate::models::table::Table;
20
21// ============================================================================
22// Property <-> Column Converters
23// ============================================================================
24
25impl From<&Property> for Column {
26    /// Convert a Property to a Column
27    ///
28    /// This flattens nested properties to dot-notation names for backwards compatibility.
29    /// For example, a nested property `address.street` becomes a column named "address.street".
30    fn from(prop: &Property) -> Self {
31        Column {
32            id: prop.id.clone(),
33            name: prop.name.clone(),
34            business_name: prop.business_name.clone(),
35            description: prop.description.clone().unwrap_or_default(),
36            data_type: prop.logical_type.clone(),
37            physical_type: prop.physical_type.clone(),
38            physical_name: prop.physical_name.clone(),
39            logical_type_options: prop.logical_type_options.as_ref().map(|opts| {
40                ColumnLogicalTypeOptions {
41                    min_length: opts.min_length,
42                    max_length: opts.max_length,
43                    pattern: opts.pattern.clone(),
44                    format: opts.format.clone(),
45                    minimum: opts.minimum.clone(),
46                    maximum: opts.maximum.clone(),
47                    exclusive_minimum: opts.exclusive_minimum.clone(),
48                    exclusive_maximum: opts.exclusive_maximum.clone(),
49                    precision: opts.precision,
50                    scale: opts.scale,
51                }
52            }),
53            primary_key: prop.primary_key,
54            primary_key_position: prop.primary_key_position,
55            unique: prop.unique,
56            nullable: !prop.required, // ODCS uses required, Column uses nullable (inverse)
57            partitioned: prop.partitioned,
58            partition_key_position: prop.partition_key_position,
59            clustered: prop.clustered,
60            classification: prop.classification.clone(),
61            critical_data_element: prop.critical_data_element,
62            encrypted_name: prop.encrypted_name.clone(),
63            transform_source_objects: prop.transform_source_objects.clone(),
64            transform_logic: prop.transform_logic.clone(),
65            transform_description: prop.transform_description.clone(),
66            examples: prop.examples.clone(),
67            default_value: prop.default_value.clone(),
68            relationships: prop
69                .relationships
70                .iter()
71                .map(|r| ColumnPropertyRelationship {
72                    relationship_type: r.relationship_type.clone(),
73                    to: r.to.clone(),
74                })
75                .collect(),
76            authoritative_definitions: prop
77                .authoritative_definitions
78                .iter()
79                .map(|d| ColumnAuthDef {
80                    definition_type: d.definition_type.clone(),
81                    url: d.url.clone(),
82                })
83                .collect(),
84            quality: prop
85                .quality
86                .iter()
87                .map(|q| serde_json::to_value(q).ok())
88                .filter_map(|v| v.and_then(|v| v.as_object().cloned()))
89                .map(|m| m.into_iter().collect())
90                .collect(),
91            enum_values: prop.enum_values.clone(),
92            tags: prop.tags.clone(),
93            custom_properties: prop
94                .custom_properties
95                .iter()
96                .map(|cp| (cp.property.clone(), cp.value.clone()))
97                .collect(),
98            // Legacy fields - default values
99            secondary_key: false,
100            composite_key: None,
101            foreign_key: None,
102            constraints: Vec::new(),
103            errors: Vec::new(),
104            column_order: 0,
105            nested_data: None,
106        }
107    }
108}
109
110impl From<&Column> for Property {
111    /// Convert a Column to a Property
112    ///
113    /// Note: This creates a flat property. To reconstruct nested structure from
114    /// dot-notation column names, use `Property::from_flat_paths()`.
115    fn from(col: &Column) -> Self {
116        Property {
117            id: col.id.clone(),
118            name: col.name.clone(),
119            business_name: col.business_name.clone(),
120            description: if col.description.is_empty() {
121                None
122            } else {
123                Some(col.description.clone())
124            },
125            logical_type: col.data_type.clone(),
126            physical_type: col.physical_type.clone(),
127            physical_name: col.physical_name.clone(),
128            logical_type_options: col.logical_type_options.as_ref().map(|opts| {
129                OdcsLogicalTypeOptions {
130                    min_length: opts.min_length,
131                    max_length: opts.max_length,
132                    pattern: opts.pattern.clone(),
133                    format: opts.format.clone(),
134                    minimum: opts.minimum.clone(),
135                    maximum: opts.maximum.clone(),
136                    exclusive_minimum: opts.exclusive_minimum.clone(),
137                    exclusive_maximum: opts.exclusive_maximum.clone(),
138                    precision: opts.precision,
139                    scale: opts.scale,
140                }
141            }),
142            required: !col.nullable, // Column uses nullable, ODCS uses required (inverse)
143            primary_key: col.primary_key,
144            primary_key_position: col.primary_key_position,
145            unique: col.unique,
146            partitioned: col.partitioned,
147            partition_key_position: col.partition_key_position,
148            clustered: col.clustered,
149            classification: col.classification.clone(),
150            critical_data_element: col.critical_data_element,
151            encrypted_name: col.encrypted_name.clone(),
152            transform_source_objects: col.transform_source_objects.clone(),
153            transform_logic: col.transform_logic.clone(),
154            transform_description: col.transform_description.clone(),
155            examples: col.examples.clone(),
156            default_value: col.default_value.clone(),
157            relationships: col
158                .relationships
159                .iter()
160                .map(|r| OdcsPropertyRelationship {
161                    relationship_type: r.relationship_type.clone(),
162                    to: r.to.clone(),
163                })
164                .collect(),
165            authoritative_definitions: col
166                .authoritative_definitions
167                .iter()
168                .map(|d| OdcsAuthDef {
169                    definition_type: d.definition_type.clone(),
170                    url: d.url.clone(),
171                })
172                .collect(),
173            quality: col
174                .quality
175                .iter()
176                .filter_map(|q| serde_json::to_value(q).ok())
177                .filter_map(|v| serde_json::from_value(v).ok())
178                .collect(),
179            enum_values: col.enum_values.clone(),
180            tags: col.tags.clone(),
181            custom_properties: col
182                .custom_properties
183                .iter()
184                .map(|(k, v)| CustomProperty::new(k.clone(), v.clone()))
185                .collect(),
186            items: None,
187            properties: Vec::new(),
188        }
189    }
190}
191
192// ============================================================================
193// SchemaObject <-> Table Converters
194// ============================================================================
195
196impl From<&SchemaObject> for Table {
197    /// Convert a SchemaObject to a Table
198    ///
199    /// This flattens nested properties to dot-notation column names.
200    fn from(schema: &SchemaObject) -> Self {
201        // Flatten all properties to columns with dot-notation names
202        let columns = flatten_properties_to_columns(&schema.properties, "");
203
204        let mut table = Table::new(schema.name.clone(), columns);
205
206        // Set schema-level fields
207        table.schema_name = schema.physical_name.clone();
208
209        // Store schema-level metadata in odcl_metadata
210        if let Some(ref id) = schema.id {
211            table
212                .odcl_metadata
213                .insert("schemaId".to_string(), serde_json::json!(id));
214        }
215        if let Some(ref physical_name) = schema.physical_name {
216            table
217                .odcl_metadata
218                .insert("physicalName".to_string(), serde_json::json!(physical_name));
219        }
220        if let Some(ref physical_type) = schema.physical_type {
221            table
222                .odcl_metadata
223                .insert("physicalType".to_string(), serde_json::json!(physical_type));
224        }
225        if let Some(ref business_name) = schema.business_name {
226            table
227                .odcl_metadata
228                .insert("businessName".to_string(), serde_json::json!(business_name));
229        }
230        if let Some(ref description) = schema.description {
231            table.odcl_metadata.insert(
232                "schemaDescription".to_string(),
233                serde_json::json!(description),
234            );
235        }
236        if let Some(ref granularity) = schema.data_granularity_description {
237            table.odcl_metadata.insert(
238                "dataGranularityDescription".to_string(),
239                serde_json::json!(granularity),
240            );
241        }
242        if !schema.tags.is_empty() {
243            table
244                .odcl_metadata
245                .insert("schemaTags".to_string(), serde_json::json!(schema.tags));
246        }
247        if !schema.relationships.is_empty() {
248            table.odcl_metadata.insert(
249                "schemaRelationships".to_string(),
250                serde_json::to_value(&schema.relationships).unwrap_or_default(),
251            );
252        }
253        if !schema.quality.is_empty() {
254            table.quality = schema
255                .quality
256                .iter()
257                .filter_map(|q| serde_json::to_value(q).ok())
258                .filter_map(|v| v.as_object().cloned())
259                .map(|m| m.into_iter().collect())
260                .collect();
261        }
262        if !schema.authoritative_definitions.is_empty() {
263            table.odcl_metadata.insert(
264                "authoritativeDefinitions".to_string(),
265                serde_json::to_value(&schema.authoritative_definitions).unwrap_or_default(),
266            );
267        }
268        if !schema.custom_properties.is_empty() {
269            table.odcl_metadata.insert(
270                "customProperties".to_string(),
271                serde_json::to_value(&schema.custom_properties).unwrap_or_default(),
272            );
273        }
274
275        table
276    }
277}
278
279/// Helper function to flatten nested properties to columns with dot-notation names
280fn flatten_properties_to_columns(properties: &[Property], prefix: &str) -> Vec<Column> {
281    let mut columns = Vec::new();
282
283    for prop in properties {
284        let full_name = if prefix.is_empty() {
285            prop.name.clone()
286        } else {
287            format!("{}.{}", prefix, prop.name)
288        };
289
290        // Create column for this property
291        let mut col = Column::from(prop);
292        col.name = full_name.clone();
293
294        columns.push(col);
295
296        // Recursively flatten nested object properties
297        if !prop.properties.is_empty() {
298            let nested = flatten_properties_to_columns(&prop.properties, &full_name);
299            columns.extend(nested);
300        }
301
302        // Handle array items
303        if let Some(ref items) = prop.items {
304            let items_prefix = format!("{}.[]", full_name);
305            let mut items_col = Column::from(items.as_ref());
306            items_col.name = items_prefix.clone();
307            columns.push(items_col);
308
309            // Recursively flatten array item properties
310            if !items.properties.is_empty() {
311                let nested = flatten_properties_to_columns(&items.properties, &items_prefix);
312                columns.extend(nested);
313            }
314        }
315    }
316
317    columns
318}
319
320impl From<&Table> for SchemaObject {
321    /// Convert a Table to a SchemaObject
322    ///
323    /// This reconstructs nested property structure from dot-notation column names.
324    fn from(table: &Table) -> Self {
325        // Build flat property list first
326        let flat_props: Vec<(String, Property)> = table
327            .columns
328            .iter()
329            .map(|col| (col.name.clone(), Property::from(col)))
330            .collect();
331
332        // Reconstruct nested structure
333        let properties = Property::from_flat_paths(&flat_props);
334
335        let mut schema = SchemaObject::new(table.name.clone()).with_properties(properties);
336
337        // Extract schema-level metadata from odcl_metadata
338        if let Some(id) = table.odcl_metadata.get("schemaId").and_then(|v| v.as_str()) {
339            schema.id = Some(id.to_string());
340        }
341        if let Some(physical_name) = table
342            .odcl_metadata
343            .get("physicalName")
344            .and_then(|v| v.as_str())
345        {
346            schema.physical_name = Some(physical_name.to_string());
347        } else if let Some(ref sn) = table.schema_name {
348            schema.physical_name = Some(sn.clone());
349        }
350        if let Some(physical_type) = table
351            .odcl_metadata
352            .get("physicalType")
353            .and_then(|v| v.as_str())
354        {
355            schema.physical_type = Some(physical_type.to_string());
356        }
357        if let Some(business_name) = table
358            .odcl_metadata
359            .get("businessName")
360            .and_then(|v| v.as_str())
361        {
362            schema.business_name = Some(business_name.to_string());
363        }
364        if let Some(description) = table
365            .odcl_metadata
366            .get("schemaDescription")
367            .and_then(|v| v.as_str())
368        {
369            schema.description = Some(description.to_string());
370        }
371        if let Some(granularity) = table
372            .odcl_metadata
373            .get("dataGranularityDescription")
374            .and_then(|v| v.as_str())
375        {
376            schema.data_granularity_description = Some(granularity.to_string());
377        }
378        if let Some(tags) = table.odcl_metadata.get("schemaTags")
379            && let Ok(parsed_tags) = serde_json::from_value::<Vec<String>>(tags.clone())
380        {
381            schema.tags = parsed_tags;
382        }
383        if let Some(rels) = table.odcl_metadata.get("schemaRelationships")
384            && let Ok(parsed_rels) = serde_json::from_value(rels.clone())
385        {
386            schema.relationships = parsed_rels;
387        }
388        if !table.quality.is_empty() {
389            schema.quality = table
390                .quality
391                .iter()
392                .filter_map(|q| serde_json::to_value(q).ok())
393                .filter_map(|v| serde_json::from_value(v).ok())
394                .collect();
395        }
396        if let Some(auth_defs) = table.odcl_metadata.get("authoritativeDefinitions")
397            && let Ok(parsed) = serde_json::from_value(auth_defs.clone())
398        {
399            schema.authoritative_definitions = parsed;
400        }
401        if let Some(custom) = table.odcl_metadata.get("customProperties")
402            && let Ok(parsed) = serde_json::from_value(custom.clone())
403        {
404            schema.custom_properties = parsed;
405        }
406
407        schema
408    }
409}
410
411// ============================================================================
412// ODCSContract <-> Vec<Table> Converters
413// ============================================================================
414
415impl ODCSContract {
416    /// Convert the contract to a vector of Tables
417    ///
418    /// Each SchemaObject becomes a Table, with contract-level metadata
419    /// stored in each table's odcl_metadata.
420    pub fn to_tables(&self) -> Vec<Table> {
421        self.schema
422            .iter()
423            .map(|schema| {
424                let mut table = Table::from(schema);
425
426                // Store contract-level metadata
427                table.odcl_metadata.insert(
428                    "apiVersion".to_string(),
429                    serde_json::json!(self.api_version),
430                );
431                table
432                    .odcl_metadata
433                    .insert("kind".to_string(), serde_json::json!(self.kind));
434                table
435                    .odcl_metadata
436                    .insert("contractId".to_string(), serde_json::json!(self.id));
437                table
438                    .odcl_metadata
439                    .insert("version".to_string(), serde_json::json!(self.version));
440                table
441                    .odcl_metadata
442                    .insert("contractName".to_string(), serde_json::json!(self.name));
443
444                if let Some(ref status) = self.status {
445                    table
446                        .odcl_metadata
447                        .insert("status".to_string(), serde_json::json!(status));
448                }
449                if let Some(ref domain) = self.domain {
450                    table
451                        .odcl_metadata
452                        .insert("domain".to_string(), serde_json::json!(domain));
453                }
454                if let Some(ref data_product) = self.data_product {
455                    table
456                        .odcl_metadata
457                        .insert("dataProduct".to_string(), serde_json::json!(data_product));
458                }
459                if let Some(ref tenant) = self.tenant {
460                    table
461                        .odcl_metadata
462                        .insert("tenant".to_string(), serde_json::json!(tenant));
463                }
464                if let Some(ref description) = self.description {
465                    table.odcl_metadata.insert(
466                        "description".to_string(),
467                        serde_json::to_value(description).unwrap_or_default(),
468                    );
469                }
470                if !self.servers.is_empty() {
471                    table.odcl_metadata.insert(
472                        "servers".to_string(),
473                        serde_json::to_value(&self.servers).unwrap_or_default(),
474                    );
475                }
476                if let Some(ref team) = self.team {
477                    table.odcl_metadata.insert(
478                        "team".to_string(),
479                        serde_json::to_value(team).unwrap_or_default(),
480                    );
481                }
482                if let Some(ref support) = self.support {
483                    table.odcl_metadata.insert(
484                        "support".to_string(),
485                        serde_json::to_value(support).unwrap_or_default(),
486                    );
487                }
488                if !self.roles.is_empty() {
489                    table.odcl_metadata.insert(
490                        "roles".to_string(),
491                        serde_json::to_value(&self.roles).unwrap_or_default(),
492                    );
493                }
494                if !self.service_levels.is_empty() {
495                    table.odcl_metadata.insert(
496                        "serviceLevels".to_string(),
497                        serde_json::to_value(&self.service_levels).unwrap_or_default(),
498                    );
499                }
500                if !self.quality.is_empty() {
501                    table.odcl_metadata.insert(
502                        "contractQuality".to_string(),
503                        serde_json::to_value(&self.quality).unwrap_or_default(),
504                    );
505                }
506                if let Some(ref price) = self.price {
507                    table.odcl_metadata.insert(
508                        "price".to_string(),
509                        serde_json::to_value(price).unwrap_or_default(),
510                    );
511                }
512                if let Some(ref terms) = self.terms {
513                    table.odcl_metadata.insert(
514                        "terms".to_string(),
515                        serde_json::to_value(terms).unwrap_or_default(),
516                    );
517                }
518                if !self.links.is_empty() {
519                    table.odcl_metadata.insert(
520                        "links".to_string(),
521                        serde_json::to_value(&self.links).unwrap_or_default(),
522                    );
523                }
524                if !self.authoritative_definitions.is_empty() {
525                    table.odcl_metadata.insert(
526                        "contractAuthoritativeDefinitions".to_string(),
527                        serde_json::to_value(&self.authoritative_definitions).unwrap_or_default(),
528                    );
529                }
530                if !self.tags.is_empty() {
531                    table
532                        .odcl_metadata
533                        .insert("contractTags".to_string(), serde_json::json!(self.tags));
534                }
535                if !self.custom_properties.is_empty() {
536                    table.odcl_metadata.insert(
537                        "contractCustomProperties".to_string(),
538                        serde_json::to_value(&self.custom_properties).unwrap_or_default(),
539                    );
540                }
541                if let Some(ref ts) = self.contract_created_ts {
542                    table
543                        .odcl_metadata
544                        .insert("contractCreatedTs".to_string(), serde_json::json!(ts));
545                }
546
547                table
548            })
549            .collect()
550    }
551
552    /// Create a contract from a vector of Tables
553    ///
554    /// Contract-level metadata is extracted from the first table's odcl_metadata.
555    /// Each table becomes a SchemaObject.
556    pub fn from_tables(tables: &[Table]) -> Self {
557        if tables.is_empty() {
558            return ODCSContract::default();
559        }
560
561        let first_table = &tables[0];
562        let mut contract = ODCSContract::default();
563
564        // Extract contract-level metadata from first table
565        if let Some(api_version) = first_table
566            .odcl_metadata
567            .get("apiVersion")
568            .and_then(|v| v.as_str())
569        {
570            contract.api_version = api_version.to_string();
571        }
572        if let Some(kind) = first_table
573            .odcl_metadata
574            .get("kind")
575            .and_then(|v| v.as_str())
576        {
577            contract.kind = kind.to_string();
578        }
579        if let Some(id) = first_table
580            .odcl_metadata
581            .get("contractId")
582            .and_then(|v| v.as_str())
583        {
584            contract.id = id.to_string();
585        }
586        if let Some(version) = first_table
587            .odcl_metadata
588            .get("version")
589            .and_then(|v| v.as_str())
590        {
591            contract.version = version.to_string();
592        }
593        if let Some(name) = first_table
594            .odcl_metadata
595            .get("contractName")
596            .and_then(|v| v.as_str())
597        {
598            contract.name = name.to_string();
599        }
600        if let Some(status) = first_table
601            .odcl_metadata
602            .get("status")
603            .and_then(|v| v.as_str())
604        {
605            contract.status = Some(status.to_string());
606        }
607        if let Some(domain) = first_table
608            .odcl_metadata
609            .get("domain")
610            .and_then(|v| v.as_str())
611        {
612            contract.domain = Some(domain.to_string());
613        }
614        if let Some(data_product) = first_table
615            .odcl_metadata
616            .get("dataProduct")
617            .and_then(|v| v.as_str())
618        {
619            contract.data_product = Some(data_product.to_string());
620        }
621        if let Some(tenant) = first_table
622            .odcl_metadata
623            .get("tenant")
624            .and_then(|v| v.as_str())
625        {
626            contract.tenant = Some(tenant.to_string());
627        }
628        if let Some(description) = first_table.odcl_metadata.get("description") {
629            contract.description = serde_json::from_value(description.clone()).ok();
630        }
631        if let Some(servers) = first_table.odcl_metadata.get("servers") {
632            contract.servers = serde_json::from_value(servers.clone()).unwrap_or_default();
633        }
634        if let Some(team) = first_table.odcl_metadata.get("team") {
635            contract.team = serde_json::from_value(team.clone()).ok();
636        }
637        if let Some(support) = first_table.odcl_metadata.get("support") {
638            contract.support = serde_json::from_value(support.clone()).ok();
639        }
640        if let Some(roles) = first_table.odcl_metadata.get("roles") {
641            contract.roles = serde_json::from_value(roles.clone()).unwrap_or_default();
642        }
643        if let Some(service_levels) = first_table.odcl_metadata.get("serviceLevels") {
644            contract.service_levels =
645                serde_json::from_value(service_levels.clone()).unwrap_or_default();
646        }
647        if let Some(quality) = first_table.odcl_metadata.get("contractQuality") {
648            contract.quality = serde_json::from_value(quality.clone()).unwrap_or_default();
649        }
650        if let Some(price) = first_table.odcl_metadata.get("price") {
651            contract.price = serde_json::from_value(price.clone()).ok();
652        }
653        if let Some(terms) = first_table.odcl_metadata.get("terms") {
654            contract.terms = serde_json::from_value(terms.clone()).ok();
655        }
656        if let Some(links) = first_table.odcl_metadata.get("links") {
657            contract.links = serde_json::from_value(links.clone()).unwrap_or_default();
658        }
659        if let Some(auth_defs) = first_table
660            .odcl_metadata
661            .get("contractAuthoritativeDefinitions")
662        {
663            contract.authoritative_definitions =
664                serde_json::from_value(auth_defs.clone()).unwrap_or_default();
665        }
666        if let Some(tags) = first_table.odcl_metadata.get("contractTags") {
667            contract.tags = serde_json::from_value(tags.clone()).unwrap_or_default();
668        }
669        if let Some(custom) = first_table.odcl_metadata.get("contractCustomProperties") {
670            contract.custom_properties = serde_json::from_value(custom.clone()).unwrap_or_default();
671        }
672        if let Some(ts) = first_table
673            .odcl_metadata
674            .get("contractCreatedTs")
675            .and_then(|v| v.as_str())
676        {
677            contract.contract_created_ts = Some(ts.to_string());
678        }
679
680        // Convert each table to a schema object
681        contract.schema = tables.iter().map(SchemaObject::from).collect();
682
683        contract
684    }
685
686    /// Convert contract to TableData for API responses
687    pub fn to_table_data(&self) -> Vec<TableData> {
688        self.schema
689            .iter()
690            .enumerate()
691            .map(|(idx, schema)| {
692                let description_value = self
693                    .description
694                    .as_ref()
695                    .map(|d| serde_json::to_value(d).unwrap_or(serde_json::Value::Null));
696
697                TableData {
698                    table_index: idx,
699                    // Contract identity
700                    id: Some(self.id.clone()),
701                    name: Some(schema.name.clone()),
702                    api_version: Some(self.api_version.clone()),
703                    version: Some(self.version.clone()),
704                    status: self.status.clone(),
705                    kind: Some(self.kind.clone()),
706                    // Domain & organization
707                    domain: self.domain.clone(),
708                    data_product: self.data_product.clone(),
709                    tenant: self.tenant.clone(),
710                    // Description
711                    description: description_value,
712                    // Schema-level fields
713                    physical_name: schema.physical_name.clone(),
714                    physical_type: schema.physical_type.clone(),
715                    business_name: schema.business_name.clone(),
716                    data_granularity_description: schema.data_granularity_description.clone(),
717                    // Columns
718                    columns: schema
719                        .properties
720                        .iter()
721                        .map(property_to_column_data)
722                        .collect(),
723                    // Server configuration
724                    servers: self
725                        .servers
726                        .iter()
727                        .filter_map(|s| serde_json::to_value(s).ok())
728                        .collect(),
729                    // Team & support
730                    team: self
731                        .team
732                        .as_ref()
733                        .and_then(|t| serde_json::to_value(t).ok()),
734                    support: self
735                        .support
736                        .as_ref()
737                        .and_then(|s| serde_json::to_value(s).ok()),
738                    // Roles
739                    roles: self
740                        .roles
741                        .iter()
742                        .filter_map(|r| serde_json::to_value(r).ok())
743                        .collect(),
744                    // SLA & quality
745                    sla_properties: self
746                        .service_levels
747                        .iter()
748                        .filter_map(|s| serde_json::to_value(s).ok())
749                        .collect(),
750                    quality: self
751                        .quality
752                        .iter()
753                        .filter_map(|q| serde_json::to_value(q).ok())
754                        .filter_map(|v| v.as_object().cloned())
755                        .map(|m| m.into_iter().collect())
756                        .collect(),
757                    // Pricing
758                    price: self
759                        .price
760                        .as_ref()
761                        .and_then(|p| serde_json::to_value(p).ok()),
762                    // Tags & custom properties
763                    tags: self.tags.clone(),
764                    custom_properties: self
765                        .custom_properties
766                        .iter()
767                        .filter_map(|cp| serde_json::to_value(cp).ok())
768                        .collect(),
769                    authoritative_definitions: self
770                        .authoritative_definitions
771                        .iter()
772                        .filter_map(|ad| serde_json::to_value(ad).ok())
773                        .collect(),
774                    // Timestamps
775                    contract_created_ts: self.contract_created_ts.clone(),
776                    // Metadata
777                    odcs_metadata: std::collections::HashMap::new(),
778                }
779            })
780            .collect()
781    }
782}
783
784/// Helper function to convert Property to ColumnData
785fn property_to_column_data(prop: &Property) -> ColumnData {
786    ColumnData {
787        id: prop.id.clone(),
788        name: prop.name.clone(),
789        business_name: prop.business_name.clone(),
790        description: prop.description.clone(),
791        data_type: prop.logical_type.clone(),
792        physical_type: prop.physical_type.clone(),
793        physical_name: prop.physical_name.clone(),
794        logical_type_options: prop.logical_type_options.as_ref().map(|opts| {
795            ColumnLogicalTypeOptions {
796                min_length: opts.min_length,
797                max_length: opts.max_length,
798                pattern: opts.pattern.clone(),
799                format: opts.format.clone(),
800                minimum: opts.minimum.clone(),
801                maximum: opts.maximum.clone(),
802                exclusive_minimum: opts.exclusive_minimum.clone(),
803                exclusive_maximum: opts.exclusive_maximum.clone(),
804                precision: opts.precision,
805                scale: opts.scale,
806            }
807        }),
808        primary_key: prop.primary_key,
809        primary_key_position: prop.primary_key_position,
810        unique: prop.unique,
811        nullable: !prop.required,
812        partitioned: prop.partitioned,
813        partition_key_position: prop.partition_key_position,
814        clustered: prop.clustered,
815        classification: prop.classification.clone(),
816        critical_data_element: prop.critical_data_element,
817        encrypted_name: prop.encrypted_name.clone(),
818        transform_source_objects: prop.transform_source_objects.clone(),
819        transform_logic: prop.transform_logic.clone(),
820        transform_description: prop.transform_description.clone(),
821        examples: prop.examples.clone(),
822        default_value: prop.default_value.clone(),
823        relationships: prop
824            .relationships
825            .iter()
826            .map(|r| ColumnPropertyRelationship {
827                relationship_type: r.relationship_type.clone(),
828                to: r.to.clone(),
829            })
830            .collect(),
831        authoritative_definitions: prop
832            .authoritative_definitions
833            .iter()
834            .map(|d| ColumnAuthDef {
835                definition_type: d.definition_type.clone(),
836                url: d.url.clone(),
837            })
838            .collect(),
839        quality: if prop.quality.is_empty() {
840            None
841        } else {
842            Some(
843                prop.quality
844                    .iter()
845                    .filter_map(|q| serde_json::to_value(q).ok())
846                    .filter_map(|v| v.as_object().cloned())
847                    .map(|m| m.into_iter().collect())
848                    .collect(),
849            )
850        },
851        enum_values: if prop.enum_values.is_empty() {
852            None
853        } else {
854            Some(prop.enum_values.clone())
855        },
856        tags: prop.tags.clone(),
857        custom_properties: prop
858            .custom_properties
859            .iter()
860            .map(|cp| (cp.property.clone(), cp.value.clone()))
861            .collect(),
862    }
863}
864
865#[cfg(test)]
866mod tests {
867    use super::*;
868
869    #[test]
870    fn test_property_to_column_roundtrip() {
871        let prop = Property::new("email", "string")
872            .with_required(true)
873            .with_description("User email address")
874            .with_classification("pii");
875
876        let col = Column::from(&prop);
877        assert_eq!(col.name, "email");
878        assert_eq!(col.data_type, "string");
879        assert!(!col.nullable); // required -> not nullable
880        assert_eq!(col.description, "User email address");
881        assert_eq!(col.classification, Some("pii".to_string()));
882
883        let prop2 = Property::from(&col);
884        assert_eq!(prop2.name, "email");
885        assert_eq!(prop2.logical_type, "string");
886        assert!(prop2.required);
887        assert_eq!(prop2.description, Some("User email address".to_string()));
888    }
889
890    #[test]
891    fn test_schema_to_table_roundtrip() {
892        let schema = SchemaObject::new("users")
893            .with_physical_name("tbl_users")
894            .with_physical_type("table")
895            .with_business_name("User Accounts")
896            .with_description("User data")
897            .with_properties(vec![
898                Property::new("id", "integer").with_primary_key(true),
899                Property::new("email", "string").with_required(true),
900            ]);
901
902        let table = Table::from(&schema);
903        assert_eq!(table.name, "users");
904        assert_eq!(table.columns.len(), 2);
905        assert_eq!(
906            table
907                .odcl_metadata
908                .get("physicalName")
909                .and_then(|v| v.as_str()),
910            Some("tbl_users")
911        );
912
913        let schema2 = SchemaObject::from(&table);
914        assert_eq!(schema2.name, "users");
915        assert_eq!(schema2.physical_name, Some("tbl_users".to_string()));
916        assert_eq!(schema2.physical_type, Some("table".to_string()));
917        assert_eq!(schema2.properties.len(), 2);
918    }
919
920    #[test]
921    fn test_contract_to_tables_roundtrip() {
922        let contract = ODCSContract::new("test-contract", "1.0.0")
923            .with_domain("test")
924            .with_status("active")
925            .with_schema(
926                SchemaObject::new("orders")
927                    .with_physical_type("table")
928                    .with_properties(vec![
929                        Property::new("id", "integer").with_primary_key(true),
930                        Property::new("total", "number"),
931                    ]),
932            )
933            .with_schema(
934                SchemaObject::new("items")
935                    .with_physical_type("table")
936                    .with_properties(vec![Property::new("id", "integer").with_primary_key(true)]),
937            );
938
939        let tables = contract.to_tables();
940        assert_eq!(tables.len(), 2);
941        assert_eq!(tables[0].name, "orders");
942        assert_eq!(tables[1].name, "items");
943
944        // Check contract metadata is in tables
945        assert_eq!(
946            tables[0]
947                .odcl_metadata
948                .get("domain")
949                .and_then(|v| v.as_str()),
950            Some("test")
951        );
952
953        let contract2 = ODCSContract::from_tables(&tables);
954        assert_eq!(contract2.name, "test-contract");
955        assert_eq!(contract2.version, "1.0.0");
956        assert_eq!(contract2.domain, Some("test".to_string()));
957        assert_eq!(contract2.schema_count(), 2);
958    }
959
960    #[test]
961    fn test_nested_property_flattening() {
962        let schema = SchemaObject::new("events").with_properties(vec![
963            Property::new("id", "string"),
964            Property::new("address", "object").with_nested_properties(vec![
965                Property::new("street", "string"),
966                Property::new("city", "string"),
967            ]),
968        ]);
969
970        let table = Table::from(&schema);
971
972        // Should have flattened columns: id, address, address.street, address.city
973        let column_names: Vec<&str> = table.columns.iter().map(|c| c.name.as_str()).collect();
974        assert!(column_names.contains(&"id"));
975        assert!(column_names.contains(&"address"));
976        assert!(column_names.contains(&"address.street"));
977        assert!(column_names.contains(&"address.city"));
978    }
979
980    #[test]
981    fn test_to_table_data() {
982        let contract = ODCSContract::new("test", "1.0.0")
983            .with_domain("test-domain")
984            .with_schema(
985                SchemaObject::new("users")
986                    .with_description("User data")
987                    .with_properties(vec![
988                        Property::new("id", "integer").with_primary_key(true),
989                        Property::new("name", "string"),
990                    ]),
991            );
992
993        let table_data = contract.to_table_data();
994        assert_eq!(table_data.len(), 1);
995        assert_eq!(table_data[0].name, Some("users".to_string()));
996        // Schema-level description is stored in the schema, not propagated to TableData description
997        // TableData.description is contract-level description
998        assert_eq!(table_data[0].domain, Some("test-domain".to_string()));
999        assert_eq!(table_data[0].columns.len(), 2);
1000    }
1001}