new backend-update > JSON patch algorithm (WIP)

feat/orm-diffs
Laurin Weger 4 days ago
parent c0637ddaee
commit f817809a2a
No known key found for this signature in database
GPG Key ID: 9B372BB0B792770F
  1. 782
      engine/verifier/src/orm/mod.rs
  2. 7
      engine/verifier/src/orm/validation.rs
  3. 7
      sdk/rust/src/tests/mod.rs
  4. 589
      sdk/rust/src/tests/orm_patches.rs

@ -681,18 +681,18 @@ impl Verifier {
// Remove old subscriptions // Remove old subscriptions
subs.retain(|sub| !sub.sender.is_closed()); subs.retain(|sub| !sub.sender.is_closed());
if scope.target == NuriTargetV0::UserSite if !(scope.target == NuriTargetV0::UserSite
|| scope || scope
.overlay .overlay
.as_ref() .as_ref()
.map_or(false, |ol| overlaylink == *ol) .map_or(false, |ol| overlaylink == *ol)
|| scope.target == NuriTargetV0::Repo(repo_id) || scope.target == NuriTargetV0::Repo(repo_id))
{ {
continue; continue;
} }
// prepare to apply updates to tracked subjects and record the changes. // prepare to apply updates to tracked subjects and record the changes.
let shapes = subs let root_shapes = subs
.iter() .iter()
.map(|sub| { .map(|sub| {
sub.shape_type sub.shape_type
@ -703,7 +703,7 @@ impl Verifier {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
scopes.push((scope.clone(), shapes)); scopes.push((scope.clone(), root_shapes));
} }
log_debug!( log_debug!(
@ -713,7 +713,7 @@ impl Verifier {
for (scope, shapes) in scopes { for (scope, shapes) in scopes {
let mut orm_changes: OrmChanges = HashMap::new(); let mut orm_changes: OrmChanges = HashMap::new();
// actually applying updates to tracked subjects and record the changes. // Apply the changes to tracked subjects.
for shape_arc in shapes { for shape_arc in shapes {
let _ = self.process_changes_for_shape_and_session( let _ = self.process_changes_for_shape_and_session(
&scope, &scope,
@ -728,330 +728,532 @@ impl Verifier {
let subs = self.orm_subscriptions.get(&scope).unwrap(); let subs = self.orm_subscriptions.get(&scope).unwrap();
for sub in subs.iter() { for sub in subs.iter() {
// TODO: This if-condition is wrong (intended to not re-apply changes coming from the same subscription).
// if sub.session_id == session_id {
// continue;
log_debug!( log_debug!(
"Applying changes to subscription with nuri {} and shape {}", "Applying changes to subscription with nuri {} and shape {}",
sub.nuri.repo(), sub.nuri.repo(),
sub.shape_type.shape sub.shape_type.shape
); );
// }
// Create diff from changes & subscription.
fn create_patches_for_nested_object( // The JSON patches to send to JS land.
pred_shape: &OrmSchemaPredicate, let mut patches: Vec<OrmDiffOp> = vec![];
// Keep track of created objects by path and if they need an id.
// Later we created patches from them to ensure the objects exist.
let mut paths_of_objects_to_create: HashSet<(Vec<String>, Option<SubjectIri>)> =
HashSet::new();
// Function to create diff objects from a given change.
// The function recurses from child to parents down to a root tracked subject.
// If multiple parents exist, it adds separate patches for each.
fn add_diff_ops_for_tracked_subject(
tracked_subject: &OrmTrackedSubject,
tracked_subjects: &HashMap< tracked_subjects: &HashMap<
String, String,
HashMap<String, Arc<RwLock<OrmTrackedSubject>>>, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>,
>, >,
patches: &mut Vec<OrmDiffOp>, root_shape: &String,
path: &mut Vec<String>, path: &mut Vec<String>,
object_iri: &String, diff_op: (
orm_changes: &OrmChanges, OrmDiffOpType,
sub: &OrmSubscription, Option<OrmDiffType>,
Option<Value>, // The value added / removed
Option<String>, // The IRI, if change is an added / removed object.
),
patches: &mut Vec<OrmDiffOp>,
paths_of_objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) { ) {
// Object was added. That means, we need to add a basic object with no value, // If this subject has no parents or its shape matches the root shape, we've reached the root
// Then add further predicates to it in a recursive call. if tracked_subject.parents.is_empty()
patches.push(OrmDiffOp { || tracked_subject.shape.iri == *root_shape
op: OrmDiffOpType::add, {
valType: Some(OrmDiffType::object), // Build the final JSON Pointer path
path: format!("/{}", path.join("/")), let escaped_path: Vec<String> =
value: None, path.iter().map(|seg| escape_json_pointer(seg)).collect();
}); let json_pointer = format!("/{}", escaped_path.join("/"));
// Create the patch
let patch = OrmDiffOp {
op: diff_op.0.clone(),
valType: diff_op.1.clone(),
path: json_pointer,
value: diff_op.2.clone(),
};
patches.push(patch);
// If this is an object being added, record it for object creation
if let Some(iri) = &diff_op.3 {
if matches!(diff_op.0, OrmDiffOpType::add) {
paths_of_objects_to_create
.insert((path.clone(), Some(iri.clone())));
}
}
// Get the shape IRI for a nested object that is valid. return;
let object_shape_iri = { }
// Get the tracked subject for this object IRI
let tracked_subjects_for_obj = tracked_subjects
.get(object_iri)
.expect("Object should be tracked");
// Find the first valid shape for this object from the allowed shapes
let allowed_shape_iris: Vec<&String> = pred_shape
.dataTypes
.iter()
.filter_map(|dt| dt.shape.as_ref())
.collect();
allowed_shape_iris
.iter()
.find(|shape_iri| {
tracked_subjects_for_obj
.get(**shape_iri)
.map(|ts| {
ts.read().unwrap().valid == OrmTrackedSubjectValidity::Valid
})
.unwrap_or(false)
})
.unwrap()
.to_string()
};
// Apply changes for nested object. // Recurse to parents
create_patches_for_changed_subj( for (parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
orm_changes, // Get predicate schema linking parent with tracked_subject
patches,
&object_shape_iri, // Use predicate schema readable_predicate to add to path.
&object_iri, // If predicate schema is multi, add our own subject iri to path first.
sub,
path, // If parent is root, we don't need to recurse.
tracked_subjects, // Instead we add new patches based on the path (we need to escape segments before)
); // and the diff_op content
let parent_ts = parent_tracked_subject.read().unwrap();
// Find the predicate schema linking parent to this tracked subject
for pred_arc in &parent_ts.shape.predicates {
// Check if this predicate has our subject as a child
if let Some(tracked_pred) =
parent_ts.tracked_predicates.get(&pred_arc.iri)
{
let tp = tracked_pred.read().unwrap();
// Check if this tracked subject is in the children
let is_child = tp.tracked_children.iter().any(|child| {
let child_read = child.read().unwrap();
child_read.subject_iri == tracked_subject.subject_iri
});
if is_child {
// Build the path segment
let mut new_path = path.clone();
let is_multi = pred_arc.maxCardinality > 1
|| pred_arc.maxCardinality == -1;
// For multi-valued predicates, add the object IRI as a key first
if is_multi {
new_path.insert(0, tracked_subject.subject_iri.clone());
}
// Add the readable predicate name
new_path.insert(0, pred_arc.readablePredicate.clone());
// Recurse to the parent
add_diff_ops_for_tracked_subject(
&parent_ts,
tracked_subjects,
root_shape,
&mut new_path,
diff_op.clone(),
patches,
paths_of_objects_to_create,
);
break;
}
}
}
}
} }
fn create_patches_for_changed_subj( fn diff_op_from_pred_change(
orm_changes: &OrmChanges, pred_change: &OrmTrackedPredicateChanges,
patches: &mut OrmDiff, ) -> Vec<(
shape_iri: &String, OrmDiffOpType,
subject_iri: &String, Option<OrmDiffType>,
sub: &OrmSubscription, Option<Value>, // The value added / removed
path: &mut Vec<String>, Option<String>, // The IRI, if change is an added / removed object.
)> {
let tracked_predicate = pred_change.tracked_predicate.read().unwrap();
let is_multi = tracked_predicate.schema.maxCardinality > 1
|| tracked_predicate.schema.maxCardinality == -1;
let is_object = tracked_predicate
.schema
.dataTypes
.iter()
.any(|dt| dt.shape.is_some());
if !is_multi && !is_object {
if pred_change.values_added.len() == 1 {
// A value was added. Another one might have been removed
// but the add patch overwrite previous values.
return [(
OrmDiffOpType::add,
None,
Some(json!(pred_change.values_added[0])),
None,
)]
.to_vec();
} else {
// Since there is only one possible value, removing the path is enough.
return [(OrmDiffOpType::remove, None, None, None)].to_vec();
}
} else if is_multi && !is_object {
let mut ops = vec![];
if pred_change.values_added.len() > 0 {
ops.push((
OrmDiffOpType::add,
Some(OrmDiffType::set),
Some(json!(pred_change.values_added)),
None,
));
}
if pred_change.values_removed.len() > 0 {
ops.push((
OrmDiffOpType::remove,
Some(OrmDiffType::set),
Some(json!(pred_change.values_removed)),
None,
));
}
return ops;
}
// objects are not handled here because objects to create
// are registered during path traversal.
return vec![];
}
// Helper function to determine the highest-priority valid shape for a subject
// given the allowed shapes in a predicate's dataTypes.
// Returns (current_valid_shape, previous_valid_shape)
#[allow(dead_code)]
fn get_highest_priority_valid_shapes(
subject_iri: &SubjectIri,
allowed_shapes: &[OrmSchemaDataType], // From predicate.dataTypes (in priority order)
tracked_subjects: &HashMap< tracked_subjects: &HashMap<
SubjectIri, String,
HashMap<ShapeIri, Arc<RwLock<OrmTrackedSubject>>>, HashMap<String, Arc<RwLock<OrmTrackedSubject>>>,
>, >,
) { ) -> (Option<String>, Option<String>) {
let change = orm_changes let Some(shapes_for_subject) = tracked_subjects.get(subject_iri) else {
.get(shape_iri) return (None, None);
.unwrap() };
.get(subject_iri)
.unwrap();
let subject_shape = sub.shape_type.schema.get(shape_iri).unwrap();
// @Niko, is it safe to do this? // Find current highest-priority valid shape
let tracked_subject = tracked_subjects let current_valid = allowed_shapes
.get(subject_iri) .iter()
.unwrap() .filter_map(|dt| dt.shape.as_ref())
.get(shape_iri) .find_map(|shape_iri| {
.unwrap() shapes_for_subject.get(shape_iri).and_then(|ts| {
.read() let tracked = ts.read().unwrap();
.unwrap(); if tracked.valid == OrmTrackedSubjectValidity::Valid {
Some(shape_iri.clone())
} else {
None
}
})
});
// Check validity changes // Find previous highest-priority valid shape
if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Invalid let previous_valid = allowed_shapes
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid .iter()
{ .filter_map(|dt| dt.shape.as_ref())
// Is the subject invalid and was it before? There is nothing we need to inform about. .find_map(|shape_iri| {
return; shapes_for_subject.get(shape_iri).and_then(|ts| {
} else if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Valid let tracked = ts.read().unwrap();
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid if tracked.prev_valid == OrmTrackedSubjectValidity::Valid {
|| tracked_subject.valid == OrmTrackedSubjectValidity::Untracked Some(shape_iri.clone())
{ } else {
// Has the subject become invalid or untracked? None
// We add a patch, deleting the object at its root. }
patches.push(OrmDiffOp { })
op: OrmDiffOpType::remove,
valType: Some(OrmDiffType::object),
path: format!("/{}", path.join("/")),
value: None,
}); });
(current_valid, previous_valid)
}
// Helper function to handle validity changes when highest-priority shape changes
#[allow(dead_code)]
fn handle_shape_priority_change(
subject_iri: &SubjectIri,
shape_iri: &ShapeIri,
tracked_subjects: &HashMap<
String,
HashMap<String, Arc<RwLock<OrmTrackedSubject>>>,
>,
root_shape: &String,
orm_changes: &OrmChanges,
patches: &mut Vec<OrmDiffOp>,
paths_of_objects_to_create: &mut HashSet<(Vec<String>, Option<SubjectIri>)>,
) {
// Step 1: Check if this subject has multiple tracked shapes
let Some(shapes_for_subject) = tracked_subjects.get(subject_iri) else {
return;
};
if shapes_for_subject.len() <= 1 {
// Only one shape, no priority conflict possible
return; return;
} else {
// The subject is valid or has become valid.
// In both cases, all information necessary to send patches are available in the orm_changes.
// In case the subject was not valid before, we create the object at the current path as an empty object though.
if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid {
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: format!("/{}", path.join("/")),
value: None,
});
// And add the id field.
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: None,
path: format!("/{}/{}", path.join("/"), subject_iri),
value: None,
});
}
} }
// Iterate over every predicate change and create patches // Step 2: Get the current tracked subject
for (pred_iri, pred_change) in change.predicates.iter() { let Some(tracked_subject_arc) = shapes_for_subject.get(shape_iri) else {
let pred_shape = subject_shape return;
.predicates };
.iter() let tracked_subject = tracked_subject_arc.read().unwrap();
.find(|p| p.iri == *pred_iri)
.unwrap(); // Step 3: For each parent, check if the highest-priority valid shape changed
for (parent_iri, parent_tracked_subject_arc) in tracked_subject.parents.iter() {
let is_multi = let parent_ts = parent_tracked_subject_arc.read().unwrap();
pred_shape.maxCardinality > 1 || pred_shape.maxCardinality == -1;
let is_object = pred_shape.dataTypes.iter().any(|dt| !dt.shape.is_none()); // Find the predicate linking parent to this subject
let pred_name = pred_shape.readablePredicate.clone(); for pred_arc in &parent_ts.shape.predicates {
path.push(pred_name); if let Some(tracked_pred) =
let path_str = format!("/{}", path.join("/")); parent_ts.tracked_predicates.get(&pred_arc.iri)
{
// Depending on the predicate type (multi / single, object / not object), let tp = tracked_pred.read().unwrap();
// add the respective diff operation.
// Check if this tracked subject is a child of this predicate
// Single primitive value let is_child = tp.tracked_children.iter().any(|child| {
if !is_multi && !is_object { let child_read = child.read().unwrap();
if pred_change.values_added.len() > 0 { child_read.subject_iri == *subject_iri
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: None,
path: path_str.clone(),
value: Some(json!(pred_change.values_added[0])),
});
}
if pred_change.values_removed.len() > 0 {
patches.push(OrmDiffOp {
op: OrmDiffOpType::remove,
valType: None,
path: path_str,
value: Some(json!(pred_change.values_added[0])),
});
}
} else if is_multi && !is_object {
// Set of primitive values
if pred_change.values_added.len() > 0 {
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::set),
path: path_str.clone(),
value: Some(json!(pred_change.values_added)),
});
}
if pred_change.values_removed.len() > 0 {
patches.push(OrmDiffOp {
op: OrmDiffOpType::remove,
valType: Some(OrmDiffType::set),
path: path_str,
value: Some(json!(pred_change.values_removed)),
}); });
}
} else if is_object { if !is_child {
// Change in single object property. continue;
if !is_multi {
let object_iri = match &pred_change.values_added[0] {
BasicType::Str(iri) => iri,
_ => panic!("Object no IRI"),
};
// Single object.
if pred_change.values_added.len() > 0 {
create_patches_for_nested_object(
pred_shape,
tracked_subjects,
patches,
path,
object_iri,
orm_changes,
sub,
);
} }
if pred_change.values_removed.len() > 0 {
// Object is removed. // Get the allowed shapes for this predicate (in priority order)
patches.push(OrmDiffOp { let allowed_shapes: Vec<_> = pred_arc
op: OrmDiffOpType::remove, .dataTypes
valType: Some(OrmDiffType::object), .iter()
path: path_str, .filter(|dt| dt.shape.is_some())
value: None, .collect();
});
if allowed_shapes.len() <= 1 {
// No priority conflict possible with single shape
continue;
} }
} else {
// Change(s) in multi object property. // Determine current and previous highest-priority valid shapes
let (current_valid, previous_valid) =
// Add every new object. get_highest_priority_valid_shapes(
for obj_iri_bt in pred_change.values_added.iter() { subject_iri,
let obj_iri = match obj_iri_bt { &pred_arc.dataTypes,
BasicType::Str(iri) => iri,
_ => panic!("Object no IRI"),
};
// First, we create a root object (if the object existed before, this has no effect).
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: path_str.clone(),
value: None,
});
// Add escaped object IRI to path.
path.push(escape_json_pointer(obj_iri));
create_patches_for_nested_object(
pred_shape,
tracked_subjects, tracked_subjects,
patches,
path,
obj_iri,
orm_changes,
sub,
); );
// Remove object IRI from stack again. // Step 4: Create patches based on what changed
path.pop(); if current_valid != previous_valid {
} let is_multi = pred_arc.maxCardinality > 1
|| pred_arc.maxCardinality == -1;
// Delete objects.
// If there are no more predicates, delete the whole object. // Case A: Shape switch (ShapeA -> ShapeB)
if pred_change if let (Some(new_shape), Some(old_shape)) =
.tracked_predicate (&current_valid, &previous_valid)
.read() {
.unwrap() // Remove the old object
.tracked_children if let Some(old_ts) = shapes_for_subject.get(old_shape) {
.len() let old_tracked = old_ts.read().unwrap();
== 0 let mut path = vec![];
{ let diff_op = (
// Or the whole thing if no children remain OrmDiffOpType::remove,
patches.push(OrmDiffOp { Some(OrmDiffType::object),
op: OrmDiffOpType::remove, None,
valType: Some(OrmDiffType::object), Some(subject_iri.clone()),
path: path_str, );
value: None,
}); add_diff_ops_for_tracked_subject(
} else { &old_tracked,
for object_iri_removed in pred_change.values_removed.iter() { tracked_subjects,
patches.push(OrmDiffOp { root_shape,
op: OrmDiffOpType::remove, &mut path,
valType: Some(OrmDiffType::object), diff_op,
path: format!( patches,
"/{}/{}", paths_of_objects_to_create,
path_str, );
match object_iri_removed { }
BasicType::Str(iri) => iri,
_ => panic!("Object IRI must be string"), // Add the new object (need to materialize it)
} if let Some(new_ts) = shapes_for_subject.get(new_shape) {
), let new_tracked = new_ts.read().unwrap();
value: None,
}); // TODO: Materialize the object with current triples
// This requires access to the change data or re-querying
// For now, we'll just create an object placeholder patch
let mut path = vec![];
let diff_op = (
OrmDiffOpType::add,
Some(OrmDiffType::object),
Some(Value::Null),
Some(subject_iri.clone()),
);
add_diff_ops_for_tracked_subject(
&new_tracked,
tracked_subjects,
root_shape,
&mut path,
diff_op,
patches,
paths_of_objects_to_create,
);
}
}
// Case B: Object became valid (None -> ShapeX)
else if let (Some(new_shape), None) =
(&current_valid, &previous_valid)
{
if let Some(new_ts) = shapes_for_subject.get(new_shape) {
let new_tracked = new_ts.read().unwrap();
let mut path = vec![];
let diff_op = (
OrmDiffOpType::add,
Some(OrmDiffType::object),
Some(Value::Null),
Some(subject_iri.clone()),
);
add_diff_ops_for_tracked_subject(
&new_tracked,
tracked_subjects,
root_shape,
&mut path,
diff_op,
patches,
paths_of_objects_to_create,
);
}
}
// Case C: Object became invalid (ShapeX -> None)
else if let (None, Some(old_shape)) =
(&current_valid, &previous_valid)
{
if let Some(old_ts) = shapes_for_subject.get(old_shape) {
let old_tracked = old_ts.read().unwrap();
let mut path = vec![];
let diff_op = (
OrmDiffOpType::remove,
Some(OrmDiffType::object),
None,
Some(subject_iri.clone()),
);
add_diff_ops_for_tracked_subject(
&old_tracked,
tracked_subjects,
root_shape,
&mut path,
diff_op,
patches,
paths_of_objects_to_create,
);
}
} }
} }
break; // Found the predicate, no need to check others
} }
} }
// Remove this predicate name from the path again.
path.pop();
} }
} }
let mut patches: OrmDiff = vec![]; // We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...}
let mut path: Vec<String> = Vec::with_capacity(4); // For each change that has a subject tracked in this subscription,
// - Get change operation (calling diff_op_from_pred_change).
// Iterate over each root subject with the right shape // - case not object, single --> either add or remove (must be one of each at max)
// For each tracked subject that has the subscription's shape, call fn above // - case not object, multi --> just add and or set patch
for (subject_iri, tracked_subjects_by_shape) in sub.tracked_subjects.iter() { // - case object, multi --> create object patch + nested object patch (will be handled when recursing paths to add primitive values)
for (shape_iri, tracked_subject) in tracked_subjects_by_shape.iter() { // - case object, single --> just object patch (will be handled when recursing paths to add primitive values)
if *shape_iri != sub.shape_type.shape { // - Add patches for each change operation for the path of the change in the schema.
// We find the path by traversing the schema up to the parents (add_diff_ops_for_tracked_subject).
// TODO: Special edge case: An object with parents changed and the parents' predicate schema has multiple allowed shapes.
// Now, there are multiple tracked subjects with the same subject IRI but different shapes of which some
// are valid or invalid. The first valid (subject, shape) pair must used for materialization.
// - if a higher-priority shape became invalid but a lower priority shape is valid, delete and new add.
// - if a higher-priority shape became valid, delete and add new valid.
// Problem: We might not have the triples present to materialize the newly valid object so we need to fetch them.
// Process changes for this subscription
// Iterate through all changes and create patches
for (shape_iri, subject_changes) in &orm_changes {
for (subject_iri, change) in subject_changes {
// Get the tracked subject for this (subject, shape) pair
let tracked_subject_opt = sub
.tracked_subjects
.get(subject_iri)
.and_then(|shapes| shapes.get(shape_iri))
.map(|ts| ts.read().unwrap());
let Some(tracked_subject) = tracked_subject_opt else {
continue; continue;
};
// Process each predicate change
for (pred_iri, pred_change) in &change.predicates {
let tracked_predicate = pred_change.tracked_predicate.read().unwrap();
let pred_name = tracked_predicate.schema.readablePredicate.clone();
// Check validity changes
if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Invalid
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid
{
// Is the subject invalid and was it before? There is nothing we need to inform about.
return;
} else if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Valid
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid
|| tracked_subject.valid == OrmTrackedSubjectValidity::Untracked
{
// Has the subject become invalid or untracked?
// We add a patch, deleting the object at its root.
let mut path: Vec<String> = vec![pred_name.clone()];
add_diff_ops_for_tracked_subject(
&tracked_subject,
&sub.tracked_subjects,
&sub.shape_type.shape,
&mut path,
(OrmDiffOpType::remove, Some(OrmDiffType::object), None, None),
&mut patches,
&mut paths_of_objects_to_create,
);
} else {
// The subject is valid or has become valid.
// Get the diff operations for this predicate change
let diff_ops = diff_op_from_pred_change(pred_change);
// For each diff operation, traverse up to the root to build the path
for diff_op in diff_ops {
let mut path = vec![pred_name.clone()];
// Start recursion from this tracked subject
add_diff_ops_for_tracked_subject(
&tracked_subject,
&sub.tracked_subjects,
&sub.shape_type.shape,
&mut path,
diff_op,
&mut patches,
&mut paths_of_objects_to_create,
);
}
}
} }
// Found a root subject for this shape. }
}
// Add subject IRI as first part of path pointer.
path.push(escape_json_pointer(subject_iri)); // Create patches for objects that need to be created
create_patches_for_changed_subj( // These are patches with {op: add, valType: object, value: Null, path: ...}
&orm_changes, // Sort by path length (shorter first) to ensure parent objects are created before children
&mut patches, let mut sorted_object_paths: Vec<_> = paths_of_objects_to_create.iter().collect();
shape_iri, sorted_object_paths.sort_by_key(|(path_segments, _)| path_segments.len());
subject_iri,
sub, for (path_segments, maybe_iri) in sorted_object_paths {
&mut path, let escaped_path: Vec<String> = path_segments
&sub.tracked_subjects, .iter()
); .map(|seg| escape_json_pointer(seg))
path.pop(); .collect();
let json_pointer = format!("/{}", escaped_path.join("/"));
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: json_pointer.clone(),
value: None,
});
if let Some(iri) = maybe_iri {
patches.push(OrmDiffOp {
op: OrmDiffOpType::add,
valType: Some(OrmDiffType::object),
path: format!("{}/id", json_pointer),
value: Some(json!(iri)),
});
} }
} }

