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.add_domain(DomainInfo::new("Person", 10)).unwrap();
236 table
237 .add_predicate(PredicateInfo::new(
238 "Parent",
239 vec!["Person".into(), "Person".into()],
240 ))
241 .unwrap();
242
243 let validator = SchemaValidator::new(&table);
244 let report = validator.validate().unwrap();
245
246 assert!(report.is_valid());
247 }
248
249 #[test]
250 fn test_validation_missing_domain() {
251 let mut table = SymbolTable::new();
252 table.predicates.insert(
254 "Parent".into(),
255 PredicateInfo::new("Parent", vec!["Person".into(), "Person".into()]),
256 );
257
258 let validator = SchemaValidator::new(&table);
259 let report = validator.validate().unwrap();
260
261 assert!(!report.is_valid());
262 assert!(!report.errors.is_empty());
263 }
264
265 #[test]
266 fn test_validation_unused_domain() {
267 let mut table = SymbolTable::new();
268 table.add_domain(DomainInfo::new("Person", 10)).unwrap();
269 table.add_domain(DomainInfo::new("City", 5)).unwrap();
270 table
271 .add_predicate(PredicateInfo::new(
272 "Parent",
273 vec!["Person".into(), "Person".into()],
274 ))
275 .unwrap();
276
277 let validator = SchemaValidator::new(&table);
278 let report = validator.validate().unwrap();
279
280 assert!(report.is_valid());
281 assert!(!report.warnings.is_empty());
282 }
283
284 #[test]
285 fn test_validation_unknown_domains() {
286 let mut table = SymbolTable::new();
287 table.add_domain(DomainInfo::new("Person", 10)).unwrap();
288 table.predicates.insert(
289 "Test".into(),
290 PredicateInfo::new("Test", vec!["Unknown".into()]),
291 );
292
293 let validator = SchemaValidator::new(&table);
294 let report = validator.validate().unwrap();
295
296 assert!(report.is_valid());
297 assert!(!report.warnings.is_empty());
298 }
299}