use super::supporting::{
AuthoritativeDefinition, CustomProperty, LogicalTypeOptions, PropertyRelationship, QualityRule,
};
use serde::{Deserialize, Serialize};
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct Property {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub business_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub logical_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_type: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub physical_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logical_type_options: Option<LogicalTypeOptions>,
#[serde(default, skip_serializing_if = "is_false")]
pub required: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub primary_key: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub primary_key_position: Option<i32>,
#[serde(default, skip_serializing_if = "is_false")]
pub unique: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub partitioned: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub partition_key_position: Option<i32>,
#[serde(default, skip_serializing_if = "is_false")]
pub clustered: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub classification: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub critical_data_element: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub encrypted_name: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub transform_source_objects: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transform_logic: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub transform_description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub relationships: Vec<PropertyRelationship>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub authoritative_definitions: Vec<AuthoritativeDefinition>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub quality: Vec<QualityRule>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub enum_values: Vec<String>,
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub items: Option<Box<Property>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub properties: Vec<Property>,
}
impl Property {
pub fn new(name: impl Into<String>, logical_type: impl Into<String>) -> Self {
Self {
name: name.into(),
logical_type: logical_type.into(),
..Default::default()
}
}
pub fn with_required(mut self, required: bool) -> Self {
self.required = required;
self
}
pub fn with_primary_key(mut self, primary_key: bool) -> Self {
self.primary_key = primary_key;
self
}
pub fn with_primary_key_position(mut self, position: i32) -> Self {
self.primary_key_position = Some(position);
self
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.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_physical_type(mut self, physical_type: impl Into<String>) -> Self {
self.physical_type = Some(physical_type.into());
self
}
pub fn with_physical_name(mut self, physical_name: impl Into<String>) -> Self {
self.physical_name = Some(physical_name.into());
self
}
pub fn with_nested_properties(mut self, properties: Vec<Property>) -> Self {
self.properties = properties;
self
}
pub fn with_items(mut self, items: Property) -> Self {
self.items = Some(Box::new(items));
self
}
pub fn with_enum_values(mut self, values: Vec<String>) -> Self {
self.enum_values = values;
self
}
pub fn with_custom_property(mut self, property: CustomProperty) -> Self {
self.custom_properties.push(property);
self
}
pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
self.tags.push(tag.into());
self
}
pub fn with_unique(mut self, unique: bool) -> Self {
self.unique = unique;
self
}
pub fn with_classification(mut self, classification: impl Into<String>) -> Self {
self.classification = Some(classification.into());
self
}
pub fn has_nested_structure(&self) -> bool {
!self.properties.is_empty() || self.items.is_some()
}
pub fn is_object(&self) -> bool {
self.logical_type.to_lowercase() == "object"
|| self.logical_type.to_lowercase() == "struct"
|| !self.properties.is_empty()
}
pub fn is_array(&self) -> bool {
self.logical_type.to_lowercase() == "array" || self.items.is_some()
}
pub fn flatten_to_paths(&self) -> Vec<(String, &Property)> {
let mut result = Vec::new();
self.flatten_recursive(&self.name, &mut result);
result
}
fn flatten_recursive<'a>(
&'a self,
current_path: &str,
result: &mut Vec<(String, &'a Property)>,
) {
result.push((current_path.to_string(), self));
for nested in &self.properties {
let nested_path = if current_path.is_empty() {
nested.name.clone()
} else {
format!("{}.{}", current_path, nested.name)
};
nested.flatten_recursive(&nested_path, result);
}
if let Some(ref items) = self.items {
let items_path = if current_path.is_empty() {
"[]".to_string()
} else {
format!("{}.[]", current_path)
};
items.flatten_recursive(&items_path, result);
}
}
pub fn from_flat_paths(paths: &[(String, Property)]) -> Vec<Property> {
use indexmap::IndexMap;
let mut top_level: IndexMap<String, Vec<(String, &Property)>> = IndexMap::new();
for (path, prop) in paths {
let parts: Vec<&str> = path.split('.').collect();
if parts.is_empty() {
continue;
}
let top_name = parts[0].to_string();
let remaining_path = if parts.len() > 1 {
parts[1..].join(".")
} else {
String::new()
};
top_level
.entry(top_name)
.or_default()
.push((remaining_path, prop));
}
let mut result = Vec::new();
for (name, children) in top_level {
let root = children
.iter()
.find(|(path, _)| path.is_empty())
.map(|(_, p)| (*p).clone());
let mut prop = root.unwrap_or_else(|| Property::new(&name, "object"));
prop.name = name;
let nested_paths: Vec<(String, Property)> = children
.iter()
.filter(|(path, _)| !path.is_empty())
.map(|(path, p)| (path.clone(), (*p).clone()))
.collect();
if !nested_paths.is_empty() {
let has_array_items = nested_paths.iter().any(|(p, _)| p.starts_with("[]"));
if has_array_items {
let items_paths: Vec<(String, Property)> = nested_paths
.iter()
.filter(|(p, _)| p.starts_with("[]"))
.map(|(p, prop)| {
let remaining = if p == "[]" {
String::new()
} else {
p.strip_prefix("[].").unwrap_or("").to_string()
};
(remaining, prop.clone())
})
.collect();
if !items_paths.is_empty() {
let item_root = items_paths
.iter()
.find(|(p, _)| p.is_empty())
.map(|(_, p)| p.clone());
let mut items_prop =
item_root.unwrap_or_else(|| Property::new("", "object"));
let nested_item_paths: Vec<(String, Property)> = items_paths
.into_iter()
.filter(|(p, _)| !p.is_empty())
.collect();
if !nested_item_paths.is_empty() {
items_prop.properties = Property::from_flat_paths(&nested_item_paths);
}
prop.items = Some(Box::new(items_prop));
}
}
let object_paths: Vec<(String, Property)> = nested_paths
.into_iter()
.filter(|(p, _)| !p.starts_with("[]"))
.collect();
if !object_paths.is_empty() {
prop.properties = Property::from_flat_paths(&object_paths);
}
}
result.push(prop);
}
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_property_creation() {
let prop = Property::new("id", "integer")
.with_primary_key(true)
.with_required(true)
.with_description("Unique identifier");
assert_eq!(prop.name, "id");
assert_eq!(prop.logical_type, "integer");
assert!(prop.primary_key);
assert!(prop.required);
assert_eq!(prop.description, Some("Unique identifier".to_string()));
}
#[test]
fn test_nested_object_property() {
let address = Property::new("address", "object").with_nested_properties(vec![
Property::new("street", "string"),
Property::new("city", "string"),
Property::new("zip", "string"),
]);
assert!(address.is_object());
assert!(!address.is_array());
assert!(address.has_nested_structure());
assert_eq!(address.properties.len(), 3);
}
#[test]
fn test_array_property() {
let tags = Property::new("tags", "array").with_items(Property::new("", "string"));
assert!(tags.is_array());
assert!(!tags.is_object());
assert!(tags.has_nested_structure());
assert!(tags.items.is_some());
}
#[test]
fn test_flatten_to_paths() {
let address = Property::new("address", "object").with_nested_properties(vec![
Property::new("street", "string"),
Property::new("city", "string"),
]);
let paths = address.flatten_to_paths();
assert_eq!(paths.len(), 3);
assert_eq!(paths[0].0, "address");
assert!(paths.iter().any(|(p, _)| p == "address.street"));
assert!(paths.iter().any(|(p, _)| p == "address.city"));
}
#[test]
fn test_flatten_array_to_paths() {
let items = Property::new("items", "array").with_items(
Property::new("", "object").with_nested_properties(vec![
Property::new("name", "string"),
Property::new("quantity", "integer"),
]),
);
let paths = items.flatten_to_paths();
assert!(paths.iter().any(|(p, _)| p == "items"));
assert!(paths.iter().any(|(p, _)| p == "items.[]"));
assert!(paths.iter().any(|(p, _)| p == "items.[].name"));
assert!(paths.iter().any(|(p, _)| p == "items.[].quantity"));
}
#[test]
fn test_serialization() {
let prop = Property::new("name", "string")
.with_required(true)
.with_description("User name");
let json = serde_json::to_string_pretty(&prop).unwrap();
assert!(json.contains("\"name\": \"name\""));
assert!(json.contains("\"logicalType\": \"string\""));
assert!(json.contains("\"required\": true"));
assert!(json.contains("logicalType"));
assert!(!json.contains("logical_type"));
}
#[test]
fn test_deserialization() {
let json = r#"{
"name": "email",
"logicalType": "string",
"required": true,
"logicalTypeOptions": {
"format": "email",
"maxLength": 255
}
}"#;
let prop: Property = serde_json::from_str(json).unwrap();
assert_eq!(prop.name, "email");
assert_eq!(prop.logical_type, "string");
assert!(prop.required);
assert!(prop.logical_type_options.is_some());
let opts = prop.logical_type_options.unwrap();
assert_eq!(opts.format, Some("email".to_string()));
assert_eq!(opts.max_length, Some(255));
}
}