@ -105,7 +105,10 @@ impl Verifier {
// Check 3) Validate subject against each predicate in shape. // Check 3) Validate subject against each predicate in shape.
for p_schema in shape.predicates.iter() { for p_schema in shape.predicates.iter() {
let p_change = s_change.predicates.get(&p_schema.iri); let p_change = s_change.predicates.get(&p_schema.iri);
let tracked_pred = p_change.map(|pc| pc.tracked_predicate.read().unwrap()); let tracked_pred = tracked_subject
.tracked_predicates
.get(&p_schema.iri)
.map(|tp_write_lock| tp_write_lock.read().unwrap());
let count = tracked_pred let count = tracked_pred
.as_ref() .as_ref()
@ -124,6 +127,8 @@ impl Verifier {
set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid); set_validity(&mut new_validity, OrmTrackedSubjectValidity::Invalid);
if count <= 0 { if count <= 0 {
// If cardinality is 0, we can remove the tracked predicate. // If cardinality is 0, we can remove the tracked predicate.
// Drop the guard to release the immutable borrow
drop(tracked_pred);
tracked_subject.tracked_predicates.remove(&p_schema.iri); tracked_subject.tracked_predicates.remove(&p_schema.iri);
} }
break; break;

@ -50,7 +50,12 @@ pub(crate) fn assert_json_eq(expected: &mut Value, actual: &mut Value) {
let diff = serde_json_diff::values(expected.clone(), actual.clone()); let diff = serde_json_diff::values(expected.clone(), actual.clone());
if let Some(diff_) = diff { if let Some(diff_) = diff {
log_err!("Expected and actual ORM JSON mismatch.\nDiff: {:?}", diff_); log_err!(
"Expected and actual ORM JSON mismatch.\nDiff: {:?}\nExpected: {}\nActual: {}",
diff_,
expected,
actual
);
assert!(false); assert!(false);
} }
} }

@ -23,6 +23,7 @@ use serde_json::json;
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use svg2pdf::usvg::tiny_skia_path::SCALAR_NEARLY_ZERO;
#[async_std::test] #[async_std::test]
async fn test_orm_path_creation() { async fn test_orm_path_creation() {
@ -32,7 +33,8 @@ async fn test_orm_path_creation() {
// Tests below all in this test, to prevent waiting times through wallet creation. // Tests below all in this test, to prevent waiting times through wallet creation.
// === // ===
test_orm_root_array(session_id).await; test_patch_add_array(session_id).await;
test_patch_remove_array(session_id).await;
// // === // // ===
// test_orm_with_optional(session_id).await; // test_orm_with_optional(session_id).await;
@ -56,7 +58,7 @@ async fn test_orm_path_creation() {
// test_orm_nested_4(session_id).await; // test_orm_nested_4(session_id).await;
} }
async fn test_orm_root_array(session_id: u64) { async fn test_patch_add_array(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,
r#" r#"
@ -148,10 +150,13 @@ INSERT DATA {
ex:arr 4 . ex:arr 4 .
<urn:test:numArrayObj2> <urn:test:numArrayObj2>
ex:arr 1 . ex:arr 1, 2 .
<urn:test:numArrayObj3> <urn:test:numArrayObj3>
ex:arr 3 . ex:arr 3 .
<urn:test:numArrayObj4>
ex:arr 0 .
} }
"# "#
.to_string(), .to_string(),
@ -160,7 +165,6 @@ INSERT DATA {
.await .await
.expect("2nd SPARQL update failed"); .expect("2nd SPARQL update failed");
cancel_fn();
while let Some(app_response) = receiver.next().await { while let Some(app_response) = receiver.next().await {
let patches = match app_response { let patches = match app_response {
AppResponse::V0(v) => match v { AppResponse::V0(v) => match v {
@ -177,133 +181,65 @@ INSERT DATA {
let mut expected = json!([ let mut expected = json!([
{ {
"id": "urn:test:numArrayObj1", "op": "add",
"type": "http://example.org/TestObject", "valType": "set",
"numArray": [1.0, 2.0, 3.0] "value": [4.0],
"path": "/urn:test:numArrayObj1/numArray",
}, },
{ {
"id": "urn:test:numArrayObj2", "op": "add",
"type": "http://example.org/TestObject", "valType": "set",
"numArray": [] "value": [1.0,2.0],
"path": "/urn:test:numArrayObj2/numArray",
}, },
{ {
"id": "urn:test:numArrayObj3", "op": "add",
"type": "http://example.org/TestObject", "valType": "set",
"numArray": [1.0, 2.0] "value": [3.0],
} "path": "/urn:test:numArrayObj3/numArray",
]);
let mut actual_mut = patches.clone();
// assert_json_eq(&mut expected, actual_mut);
break;
}
}
/*
*/
//
async fn test_orm_with_optional(session_id: u64) {
let doc_nuri = create_doc_with_data(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:oj1>
ex:opt true ;
ex:str "s1" .
# Contains no matching data
<urn:test:oj2>
ex:str "s2" .
}
"#
.to_string(),
)
.await;
// Define the ORM schema
let mut schema = HashMap::new();
schema.insert(
"http://example.org/OptionShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/OptionShape".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://example.org/opt".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "opt".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::boolean,
literals: None,
shape: None,
}],
}
.into()],
}
.into(),
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/OptionShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id)
.await
.expect("orm_start");
while let Some(app_response) = receiver.next().await {
let orm_json = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json),
_ => None,
}, },
}
.unwrap();
let mut expected = json!([
{ {
"id": "urn:test:oj1", "op": "add",
"opt": true "valType": "object",
} "path": "/urn:test:numArrayObj4",
"value": Value::Null
},
{
"op": "add",
"value": "urn:test:numArrayObj4",
"path": "/urn:test:numArrayObj4/id",
"valType": Value::Null,
},
{
"op": "add",
"valType": "set",
"value": [0.0],
"path": "/urn:test:numArrayObj4/numArray",
},
]); ]);
let mut actual_mut = orm_json.clone(); let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual_mut); assert_json_eq(&mut expected, &mut actual);
break; break;
} }
cancel_fn();
} }
async fn test_orm_literal(session_id: u64) { async fn test_patch_remove_array(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,
r#" r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
INSERT DATA { INSERT DATA {
<urn:test:oj1> <urn:test:numArrayObj1> a ex:TestObject ;
ex:lit1 "lit 1" ; ex:arr 1, 2, 3 .
ex:lit2 "lit 2" .
<urn:test:numArrayObj2> a ex:TestObject .
# Valid because ex:lit1 allows extra.
<urn:test:obj2> <urn:test:numArrayObj3> a ex:TestObject ;
ex:lit1 "lit 1", "lit 1 extra" ; ex:unrelated ex:TestObject ;
ex:lit2 "lit 2" . ex:arr 1, 2 .
# Invalid because ex:lit2 does not allow extra.
<urn:test:obj3>
ex:lit1 "lit 1" ;
ex:lit2 "lit 2", "lit 2 extra" .
} }
"# "#
.to_string(), .to_string(),
@ -313,34 +249,36 @@ INSERT DATA {
// Define the ORM schema // Define the ORM schema
let mut schema = HashMap::new(); let mut schema = HashMap::new();
schema.insert( schema.insert(
"http://example.org/OptionShape".to_string(), "http://example.org/TestShape".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/OptionShape".to_string(), iri: "http://example.org/TestShape".to_string(),
predicates: vec![ predicates: vec![
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/lit1".to_string(), iri: "http://www.w3.org/1999/02/22-rdf-syntax-ns#type".to_string(),
extra: Some(true), extra: Some(false),
maxCardinality: -1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "lit1".to_string(), readablePredicate: "type".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaLiteralType::literal,
literals: Some(vec![BasicType::Str("lit 1".to_string())]), literals: Some(vec![BasicType::Str(
"http://example.org/TestObject".to_string(),
)]),
shape: None, shape: None,
}], }],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/lit2".to_string(), iri: "http://example.org/arr".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "lit2".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::literal, valType: OrmSchemaLiteralType::number,
literals: Some(vec![BasicType::Str("lit 2".to_string())]), literals: None,
shape: None, shape: None,
}], }],
extra: Some(false),
maxCardinality: -1,
minCardinality: 0,
readablePredicate: "numArray".to_string(),
} }
.into(), .into(),
], ],
@ -350,7 +288,7 @@ INSERT DATA {
let shape_type = OrmShapeType { let shape_type = OrmShapeType {
schema, schema,
shape: "http://example.org/OptionShape".to_string(), shape: "http://example.org/TestShape".to_string(),
}; };
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri"); let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
@ -359,7 +297,7 @@ INSERT DATA {
.expect("orm_start"); .expect("orm_start");
while let Some(app_response) = receiver.next().await { while let Some(app_response) = receiver.next().await {
let orm_json = match app_response { let _ = match app_response {
AppResponse::V0(v) => match v { AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json), AppResponseV0::OrmInitial(json) => Some(json),
_ => None, _ => None,
@ -367,153 +305,74 @@ INSERT DATA {
} }
.unwrap(); .unwrap();
let mut expected = json!([
{
"id": "urn:test:oj1",
"lit1": ["lit 1"],
"lit2": "lit 2"
},
{
"id": "urn:test:obj2",
"lit1": ["lit 1", "lit 1 extra"],
"lit2": "lit 2"
}
]);
let mut actual_mut = orm_json.clone();
assert_json_eq(&mut expected, &mut actual_mut);
break; break;
} }
cancel_fn();
}
async fn test_orm_multi_type(session_id: u64) { // Add more data, remove some
let doc_nuri = create_doc_with_data( doc_sparql_update(
session_id, session_id,
r#" r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
INSERT DATA { DELETE DATA {
<urn:test:oj1> <urn:test:numArrayObj1>
ex:strOrNum "a string" ; ex:arr 1 .
ex:strOrNum "another string" ;
ex:strOrNum 2 .
# Invalid because false is not string or number.
<urn:test:obj2>
ex:strOrNum "a string2" ;
ex:strOrNum 2 ;
ex:strOrNum false .
} }
"# "#
.to_string(), .to_string(),
Some(doc_nuri.clone()),
) )
.await; .await
.expect("2nd SPARQL update failed");
// Define the ORM schema
let mut schema = HashMap::new();
schema.insert(
"http://example.org/MultiTypeShape".to_string(),
OrmSchemaShape {
iri: "http://example.org/MultiTypeShape".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://example.org/strOrNum".to_string(),
extra: Some(true),
maxCardinality: -1,
minCardinality: 1,
readablePredicate: "strOrNum".to_string(),
dataTypes: vec![
OrmSchemaDataType {
valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
},
OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
},
],
}
.into()],
}
.into(),
);
let shape_type = OrmShapeType {
schema,
shape: "http://example.org/MultiTypeShape".to_string(),
};
let nuri = NuriV0::new_from(&doc_nuri).expect("parse nuri");
let (mut receiver, cancel_fn) = orm_start(nuri, shape_type, session_id)
.await
.expect("orm_start");
while let Some(app_response) = receiver.next().await { while let Some(app_response) = receiver.next().await {
let orm_json = match app_response { let patches = match app_response {
AppResponse::V0(v) => match v { AppResponse::V0(v) => match v {
AppResponseV0::OrmInitial(json) => Some(json), AppResponseV0::OrmUpdate(json) => Some(json),
_ => None, _ => None,
}, },
} }
.unwrap(); .unwrap();
log_info!("Diff ops arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
}
let mut expected = json!([ let mut expected = json!([
{ {
"id": "urn:test:oj1", "op": "remove",
"strOrNum": ["a string", "another string", 2.0] "valType": "set",
"value": [1.0],
"path": "/urn:test:numArrayObj1/numArray",
} }
]); ]);
let mut actual_mut = orm_json.clone(); let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual_mut); assert_json_eq(&mut expected, &mut actual);
break; break;
} }
cancel_fn();
} }
async fn test_orm_nested_1(session_id: u64) { async fn test_patch_add_nested_1(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,
r#" r#"
PREFIX ex: <http://example.org/> PREFIX ex: <http://example.org/>
INSERT DATA { INSERT DATA {
# Valid
<urn:test:oj1> <urn:test:oj1>
ex:str "obj1 str" ; ex:multiNest <urn:test:multiNested1>, <urn:test:multiNested2> ;
ex:nestedWithExtra <urn:test:nested1>, <urn:test:nested2> ; ex:singleNest <urn:test:nested3> .
ex:nestedWithoutExtra <urn:test:nested3> .
<urn:test:nested1> <urn:test:multiNested1>
ex:nestedStr "obj1 nested with extra valid" ; ex:multiNest1Str "a multi 1 string" .
ex:nestedNum 2 .
<urn:test:nested2> <urn:test:multiNested2>
ex:nestedStr "obj1 nested with extra invalid" . ex:multiNest2Str "a multi 2 string" .
<urn:test:nested3> <urn:test:nested3>
ex:nestedStr "obj1 nested without extra valid" ; ex:singleNestStr "a single nest string" .
ex:nestedNum 2 .
# Invalid because nestedWithoutExtra has an invalid child.
<urn:test:oj2>
ex:str "obj2 str" ;
ex:nestedWithExtra <urn:test:nested4> ;
ex:nestedWithoutExtra <urn:test:nested5>, <urn:test:nested6> .
<urn:test:nested4>
ex:nestedStr "obj2: a nested string valid" ;
ex:nestedNum 2 .
<urn:test:nested5>
ex:nestedStr "obj2 nested without extra valid" ;
ex:nestedNum 2 .
# Invalid because nestedNum is missing.
<urn:test:nested6>
ex:nestedStr "obj2 nested without extra invalid" .
} }
"# "#
.to_string(), .to_string(),
@ -528,41 +387,35 @@ INSERT DATA {
iri: "http://example.org/RootShape".to_string(), iri: "http://example.org/RootShape".to_string(),
predicates: vec![ predicates: vec![
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/str".to_string(), iri: "http://example.org/multiNest".to_string(),
extra: None, extra: None,
maxCardinality: 1, maxCardinality: 6,
minCardinality: 1, minCardinality: 1,
readablePredicate: "str".to_string(), readablePredicate: "multiNest".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![
valType: OrmSchemaLiteralType::string, OrmSchemaDataType {
literals: None, valType: OrmSchemaLiteralType::shape,
shape: None, literals: None,
}], shape: Some("http://example.org/MultiNestShape1".to_string()),
},
OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/MultiNestShape2".to_string()),
},
],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/nestedWithExtra".to_string(), iri: "http://example.org/singleNest".to_string(),
extra: Some(true), extra: Some(true),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "nestedWithExtra".to_string(), readablePredicate: "singleNest".to_string(),
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape, valType: OrmSchemaLiteralType::shape,
literals: None, literals: None,
shape: Some("http://example.org/NestedShapeWithExtra".to_string()), shape: Some("http://example.org/SingleNestShape".to_string()),
}],
}
.into(),
OrmSchemaPredicate {
iri: "http://example.org/nestedWithoutExtra".to_string(),
extra: Some(false),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "nestedWithoutExtra".to_string(),
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::shape,
literals: None,
shape: Some("http://example.org/NestedShapeWithoutExtra".to_string()),
}], }],
} }
.into(), .into(),
@ -571,72 +424,62 @@ INSERT DATA {
.into(), .into(),
); );
schema.insert( schema.insert(
"http://example.org/NestedShapeWithExtra".to_string(), "http://example.org/SingleNestShape".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/NestedShapeWithExtra".to_string(), iri: "http://example.org/SingleNestShape".to_string(),
predicates: vec![ predicates: vec![OrmSchemaPredicate {
OrmSchemaPredicate { iri: "http://example.org/singleNestStr".to_string(),
iri: "http://example.org/nestedStr".to_string(), extra: None,
extra: None, readablePredicate: "str".to_string(),
readablePredicate: "nestedStr".to_string(), maxCardinality: 1,
maxCardinality: 1, minCardinality: 1,
minCardinality: 1, dataTypes: vec![OrmSchemaDataType {
dataTypes: vec![OrmSchemaDataType { valType: OrmSchemaLiteralType::string,
valType: OrmSchemaLiteralType::string, literals: None,
literals: None, shape: None,
shape: None, }],
}], }
} .into()],
.into(),
OrmSchemaPredicate {
iri: "http://example.org/nestedNum".to_string(),
extra: None,
readablePredicate: "nestedNum".to_string(),
maxCardinality: 1,
minCardinality: 1,
dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number,
literals: None,
shape: None,
}],
}
.into(),
],
} }
.into(), .into(),
); );
schema.insert( schema.insert(
"http://example.org/NestedShapeWithoutExtra".to_string(), "http://example.org/MultiNestShape1".to_string(),
OrmSchemaShape { OrmSchemaShape {
iri: "http://example.org/NestedShapeWithoutExtra".to_string(), iri: "http://example.org/MultiNestShape1".to_string(),
predicates: vec![ predicates: vec![OrmSchemaPredicate {
OrmSchemaPredicate { iri: "http://example.org/multiNest1Str".to_string(),
iri: "http://example.org/nestedStr".to_string(), extra: None,
extra: None, readablePredicate: "string1".to_string(),
readablePredicate: "nestedStr".to_string(), maxCardinality: 1,
maxCardinality: 1, minCardinality: 1,
minCardinality: 1, dataTypes: vec![OrmSchemaDataType {
dataTypes: vec![OrmSchemaDataType { valType: OrmSchemaLiteralType::string,
valType: OrmSchemaLiteralType::string, literals: None,
literals: None, shape: None,
shape: None, }],
}], }
} .into()],
.into(), }
OrmSchemaPredicate { .into(),
iri: "http://example.org/nestedNum".to_string(), );
extra: None, schema.insert(
readablePredicate: "nestedNum".to_string(), "http://example.org/MultiNestShape2".to_string(),
maxCardinality: 1, OrmSchemaShape {
minCardinality: 1, iri: "http://example.org/MultiNestShape2".to_string(),
dataTypes: vec![OrmSchemaDataType { predicates: vec![OrmSchemaPredicate {
valType: OrmSchemaLiteralType::number, iri: "http://example.org/multiNest2Str".to_string(),
literals: None, extra: None,
shape: None, readablePredicate: "string2".to_string(),
}], maxCardinality: 1,
} minCardinality: 1,
.into(), dataTypes: vec![OrmSchemaDataType {
], valType: OrmSchemaLiteralType::string,
literals: None,
shape: None,
}],
}
.into()],
} }
.into(), .into(),
); );
@ -660,29 +503,103 @@ INSERT DATA {
} }
.unwrap(); .unwrap();
break;
}
// Add more data, remove some
doc_sparql_update(
session_id,
r#"
PREFIX ex: <http://example.org/>
INSERT DATA {
<urn:test:multiNested2>
ex:multiNest1Str "replacing object shape view" .
<urn:test:multiNested4>
ex:multiNest2Str "multi 4 added" .
<urn:test:nested3>
ex:singleNestStr "Different nested val" .
}
"#
.to_string(),
Some(doc_nuri.clone()),
)
.await
.expect("2nd SPARQL update failed");
while let Some(app_response) = receiver.next().await {
let patches = match app_response {
AppResponse::V0(v) => match v {
AppResponseV0::OrmUpdate(json) => Some(json),
_ => None,
},
}
.unwrap();
log_info!("Diff ops arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
}
let mut expected = json!([ let mut expected = json!([
{ {
"id": "urn:test:oj1", "op": "remove",
"str": "obj1 str", "path": "/urn:test:oj1/multiNest/urn:test:multiNested2/string2",
"nestedWithExtra": { // "valType": None,
"nestedStr": "obj1 nested with extra valid", // "value": None,
"nestedNum": 2.0 },
}, {
"nestedWithoutExtra": { "op": "add",
"nestedStr": "obj1 nested without extra valid", // "valType": None,
"nestedNum": 2.0 "value": "replacing object shape view",
} "path": "/urn:test:oj1/multiNest/urn:test:multiNested2/string1",
} },
{
"op": "add",
"valType": "object",
// "value": None,
"path": "/urn:test:oj1/multiNest/urn:test:multiNested4",
},
{
"op": "add",
// "valType": None,
"value": "urn:test:multiNested4",
"path": "/urn:test:oj1/multiNest/urn:test:multiNested4/id",
},
{
"op": "add",
// "valType": None,
"value": "multi 4 added",
"path": "/urn:test:oj1/multiNest/urn:test:multiNested4/string2",
},
{
"op": "remove",
// "valType": None,
// "value": None,
"path": "/urn:test:oj1/singleNest/str",
},
{
"op": "add",
// "valType": None,
"value": "Different nested val",
"path": "/urn:test:oj1/singleNest/str",
},
]); ]);
let mut actual_mut = orm_json.clone(); let mut actual = json!(patches);
assert_json_eq(&mut expected, &mut actual_mut); assert_json_eq(&mut expected, &mut actual);
break; break;
} }
cancel_fn();
} }
/*
Old things
*/
async fn test_orm_nested_2(session_id: u64) { async fn test_orm_nested_2(session_id: u64) {
let doc_nuri = create_doc_with_data( let doc_nuri = create_doc_with_data(
session_id, session_id,

Loading…
Cancel
Save