data_modelling_core/models/odcs/
schema.rs1use super::property::Property;
7use super::supporting::{AuthoritativeDefinition, CustomProperty, QualityRule, SchemaRelationship};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
33#[serde(rename_all = "camelCase")]
34pub struct SchemaObject {
35 #[serde(skip_serializing_if = "Option::is_none")]
38 pub id: Option<String>,
39 pub name: String,
41 #[serde(skip_serializing_if = "Option::is_none")]
43 pub physical_name: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub physical_type: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
49 pub business_name: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
52 pub description: Option<String>,
53
54 #[serde(skip_serializing_if = "Option::is_none")]
57 pub data_granularity_description: Option<String>,
58
59 #[serde(default)]
62 pub properties: Vec<Property>,
63
64 #[serde(default, skip_serializing_if = "Vec::is_empty")]
67 pub relationships: Vec<SchemaRelationship>,
68
69 #[serde(default, skip_serializing_if = "Vec::is_empty")]
72 pub quality: Vec<QualityRule>,
73
74 #[serde(default, skip_serializing_if = "Vec::is_empty")]
77 pub authoritative_definitions: Vec<AuthoritativeDefinition>,
78
79 #[serde(default, skip_serializing_if = "Vec::is_empty")]
82 pub tags: Vec<String>,
83 #[serde(default, skip_serializing_if = "Vec::is_empty")]
85 pub custom_properties: Vec<CustomProperty>,
86}
87
88impl SchemaObject {
89 pub fn new(name: impl Into<String>) -> Self {
91 Self {
92 name: name.into(),
93 ..Default::default()
94 }
95 }
96
97 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 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 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 pub fn with_description(mut self, description: impl Into<String>) -> Self {
117 self.description = Some(description.into());
118 self
119 }
120
121 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 pub fn with_properties(mut self, properties: Vec<Property>) -> Self {
129 self.properties = properties;
130 self
131 }
132
133 pub fn with_property(mut self, property: Property) -> Self {
135 self.properties.push(property);
136 self
137 }
138
139 pub fn with_relationships(mut self, relationships: Vec<SchemaRelationship>) -> Self {
141 self.relationships = relationships;
142 self
143 }
144
145 pub fn with_relationship(mut self, relationship: SchemaRelationship) -> Self {
147 self.relationships.push(relationship);
148 self
149 }
150
151 pub fn with_quality(mut self, quality: Vec<QualityRule>) -> Self {
153 self.quality = quality;
154 self
155 }
156
157 pub fn with_quality_rule(mut self, rule: QualityRule) -> Self {
159 self.quality.push(rule);
160 self
161 }
162
163 pub fn with_authoritative_definitions(
165 mut self,
166 definitions: Vec<AuthoritativeDefinition>,
167 ) -> Self {
168 self.authoritative_definitions = definitions;
169 self
170 }
171
172 pub fn with_authoritative_definition(mut self, definition: AuthoritativeDefinition) -> Self {
174 self.authoritative_definitions.push(definition);
175 self
176 }
177
178 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
180 self.tags = tags;
181 self
182 }
183
184 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
186 self.tags.push(tag.into());
187 self
188 }
189
190 pub fn with_custom_properties(mut self, custom_properties: Vec<CustomProperty>) -> Self {
192 self.custom_properties = custom_properties;
193 self
194 }
195
196 pub fn with_custom_property(mut self, custom_property: CustomProperty) -> Self {
198 self.custom_properties.push(custom_property);
199 self
200 }
201
202 pub fn with_id(mut self, id: impl Into<String>) -> Self {
204 self.id = Some(id.into());
205 self
206 }
207
208 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 pub fn required_properties(&self) -> Vec<&Property> {
218 self.properties.iter().filter(|p| p.required).collect()
219 }
220
221 pub fn get_property(&self, name: &str) -> Option<&Property> {
223 self.properties.iter().find(|p| p.name == name)
224 }
225
226 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 pub fn property_count(&self) -> usize {
233 self.properties.len()
234 }
235
236 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 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}