tensorlogic_adapters/
validation.rs1use anyhow::Result;
4use std::collections::HashSet;
5
6use crate::{DomainHierarchy, SymbolTable};
7
8#[derive(Clone, Debug, Default)]
10pub struct ValidationReport {
11 pub errors: Vec<String>,
12 pub warnings: Vec<String>,
13 pub hints: Vec<String>,
14}
15
16impl ValidationReport {
17 pub fn new() -> Self {
18 Self::default()
19 }
20
21 pub fn add_error(&mut self, error: impl Into<String>) {
22 self.errors.push(error.into());
23 }
24
25 pub fn add_warning(&mut self, warning: impl Into<String>) {
26 self.warnings.push(warning.into());
27 }
28
29 pub fn add_hint(&mut self, hint: impl Into<String>) {
30 self.hints.push(hint.into());
31 }
32
33 pub fn is_valid(&self) -> bool {
34 self.errors.is_empty()
35 }
36
37 pub fn has_issues(&self) -> bool {
38 !self.errors.is_empty() || !self.warnings.is_empty()
39 }
40}
41
42pub struct SchemaValidator<'a> {
44 table: &'a SymbolTable,
45 hierarchy: Option<&'a DomainHierarchy>,
46}
47
48impl<'a> SchemaValidator<'a> {
49 pub fn new(table: &'a SymbolTable) -> Self {
50 Self {
51 table,
52 hierarchy: None,
53 }
54 }
55
56 pub fn with_hierarchy(mut self, hierarchy: &'a DomainHierarchy) -> Self {
57 self.hierarchy = Some(hierarchy);
58 self
59 }
60
61 pub fn validate(&self) -> Result<ValidationReport> {
63 let mut report = ValidationReport::new();
64
65 self.check_completeness(&mut report)?;
66 self.check_consistency(&mut report)?;
67 self.check_semantic(&mut report)?;
68
69 Ok(report)
70 }
71
72 fn check_completeness(&self, report: &mut ValidationReport) -> Result<()> {
74 for (pred_name, pred) in &self.table.predicates {
76 for domain in &pred.arg_domains {
77 if domain != "Unknown" && !self.table.domains.contains_key(domain) {
78 report.add_error(format!(
79 "Predicate '{}' references undefined domain '{}'",
80 pred_name, domain
81 ));
82 }
83 }
84 }
85
86 for (var, domain) in &self.table.variables {
88 if !self.table.domains.contains_key(domain) {
89 report.add_error(format!(
90 "Variable '{}' is bound to undefined domain '{}'",
91 var, domain
92 ));
93 }
94 }
95
96 if let Some(hierarchy) = self.hierarchy {
98 for domain in hierarchy.get_all_domains() {
99 if !self.table.domains.contains_key(&domain) {
100 report.add_error(format!(
101 "Domain hierarchy references undefined domain '{}'",
102 domain
103 ));
104 }
105 }
106 }
107
108 Ok(())
109 }
110
111 fn check_consistency(&self, report: &mut ValidationReport) -> Result<()> {
113 let mut seen_domains = HashSet::new();
115 for domain_name in self.table.domains.keys() {
116 if !seen_domains.insert(domain_name) {
117 report.add_error(format!("Duplicate domain definition: '{}'", domain_name));
118 }
119 }
120
121 let mut seen_predicates = HashSet::new();
123 for pred_name in self.table.predicates.keys() {
124 if !seen_predicates.insert(pred_name) {
125 report.add_error(format!("Duplicate predicate definition: '{}'", pred_name));
126 }
127 }
128
129 if let Some(hierarchy) = self.hierarchy {
131 if let Err(e) = hierarchy.validate_acyclic() {
132 report.add_error(format!("Domain hierarchy contains cycles: {}", e));
133 }
134 }
135
136 for (domain_name, domain) in &self.table.domains {
138 if domain.cardinality == 0 && domain.elements.is_none() {
139 report.add_warning(format!(
140 "Domain '{}' has cardinality 0 and no elements defined",
141 domain_name
142 ));
143 }
144 }
145
146 Ok(())
147 }
148
149 fn check_semantic(&self, report: &mut ValidationReport) -> Result<()> {
151 let mut used_domains = HashSet::new();
153
154 for pred in self.table.predicates.values() {
156 for domain in &pred.arg_domains {
157 used_domains.insert(domain.as_str());
158 }
159 }
160
161 for domain in self.table.variables.values() {
163 used_domains.insert(domain.as_str());
164 }
165
166 for domain_name in self.table.domains.keys() {
168 if !used_domains.contains(domain_name.as_str()) {
169 report.add_warning(format!(
170 "Domain '{}' is defined but never used",
171 domain_name
172 ));
173 }
174 }
175
176 for (pred_name, pred) in &self.table.predicates {
178 if pred.arg_domains.iter().any(|d| d == "Unknown") {
179 report.add_warning(format!(
180 "Predicate '{}' has 'Unknown' domain types - consider specifying explicit types",
181 pred_name
182 ));
183 }
184 }
185
186 if let Some(hierarchy) = self.hierarchy {
188 self.suggest_equality_predicates(hierarchy, report);
189 }
190
191 Ok(())
192 }
193
194 fn suggest_equality_predicates(
196 &self,
197 _hierarchy: &DomainHierarchy,
198 report: &mut ValidationReport,
199 ) {
200 let has_eq = self.table.predicates.iter().any(|(name, _)| {
202 name.to_lowercase().contains("eq")
203 || name.to_lowercase().contains("equal")
204 || name == "="
205 });
206
207 if !has_eq && !self.table.domains.is_empty() {
208 report.add_hint("Consider defining equality predicates for your domains".to_string());
209 }
210 }
211}
212
213trait HierarchyHelper {
215 fn get_all_domains(&self) -> Vec<String>;
216}
217
218impl HierarchyHelper for DomainHierarchy {
219 fn get_all_domains(&self) -> Vec<String> {
220 Vec::new()
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230 use crate::{DomainInfo, PredicateInfo};
231
232 #[test]
233 fn test_validation_complete_schema() {
234 let mut table = SymbolTable::new();
235 table
236 .add_domain(DomainInfo::new("Person", 10))
237 .expect("unwrap");
238 table
239 .add_predicate(PredicateInfo::new(
240 "Parent",
241 vec!["Person".into(), "Person".into()],
242 ))
243 .expect("unwrap");
244
245 let validator = SchemaValidator::new(&table);
246 let report = validator.validate().expect("unwrap");
247
248 assert!(report.is_valid());
249 }
250
251 #[test]
252 fn test_validation_missing_domain() {
253 let mut table = SymbolTable::new();
254 table.predicates.insert(
256 "Parent".into(),
257 PredicateInfo::new("Parent", vec!["Person".into(), "Person".into()]),
258 );
259
260 let validator = SchemaValidator::new(&table);
261 let report = validator.validate().expect("unwrap");
262
263 assert!(!report.is_valid());
264 assert!(!report.errors.is_empty());
265 }
266
267 #[test]
268 fn test_validation_unused_domain() {
269 let mut table = SymbolTable::new();
270 table
271 .add_domain(DomainInfo::new("Person", 10))
272 .expect("unwrap");
273 table
274 .add_domain(DomainInfo::new("City", 5))
275 .expect("unwrap");
276 table
277 .add_predicate(PredicateInfo::new(
278 "Parent",
279 vec!["Person".into(), "Person".into()],
280 ))
281 .expect("unwrap");
282
283 let validator = SchemaValidator::new(&table);
284 let report = validator.validate().expect("unwrap");
285
286 assert!(report.is_valid());
287 assert!(!report.warnings.is_empty());
288 }
289
290 #[test]
291 fn test_validation_unknown_domains() {
292 let mut table = SymbolTable::new();
293 table
294 .add_domain(DomainInfo::new("Person", 10))
295 .expect("unwrap");
296 table.predicates.insert(
297 "Test".into(),
298 PredicateInfo::new("Test", vec!["Unknown".into()]),
299 );
300
301 let validator = SchemaValidator::new(&table);
302 let report = validator.validate().expect("unwrap");
303
304 assert!(report.is_valid());
305 assert!(!report.warnings.is_empty());
306 }
307}