use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::attribute::ValueType;
use crate::entity::Annotation;
use super::diff::SchemaDiff;
use super::error::SchemaError;
use super::generator;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OwnedAttributeEntry {
pub attr_name: String,
pub value_type: ValueType,
pub annotations: Vec<Annotation>,
}
impl OwnedAttributeEntry {
pub fn flags_string(&self) -> String {
let mut parts = Vec::new();
for ann in &self.annotations {
match ann {
Annotation::Key => parts.push("@key".to_string()),
Annotation::Unique => parts.push("@unique".to_string()),
Annotation::Card(min, max) => {
let max_str = match max {
Some(m) => m.to_string(),
None => "*".to_string(),
};
parts.push(format!("@card({min}..{max_str})"));
}
}
}
parts.join(" ")
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RoleEntry {
pub role_name: String,
pub player_type_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EntitySchemaEntry {
pub type_name: String,
pub is_abstract: bool,
pub parent_type: Option<String>,
pub owned_attributes: Vec<OwnedAttributeEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationSchemaEntry {
pub type_name: String,
pub is_abstract: bool,
pub parent_type: Option<String>,
pub owned_attributes: Vec<OwnedAttributeEntry>,
pub roles: Vec<RoleEntry>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AttributeSchemaEntry {
pub attr_name: String,
pub value_type: ValueType,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct SchemaInfo {
pub entities: BTreeMap<String, EntitySchemaEntry>,
pub relations: BTreeMap<String, RelationSchemaEntry>,
pub attributes: BTreeMap<String, AttributeSchemaEntry>,
}
impl SchemaInfo {
pub fn get_entity_by_name(&self, name: &str) -> Option<&EntitySchemaEntry> {
self.entities.get(name)
}
pub fn get_relation_by_name(&self, name: &str) -> Option<&RelationSchemaEntry> {
self.relations.get(name)
}
pub fn validate(&self) -> Result<(), SchemaError> {
let mut attr_types: BTreeMap<&str, ValueType> = BTreeMap::new();
for attr in self.attributes.values() {
if let Some(&existing) = attr_types.get(attr.attr_name.as_str()) {
if existing != attr.value_type {
return Err(SchemaError::Validation {
message: format!(
"Attribute '{}' has conflicting value types: {} vs {}",
attr.attr_name, existing, attr.value_type
),
});
}
} else {
attr_types.insert(&attr.attr_name, attr.value_type);
}
}
Ok(())
}
pub fn to_typeql(&self) -> Result<String, SchemaError> {
self.validate()?;
Ok(generator::generate_define_block(self))
}
pub fn compare(&self, other: &SchemaInfo) -> SchemaDiff {
SchemaDiff::compute(self, other)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn schema_info_empty() {
let info = SchemaInfo::default();
assert!(info.entities.is_empty());
assert!(info.relations.is_empty());
assert!(info.attributes.is_empty());
}
#[test]
fn get_entity_by_name_found() {
let mut info = SchemaInfo::default();
info.entities.insert(
"person".into(),
EntitySchemaEntry {
type_name: "person".into(),
is_abstract: false,
parent_type: None,
owned_attributes: vec![],
},
);
assert!(info.get_entity_by_name("person").is_some());
assert!(info.get_entity_by_name("nonexistent").is_none());
}
#[test]
fn get_relation_by_name_found() {
let mut info = SchemaInfo::default();
info.relations.insert(
"employment".into(),
RelationSchemaEntry {
type_name: "employment".into(),
is_abstract: false,
parent_type: None,
owned_attributes: vec![],
roles: vec![],
},
);
assert!(info.get_relation_by_name("employment").is_some());
}
#[test]
fn validate_passes_for_consistent_attrs() {
let mut info = SchemaInfo::default();
info.attributes.insert(
"name".into(),
AttributeSchemaEntry {
attr_name: "name".into(),
value_type: ValueType::String,
},
);
info.attributes.insert(
"age".into(),
AttributeSchemaEntry {
attr_name: "age".into(),
value_type: ValueType::Long,
},
);
assert!(info.validate().is_ok());
}
#[test]
fn flags_string_key() {
let entry = OwnedAttributeEntry {
attr_name: "name".into(),
value_type: ValueType::String,
annotations: vec![Annotation::Key],
};
assert_eq!(entry.flags_string(), "@key");
}
#[test]
fn flags_string_card_bounded() {
let entry = OwnedAttributeEntry {
attr_name: "tag".into(),
value_type: ValueType::String,
annotations: vec![Annotation::Card(2, Some(5))],
};
assert_eq!(entry.flags_string(), "@card(2..5)");
}
#[test]
fn flags_string_card_unbounded() {
let entry = OwnedAttributeEntry {
attr_name: "phone".into(),
value_type: ValueType::String,
annotations: vec![Annotation::Card(0, None)],
};
assert_eq!(entry.flags_string(), "@card(0..*)");
}
#[test]
fn flags_string_multiple() {
let entry = OwnedAttributeEntry {
attr_name: "email".into(),
value_type: ValueType::String,
annotations: vec![Annotation::Unique, Annotation::Card(1, Some(3))],
};
assert_eq!(entry.flags_string(), "@unique @card(1..3)");
}
#[test]
fn schema_info_serde_roundtrip() {
let mut info = SchemaInfo::default();
info.entities.insert(
"person".into(),
EntitySchemaEntry {
type_name: "person".into(),
is_abstract: false,
parent_type: None,
owned_attributes: vec![OwnedAttributeEntry {
attr_name: "name".into(),
value_type: ValueType::String,
annotations: vec![Annotation::Key],
}],
},
);
info.relations.insert(
"employment".into(),
RelationSchemaEntry {
type_name: "employment".into(),
is_abstract: false,
parent_type: None,
owned_attributes: vec![],
roles: vec![RoleEntry {
role_name: "employee".into(),
player_type_name: "person".into(),
}],
},
);
info.attributes.insert(
"name".into(),
AttributeSchemaEntry {
attr_name: "name".into(),
value_type: ValueType::String,
},
);
let json = serde_json::to_string(&info).unwrap();
let parsed: SchemaInfo = serde_json::from_str(&json).unwrap();
assert_eq!(info.entities.len(), parsed.entities.len());
assert_eq!(info.relations.len(), parsed.relations.len());
assert_eq!(info.attributes.len(), parsed.attributes.len());
assert_eq!(
info.entities.get("person").unwrap().owned_attributes,
parsed.entities.get("person").unwrap().owned_attributes,
);
}
}