use serde::{Deserialize, Serialize};
use crate::models::conceptual::{ConceptualModel, EntityId};
use crate::models::logical::{LogicalModel, TableId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum Severity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Diagnostic {
pub severity: Severity,
pub code: &'static str,
pub message: String,
}
impl Diagnostic {
pub fn error(code: &'static str, message: impl Into<String>) -> Self {
Self { severity: Severity::Error, code, message: message.into() }
}
pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
Self { severity: Severity::Warning, code, message: message.into() }
}
}
pub fn validate_conceptual(model: &ConceptualModel) -> Vec<Diagnostic> {
let mut out = Vec::new();
for entity in model.entities.values() {
if entity.attributes.is_empty() && !is_in_specialization_child(model, entity.id) {
out.push(Diagnostic::warning(
"W001",
format!("entity `{}` has no attributes", entity.name),
));
}
}
for entity in model.entities.values() {
if !entity.attributes.is_empty()
&& !entity.attributes.iter().any(|a| {
model.attributes.get(a).map(|x| x.is_primary).unwrap_or(false)
})
&& !is_in_specialization_child(model, entity.id)
&& !entity.weak
{
out.push(Diagnostic::error(
"E002",
format!("entity `{}` has no primary-key attribute", entity.name),
));
}
}
for rel in model.relationships.values() {
if rel.endpoints.len() < 2 {
out.push(Diagnostic::error(
"E003",
format!(
"relationship `{}` has only {} endpoint(s); at least 2 required",
rel.name,
rel.endpoints.len()
),
));
}
}
for rel in model.relationships.values() {
for ep in &rel.endpoints {
if !model.entities.contains_key(&ep.entity) {
out.push(Diagnostic::error(
"E004",
format!(
"relationship `{}` references unknown entity #{}",
rel.name, ep.entity.0
),
));
}
}
}
out
}
pub fn validate_logical(model: &LogicalModel) -> Vec<Diagnostic> {
let mut out = Vec::new();
for table in model.tables.values() {
if table.columns.is_empty() {
out.push(Diagnostic::error(
"L001",
format!("table `{}` has no columns", table.name),
));
continue;
}
if table.primary_key().is_none() {
out.push(Diagnostic::warning(
"L002",
format!("table `{}` has no primary key", table.name),
));
}
for c in table.constraints.values() {
if let crate::models::logical::ConstraintKind::ForeignKey(fk) = &c.kind {
if !model.tables.contains_key(&fk.references_table) {
out.push(Diagnostic::error(
"L003",
format!(
"table `{}` has FK referencing unknown table #{}",
table.name, fk.references_table.0
),
));
}
if fk.columns.len() != fk.references_columns.len() {
out.push(Diagnostic::error(
"L004",
format!(
"table `{}` has FK with mismatched arity ({} local vs {} referenced)",
table.name,
fk.columns.len(),
fk.references_columns.len()
),
));
}
}
}
}
let _ = (TableId(0),);
out
}
fn is_in_specialization_child(model: &ConceptualModel, eid: EntityId) -> bool {
model
.specializations
.values()
.any(|s| s.children.iter().any(|c| *c == eid))
}