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. 728
      engine/verifier/src/orm/mod.rs
  2. 7
      engine/verifier/src/orm/validation.rs
  3. 7
      sdk/rust/src/tests/mod.rs
  4. 527
      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())));
}
}
return;
}
// Recurse to parents
for (parent_iri, parent_tracked_subject) in tracked_subject.parents.iter() {
// Get predicate schema linking parent with tracked_subject
// Use predicate schema readable_predicate to add to path.
// If predicate schema is multi, add our own subject iri to path first.
// If parent is root, we don't need to recurse.
// 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
}); });
// Get the shape IRI for a nested object that is valid. if is_child {
let object_shape_iri = { // Build the path segment
// Get the tracked subject for this object IRI let mut new_path = path.clone();
let tracked_subjects_for_obj = tracked_subjects
.get(object_iri) let is_multi = pred_arc.maxCardinality > 1
.expect("Object should be tracked"); || pred_arc.maxCardinality == -1;
// Find the first valid shape for this object from the allowed shapes // For multi-valued predicates, add the object IRI as a key first
let allowed_shape_iris: Vec<&String> = pred_shape 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 diff_op_from_pred_change(
pred_change: &OrmTrackedPredicateChanges,
) -> Vec<(
OrmDiffOpType,
Option<OrmDiffType>,
Option<Value>, // The value added / removed
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 .dataTypes
.iter() .iter()
.filter_map(|dt| dt.shape.as_ref()) .any(|dt| dt.shape.is_some());
.collect();
allowed_shape_iris 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<
String,
HashMap<String, Arc<RwLock<OrmTrackedSubject>>>,
>,
) -> (Option<String>, Option<String>) {
let Some(shapes_for_subject) = tracked_subjects.get(subject_iri) else {
return (None, None);
};
// Find current highest-priority valid shape
let current_valid = allowed_shapes
.iter() .iter()
.find(|shape_iri| { .filter_map(|dt| dt.shape.as_ref())
tracked_subjects_for_obj .find_map(|shape_iri| {
.get(**shape_iri) shapes_for_subject.get(shape_iri).and_then(|ts| {
.map(|ts| { let tracked = ts.read().unwrap();
ts.read().unwrap().valid == OrmTrackedSubjectValidity::Valid if tracked.valid == OrmTrackedSubjectValidity::Valid {
Some(shape_iri.clone())
} else {
None
}
}) })
.unwrap_or(false) });
// Find previous highest-priority valid shape
let previous_valid = allowed_shapes
.iter()
.filter_map(|dt| dt.shape.as_ref())
.find_map(|shape_iri| {
shapes_for_subject.get(shape_iri).and_then(|ts| {
let tracked = ts.read().unwrap();
if tracked.prev_valid == OrmTrackedSubjectValidity::Valid {
Some(shape_iri.clone())
} else {
None
}
}) })
.unwrap() });
.to_string()
};
// Apply changes for nested object. (current_valid, previous_valid)
create_patches_for_changed_subj(
orm_changes,
patches,
&object_shape_iri,
&object_iri,
sub,
path,
tracked_subjects,
);
} }
fn create_patches_for_changed_subj( // Helper function to handle validity changes when highest-priority shape changes
orm_changes: &OrmChanges, #[allow(dead_code)]
patches: &mut OrmDiff, fn handle_shape_priority_change(
shape_iri: &String, subject_iri: &SubjectIri,
subject_iri: &String, shape_iri: &ShapeIri,
sub: &OrmSubscription,
path: &mut Vec<String>,
tracked_subjects: &HashMap< tracked_subjects: &HashMap<
SubjectIri, String,
HashMap<ShapeIri, Arc<RwLock<OrmTrackedSubject>>>, 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>)>,
) { ) {
let change = orm_changes // Step 1: Check if this subject has multiple tracked shapes
.get(shape_iri) let Some(shapes_for_subject) = tracked_subjects.get(subject_iri) else {
.unwrap() return;
.get(subject_iri) };
.unwrap();
let subject_shape = sub.shape_type.schema.get(shape_iri).unwrap();
// @Niko, is it safe to do this? if shapes_for_subject.len() <= 1 {
let tracked_subject = tracked_subjects // Only one shape, no priority conflict possible
.get(subject_iri) return;
.unwrap() }
.get(shape_iri)
.unwrap()
.read()
.unwrap();
// Check validity changes // Step 2: Get the current tracked subject
if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Invalid let Some(tracked_subject_arc) = shapes_for_subject.get(shape_iri) else {
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid
{
// Is the subject invalid and was it before? There is nothing we need to inform about.
return; return;
} else if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Valid };
&& tracked_subject.valid == OrmTrackedSubjectValidity::Invalid let tracked_subject = tracked_subject_arc.read().unwrap();
|| tracked_subject.valid == OrmTrackedSubjectValidity::Untracked
// 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 parent_ts = parent_tracked_subject_arc.read().unwrap();
// Find the predicate linking parent to this subject
for pred_arc in &parent_ts.shape.predicates {
if let Some(tracked_pred) =
parent_ts.tracked_predicates.get(&pred_arc.iri)
{ {
// Has the subject become invalid or untracked? let tp = tracked_pred.read().unwrap();
// 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,
});
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. // Check if this tracked subject is a child of this predicate
if tracked_subject.prev_valid != OrmTrackedSubjectValidity::Valid { let is_child = tp.tracked_children.iter().any(|child| {
patches.push(OrmDiffOp { let child_read = child.read().unwrap();
op: OrmDiffOpType::add, child_read.subject_iri == *subject_iri
valType: Some(OrmDiffType::object),
path: format!("/{}", path.join("/")),
value: None,
}); });
// And add the id field.
patches.push(OrmDiffOp { if !is_child {
op: OrmDiffOpType::add, continue;
valType: None,
path: format!("/{}/{}", path.join("/"), subject_iri),
value: None,
});
}
} }
// Iterate over every predicate change and create patches // Get the allowed shapes for this predicate (in priority order)
for (pred_iri, pred_change) in change.predicates.iter() { let allowed_shapes: Vec<_> = pred_arc
let pred_shape = subject_shape .dataTypes
.predicates
.iter() .iter()
.find(|p| p.iri == *pred_iri) .filter(|dt| dt.shape.is_some())
.unwrap(); .collect();
let is_multi = if allowed_shapes.len() <= 1 {
pred_shape.maxCardinality > 1 || pred_shape.maxCardinality == -1; // No priority conflict possible with single shape
let is_object = pred_shape.dataTypes.iter().any(|dt| !dt.shape.is_none()); continue;
let pred_name = pred_shape.readablePredicate.clone(); }
path.push(pred_name);
let path_str = format!("/{}", path.join("/"));
// Depending on the predicate type (multi / single, object / not object), // Determine current and previous highest-priority valid shapes
// add the respective diff operation. let (current_valid, previous_valid) =
get_highest_priority_valid_shapes(
subject_iri,
&pred_arc.dataTypes,
tracked_subjects,
);
// Single primitive value // Step 4: Create patches based on what changed
if !is_multi && !is_object { if current_valid != previous_valid {
if pred_change.values_added.len() > 0 { let is_multi = pred_arc.maxCardinality > 1
patches.push(OrmDiffOp { || pred_arc.maxCardinality == -1;
op: OrmDiffOpType::add,
valType: None, // Case A: Shape switch (ShapeA -> ShapeB)
path: path_str.clone(), if let (Some(new_shape), Some(old_shape)) =
value: Some(json!(pred_change.values_added[0])), (&current_valid, &previous_valid)
}); {
} // Remove the old object
if pred_change.values_removed.len() > 0 { if let Some(old_ts) = shapes_for_subject.get(old_shape) {
patches.push(OrmDiffOp { let old_tracked = old_ts.read().unwrap();
op: OrmDiffOpType::remove, let mut path = vec![];
valType: None, let diff_op = (
path: path_str, OrmDiffOpType::remove,
value: Some(json!(pred_change.values_added[0])), Some(OrmDiffType::object),
}); None,
} Some(subject_iri.clone()),
} else if is_multi && !is_object { );
// Set of primitive values
if pred_change.values_added.len() > 0 { add_diff_ops_for_tracked_subject(
patches.push(OrmDiffOp { &old_tracked,
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 {
// Change in single object property.
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, tracked_subjects,
root_shape,
&mut path,
diff_op,
patches, patches,
path, paths_of_objects_to_create,
object_iri,
orm_changes,
sub,
); );
} }
if pred_change.values_removed.len() > 0 {
// Object is removed.
patches.push(OrmDiffOp {
op: OrmDiffOpType::remove,
valType: Some(OrmDiffType::object),
path: path_str,
value: None,
});
}
} else {
// Change(s) in multi object property.
// Add every new object. // Add the new object (need to materialize it)
for obj_iri_bt in pred_change.values_added.iter() { if let Some(new_ts) = shapes_for_subject.get(new_shape) {
let obj_iri = match obj_iri_bt { let new_tracked = new_ts.read().unwrap();
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. // TODO: Materialize the object with current triples
path.push(escape_json_pointer(obj_iri)); // 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()),
);
create_patches_for_nested_object( add_diff_ops_for_tracked_subject(
pred_shape, &new_tracked,
tracked_subjects, tracked_subjects,
root_shape,
&mut path,
diff_op,
patches, patches,
path, paths_of_objects_to_create,
obj_iri,
orm_changes,
sub,
); );
// Remove object IRI from stack again.
path.pop();
} }
}
// Delete objects. // Case B: Object became valid (None -> ShapeX)
// If there are no more predicates, delete the whole object. else if let (Some(new_shape), None) =
if pred_change (&current_valid, &previous_valid)
.tracked_predicate
.read()
.unwrap()
.tracked_children
.len()
== 0
{ {
// Or the whole thing if no children remain if let Some(new_ts) = shapes_for_subject.get(new_shape) {
patches.push(OrmDiffOp { let new_tracked = new_ts.read().unwrap();
op: OrmDiffOpType::remove, let mut path = vec![];
valType: Some(OrmDiffType::object), let diff_op = (
path: path_str, OrmDiffOpType::add,
value: None, Some(OrmDiffType::object),
}); Some(Value::Null),
} else { Some(subject_iri.clone()),
for object_iri_removed in pred_change.values_removed.iter() { );
patches.push(OrmDiffOp {
op: OrmDiffOpType::remove, add_diff_ops_for_tracked_subject(
valType: Some(OrmDiffType::object), &new_tracked,
path: format!( tracked_subjects,
"/{}/{}", root_shape,
path_str, &mut path,
match object_iri_removed { diff_op,
BasicType::Str(iri) => iri, patches,
_ => panic!("Object IRI must be string"), paths_of_objects_to_create,
);
} }
),
value: None,
});
} }
// 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,
);
} }
} }
} }
// Remove this predicate name from the path again. break; // Found the predicate, no need to check others
path.pop(); }
} }
} }
}
// We construct object patches from a change (which is associated with a shape type). {op: add, valType: object, value: Null, path: ...}
// For each change that has a subject tracked in this subscription,
// - Get change operation (calling diff_op_from_pred_change).
// - case not object, single --> either add or remove (must be one of each at max)
// - case not object, multi --> just add and or set patch
// - case object, multi --> create object patch + nested object patch (will be handled when recursing paths to add primitive values)
// - case object, single --> just object patch (will be handled when recursing paths to add primitive values)
// - 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).
let mut patches: OrmDiff = vec![]; // TODO: Special edge case: An object with parents changed and the parents' predicate schema has multiple allowed shapes.
let mut path: Vec<String> = Vec::with_capacity(4); // 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.
// Iterate over each root subject with the right shape // Process changes for this subscription
// For each tracked subject that has the subscription's shape, call fn above // Iterate through all changes and create patches
for (subject_iri, tracked_subjects_by_shape) in sub.tracked_subjects.iter() { for (shape_iri, subject_changes) in &orm_changes {
for (shape_iri, tracked_subject) in tracked_subjects_by_shape.iter() { for (subject_iri, change) in subject_changes {
if *shape_iri != sub.shape_type.shape { // 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;
} };
// Found a root subject for this shape.
// Add subject IRI as first part of path pointer. // Process each predicate change
path.push(escape_json_pointer(subject_iri)); for (pred_iri, pred_change) in &change.predicates {
create_patches_for_changed_subj( let tracked_predicate = pred_change.tracked_predicate.read().unwrap();
&orm_changes, let pred_name = tracked_predicate.schema.readablePredicate.clone();
&mut patches, // Check validity changes
shape_iri, if tracked_subject.prev_valid == OrmTrackedSubjectValidity::Invalid
subject_iri, && tracked_subject.valid == OrmTrackedSubjectValidity::Invalid
sub, {
// 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, &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.tracked_subjects,
&sub.shape_type.shape,
&mut path,
diff_op,
&mut patches,
&mut paths_of_objects_to_create,
); );
path.pop(); }
}
}
}
}
// Create patches for objects that need to be created
// These are patches with {op: add, valType: object, value: Null, path: ...}
// Sort by path length (shorter first) to ensure parent objects are created before children
let mut sorted_object_paths: Vec<_> = paths_of_objects_to_create.iter().collect();
sorted_object_paths.sort_by_key(|(path_segments, _)| path_segments.len());
for (path_segments, maybe_iri) in sorted_object_paths {
let escaped_path: Vec<String> = path_segments
.iter()
.map(|seg| escape_json_pointer(seg))
.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;
// 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 .await
.expect("orm_start"); .expect("2nd SPARQL update failed");
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 {
valType: OrmSchemaLiteralType::shape,
literals: None, literals: None,
shape: None, shape: Some("http://example.org/MultiNestShape1".to_string()),
}], },
} OrmSchemaDataType {
.into(),
OrmSchemaPredicate {
iri: "http://example.org/nestedWithExtra".to_string(),
extra: Some(true),
maxCardinality: 1,
minCardinality: 1,
readablePredicate: "nestedWithExtra".to_string(),
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/MultiNestShape2".to_string()),
}], },
],
} }
.into(), .into(),
OrmSchemaPredicate { OrmSchemaPredicate {
iri: "http://example.org/nestedWithoutExtra".to_string(), iri: "http://example.org/singleNest".to_string(),
extra: Some(false), extra: Some(true),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
readablePredicate: "nestedWithoutExtra".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/NestedShapeWithoutExtra".to_string()), shape: Some("http://example.org/SingleNestShape".to_string()),
}], }],
} }
.into(), .into(),
@ -571,14 +424,13 @@ 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: "nestedStr".to_string(), readablePredicate: "str".to_string(),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
@ -587,33 +439,18 @@ INSERT DATA {
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: "nestedStr".to_string(), readablePredicate: "string1".to_string(),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
@ -622,21 +459,27 @@ INSERT DATA {
shape: None, shape: None,
}], }],
} }
.into()],
}
.into(), .into(),
OrmSchemaPredicate { );
iri: "http://example.org/nestedNum".to_string(), schema.insert(
"http://example.org/MultiNestShape2".to_string(),
OrmSchemaShape {
iri: "http://example.org/MultiNestShape2".to_string(),
predicates: vec![OrmSchemaPredicate {
iri: "http://example.org/multiNest2Str".to_string(),
extra: None, extra: None,
readablePredicate: "nestedNum".to_string(), readablePredicate: "string2".to_string(),
maxCardinality: 1, maxCardinality: 1,
minCardinality: 1, minCardinality: 1,
dataTypes: vec![OrmSchemaDataType { dataTypes: vec![OrmSchemaDataType {
valType: OrmSchemaLiteralType::number, valType: OrmSchemaLiteralType::string,
literals: None, literals: None,
shape: None, shape: None,
}], }],
} }
.into(), .into()],
],
} }
.into(), .into(),
); );
@ -660,29 +503,103 @@ INSERT DATA {
} }
.unwrap(); .unwrap();
let mut expected = json!([ break;
{ }
"id": "urn:test:oj1",
"str": "obj1 str", // Add more data, remove some
"nestedWithExtra": { doc_sparql_update(
"nestedStr": "obj1 nested with extra valid", session_id,
"nestedNum": 2.0 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,
}, },
"nestedWithoutExtra": {
"nestedStr": "obj1 nested without extra valid",
"nestedNum": 2.0
} }
.unwrap();
log_info!("Diff ops arrived:\n");
for patch in patches.iter() {
log_info!("{:?}", patch);
} }
let mut expected = json!([
{
"op": "remove",
"path": "/urn:test:oj1/multiNest/urn:test:multiNested2/string2",
// "valType": None,
// "value": None,
},
{
"op": "add",
// "valType": None,
"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