remodel_core/validation/
mod.rs1use serde::{Deserialize, Serialize};
9
10use crate::models::conceptual::{ConceptualModel, EntityId};
11use crate::models::logical::{LogicalModel, TableId};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15pub enum Severity {
16 Info,
18 Warning,
20 Error,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct Diagnostic {
27 pub severity: Severity,
29 pub code: &'static str,
31 pub message: String,
33}
34
35impl Diagnostic {
36 pub fn error(code: &'static str, message: impl Into<String>) -> Self {
38 Self { severity: Severity::Error, code, message: message.into() }
39 }
40
41 pub fn warning(code: &'static str, message: impl Into<String>) -> Self {
43 Self { severity: Severity::Warning, code, message: message.into() }
44 }
45}
46
47pub fn validate_conceptual(model: &ConceptualModel) -> Vec<Diagnostic> {
49 let mut out = Vec::new();
50
51 for entity in model.entities.values() {
52 if entity.attributes.is_empty() && !is_in_specialization_child(model, entity.id) {
53 out.push(Diagnostic::warning(
54 "W001",
55 format!("entity `{}` has no attributes", entity.name),
56 ));
57 }
58 }
59
60 for entity in model.entities.values() {
61 if !entity.attributes.is_empty()
62 && !entity.attributes.iter().any(|a| {
63 model.attributes.get(a).map(|x| x.is_primary).unwrap_or(false)
64 })
65 && !is_in_specialization_child(model, entity.id)
66 && !entity.weak
67 {
68 out.push(Diagnostic::error(
69 "E002",
70 format!("entity `{}` has no primary-key attribute", entity.name),
71 ));
72 }
73 }
74
75 for rel in model.relationships.values() {
76 if rel.endpoints.len() < 2 {
77 out.push(Diagnostic::error(
78 "E003",
79 format!(
80 "relationship `{}` has only {} endpoint(s); at least 2 required",
81 rel.name,
82 rel.endpoints.len()
83 ),
84 ));
85 }
86 }
87
88 for rel in model.relationships.values() {
89 for ep in &rel.endpoints {
90 if !model.entities.contains_key(&ep.entity) {
91 out.push(Diagnostic::error(
92 "E004",
93 format!(
94 "relationship `{}` references unknown entity #{}",
95 rel.name, ep.entity.0
96 ),
97 ));
98 }
99 }
100 }
101
102 out
103}
104
105pub fn validate_logical(model: &LogicalModel) -> Vec<Diagnostic> {
107 let mut out = Vec::new();
108
109 for table in model.tables.values() {
110 if table.columns.is_empty() {
111 out.push(Diagnostic::error(
112 "L001",
113 format!("table `{}` has no columns", table.name),
114 ));
115 continue;
116 }
117 if table.primary_key().is_none() {
118 out.push(Diagnostic::warning(
119 "L002",
120 format!("table `{}` has no primary key", table.name),
121 ));
122 }
123 for c in table.constraints.values() {
124 if let crate::models::logical::ConstraintKind::ForeignKey(fk) = &c.kind {
125 if !model.tables.contains_key(&fk.references_table) {
126 out.push(Diagnostic::error(
127 "L003",
128 format!(
129 "table `{}` has FK referencing unknown table #{}",
130 table.name, fk.references_table.0
131 ),
132 ));
133 }
134 if fk.columns.len() != fk.references_columns.len() {
135 out.push(Diagnostic::error(
136 "L004",
137 format!(
138 "table `{}` has FK with mismatched arity ({} local vs {} referenced)",
139 table.name,
140 fk.columns.len(),
141 fk.references_columns.len()
142 ),
143 ));
144 }
145 }
146 }
147 }
148
149 let _ = (TableId(0),);
150 out
151}
152
153fn is_in_specialization_child(model: &ConceptualModel, eid: EntityId) -> bool {
154 model
155 .specializations
156 .values()
157 .any(|s| s.children.iter().any(|c| *c == eid))
158}