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