Rust implementation of NextGraph, a Decentralized and local-first web 3.0 ecosystem https://nextgraph.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
nextgraph-rs/engine/verifier/src/orm/validation.rs

419 lines
19 KiB

// Copyright (c) 2022-2025 Niko Bonnieure, Par le Peuple, NextGraph.org developers
// All rights reserved.
// Licensed under the Apache License, Version 2.0
// <LICENSE-APACHE2 or http://www.apache.org/licenses/LICENSE-2.0>
// or the MIT license <LICENSE-MIT or http://opensource.org/licenses/MIT>,
// at your option. All files in the project carrying such
// notice may not be copied, modified, or distributed except
// according to those terms.
use crate::orm::types::*;
use crate::verifier::*;
use ng_net::orm::*;
use ng_repo::log::*;
type NeedsFetchBool = bool;
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 OrmSubscription,
) -> Vec<(SubjectIri, ShapeIri, NeedsFetchBool)> {
let tracked_subjects = &mut orm_subscription.tracked_subjects;
let Some(tracked_shapes) = tracked_subjects.get(&s_change.subject_iri) else {
return vec![];
};
let Some(tracked_subject) = tracked_shapes.get(&shape.iri) else {
return vec![];
};
let mut tracked_subject = tracked_subject.write().unwrap();
let previous_validity = tracked_subject.prev_valid.clone();
tracked_subject.prev_valid = tracked_subject.valid.clone();
// 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![];
log_debug!(
"[Validation] for shape {} and subject {}",
shape.iri,
s_change.subject_iri
);
// Check 1) Check if this object is untracked and we need to remove children and ourselves.
if previous_validity == OrmTrackedSubjectValidity::Untracked {
// 1.1) Schedule children for deletion
// 1.1.1) Set all children to `untracked` that don't have other parents.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
for child in &tracked_predicate.write().unwrap().tracked_children {
let mut tracked_child = child.write().unwrap();
if tracked_child.parents.is_empty()
|| (tracked_child.parents.len() == 1
&& tracked_child
.parents
.contains_key(&tracked_subject.subject_iri))
{
tracked_child.valid = OrmTrackedSubjectValidity::Untracked;
}
}
}
// 1.1.2) Add all children to need_evaluation for their cleanup.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
for child in &tracked_predicate.write().unwrap().tracked_children {
let child = child.read().unwrap();
need_evaluation.push((
child.subject_iri.clone(),
child.shape.iri.clone(),
false,
));
}
}
// 1.2) If we don't have parents, we need to remove ourself too.
if tracked_subject.parents.is_empty() {
// Drop the guard to release the immutable borrow
drop(tracked_subject);
tracked_subjects.remove(&s_change.subject_iri);
}
return need_evaluation;
}
// 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) 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.map(|pc| pc.tracked_predicate.read().unwrap());
let count = tracked_pred
.as_ref()
.map_or_else(|| 0, |tp| tp.current_cardinality);
// Check 3.1) Cardinality
if count < p_schema.minCardinality {
log_debug!(
" - Invalid: minCardinality not met | predicate: {:?} | count: {} | min: {} | schema: {:?} | changed: {:?}",
p_schema.iri,
count,
p_schema.minCardinality,
shape.iri,
p_change
);
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 3.2) Cardinality too high and extra values not allowed.
} else if count > p_schema.maxCardinality
&& p_schema.maxCardinality != -1
&& p_schema.extra != Some(true)
{
log_debug!(
" - Invalid: maxCardinality exceeded | predicate: {:?} | count: {} | max: {} | schema: {:?} | changed: {:?}",
p_schema.iri,
count,
p_schema.maxCardinality,
shape.iri,
p_change
);
// If cardinality is too high and no extra allowed, invalid.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
break;
// Check 3.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 {
log_debug!(
" - Invalid: required literals missing | predicate: {:?} | schema: {:?} | changed: {:?}",
p_schema.iri,
shape.iri,
p_change
);
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
break;
}
// Check 3.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()
.map(|tc| tc.read().unwrap())
.collect::<Vec<_>>()
});
// 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)
})
});
log_debug!(" - checking nested - Counts: {:?}", counts);
if counts.1 > 0 && p_schema.extra != Some(true) {
log_debug!(
" - Invalid: nested invalid child | predicate: {:?} | schema: {:?} | changed: {:?}",
p_schema.iri,
shape.iri,
p_change
);
// If we have at least one invalid nested object
// and no extra (in this case this means invalid) allowed, invalid.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
break;
} else if counts.0 > p_schema.maxCardinality && p_schema.maxCardinality != -1 {
log_debug!(
" - Invalid: Too many valid children: | predicate: {:?} | schema: {:?} | changed: {:?}",
p_schema.iri,
shape.iri,
p_change
);
// If there are more valid children than what's allowed, break.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
break;
} else if counts.0 + counts.2 + counts.3 < p_schema.minCardinality {
log_debug!(
" - Invalid: not enough nested children | predicate: {:?} | valid_count: {} | min: {} | schema: {:?} | changed: {:?}",
p_schema.iri,
counts.0,
p_schema.minCardinality,
shape.iri,
p_change
);
// If we don't have enough 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 our own validity to pending and add it to need_evaluation for later.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Pending);
need_evaluation.push((
s_change.subject_iri.to_string(),
shape.iri.clone(),
false,
));
// 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 pending children, we need to wait for their evaluation.
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Pending);
// Schedule pending children 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 3.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 {
log_debug!(
" - Invalid: value type mismatch | predicate: {:?} | value: {:?} | allowed_types: {:?} | schema: {:?} | changed: {:?}",
p_schema.iri,
val_added,
allowed_types,
shape.iri,
p_change
);
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
break;
}
}
// Break again if validity has become invalid.
if new_validity == OrmTrackedSubjectValidity::Invalid {
break;
}
};
}
tracked_subject.valid = new_validity.clone();
if new_validity == OrmTrackedSubjectValidity::Invalid {
// For invalid subjects, we need to to cleanup.
let has_parents = !tracked_subject.parents.is_empty();
if has_parents {
// This object is not a root object. Tracked child objects can be dropped.
// We therefore delete the child -> parent links.
// Untracked objects (with no parents) will be deleted in the subsequent child validation.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
for child in &tracked_predicate.write().unwrap().tracked_children {
child
.write()
.unwrap()
.parents
.remove(&tracked_subject.subject_iri);
}
}
} else {
// This is a root objects, we will set the children to untracked
// but don't delete the child > parent relationship.
}
// Set all children to `untracked` that don't have other parents.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
for child in &tracked_predicate.write().unwrap().tracked_children {
let mut tracked_child = child.write().unwrap();
if tracked_child.parents.is_empty()
|| (tracked_child.parents.len() == 1
&& tracked_child
.parents
.contains_key(&tracked_subject.subject_iri))
{
tracked_child.valid = OrmTrackedSubjectValidity::Untracked;
}
}
}
// Add all children to need_evaluation for their cleanup.
for tracked_predicate in tracked_subject.tracked_predicates.values() {
for child in &tracked_predicate.write().unwrap().tracked_children {
let child = child.read().unwrap();
need_evaluation.push((
child.subject_iri.clone(),
child.shape.iri.clone(),
false,
));
}
}
// Remove all tracked_predicates.
tracked_subject.tracked_predicates.clear();
} else if new_validity == OrmTrackedSubjectValidity::Valid
&& previous_validity != OrmTrackedSubjectValidity::Valid
{
// If this subject became valid, we need to refetch this subject.
// If the data has already been fetched, the parent function will prevent the refetch.
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 {
// Parents that are not tracking this subject, don't need to be added.
// Remember that the last elements are evaluated first.
return tracked_subject
.parents
.values()
.map(|parent| {
let p = parent.read().unwrap();
(p.subject_iri.clone(), p.shape.iri.clone(), false)
})
// Add `need_evaluation`.
.chain(need_evaluation)
.collect();
}
return need_evaluation;
}
}