Skip to main content

tensorlogic_adapters/
validation.rs

1//! Schema validation and completeness checking.
2
3use anyhow::Result;
4use std::collections::HashSet;
5
6use crate::{DomainHierarchy, SymbolTable};
7
8/// Validation results with errors, warnings, and hints
9#[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
42/// Schema validator for symbol tables
43pub 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    /// Perform comprehensive schema validation
62    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    /// Check schema completeness
73    fn check_completeness(&self, report: &mut ValidationReport) -> Result<()> {
74        // Check that all domains referenced by predicates exist
75        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        // Check that all variables are bound to existing domains
87        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        // Check for hierarchy references
97        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    /// Check schema consistency
112    fn check_consistency(&self, report: &mut ValidationReport) -> Result<()> {
113        // Check for duplicate domain definitions
114        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        // Check for duplicate predicate definitions
122        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        // Validate hierarchy is acyclic
130        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        // Check domain cardinalities are non-negative
137        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    /// Check semantic validity
150    fn check_semantic(&self, report: &mut ValidationReport) -> Result<()> {
151        // Warn about unused domains
152        let mut used_domains = HashSet::new();
153
154        // Collect domains used in predicates
155        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        // Collect domains used in variables
162        for domain in self.table.variables.values() {
163            used_domains.insert(domain.as_str());
164        }
165
166        // Check for unused domains
167        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        // Warn about predicates with "Unknown" domains
177        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        // Suggest missing predicates for equivalence relations
187        if let Some(hierarchy) = self.hierarchy {
188            self.suggest_equality_predicates(hierarchy, report);
189        }
190
191        Ok(())
192    }
193
194    /// Suggest equality predicates for domains in hierarchy
195    fn suggest_equality_predicates(
196        &self,
197        _hierarchy: &DomainHierarchy,
198        report: &mut ValidationReport,
199    ) {
200        // Check if there's an equality predicate for each domain
201        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
213/// Helper trait for DomainHierarchy to get all domains
214trait HierarchyHelper {
215    fn get_all_domains(&self) -> Vec<String>;
216}
217
218impl HierarchyHelper for DomainHierarchy {
219    fn get_all_domains(&self) -> Vec<String> {
220        // This is a workaround since we can't access private fields
221        // In practice, we'd need to add a public method to DomainHierarchy
222        // For now, return empty vec
223        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        // Bypass normal add_predicate validation by directly inserting
253        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}