1use 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
21impl From<&Property> for Column {
26 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, 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 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 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, 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
192impl From<&SchemaObject> for Table {
197 fn from(schema: &SchemaObject) -> Self {
201 let columns = flatten_properties_to_columns(&schema.properties, "");
203
204 let mut table = Table::new(schema.name.clone(), columns);
205
206 table.schema_name = schema.physical_name.clone();
208
209 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
279fn 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 let mut col = Column::from(prop);
292 col.name = full_name.clone();
293
294 columns.push(col);
295
296 if !prop.properties.is_empty() {
298 let nested = flatten_properties_to_columns(&prop.properties, &full_name);
299 columns.extend(nested);
300 }
301
302 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 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 fn from(table: &Table) -> Self {
325 let flat_props: Vec<(String, Property)> = table
327 .columns
328 .iter()
329 .map(|col| (col.name.clone(), Property::from(col)))
330 .collect();
331
332 let properties = Property::from_flat_paths(&flat_props);
334
335 let mut schema = SchemaObject::new(table.name.clone()).with_properties(properties);
336
337 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
411impl ODCSContract {
416 pub fn to_tables(&self) -> Vec<Table> {
421 self.schema
422 .iter()
423 .map(|schema| {
424 let mut table = Table::from(schema);
425
426 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 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 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 contract.schema = tables.iter().map(SchemaObject::from).collect();
682
683 contract
684 }
685
686 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 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: self.domain.clone(),
708 data_product: self.data_product.clone(),
709 tenant: self.tenant.clone(),
710 description: description_value,
712 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: schema
719 .properties
720 .iter()
721 .map(property_to_column_data)
722 .collect(),
723 servers: self
725 .servers
726 .iter()
727 .filter_map(|s| serde_json::to_value(s).ok())
728 .collect(),
729 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: self
740 .roles
741 .iter()
742 .filter_map(|r| serde_json::to_value(r).ok())
743 .collect(),
744 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 price: self
759 .price
760 .as_ref()
761 .and_then(|p| serde_json::to_value(p).ok()),
762 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 contract_created_ts: self.contract_created_ts.clone(),
776 odcs_metadata: std::collections::HashMap::new(),
778 }
779 })
780 .collect()
781 }
782}
783
784fn 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); 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 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 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 assert_eq!(table_data[0].domain, Some("test-domain".to_string()));
999 assert_eq!(table_data[0].columns.len(), 2);
1000 }
1001}