use super::property::Property;
use super::supporting::{AuthoritativeDefinition, CustomProperty, QualityRule, SchemaRelationship};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct SchemaObject {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub business_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_granularity_description: Option<String>,
#[serde(default)]
pub properties: Vec<Property>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relationships: Vec<SchemaRelationship>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub quality: Vec<QualityRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authoritative_definitions: Vec<AuthoritativeDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub tags: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub custom_properties: Vec<CustomProperty>,
}
impl SchemaObject {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Default::default()
}
}
pub fn with_physical_name(mut self, physical_name: impl Into<String>) -> Self {
self.physical_name = Some(physical_name.into());
self
}
pub fn with_physical_type(mut self, physical_type: impl Into<String>) -> Self {
self.physical_type = Some(physical_type.into());
self
}
pub fn with_business_name(mut self, business_name: impl Into<String>) -> Self {
self.business_name = Some(business_name.into());
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn with_data_granularity_description(mut self, description: impl Into<String>) -> Self {
self.data_granularity_description = Some(description.into());
self
}
pub fn with_properties(mut self, properties: Vec<Property>) -> Self {
self.properties = properties;
self
}
pub fn with_property(mut self, property: Property) -> Self {
self.properties.push(property);
self
}
pub fn with_relationships(mut self, relationships: Vec<SchemaRelationship>) -> Self {
self.relationships = relationships;
self
}
pub fn with_relationship(mut self, relationship: SchemaRelationship) -> Self {
self.relationships.push(relationship);
self
}
pub fn with_quality(mut self, quality: Vec<QualityRule>) -> Self {
self.quality = quality;
self
}
pub fn with_quality_rule(mut self, rule: QualityRule) -> Self {
self.quality.push(rule);
self
}
pub fn with_authoritative_definitions(
mut self,
definitions: Vec<AuthoritativeDefinition>,
) -> Self {
self.authoritative_definitions = definitions;
self
}
pub fn with_authoritative_definition(mut self, definition: AuthoritativeDefinition) -> Self {
self.authoritative_definitions.push(definition);
self
}
pub fn with_tags(mut self, tags: Vec<String>) -> Self {
self.tags = tags;
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_custom_properties(mut self, custom_properties: Vec<CustomProperty>) -> Self {
self.custom_properties = custom_properties;
self
}
pub fn with_custom_property(mut self, custom_property: CustomProperty) -> Self {
self.custom_properties.push(custom_property);
self
}
pub fn with_id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
pub fn primary_key_properties(&self) -> Vec<&Property> {
let mut pk_props: Vec<&Property> =
self.properties.iter().filter(|p| p.primary_key).collect();
pk_props.sort_by_key(|p| p.primary_key_position.unwrap_or(i32::MAX));
pk_props
}
pub fn required_properties(&self) -> Vec<&Property> {
self.properties.iter().filter(|p| p.required).collect()
}
pub fn get_property(&self, name: &str) -> Option<&Property> {
self.properties.iter().find(|p| p.name == name)
}
pub fn get_property_mut(&mut self, name: &str) -> Option<&mut Property> {
self.properties.iter_mut().find(|p| p.name == name)
}
pub fn property_count(&self) -> usize {
self.properties.len()
}
pub fn has_nested_properties(&self) -> bool {
self.properties.iter().any(|p| p.has_nested_structure())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_schema_object_creation() {
let schema = SchemaObject::new("users")
.with_physical_name("tbl_users")
.with_physical_type("table")
.with_business_name("User Accounts")
.with_description("Contains user data");
assert_eq!(schema.name, "users");
assert_eq!(schema.physical_name, Some("tbl_users".to_string()));
assert_eq!(schema.physical_type, Some("table".to_string()));
assert_eq!(schema.business_name, Some("User Accounts".to_string()));
assert_eq!(schema.description, Some("Contains user data".to_string()));
}
#[test]
fn test_schema_with_properties() {
let schema = SchemaObject::new("orders").with_properties(vec![
Property::new("id", "integer")
.with_primary_key(true)
.with_primary_key_position(1),
Property::new("customer_id", "integer").with_required(true),
Property::new("total", "number"),
]);
assert_eq!(schema.property_count(), 3);
let pk_props = schema.primary_key_properties();
assert_eq!(pk_props.len(), 1);
assert_eq!(pk_props[0].name, "id");
let required_props = schema.required_properties();
assert_eq!(required_props.len(), 1);
assert_eq!(required_props[0].name, "customer_id");
}
#[test]
fn test_get_property() {
let schema = SchemaObject::new("products")
.with_property(Property::new("id", "integer"))
.with_property(Property::new("name", "string"));
let id_prop = schema.get_property("id");
assert!(id_prop.is_some());
assert_eq!(id_prop.unwrap().name, "id");
let missing = schema.get_property("nonexistent");
assert!(missing.is_none());
}
#[test]
fn test_serialization() {
let schema = SchemaObject::new("events")
.with_physical_type("topic")
.with_properties(vec![
Property::new("event_id", "string").with_primary_key(true),
Property::new("timestamp", "timestamp"),
]);
let json = serde_json::to_string_pretty(&schema).unwrap();
assert!(json.contains("\"name\": \"events\""));
assert!(json.contains("\"physicalType\": \"topic\""));
assert!(json.contains("\"properties\""));
assert!(json.contains("physicalType"));
assert!(!json.contains("physical_type"));
}
#[test]
fn test_deserialization() {
let json = r#"{
"name": "customers",
"physicalName": "customer_table",
"physicalType": "table",
"businessName": "Customer Records",
"description": "All customer information",
"dataGranularityDescription": "One row per customer",
"properties": [
{
"name": "id",
"logicalType": "integer",
"primaryKey": true
},
{
"name": "email",
"logicalType": "string",
"required": true
}
],
"tags": ["pii", "customer-data"]
}"#;
let schema: SchemaObject = serde_json::from_str(json).unwrap();
assert_eq!(schema.name, "customers");
assert_eq!(schema.physical_name, Some("customer_table".to_string()));
assert_eq!(schema.physical_type, Some("table".to_string()));
assert_eq!(schema.business_name, Some("Customer Records".to_string()));
assert_eq!(
schema.data_granularity_description,
Some("One row per customer".to_string())
);
assert_eq!(schema.properties.len(), 2);
assert_eq!(schema.tags, vec!["pii", "customer-data"]);
}
#[test]
fn test_has_nested_properties() {
let simple_schema = SchemaObject::new("simple")
.with_property(Property::new("id", "integer"))
.with_property(Property::new("name", "string"));
assert!(!simple_schema.has_nested_properties());
let nested_schema = SchemaObject::new("nested")
.with_property(Property::new("id", "integer"))
.with_property(
Property::new("address", "object")
.with_nested_properties(vec![Property::new("city", "string")]),
);
assert!(nested_schema.has_nested_properties());
}
}