// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers // All rights reserved. // Licensed under the Apache License, Version 2.0 // // or the MIT license , // at your option. All files in the project carrying such // notice may not be copied, modified, or distributed except // according to those terms. use std::collections::HashMap; use std::collections::HashSet; use std::sync::Arc; use crate::verifier::*; use ng_net::orm::*; impl Verifier { /// Check the validity of a subject and update affecting tracked subjects' validity. /// Might return nested objects that need to be validated. /// Assumes all triples to be of same subject. pub fn update_subject_validity( s_change: &OrmTrackedSubjectChange, shape: &OrmSchemaShape, orm_subscription: &mut Arc, previous_validity: OrmTrackedSubjectValidity, ) -> Vec<(String, String, bool)> { let orm_sub = Arc::get_mut(orm_subscription).unwrap(); let tracked_subjects = &mut orm_sub.tracked_subjects; let Some(tracked_shapes) = tracked_subjects.get_mut(&s_change.subject_iri) else { return vec![]; }; let Some(tracked_subject) = tracked_shapes.get_mut(&shape.iri) else { return vec![]; }; let tracked_subject = Arc::get_mut(tracked_subject).unwrap(); // Keep track of objects that need to be validated against a shape to fetch and validate. let mut need_evaluation: Vec<(String, String, bool)> = vec![]; // Check 1) Check if we need to fetch this object or all parents are untracked. if tracked_subject.parents.len() != 0 { let no_parents_tracking = tracked_subject .parents .values() .all(|parent| match parent.upgrade() { Some(subject) => { subject.valid == OrmTrackedSubjectValidity::Untracked || subject.valid == OrmTrackedSubjectValidity::Invalid } None => true, }); if no_parents_tracking { // Remove tracked predicates and set untracked. tracked_subject.tracked_predicates = HashMap::new(); tracked_subject.valid = OrmTrackedSubjectValidity::Untracked; return vec![]; } else if !no_parents_tracking && previous_validity == OrmTrackedSubjectValidity::Untracked { // We need to fetch the subject's current state: // We have new parents but were previously not recording changes. return vec![(s_change.subject_iri.clone(), shape.iri.clone(), true)]; // TODO } } // Check 2) If there are no changes, there is nothing to do. if s_change.predicates.is_empty() { return vec![]; } let mut new_validity = OrmTrackedSubjectValidity::Valid; fn set_validity( current: &mut OrmTrackedSubjectValidity, new_val: OrmTrackedSubjectValidity, ) { if new_val == OrmTrackedSubjectValidity::Invalid { *current = OrmTrackedSubjectValidity::Invalid; } else { *current = new_val; } } // Check 3) If there is an infinite loop of parents pointing back to us, return invalid. // Create a set of visited parents to detect cycles. if has_cycle(tracked_subject, &mut HashSet::new()) { // Remove tracked predicates and set invalid. tracked_subject.tracked_predicates = HashMap::new(); tracked_subject.valid = OrmTrackedSubjectValidity::Invalid; return vec![]; } // Check 4) Validate subject against each predicate in shape. for p_schema in shape.predicates.iter() { let p_change = s_change.predicates.get(&p_schema.iri); let tracked_pred = p_change.and_then(|pc| pc.tracked_predicate.upgrade()); let count = tracked_pred .as_ref() .map_or_else(|| 0, |tp| tp.current_cardinality); // Check 4.1) Cardinality if count < p_schema.minCardinality { set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); if count <= 0 { // If cardinality is 0, we can remove the tracked predicate. tracked_subject.tracked_predicates.remove(&p_schema.iri); } break; // Check 4.2) Cardinality too high and extra values not allowed. } else if count > p_schema.maxCardinality && p_schema.maxCardinality != -1 && p_schema.extra != Some(true) { // If cardinality is too high and no extra allowed, invalid. set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); break; // Check 4.3) Required literals present. } else if p_schema .dataTypes .iter() .any(|dt| dt.valType == OrmSchemaLiteralType::literal) { // If we have literals, check if all required literals are present. // At least one datatype must match. let some_valid = p_schema.dataTypes.iter().flat_map(|dt| &dt.literals).any( |required_literals| { // Early stop: If no extra values allowed but the sizes // between required and given values mismatches. if !p_schema.extra.unwrap_or(false) && ((required_literals.len() as i32) != tracked_pred.as_ref().map_or(0, |p| p.current_cardinality)) { return false; } // Check that each required literal is present. for required_literal in required_literals { // Is tracked predicate present? if !tracked_pred.as_ref().map_or(false, |t| { t.current_literals.as_ref().map_or(false, |tt| { tt.iter().any(|literal| *literal == *required_literal) }) }) { return false; } } // All required literals present. return true; }, ); if !some_valid { set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); } // Check 4.4) Nested shape correct. } else if p_schema .dataTypes .iter() .any(|dt| dt.valType == OrmSchemaLiteralType::shape) { // If we have a nested shape, we need to check if the nested objects are tracked and valid. let tracked_children = tracked_pred.as_ref().map(|tp| { tp.tracked_children .iter() .filter_map(|weak_tc| weak_tc.upgrade()) .collect::>() }); // First, Count valid, invalid, unknowns, and untracked let counts = tracked_children.as_ref().map_or((0, 0, 0, 0), |children| { children .iter() .map(|tc| { if tc.valid == OrmTrackedSubjectValidity::Valid { (1, 0, 0, 0) } else if tc.valid == OrmTrackedSubjectValidity::Invalid { (0, 1, 0, 0) } else if tc.valid == OrmTrackedSubjectValidity::Pending { (0, 0, 1, 0) } else if tc.valid == OrmTrackedSubjectValidity::Untracked { (0, 0, 0, 1) } else { (0, 0, 0, 0) } }) .fold((0, 0, 0, 0), |(v1, i1, u1, ut1), o| { (v1 + o.0, i1 + o.1, u1 + o.2, ut1 + o.3) }) }); if counts.1 > 0 && p_schema.extra != Some(true) { // If we have at least one invalid nested object and no extra allowed, invalid. set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); break; } else if counts.0 < p_schema.minCardinality { // If we have not enough valid nested objects, invalid. set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); break; } else if counts.3 > 0 { // If we have untracked nested objects, we need to fetch them and validate. set_validity(&mut new_validity, OrmTrackedSubjectValidity::Pending); // After that we need to reevaluate this (subject,shape) again. need_evaluation.push(( s_change.subject_iri.to_string(), shape.iri.clone(), false, )); // Also schedule untracked children for fetching and validation. tracked_children.as_ref().map(|children| { for child in children { if child.valid == OrmTrackedSubjectValidity::Untracked { need_evaluation.push(( child.subject_iri.clone(), child.shape.iri.clone(), true, )); } } }); } else if counts.2 > 0 { // If we have unknown nested objects, we need to wait for their evaluation. set_validity(&mut new_validity, OrmTrackedSubjectValidity::Pending); // Schedule unknown children (NotEvaluated) for re-evaluation without fetch. tracked_children.as_ref().map(|children| { for child in children { if child.valid == OrmTrackedSubjectValidity::Pending { need_evaluation.push(( child.subject_iri.clone(), child.shape.iri.clone(), false, )); } } }); } else { // All nested objects are valid and cardinality is correct. // We are valid with this predicate. } // Check 4.5) Data types correct. } else { // Check if the data type is correct. let allowed_types: Vec<&OrmSchemaLiteralType> = p_schema.dataTypes.iter().map(|dt| &dt.valType).collect(); // For each new value, check that data type is in allowed_types. for val_added in p_change.iter().map(|pc| &pc.values_added).flatten() { let matches = match val_added { BasicType::Bool(_) => allowed_types .iter() .any(|t| **t == OrmSchemaLiteralType::boolean), BasicType::Num(_) => allowed_types .iter() .any(|t| **t == OrmSchemaLiteralType::number), BasicType::Str(_) => allowed_types.iter().any(|t| { **t == OrmSchemaLiteralType::string || **t == OrmSchemaLiteralType::iri }), }; if !matches { set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); break; } } // Break again if validity has become invalid. if new_validity == OrmTrackedSubjectValidity::Invalid { break; } }; } if new_validity == OrmTrackedSubjectValidity::Invalid { // If we are invalid, we can discard new unknowns again - they won't be kept in memory. // We need to remove ourself from child objects parents field and // remove them if no other is tracking. // Child relationship cleanup disabled (nested tracking disabled in this refactor step) // Remove tracked predicates and set untracked. tracked_subject.tracked_predicates = HashMap::new(); // Empty list of children that need evaluation. need_evaluation.retain(|_| false); } else if new_validity == OrmTrackedSubjectValidity::Valid && previous_validity != OrmTrackedSubjectValidity::Valid { // If this subject became valid, we need to refetch this subject; // We fetch need_evaluation.insert(0, (s_change.subject_iri.clone(), shape.iri.clone(), true)); } // If validity changed, parents need to be re-evaluated. if new_validity != previous_validity { // We return the tracking parents which need re-evaluation. // Remember that the last elements (i.e. children or needs_fetch) are evaluated first. return tracked_subject .parents .values() .filter_map(|parent| { parent .upgrade() .map(|parent| (parent.subject_iri.clone(), parent.shape.iri.clone(), false)) }) // Add `need_evaluation`. .chain(need_evaluation) .collect(); } tracked_subject.valid = new_validity; return need_evaluation; } } fn has_cycle(subject: &OrmTrackedSubject, visited: &mut HashSet) -> bool { if visited.contains(&subject.subject_iri) { return true; } visited.insert(subject.subject_iri.clone()); for (_parent_iri, parent_subject) in &subject.parents { if let Some(parent_subject) = parent_subject.upgrade() { if has_cycle(&parent_subject, visited) { return true; } } } visited.remove(&subject.subject_iri); false }