use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
use crate::models::cardinality::Cardinality;
use crate::models::types::DataType;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct EntityId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AttributeId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct RelationshipId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct SpecializationId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct UnionId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
pub struct AssociativeEntityId(pub u32);
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub enum AttributeKind {
#[default]
Simple,
Composite,
Multivalued {
min: u32,
max: Option<u32>,
},
Derived,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Entity {
pub id: EntityId,
pub name: String,
pub note: String,
pub attributes: Vec<AttributeId>,
pub weak: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Attribute {
pub id: AttributeId,
pub name: String,
pub data_type: DataType,
pub is_primary: bool,
pub is_partial_key: bool,
pub is_optional: bool,
pub kind: AttributeKind,
pub children: Vec<AttributeId>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationshipEndpoint {
pub entity: EntityId,
pub cardinality: Cardinality,
pub role: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Relationship {
pub id: RelationshipId,
pub name: String,
pub endpoints: Vec<RelationshipEndpoint>,
pub attributes: Vec<AttributeId>,
}
impl Relationship {
pub fn is_self(&self) -> bool {
if self.endpoints.is_empty() {
return false;
}
let first = self.endpoints[0].entity;
self.endpoints.iter().all(|e| e.entity == first)
}
pub fn is_binary(&self) -> bool {
self.endpoints.len() == 2
}
pub fn is_nary(&self) -> bool {
self.endpoints.len() >= 3
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
pub struct SpecializationKind {
pub total: bool,
pub overlapping: bool,
}
impl SpecializationKind {
pub const PARTIAL_DISJOINT: Self = Self { total: false, overlapping: false };
pub const TOTAL_DISJOINT: Self = Self { total: true, overlapping: false };
pub const PARTIAL_OVERLAPPING: Self = Self { total: false, overlapping: true };
pub const TOTAL_OVERLAPPING: Self = Self { total: true, overlapping: true };
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Specialization {
pub id: SpecializationId,
pub name: String,
pub parent: EntityId,
pub children: Vec<EntityId>,
pub kind: SpecializationKind,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Union {
pub id: UnionId,
pub name: String,
pub parents: Vec<EntityId>,
pub category: EntityId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AssociativeEntity {
pub id: AssociativeEntityId,
pub relationship: RelationshipId,
pub name: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ConceptualModel {
pub name: String,
next_id: u32,
pub entities: IndexMap<EntityId, Entity>,
pub attributes: IndexMap<AttributeId, Attribute>,
pub relationships: IndexMap<RelationshipId, Relationship>,
pub specializations: IndexMap<SpecializationId, Specialization>,
pub unions: IndexMap<UnionId, Union>,
pub associative_entities: IndexMap<AssociativeEntityId, AssociativeEntity>,
}
impl ConceptualModel {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
..Self::default()
}
}
fn mint(&mut self) -> u32 {
self.next_id = self.next_id.checked_add(1).expect("ID space exhausted");
self.next_id
}
pub fn add_entity(&mut self, name: impl Into<String>) -> EntityId {
let id = EntityId(self.mint());
self.entities.insert(
id,
Entity {
id,
name: name.into(),
note: String::new(),
attributes: Vec::new(),
weak: false,
},
);
id
}
pub fn add_weak_entity(&mut self, name: impl Into<String>) -> EntityId {
let id = self.add_entity(name);
self.entity_mut(id).expect("just inserted").weak = true;
id
}
pub fn entity(&self, id: EntityId) -> Result<&Entity> {
self.entities
.get(&id)
.ok_or_else(|| Error::UnknownReference { kind: "entity", id: format!("{}", id.0) })
}
pub fn entity_mut(&mut self, id: EntityId) -> Result<&mut Entity> {
self.entities
.get_mut(&id)
.ok_or_else(|| Error::UnknownReference { kind: "entity", id: format!("{}", id.0) })
}
pub fn add_attribute(
&mut self,
owner: AttributeOwner,
name: impl Into<String>,
data_type: DataType,
) -> Result<AttributeId> {
let id = AttributeId(self.mint());
self.attributes.insert(
id,
Attribute {
id,
name: name.into(),
data_type,
is_primary: false,
is_partial_key: false,
is_optional: false,
kind: AttributeKind::Simple,
children: Vec::new(),
},
);
match owner {
AttributeOwner::Entity(eid) => self.entity_mut(eid)?.attributes.push(id),
AttributeOwner::Relationship(rid) => self.relationship_mut(rid)?.attributes.push(id),
}
Ok(id)
}
pub fn add_primary_attribute(
&mut self,
entity: EntityId,
name: impl Into<String>,
data_type: DataType,
) -> Result<AttributeId> {
let id = self.add_attribute(AttributeOwner::Entity(entity), name, data_type)?;
let attr = self.attribute_mut(id)?;
attr.is_primary = true;
Ok(id)
}
pub fn attribute(&self, id: AttributeId) -> Result<&Attribute> {
self.attributes
.get(&id)
.ok_or_else(|| Error::UnknownReference { kind: "attribute", id: format!("{}", id.0) })
}
pub fn attribute_mut(&mut self, id: AttributeId) -> Result<&mut Attribute> {
self.attributes
.get_mut(&id)
.ok_or_else(|| Error::UnknownReference { kind: "attribute", id: format!("{}", id.0) })
}
pub fn relate(
&mut self,
name: impl Into<String>,
first: EntityId,
first_card: Cardinality,
) -> RelationshipBuilder<'_> {
let id = RelationshipId(self.mint());
let rel = Relationship {
id,
name: name.into(),
endpoints: vec![RelationshipEndpoint {
entity: first,
cardinality: first_card,
role: None,
}],
attributes: Vec::new(),
};
self.relationships.insert(id, rel);
RelationshipBuilder { model: self, id }
}
pub fn relationship(&self, id: RelationshipId) -> Result<&Relationship> {
self.relationships.get(&id).ok_or_else(|| Error::UnknownReference {
kind: "relationship",
id: format!("{}", id.0),
})
}
pub fn relationship_mut(&mut self, id: RelationshipId) -> Result<&mut Relationship> {
self.relationships.get_mut(&id).ok_or_else(|| Error::UnknownReference {
kind: "relationship",
id: format!("{}", id.0),
})
}
pub fn add_specialization(
&mut self,
name: impl Into<String>,
parent: EntityId,
children: Vec<EntityId>,
kind: SpecializationKind,
) -> Result<SpecializationId> {
if children.len() < 2 {
return Err(Error::InvalidSpecialization(format!(
"specialization `{}` must have at least 2 children, got {}",
name.into(),
children.len()
)));
}
let _ = self.entity(parent)?;
for c in &children {
let _ = self.entity(*c)?;
}
let id = SpecializationId(self.mint());
let name = name.into();
self.specializations.insert(id, Specialization { id, name, parent, children, kind });
Ok(id)
}
pub fn add_union(
&mut self,
name: impl Into<String>,
parents: Vec<EntityId>,
category: EntityId,
) -> Result<UnionId> {
if parents.len() < 2 {
return Err(Error::InvalidSpecialization(format!(
"union `{}` must have at least 2 parents, got {}",
name.into(),
parents.len()
)));
}
let _ = self.entity(category)?;
for p in &parents {
let _ = self.entity(*p)?;
}
let id = UnionId(self.mint());
self.unions
.insert(id, Union { id, name: name.into(), parents, category });
Ok(id)
}
pub fn add_associative_entity(
&mut self,
relationship: RelationshipId,
) -> Result<AssociativeEntityId> {
let rel = self.relationship(relationship)?;
let name = rel.name.clone();
let id = AssociativeEntityId(self.mint());
self.associative_entities
.insert(id, AssociativeEntity { id, relationship, name });
Ok(id)
}
}
#[derive(Debug, Clone, Copy)]
pub enum AttributeOwner {
Entity(EntityId),
Relationship(RelationshipId),
}
pub struct RelationshipBuilder<'m> {
model: &'m mut ConceptualModel,
id: RelationshipId,
}
impl<'m> RelationshipBuilder<'m> {
pub fn with(self, entity: EntityId, cardinality: Cardinality) -> Self {
if let Some(rel) = self.model.relationships.get_mut(&self.id) {
rel.endpoints.push(RelationshipEndpoint { entity, cardinality, role: None });
}
self
}
pub fn with_role(self, role: impl Into<String>) -> Self {
if let Some(rel) = self.model.relationships.get_mut(&self.id) {
if let Some(last) = rel.endpoints.last_mut() {
last.role = Some(role.into());
}
}
self
}
pub fn carry(self, name: impl Into<String>, data_type: DataType) -> Self {
let _ = self
.model
.add_attribute(AttributeOwner::Relationship(self.id), name, data_type);
self
}
pub fn id(self) -> RelationshipId {
self.id
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> ConceptualModel {
let mut m = ConceptualModel::new("library");
let book = m.add_entity("Book");
let author = m.add_entity("Author");
m.add_primary_attribute(book, "id", DataType::Integer).unwrap();
m.add_attribute(AttributeOwner::Entity(book), "title", DataType::Varchar(255)).unwrap();
m.add_primary_attribute(author, "id", DataType::Integer).unwrap();
m.relate("wrote", book, Cardinality::ZeroToMany)
.with(author, Cardinality::OneToMany)
.id();
m
}
#[test]
fn build_basic_model() {
let m = sample();
assert_eq!(m.entities.len(), 2);
assert_eq!(m.attributes.len(), 3);
assert_eq!(m.relationships.len(), 1);
let rel = m.relationships.values().next().unwrap();
assert!(rel.is_binary());
assert!(!rel.is_self());
}
#[test]
fn relationship_self_check() {
let mut m = ConceptualModel::new("hr");
let person = m.add_entity("Person");
let rel = m
.relate("manages", person, Cardinality::ZeroToOne)
.with(person, Cardinality::ZeroToMany)
.id();
assert!(m.relationship(rel).unwrap().is_self());
}
#[test]
fn specialization_requires_two_children() {
let mut m = ConceptualModel::new("vehicles");
let v = m.add_entity("Vehicle");
let car = m.add_entity("Car");
let err = m
.add_specialization("kind", v, vec![car], SpecializationKind::PARTIAL_DISJOINT)
.unwrap_err();
assert!(matches!(err, Error::InvalidSpecialization(_)));
}
#[test]
fn unknown_entity_error() {
let m = ConceptualModel::new("x");
let err = m.entity(EntityId(99)).unwrap_err();
assert!(matches!(err, Error::UnknownReference { kind: "entity", .. }));
}
#[test]
fn json_round_trip() {
let m = sample();
let s = serde_json::to_string(&m).unwrap();
let back: ConceptualModel = serde_json::from_str(&s).unwrap();
assert_eq!(m.entities.len(), back.entities.len());
assert_eq!(m.attributes.len(), back.attributes.len());
assert_eq!(m.relationships.len(), back.relationships.len());
}
}