Skip to main content

data_modelling_core/models/odcs/
property.rs

1//! Property type for ODCS native data structures
2//!
3//! Represents a column/field in an ODCS schema object with full support
4//! for nested properties (OBJECT and ARRAY types).
5
6use super::supporting::{
7    AuthoritativeDefinition, CustomProperty, LogicalTypeOptions, PropertyRelationship, QualityRule,
8};
9use serde::{Deserialize, Serialize};
10
11/// Helper function to skip serializing false boolean values
12fn is_false(b: &bool) -> bool {
13    !*b
14}
15
16/// Property - one column in a schema object (ODCS v3.1.0)
17///
18/// Properties represent individual fields in a schema. They support nested
19/// structures through the `properties` field (for OBJECT types) and the
20/// `items` field (for ARRAY types).
21///
22/// # Example
23///
24/// ```rust
25/// use data_modelling_core::models::odcs::{Property, LogicalTypeOptions};
26///
27/// // Simple property
28/// let id_prop = Property::new("id", "integer")
29///     .with_primary_key(true)
30///     .with_required(true);
31///
32/// // Nested object property
33/// let address_prop = Property::new("address", "object")
34///     .with_nested_properties(vec![
35///         Property::new("street", "string"),
36///         Property::new("city", "string"),
37///         Property::new("zip", "string"),
38///     ]);
39///
40/// // Array property
41/// let tags_prop = Property::new("tags", "array")
42///     .with_items(Property::new("", "string"));
43/// ```
44#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
45#[serde(rename_all = "camelCase")]
46pub struct Property {
47    // === Core Identity Fields ===
48    /// Stable technical identifier
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub id: Option<String>,
51    /// Property name
52    pub name: String,
53    /// Business name for the property
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub business_name: Option<String>,
56    /// Property description/documentation
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub description: Option<String>,
59
60    // === Type Information ===
61    /// Logical data type (e.g., "string", "integer", "number", "boolean", "object", "array")
62    pub logical_type: String,
63    /// Physical database type (e.g., "VARCHAR(100)", "BIGINT")
64    #[serde(skip_serializing_if = "Option::is_none")]
65    pub physical_type: Option<String>,
66    /// Physical name in the data source
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub physical_name: Option<String>,
69    /// Additional type options (min/max length, pattern, precision, etc.)
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub logical_type_options: Option<LogicalTypeOptions>,
72
73    // === Key Constraints ===
74    /// Whether the property is required (inverse of nullable)
75    #[serde(default, skip_serializing_if = "is_false")]
76    pub required: bool,
77    /// Whether this property is part of the primary key
78    #[serde(default, skip_serializing_if = "is_false")]
79    pub primary_key: bool,
80    /// Position in composite primary key, 1-based
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub primary_key_position: Option<i32>,
83    /// Whether the property contains unique values
84    #[serde(default, skip_serializing_if = "is_false")]
85    pub unique: bool,
86
87    // === Partitioning & Clustering ===
88    /// Whether the property is used for partitioning
89    #[serde(default, skip_serializing_if = "is_false")]
90    pub partitioned: bool,
91    /// Position in partition key, 1-based
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub partition_key_position: Option<i32>,
94    /// Whether the property is used for clustering
95    #[serde(default, skip_serializing_if = "is_false")]
96    pub clustered: bool,
97
98    // === Data Classification & Security ===
99    /// Data classification level (e.g., "confidential", "public")
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub classification: Option<String>,
102    /// Whether this is a critical data element
103    #[serde(default, skip_serializing_if = "is_false")]
104    pub critical_data_element: bool,
105    /// Name of the encrypted version of this property
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub encrypted_name: Option<String>,
108
109    // === Transformation Metadata ===
110    /// Source objects used in transformation
111    #[serde(default, skip_serializing_if = "Vec::is_empty")]
112    pub transform_source_objects: Vec<String>,
113    /// Transformation logic/expression
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub transform_logic: Option<String>,
116    /// Human-readable transformation description
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub transform_description: Option<String>,
119
120    // === Examples & Defaults ===
121    /// Example values for this property
122    #[serde(default, skip_serializing_if = "Vec::is_empty")]
123    pub examples: Vec<serde_json::Value>,
124    /// Default value for the property
125    #[serde(skip_serializing_if = "Option::is_none")]
126    pub default_value: Option<serde_json::Value>,
127
128    // === Relationships & References ===
129    /// Property-level relationships
130    #[serde(default, skip_serializing_if = "Vec::is_empty")]
131    pub relationships: Vec<PropertyRelationship>,
132    /// Authoritative definitions
133    #[serde(default, skip_serializing_if = "Vec::is_empty")]
134    pub authoritative_definitions: Vec<AuthoritativeDefinition>,
135
136    // === Quality & Validation ===
137    /// Quality rules and checks
138    #[serde(default, skip_serializing_if = "Vec::is_empty")]
139    pub quality: Vec<QualityRule>,
140    /// Enum values if this property is an enumeration type
141    #[serde(default, skip_serializing_if = "Vec::is_empty")]
142    pub enum_values: Vec<String>,
143
144    // === Tags & Custom Properties ===
145    /// Property-level tags
146    #[serde(default, skip_serializing_if = "Vec::is_empty")]
147    pub tags: Vec<String>,
148    /// Custom properties for format-specific metadata
149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
150    pub custom_properties: Vec<CustomProperty>,
151
152    // === Nested Properties (for OBJECT/ARRAY types) ===
153    /// For ARRAY types: the item type definition
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub items: Option<Box<Property>>,
156    /// For OBJECT types: nested property definitions
157    #[serde(default, skip_serializing_if = "Vec::is_empty")]
158    pub properties: Vec<Property>,
159}
160
161impl Property {
162    /// Create a new property with the given name and logical type
163    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    /// Set the property as required
172    pub fn with_required(mut self, required: bool) -> Self {
173        self.required = required;
174        self
175    }
176
177    /// Set the property as a primary key
178    pub fn with_primary_key(mut self, primary_key: bool) -> Self {
179        self.primary_key = primary_key;
180        self
181    }
182
183    /// Set the primary key position
184    pub fn with_primary_key_position(mut self, position: i32) -> Self {
185        self.primary_key_position = Some(position);
186        self
187    }
188
189    /// Set the property description
190    pub fn with_description(mut self, description: impl Into<String>) -> Self {
191        self.description = Some(description.into());
192        self
193    }
194
195    /// Set the business name
196    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    /// Set the physical type
202    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    /// Set the physical name
208    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    /// Set nested properties (for OBJECT types)
214    pub fn with_nested_properties(mut self, properties: Vec<Property>) -> Self {
215        self.properties = properties;
216        self
217    }
218
219    /// Set items property (for ARRAY types)
220    pub fn with_items(mut self, items: Property) -> Self {
221        self.items = Some(Box::new(items));
222        self
223    }
224
225    /// Set enum values
226    pub fn with_enum_values(mut self, values: Vec<String>) -> Self {
227        self.enum_values = values;
228        self
229    }
230
231    /// Add a custom property
232    pub fn with_custom_property(mut self, property: CustomProperty) -> Self {
233        self.custom_properties.push(property);
234        self
235    }
236
237    /// Add a tag
238    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
239        self.tags.push(tag.into());
240        self
241    }
242
243    /// Set unique constraint
244    pub fn with_unique(mut self, unique: bool) -> Self {
245        self.unique = unique;
246        self
247    }
248
249    /// Set classification
250    pub fn with_classification(mut self, classification: impl Into<String>) -> Self {
251        self.classification = Some(classification.into());
252        self
253    }
254
255    /// Check if this property has nested structure (OBJECT or ARRAY type)
256    pub fn has_nested_structure(&self) -> bool {
257        !self.properties.is_empty() || self.items.is_some()
258    }
259
260    /// Check if this is an object type
261    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    /// Check if this is an array type
268    pub fn is_array(&self) -> bool {
269        self.logical_type.to_lowercase() == "array" || self.items.is_some()
270    }
271
272    /// Get all nested properties recursively, returning (path, property) pairs
273    /// Path uses dot notation for nested objects and `[]` for arrays
274    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        // Add current property
286        result.push((current_path.to_string(), self));
287
288        // Recurse into nested object properties
289        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        // Recurse into array items
299        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    /// Create a property tree from a list of flattened columns with dot-notation names
310    ///
311    /// This reconstructs the hierarchical structure from paths like:
312    /// - "address.street" -> nested object
313    /// - "tags.[]" -> array items
314    /// - "items.[].name" -> array of objects
315    pub fn from_flat_paths(paths: &[(String, Property)]) -> Vec<Property> {
316        use std::collections::HashMap;
317
318        // Group by top-level name
319        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        // Build properties from grouped paths
341        let mut result = Vec::new();
342        for (name, children) in top_level {
343            // Find the root property (empty remaining path)
344            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            // Process nested paths
353            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                // Check if it's an array type (has [] in path)
361                let has_array_items = nested_paths.iter().any(|(p, _)| p.starts_with("[]"));
362
363                if has_array_items {
364                    // Build array items
365                    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                        // Find the array item type
380                        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                        // Recursively build nested items
389                        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                // Build object properties (non-array paths)
403                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        // Verify camelCase
504        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}