data_modelling_core/models/odcs/
property.rs1use super::supporting::{
7 AuthoritativeDefinition, CustomProperty, LogicalTypeOptions, PropertyRelationship, QualityRule,
8};
9use serde::{Deserialize, Serialize};
10
11fn is_false(b: &bool) -> bool {
13 !*b
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
45#[serde(rename_all = "camelCase")]
46pub struct Property {
47 #[serde(skip_serializing_if = "Option::is_none")]
50 pub id: Option<String>,
51 pub name: String,
53 #[serde(skip_serializing_if = "Option::is_none")]
55 pub business_name: Option<String>,
56 #[serde(skip_serializing_if = "Option::is_none")]
58 pub description: Option<String>,
59
60 pub logical_type: String,
63 #[serde(skip_serializing_if = "Option::is_none")]
65 pub physical_type: Option<String>,
66 #[serde(skip_serializing_if = "Option::is_none")]
68 pub physical_name: Option<String>,
69 #[serde(skip_serializing_if = "Option::is_none")]
71 pub logical_type_options: Option<LogicalTypeOptions>,
72
73 #[serde(default, skip_serializing_if = "is_false")]
76 pub required: bool,
77 #[serde(default, skip_serializing_if = "is_false")]
79 pub primary_key: bool,
80 #[serde(skip_serializing_if = "Option::is_none")]
82 pub primary_key_position: Option<i32>,
83 #[serde(default, skip_serializing_if = "is_false")]
85 pub unique: bool,
86
87 #[serde(default, skip_serializing_if = "is_false")]
90 pub partitioned: bool,
91 #[serde(skip_serializing_if = "Option::is_none")]
93 pub partition_key_position: Option<i32>,
94 #[serde(default, skip_serializing_if = "is_false")]
96 pub clustered: bool,
97
98 #[serde(skip_serializing_if = "Option::is_none")]
101 pub classification: Option<String>,
102 #[serde(default, skip_serializing_if = "is_false")]
104 pub critical_data_element: bool,
105 #[serde(skip_serializing_if = "Option::is_none")]
107 pub encrypted_name: Option<String>,
108
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
112 pub transform_source_objects: Vec<String>,
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub transform_logic: Option<String>,
116 #[serde(skip_serializing_if = "Option::is_none")]
118 pub transform_description: Option<String>,
119
120 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub examples: Vec<serde_json::Value>,
124 #[serde(skip_serializing_if = "Option::is_none")]
126 pub default_value: Option<serde_json::Value>,
127
128 #[serde(default, skip_serializing_if = "Vec::is_empty")]
131 pub relationships: Vec<PropertyRelationship>,
132 #[serde(default, skip_serializing_if = "Vec::is_empty")]
134 pub authoritative_definitions: Vec<AuthoritativeDefinition>,
135
136 #[serde(default, skip_serializing_if = "Vec::is_empty")]
139 pub quality: Vec<QualityRule>,
140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
142 pub enum_values: Vec<String>,
143
144 #[serde(default, skip_serializing_if = "Vec::is_empty")]
147 pub tags: Vec<String>,
148 #[serde(default, skip_serializing_if = "Vec::is_empty")]
150 pub custom_properties: Vec<CustomProperty>,
151
152 #[serde(skip_serializing_if = "Option::is_none")]
155 pub items: Option<Box<Property>>,
156 #[serde(default, skip_serializing_if = "Vec::is_empty")]
158 pub properties: Vec<Property>,
159}
160
161impl Property {
162 pub fn new(name: impl Into<String>, logical_type: impl Into<String>) -> Self {
164 Self {
165 name: name.into(),
166 logical_type: logical_type.into(),
167 ..Default::default()
168 }
169 }
170
171 pub fn with_required(mut self, required: bool) -> Self {
173 self.required = required;
174 self
175 }
176
177 pub fn with_primary_key(mut self, primary_key: bool) -> Self {
179 self.primary_key = primary_key;
180 self
181 }
182
183 pub fn with_primary_key_position(mut self, position: i32) -> Self {
185 self.primary_key_position = Some(position);
186 self
187 }
188
189 pub fn with_description(mut self, description: impl Into<String>) -> Self {
191 self.description = Some(description.into());
192 self
193 }
194
195 pub fn with_business_name(mut self, business_name: impl Into<String>) -> Self {
197 self.business_name = Some(business_name.into());
198 self
199 }
200
201 pub fn with_physical_type(mut self, physical_type: impl Into<String>) -> Self {
203 self.physical_type = Some(physical_type.into());
204 self
205 }
206
207 pub fn with_physical_name(mut self, physical_name: impl Into<String>) -> Self {
209 self.physical_name = Some(physical_name.into());
210 self
211 }
212
213 pub fn with_nested_properties(mut self, properties: Vec<Property>) -> Self {
215 self.properties = properties;
216 self
217 }
218
219 pub fn with_items(mut self, items: Property) -> Self {
221 self.items = Some(Box::new(items));
222 self
223 }
224
225 pub fn with_enum_values(mut self, values: Vec<String>) -> Self {
227 self.enum_values = values;
228 self
229 }
230
231 pub fn with_custom_property(mut self, property: CustomProperty) -> Self {
233 self.custom_properties.push(property);
234 self
235 }
236
237 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
239 self.tags.push(tag.into());
240 self
241 }
242
243 pub fn with_unique(mut self, unique: bool) -> Self {
245 self.unique = unique;
246 self
247 }
248
249 pub fn with_classification(mut self, classification: impl Into<String>) -> Self {
251 self.classification = Some(classification.into());
252 self
253 }
254
255 pub fn has_nested_structure(&self) -> bool {
257 !self.properties.is_empty() || self.items.is_some()
258 }
259
260 pub fn is_object(&self) -> bool {
262 self.logical_type.to_lowercase() == "object"
263 || self.logical_type.to_lowercase() == "struct"
264 || !self.properties.is_empty()
265 }
266
267 pub fn is_array(&self) -> bool {
269 self.logical_type.to_lowercase() == "array" || self.items.is_some()
270 }
271
272 pub fn flatten_to_paths(&self) -> Vec<(String, &Property)> {
275 let mut result = Vec::new();
276 self.flatten_recursive(&self.name, &mut result);
277 result
278 }
279
280 fn flatten_recursive<'a>(
281 &'a self,
282 current_path: &str,
283 result: &mut Vec<(String, &'a Property)>,
284 ) {
285 result.push((current_path.to_string(), self));
287
288 for nested in &self.properties {
290 let nested_path = if current_path.is_empty() {
291 nested.name.clone()
292 } else {
293 format!("{}.{}", current_path, nested.name)
294 };
295 nested.flatten_recursive(&nested_path, result);
296 }
297
298 if let Some(ref items) = self.items {
300 let items_path = if current_path.is_empty() {
301 "[]".to_string()
302 } else {
303 format!("{}.[]", current_path)
304 };
305 items.flatten_recursive(&items_path, result);
306 }
307 }
308
309 pub fn from_flat_paths(paths: &[(String, Property)]) -> Vec<Property> {
316 use std::collections::HashMap;
317
318 let mut top_level: HashMap<String, Vec<(String, &Property)>> = HashMap::new();
320
321 for (path, prop) in paths {
322 let parts: Vec<&str> = path.split('.').collect();
323 if parts.is_empty() {
324 continue;
325 }
326
327 let top_name = parts[0].to_string();
328 let remaining_path = if parts.len() > 1 {
329 parts[1..].join(".")
330 } else {
331 String::new()
332 };
333
334 top_level
335 .entry(top_name)
336 .or_default()
337 .push((remaining_path, prop));
338 }
339
340 let mut result = Vec::new();
342 for (name, children) in top_level {
343 let root = children
345 .iter()
346 .find(|(path, _)| path.is_empty())
347 .map(|(_, p)| (*p).clone());
348
349 let mut prop = root.unwrap_or_else(|| Property::new(&name, "object"));
350 prop.name = name;
351
352 let nested_paths: Vec<(String, Property)> = children
354 .iter()
355 .filter(|(path, _)| !path.is_empty())
356 .map(|(path, p)| (path.clone(), (*p).clone()))
357 .collect();
358
359 if !nested_paths.is_empty() {
360 let has_array_items = nested_paths.iter().any(|(p, _)| p.starts_with("[]"));
362
363 if has_array_items {
364 let items_paths: Vec<(String, Property)> = nested_paths
366 .iter()
367 .filter(|(p, _)| p.starts_with("[]"))
368 .map(|(p, prop)| {
369 let remaining = if p == "[]" {
370 String::new()
371 } else {
372 p.strip_prefix("[].").unwrap_or("").to_string()
373 };
374 (remaining, prop.clone())
375 })
376 .collect();
377
378 if !items_paths.is_empty() {
379 let item_root = items_paths
381 .iter()
382 .find(|(p, _)| p.is_empty())
383 .map(|(_, p)| p.clone());
384
385 let mut items_prop =
386 item_root.unwrap_or_else(|| Property::new("", "object"));
387
388 let nested_item_paths: Vec<(String, Property)> = items_paths
390 .into_iter()
391 .filter(|(p, _)| !p.is_empty())
392 .collect();
393
394 if !nested_item_paths.is_empty() {
395 items_prop.properties = Property::from_flat_paths(&nested_item_paths);
396 }
397
398 prop.items = Some(Box::new(items_prop));
399 }
400 }
401
402 let object_paths: Vec<(String, Property)> = nested_paths
404 .into_iter()
405 .filter(|(p, _)| !p.starts_with("[]"))
406 .collect();
407
408 if !object_paths.is_empty() {
409 prop.properties = Property::from_flat_paths(&object_paths);
410 }
411 }
412
413 result.push(prop);
414 }
415
416 result
417 }
418}
419
420#[cfg(test)]
421mod tests {
422 use super::*;
423
424 #[test]
425 fn test_property_creation() {
426 let prop = Property::new("id", "integer")
427 .with_primary_key(true)
428 .with_required(true)
429 .with_description("Unique identifier");
430
431 assert_eq!(prop.name, "id");
432 assert_eq!(prop.logical_type, "integer");
433 assert!(prop.primary_key);
434 assert!(prop.required);
435 assert_eq!(prop.description, Some("Unique identifier".to_string()));
436 }
437
438 #[test]
439 fn test_nested_object_property() {
440 let address = Property::new("address", "object").with_nested_properties(vec![
441 Property::new("street", "string"),
442 Property::new("city", "string"),
443 Property::new("zip", "string"),
444 ]);
445
446 assert!(address.is_object());
447 assert!(!address.is_array());
448 assert!(address.has_nested_structure());
449 assert_eq!(address.properties.len(), 3);
450 }
451
452 #[test]
453 fn test_array_property() {
454 let tags = Property::new("tags", "array").with_items(Property::new("", "string"));
455
456 assert!(tags.is_array());
457 assert!(!tags.is_object());
458 assert!(tags.has_nested_structure());
459 assert!(tags.items.is_some());
460 }
461
462 #[test]
463 fn test_flatten_to_paths() {
464 let address = Property::new("address", "object").with_nested_properties(vec![
465 Property::new("street", "string"),
466 Property::new("city", "string"),
467 ]);
468
469 let paths = address.flatten_to_paths();
470 assert_eq!(paths.len(), 3);
471 assert_eq!(paths[0].0, "address");
472 assert!(paths.iter().any(|(p, _)| p == "address.street"));
473 assert!(paths.iter().any(|(p, _)| p == "address.city"));
474 }
475
476 #[test]
477 fn test_flatten_array_to_paths() {
478 let items = Property::new("items", "array").with_items(
479 Property::new("", "object").with_nested_properties(vec![
480 Property::new("name", "string"),
481 Property::new("quantity", "integer"),
482 ]),
483 );
484
485 let paths = items.flatten_to_paths();
486 assert!(paths.iter().any(|(p, _)| p == "items"));
487 assert!(paths.iter().any(|(p, _)| p == "items.[]"));
488 assert!(paths.iter().any(|(p, _)| p == "items.[].name"));
489 assert!(paths.iter().any(|(p, _)| p == "items.[].quantity"));
490 }
491
492 #[test]
493 fn test_serialization() {
494 let prop = Property::new("name", "string")
495 .with_required(true)
496 .with_description("User name");
497
498 let json = serde_json::to_string_pretty(&prop).unwrap();
499 assert!(json.contains("\"name\": \"name\""));
500 assert!(json.contains("\"logicalType\": \"string\""));
501 assert!(json.contains("\"required\": true"));
502
503 assert!(json.contains("logicalType"));
505 assert!(!json.contains("logical_type"));
506 }
507
508 #[test]
509 fn test_deserialization() {
510 let json = r#"{
511 "name": "email",
512 "logicalType": "string",
513 "required": true,
514 "logicalTypeOptions": {
515 "format": "email",
516 "maxLength": 255
517 }
518 }"#;
519
520 let prop: Property = serde_json::from_str(json).unwrap();
521 assert_eq!(prop.name, "email");
522 assert_eq!(prop.logical_type, "string");
523 assert!(prop.required);
524 assert!(prop.logical_type_options.is_some());
525 let opts = prop.logical_type_options.unwrap();
526 assert_eq!(opts.format, Some("email".to_string()));
527 assert_eq!(opts.max_length, Some(255));
528 }
529}