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