data_modelling_core/models/odcs/
schema.rs

1//! SchemaObject type for ODCS native data structures
2//!
3//! Represents a table/view/topic in an ODCS contract with full support
4//! for all schema-level metadata fields.
5
6use super::property::Property;
7use super::supporting::{AuthoritativeDefinition, CustomProperty, QualityRule, SchemaRelationship};
8use serde::{Deserialize, Serialize};
9
10/// SchemaObject - one table/view/topic in a contract (ODCS v3.1.0)
11///
12/// Schema objects represent individual data structures within a contract.
13/// Each schema object contains properties (columns) and can have its own
14/// metadata like quality rules, relationships, and authoritative definitions.
15///
16/// # Example
17///
18/// ```rust
19/// use data_modelling_core::models::odcs::{SchemaObject, Property};
20///
21/// let users_table = SchemaObject::new("users")
22///     .with_physical_name("tbl_users")
23///     .with_physical_type("table")
24///     .with_business_name("User Accounts")
25///     .with_description("Contains registered user information")
26///     .with_properties(vec![
27///         Property::new("id", "integer").with_primary_key(true),
28///         Property::new("email", "string").with_required(true),
29///         Property::new("name", "string"),
30///     ]);
31/// ```
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
33#[serde(rename_all = "camelCase")]
34pub struct SchemaObject {
35    // === Core Identity Fields ===
36    /// Stable technical identifier
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub id: Option<String>,
39    /// Schema object name (table/view name)
40    pub name: String,
41    /// Physical name in the data source
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub physical_name: Option<String>,
44    /// Physical type ("table", "view", "topic", "file", "object", "stream")
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub physical_type: Option<String>,
47    /// Business name for the schema object
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub business_name: Option<String>,
50    /// Schema object description/documentation
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub description: Option<String>,
53
54    // === Granularity ===
55    /// Description of the data granularity (e.g., "One row per customer per day")
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub data_granularity_description: Option<String>,
58
59    // === Properties (Columns) ===
60    /// List of properties/columns in this schema object
61    #[serde(default)]
62    pub properties: Vec<Property>,
63
64    // === Relationships ===
65    /// Schema-level relationships to other schema objects
66    #[serde(default, skip_serializing_if = "Vec::is_empty")]
67    pub relationships: Vec<SchemaRelationship>,
68
69    // === Quality & Validation ===
70    /// Quality rules and checks at schema level
71    #[serde(default, skip_serializing_if = "Vec::is_empty")]
72    pub quality: Vec<QualityRule>,
73
74    // === References ===
75    /// Authoritative definitions for this schema object
76    #[serde(default, skip_serializing_if = "Vec::is_empty")]
77    pub authoritative_definitions: Vec<AuthoritativeDefinition>,
78
79    // === Tags & Custom Properties ===
80    /// Schema-level tags
81    #[serde(default, skip_serializing_if = "Vec::is_empty")]
82    pub tags: Vec<String>,
83    /// Custom properties for format-specific metadata
84    #[serde(default, skip_serializing_if = "Vec::is_empty")]
85    pub custom_properties: Vec<CustomProperty>,
86}
87
88impl SchemaObject {
89    /// Create a new schema object with the given name
90    pub fn new(name: impl Into<String>) -> Self {
91        Self {
92            name: name.into(),
93            ..Default::default()
94        }
95    }
96
97    /// Set the physical name
98    pub fn with_physical_name(mut self, physical_name: impl Into<String>) -> Self {
99        self.physical_name = Some(physical_name.into());
100        self
101    }
102
103    /// Set the physical type
104    pub fn with_physical_type(mut self, physical_type: impl Into<String>) -> Self {
105        self.physical_type = Some(physical_type.into());
106        self
107    }
108
109    /// Set the business name
110    pub fn with_business_name(mut self, business_name: impl Into<String>) -> Self {
111        self.business_name = Some(business_name.into());
112        self
113    }
114
115    /// Set the description
116    pub fn with_description(mut self, description: impl Into<String>) -> Self {
117        self.description = Some(description.into());
118        self
119    }
120
121    /// Set the data granularity description
122    pub fn with_data_granularity_description(mut self, description: impl Into<String>) -> Self {
123        self.data_granularity_description = Some(description.into());
124        self
125    }
126
127    /// Set the properties (columns)
128    pub fn with_properties(mut self, properties: Vec<Property>) -> Self {
129        self.properties = properties;
130        self
131    }
132
133    /// Add a property
134    pub fn with_property(mut self, property: Property) -> Self {
135        self.properties.push(property);
136        self
137    }
138
139    /// Set the relationships
140    pub fn with_relationships(mut self, relationships: Vec<SchemaRelationship>) -> Self {
141        self.relationships = relationships;
142        self
143    }
144
145    /// Add a relationship
146    pub fn with_relationship(mut self, relationship: SchemaRelationship) -> Self {
147        self.relationships.push(relationship);
148        self
149    }
150
151    /// Set the quality rules
152    pub fn with_quality(mut self, quality: Vec<QualityRule>) -> Self {
153        self.quality = quality;
154        self
155    }
156
157    /// Add a quality rule
158    pub fn with_quality_rule(mut self, rule: QualityRule) -> Self {
159        self.quality.push(rule);
160        self
161    }
162
163    /// Set the authoritative definitions
164    pub fn with_authoritative_definitions(
165        mut self,
166        definitions: Vec<AuthoritativeDefinition>,
167    ) -> Self {
168        self.authoritative_definitions = definitions;
169        self
170    }
171
172    /// Add an authoritative definition
173    pub fn with_authoritative_definition(mut self, definition: AuthoritativeDefinition) -> Self {
174        self.authoritative_definitions.push(definition);
175        self
176    }
177
178    /// Set the tags
179    pub fn with_tags(mut self, tags: Vec<String>) -> Self {
180        self.tags = tags;
181        self
182    }
183
184    /// Add a tag
185    pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
186        self.tags.push(tag.into());
187        self
188    }
189
190    /// Set the custom properties
191    pub fn with_custom_properties(mut self, custom_properties: Vec<CustomProperty>) -> Self {
192        self.custom_properties = custom_properties;
193        self
194    }
195
196    /// Add a custom property
197    pub fn with_custom_property(mut self, custom_property: CustomProperty) -> Self {
198        self.custom_properties.push(custom_property);
199        self
200    }
201
202    /// Set the ID
203    pub fn with_id(mut self, id: impl Into<String>) -> Self {
204        self.id = Some(id.into());
205        self
206    }
207
208    /// Get the primary key properties
209    pub fn primary_key_properties(&self) -> Vec<&Property> {
210        let mut pk_props: Vec<&Property> =
211            self.properties.iter().filter(|p| p.primary_key).collect();
212        pk_props.sort_by_key(|p| p.primary_key_position.unwrap_or(i32::MAX));
213        pk_props
214    }
215
216    /// Get the required properties
217    pub fn required_properties(&self) -> Vec<&Property> {
218        self.properties.iter().filter(|p| p.required).collect()
219    }
220
221    /// Get a property by name
222    pub fn get_property(&self, name: &str) -> Option<&Property> {
223        self.properties.iter().find(|p| p.name == name)
224    }
225
226    /// Get a mutable property by name
227    pub fn get_property_mut(&mut self, name: &str) -> Option<&mut Property> {
228        self.properties.iter_mut().find(|p| p.name == name)
229    }
230
231    /// Count of properties
232    pub fn property_count(&self) -> usize {
233        self.properties.len()
234    }
235
236    /// Check if this schema has any nested/complex properties
237    pub fn has_nested_properties(&self) -> bool {
238        self.properties.iter().any(|p| p.has_nested_structure())
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245
246    #[test]
247    fn test_schema_object_creation() {
248        let schema = SchemaObject::new("users")
249            .with_physical_name("tbl_users")
250            .with_physical_type("table")
251            .with_business_name("User Accounts")
252            .with_description("Contains user data");
253
254        assert_eq!(schema.name, "users");
255        assert_eq!(schema.physical_name, Some("tbl_users".to_string()));
256        assert_eq!(schema.physical_type, Some("table".to_string()));
257        assert_eq!(schema.business_name, Some("User Accounts".to_string()));
258        assert_eq!(schema.description, Some("Contains user data".to_string()));
259    }
260
261    #[test]
262    fn test_schema_with_properties() {
263        let schema = SchemaObject::new("orders").with_properties(vec![
264            Property::new("id", "integer")
265                .with_primary_key(true)
266                .with_primary_key_position(1),
267            Property::new("customer_id", "integer").with_required(true),
268            Property::new("total", "number"),
269        ]);
270
271        assert_eq!(schema.property_count(), 3);
272
273        let pk_props = schema.primary_key_properties();
274        assert_eq!(pk_props.len(), 1);
275        assert_eq!(pk_props[0].name, "id");
276
277        let required_props = schema.required_properties();
278        assert_eq!(required_props.len(), 1);
279        assert_eq!(required_props[0].name, "customer_id");
280    }
281
282    #[test]
283    fn test_get_property() {
284        let schema = SchemaObject::new("products")
285            .with_property(Property::new("id", "integer"))
286            .with_property(Property::new("name", "string"));
287
288        let id_prop = schema.get_property("id");
289        assert!(id_prop.is_some());
290        assert_eq!(id_prop.unwrap().name, "id");
291
292        let missing = schema.get_property("nonexistent");
293        assert!(missing.is_none());
294    }
295
296    #[test]
297    fn test_serialization() {
298        let schema = SchemaObject::new("events")
299            .with_physical_type("topic")
300            .with_properties(vec![
301                Property::new("event_id", "string").with_primary_key(true),
302                Property::new("timestamp", "timestamp"),
303            ]);
304
305        let json = serde_json::to_string_pretty(&schema).unwrap();
306        assert!(json.contains("\"name\": \"events\""));
307        assert!(json.contains("\"physicalType\": \"topic\""));
308        assert!(json.contains("\"properties\""));
309
310        // Verify camelCase
311        assert!(json.contains("physicalType"));
312        assert!(!json.contains("physical_type"));
313    }
314
315    #[test]
316    fn test_deserialization() {
317        let json = r#"{
318            "name": "customers",
319            "physicalName": "customer_table",
320            "physicalType": "table",
321            "businessName": "Customer Records",
322            "description": "All customer information",
323            "dataGranularityDescription": "One row per customer",
324            "properties": [
325                {
326                    "name": "id",
327                    "logicalType": "integer",
328                    "primaryKey": true
329                },
330                {
331                    "name": "email",
332                    "logicalType": "string",
333                    "required": true
334                }
335            ],
336            "tags": ["pii", "customer-data"]
337        }"#;
338
339        let schema: SchemaObject = serde_json::from_str(json).unwrap();
340        assert_eq!(schema.name, "customers");
341        assert_eq!(schema.physical_name, Some("customer_table".to_string()));
342        assert_eq!(schema.physical_type, Some("table".to_string()));
343        assert_eq!(schema.business_name, Some("Customer Records".to_string()));
344        assert_eq!(
345            schema.data_granularity_description,
346            Some("One row per customer".to_string())
347        );
348        assert_eq!(schema.properties.len(), 2);
349        assert_eq!(schema.tags, vec!["pii", "customer-data"]);
350    }
351
352    #[test]
353    fn test_has_nested_properties() {
354        let simple_schema = SchemaObject::new("simple")
355            .with_property(Property::new("id", "integer"))
356            .with_property(Property::new("name", "string"));
357
358        assert!(!simple_schema.has_nested_properties());
359
360        let nested_schema = SchemaObject::new("nested")
361            .with_property(Property::new("id", "integer"))
362            .with_property(
363                Property::new("address", "object")
364                    .with_nested_properties(vec![Property::new("city", "string")]),
365            );
366
367        assert!(nested_schema.has_nested_properties());
368    }
369}