use std::collections::HashMap;
use aingle_graph::{GraphDB, NodeId, Predicate, Triple, TriplePattern, Value};
use serde::{Deserialize, Serialize};
use crate::engine::RuleEngine;
use crate::error::Result;
use crate::rule::{Rule, RuleSet};
pub trait LogicValidator {
fn validate(&self, triple: &Triple) -> Result<ValidationResult>;
fn validate_with_context(&self, triple: &Triple, graph: &GraphDB) -> Result<ValidationResult>;
fn check_contradictions(&self, graph: &GraphDB) -> Result<Vec<Contradiction>>;
fn severity(&self) -> Severity;
}
pub struct PoLValidator {
engine: RuleEngine,
severity: Severity,
contradiction_pairs: Vec<(String, String)>,
#[allow(dead_code)]
cache: HashMap<String, ValidationResult>,
}
impl PoLValidator {
pub fn new() -> Self {
Self {
engine: RuleEngine::new(),
severity: Severity::Error,
contradiction_pairs: Self::default_contradiction_pairs(),
cache: HashMap::new(),
}
}
pub fn with_engine(engine: RuleEngine) -> Self {
Self {
engine,
severity: Severity::Error,
contradiction_pairs: Self::default_contradiction_pairs(),
cache: HashMap::new(),
}
}
pub fn with_rules(rules: RuleSet) -> Self {
Self {
engine: RuleEngine::with_rules(rules),
severity: Severity::Error,
contradiction_pairs: Self::default_contradiction_pairs(),
cache: HashMap::new(),
}
}
fn default_contradiction_pairs() -> Vec<(String, String)> {
vec![
("is".to_string(), "is_not".to_string()),
("has".to_string(), "lacks".to_string()),
("true".to_string(), "false".to_string()),
("alive".to_string(), "dead".to_string()),
("exists".to_string(), "not_exists".to_string()),
("enables".to_string(), "disables".to_string()),
("allows".to_string(), "forbids".to_string()),
("before".to_string(), "after".to_string()),
]
}
pub fn add_contradiction_pair(&mut self, pred1: impl Into<String>, pred2: impl Into<String>) {
self.contradiction_pairs.push((pred1.into(), pred2.into()));
}
pub fn set_severity(&mut self, severity: Severity) {
self.severity = severity;
}
pub fn add_rule(&mut self, rule: Rule) {
self.engine.add_rule(rule);
}
pub fn engine(&self) -> &RuleEngine {
&self.engine
}
pub fn engine_mut(&mut self) -> &mut RuleEngine {
&mut self.engine
}
fn get_contradiction(&self, predicate: &str) -> Option<&str> {
for (p1, p2) in &self.contradiction_pairs {
if predicate == p1 {
return Some(p2);
}
if predicate == p2 {
return Some(p1);
}
}
None
}
fn check_temporal_consistency(
&self,
triple: &Triple,
graph: &GraphDB,
) -> Result<Vec<ValidationError>> {
let mut errors = Vec::new();
let pred = triple.predicate.as_str();
if pred == "before" || pred == "after" || pred == "happens_at" {
let related = graph.get_subject(&triple.subject)?;
for other in related {
if other.predicate.as_str() == pred && other.object != triple.object {
if pred == "before" {
if let Some(obj_node) = triple.object.as_node() {
let reverse = graph.find(
TriplePattern::subject(obj_node.clone())
.with_predicate(triple.predicate.clone())
.with_object(Value::Node(triple.subject.clone())),
)?;
if !reverse.is_empty() {
errors.push(ValidationError {
kind: ErrorKind::TemporalInconsistency,
message: format!(
"Temporal cycle detected: {} before {} and {} before {}",
node_to_string(&triple.subject),
value_str(&triple.object),
value_str(&triple.object),
node_to_string(&triple.subject)
),
severity: Severity::Error,
source_rule: None,
});
}
}
}
}
}
}
Ok(errors)
}
fn check_type_consistency(
&self,
triple: &Triple,
graph: &GraphDB,
) -> Result<Vec<ValidationError>> {
let mut errors = Vec::new();
if triple.predicate.as_str() == "type" || triple.predicate.as_str() == "rdf:type" {
let type_value = value_str(&triple.object);
let existing_types = graph.find(
TriplePattern::subject(triple.subject.clone())
.with_predicate(triple.predicate.clone()),
)?;
for existing in existing_types {
let existing_type = value_str(&existing.object);
if existing_type != type_value {
let disjoint = graph.find(
TriplePattern::subject(NodeId::named(&existing_type))
.with_predicate(Predicate::named("disjoint_with"))
.with_object(Value::Node(NodeId::named(&type_value))),
)?;
if !disjoint.is_empty() {
errors.push(ValidationError {
kind: ErrorKind::TypeConflict,
message: format!(
"{} cannot be both {} and {} (disjoint types)",
node_to_string(&triple.subject),
existing_type,
type_value
),
severity: Severity::Error,
source_rule: None,
});
}
}
}
}
Ok(errors)
}
}
impl Default for PoLValidator {
fn default() -> Self {
Self::new()
}
}
impl LogicValidator for PoLValidator {
fn validate(&self, triple: &Triple) -> Result<ValidationResult> {
let engine_result = self.engine.validate(triple);
let mut result = ValidationResult {
is_valid: engine_result.is_valid(),
errors: Vec::new(),
warnings: Vec::new(),
info: Vec::new(),
};
for rejection in engine_result.rejections {
result.errors.push(ValidationError {
kind: ErrorKind::RuleViolation,
message: rejection.reason,
severity: self.severity,
source_rule: Some(rejection.rule_id),
});
}
for warning in engine_result.warnings {
result.warnings.push(ValidationWarning {
message: warning.message,
source_rule: Some(warning.rule_id),
});
}
Ok(result)
}
fn validate_with_context(&self, triple: &Triple, graph: &GraphDB) -> Result<ValidationResult> {
let mut result = self.validate(triple)?;
if let Some(contradicting_pred) = self.get_contradiction(triple.predicate.as_str()) {
let pattern = TriplePattern::subject(triple.subject.clone())
.with_predicate(Predicate::named(contradicting_pred))
.with_object(triple.object.clone());
if !graph.find(pattern)?.is_empty() {
result.errors.push(ValidationError {
kind: ErrorKind::Contradiction,
message: format!(
"Contradiction: {} {} {} conflicts with existing {} relation",
node_to_string(&triple.subject),
triple.predicate.as_str(),
value_str(&triple.object),
contradicting_pred
),
severity: Severity::Error,
source_rule: None,
});
result.is_valid = false;
}
}
let temporal_errors = self.check_temporal_consistency(triple, graph)?;
if !temporal_errors.is_empty() {
result.errors.extend(temporal_errors);
result.is_valid = false;
}
let type_errors = self.check_type_consistency(triple, graph)?;
if !type_errors.is_empty() {
result.errors.extend(type_errors);
result.is_valid = false;
}
Ok(result)
}
fn check_contradictions(&self, graph: &GraphDB) -> Result<Vec<Contradiction>> {
let mut contradictions = Vec::new();
for (pred1, pred2) in &self.contradiction_pairs {
let triples1 = graph.get_predicate(&Predicate::named(pred1))?;
for t1 in triples1 {
let pattern = TriplePattern::subject(t1.subject.clone())
.with_predicate(Predicate::named(pred2))
.with_object(t1.object.clone());
let contradicting = graph.find(pattern)?;
for t2 in contradicting {
contradictions.push(Contradiction {
triple1: t1.clone(),
triple2: t2,
description: format!("{} contradicts {}", pred1, pred2),
});
}
}
}
Ok(contradictions)
}
fn severity(&self) -> Severity {
self.severity
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ValidationResult {
pub is_valid: bool,
pub errors: Vec<ValidationError>,
pub warnings: Vec<ValidationWarning>,
pub info: Vec<String>,
}
impl ValidationResult {
pub fn valid() -> Self {
Self {
is_valid: true,
errors: Vec::new(),
warnings: Vec::new(),
info: Vec::new(),
}
}
pub fn invalid(error: ValidationError) -> Self {
Self {
is_valid: false,
errors: vec![error],
warnings: Vec::new(),
info: Vec::new(),
}
}
pub fn is_valid(&self) -> bool {
self.is_valid && self.errors.is_empty()
}
pub fn add_error(&mut self, error: ValidationError) {
self.is_valid = false;
self.errors.push(error);
}
pub fn add_warning(&mut self, warning: ValidationWarning) {
self.warnings.push(warning);
}
pub fn merge(&mut self, other: ValidationResult) {
if !other.is_valid {
self.is_valid = false;
}
self.errors.extend(other.errors);
self.warnings.extend(other.warnings);
self.info.extend(other.info);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub kind: ErrorKind,
pub message: String,
pub severity: Severity,
pub source_rule: Option<String>,
}
impl ValidationError {
pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
severity: Severity::Error,
source_rule: None,
}
}
pub fn with_severity(mut self, severity: Severity) -> Self {
self.severity = severity;
self
}
pub fn with_rule(mut self, rule_id: impl Into<String>) -> Self {
self.source_rule = Some(rule_id.into());
self
}
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{:?}] {}", self.kind, self.message)
}
}
impl std::error::Error for ValidationError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationWarning {
pub message: String,
pub source_rule: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum ErrorKind {
Contradiction,
RuleViolation,
AuthorityViolation,
TemporalInconsistency,
TypeConflict,
MissingPrecondition,
InvalidReference,
ConstraintViolation,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Info,
Warning,
Error,
Critical,
}
#[derive(Debug, Clone)]
pub struct Contradiction {
pub triple1: Triple,
pub triple2: Triple,
pub description: String,
}
impl Contradiction {
pub fn explain(&self) -> String {
format!(
"Contradiction detected:\n 1. {} {} {}\n 2. {} {} {}\n Reason: {}",
node_to_string(&self.triple1.subject),
self.triple1.predicate.as_str(),
value_str(&self.triple1.object),
node_to_string(&self.triple2.subject),
self.triple2.predicate.as_str(),
value_str(&self.triple2.object),
self.description
)
}
}
fn node_to_string(node: &NodeId) -> String {
match node {
NodeId::Named(s) => s.clone(),
NodeId::Hash(h) => format!("hash:{}", hex::encode(&h[..8])),
NodeId::Blank(id) => format!("_:b{}", id),
}
}
fn value_str(value: &Value) -> String {
match value {
Value::Node(node) => node_to_string(node),
Value::String(s) => format!("\"{}\"", s),
Value::Integer(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::Boolean(b) => b.to_string(),
Value::DateTime(dt) => dt.clone(),
Value::Bytes(b) => format!("<{} bytes>", b.len()),
Value::Typed { value, .. } => format!("\"{}\"", value),
Value::LangString { value, lang } => format!("\"{}\"@{}", value, lang),
Value::Json(v) => v.to_string(),
Value::Null => "null".to_string(),
}
}
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validator_creation() {
let validator = PoLValidator::new();
assert_eq!(validator.severity(), Severity::Error);
}
#[test]
fn test_simple_validation() {
let validator = PoLValidator::new();
let triple = Triple::new(
NodeId::named("alice"),
Predicate::named("knows"),
Value::Node(NodeId::named("bob")),
);
let result = validator.validate(&triple).unwrap();
assert!(result.is_valid());
}
#[test]
fn test_contradiction_detection() {
let validator = PoLValidator::new();
let graph = GraphDB::memory().unwrap();
graph
.insert(Triple::new(
NodeId::named("alice"),
Predicate::named("is"),
Value::Node(NodeId::named("human")),
))
.unwrap();
graph
.insert(Triple::new(
NodeId::named("alice"),
Predicate::named("is_not"),
Value::Node(NodeId::named("human")),
))
.unwrap();
let contradictions = validator.check_contradictions(&graph).unwrap();
assert_eq!(contradictions.len(), 1);
assert!(contradictions[0].description.contains("is"));
}
#[test]
fn test_validation_with_context() {
let validator = PoLValidator::new();
let graph = GraphDB::memory().unwrap();
graph
.insert(Triple::new(
NodeId::named("alice"),
Predicate::named("is"),
Value::Node(NodeId::named("alive")),
))
.unwrap();
let contradicting = Triple::new(
NodeId::named("alice"),
Predicate::named("is_not"),
Value::Node(NodeId::named("alive")),
);
let result = validator
.validate_with_context(&contradicting, &graph)
.unwrap();
assert!(!result.is_valid());
assert!(!result.errors.is_empty());
}
#[test]
fn test_validation_result() {
let mut result = ValidationResult::valid();
assert!(result.is_valid());
result.add_error(ValidationError::new(ErrorKind::Contradiction, "Test error"));
assert!(!result.is_valid());
}
#[test]
fn test_severity_ordering() {
assert!(Severity::Info < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
assert!(Severity::Error < Severity::Critical);
}
}