Files
sbox-public/engine/Sandbox.Engine/Utility/Json/Json.Diff.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

896 lines
28 KiB
C#

using System.Data;
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
using static Sandbox.Json;
namespace Sandbox;
public static partial class Json
{
/// <summary>
/// Uniquely identifies a tracked object by its type and identifier value.
/// </summary>
internal record struct ObjectIdentifier
{
[JsonInclude]
public string Type;
[JsonInclude]
public string IdValue;
}
/// <summary>
/// Represents a property change to apply during patching.
/// </summary>
internal record struct PropertyOverride
{
/// <summary>The object whose property should be modified</summary>
[JsonInclude]
public ObjectIdentifier Target;
/// <summary>The name of the property to modify</summary>
[JsonInclude]
public string Property;
/// <summary>The new value to assign to the property</summary>
[JsonInclude]
public JsonNode Value;
}
/// <summary>
/// Represents an object that needs to be added during patching.
/// </summary>
internal record struct AddedObject
{
/// <summary>The identifier for the new object</summary>
[JsonInclude]
public ObjectIdentifier Id;
/// <summary>The parent object that will contain this object</summary>
[JsonInclude]
public ObjectIdentifier Parent;
/// <summary>The previous sibling when adding to an array (null if first or not in array)</summary>
[JsonInclude]
public ObjectIdentifier? PreviousElement;
/// <summary>The property name in the parent that will contain this object</summary>
[JsonInclude]
public string ContainerProperty;
/// <summary>Whether this object is being added to an array (true) or as a direct property (false)</summary>
[JsonInclude]
public bool IsContainerArray;
/// <summary>The data for the new object</summary>
[JsonInclude]
public JsonObject Data;
}
/// <summary>
/// Represents an object that should be removed during patching.
/// </summary>
internal record struct RemovedObject
{
/// <summary>The identifier of the object to remove</summary>
[JsonInclude]
public ObjectIdentifier Id;
}
/// <summary>
/// Represents an object that should be moved to a new location during patching.
/// </summary>
internal record struct MovedObject
{
/// <summary>The identifier of the object to move</summary>
[JsonInclude]
public ObjectIdentifier Id;
/// <summary>The new parent object</summary>
[JsonInclude]
public ObjectIdentifier NewParent;
/// <summary>The property name in the new parent that will contain this object</summary>
[JsonInclude]
public string NewContainerProperty;
/// <summary>Whether the object is being moved to an array (true) or as a direct property (false)</summary>
[JsonInclude]
public bool IsNewContainerArray;
/// <summary>The previous sibling in the new location (null if first or not in array)</summary>
[JsonInclude]
public ObjectIdentifier? NewPreviousElement;
}
/// <summary>
/// Defines characteristics of an object type that should be tracked within a JSON tree structure.
/// These definitions are used to identify, track, and manage specific types of objects during JSON diffing and patching operations.
/// </summary>
internal class TrackedObjectDefinition
{
/// <summary>
/// A unique identifier for this object type. This is used to categorize objects.
/// </summary>
public string Type;
/// <summary>
/// Determines whether a JSON object should be considered an instance of this tracked object type.
/// </summary>
/// <remarks>
/// The function returns a float value indicating how well the JSON object matches this definition.
/// A return value of 0 indicates no match, while higher values indicate stronger matches.
/// This allows for heuristic-based matching when exact matches aren't possible.
/// </remarks>
public Func<JsonObject, float> MatchScore;
/// <summary>
/// Maps a JSON object to a unique identifier string.
/// </summary>
/// <remarks>
/// The identifier could be derived from a specific property, a combination of properties, or a computed hash.
/// It's critical that this function:
/// 1. Produces a truly unique value for each distinct object of this type
/// 2. Never maps two different objects to the same ID
/// 3. Is deterministic - always returns the same ID when applied to the same object
///
/// If you can just use a UUID or other guaranteed unique identifier.
/// </remarks>
public Func<JsonObject, string> ToId;
/// <summary>
/// Specifies the required type of the parent object. If null, AllowedAsRoot must be true.
/// </summary>
/// <remarks>
/// This enforces type hierarchy constraints within the JSON structure.
/// </remarks>
public string ParentType;
/// <summary>
/// If true, objects of this type can be the root of the object tree.
/// </summary>
/// <remarks>
/// Root objects don't require a parent, and they don't need an ID since there can only be one root.
/// If AllowedAsRoot is false, ParentType must be specified.
/// </remarks>
public bool AllowedAsRoot;
/// <summary>
/// When true, treats this object as an atomic unit during tracking operations.
/// </summary>
/// <remarks>
/// Objects with AtomicTracking enabled:
/// 1. Have their children excluded from individual tracking
/// 2. Skip property-level diffing (changes are handled as whole object replacements)
/// 3. Are treated as "black boxes" where internal structure is ignored
///
/// This is useful for:
/// - Objects containing data that shouldn't be tracked independently (like patches)
/// - Preventing recursive tracking of complex nested structures
/// </remarks>
public bool Atomic;
public HashSet<string> IgnoredProperties;
/// <summary>
/// Creates a TrackedObjectDefinition that identifies objects based on the presence of specific fields.
/// </summary>
internal static TrackedObjectDefinition CreatePresenceBasedDefinition(
string type,
IEnumerable<string> requiredFields,
string idProperty = null,
string parentType = null,
bool allowedAsRoot = false,
bool atomic = false,
IEnumerable<string> ignoredProperties = null )
{
return new TrackedObjectDefinition
{
Type = type,
// Return the count of required fields if all required fields are present
MatchScore = ( jsonObject ) =>
{
if ( idProperty != null && !jsonObject.ContainsKey( idProperty ) ) return 0f;
if ( requiredFields == null || requiredFields.Count() == 0 ) return 0f;
var matchingRequiredFields = requiredFields.Count( jsonObject.ContainsKey );
// Only match if all required fields are present
return matchingRequiredFields == requiredFields.Count() ? requiredFields.Count() : 0f;
},
// Extract the ID from the specified property
ToId = ( jsonObject ) =>
{
if ( idProperty == null ) return null;
if ( !jsonObject.TryGetPropertyValue( idProperty, out var idValue ) )
{
Log.Error( $"Object of type '{type}' does not have a valid id property '{idProperty}'" );
return null;
}
return idValue.AsValue().GetValue<object>().ToString();
},
ParentType = parentType,
AllowedAsRoot = allowedAsRoot,
Atomic = atomic,
IgnoredProperties = ignoredProperties is null ? new HashSet<string>() : ignoredProperties.ToHashSet()
};
}
}
/// <summary>
/// Represents a tracked object in a JSON tree with metadata for diffing and patching operations.
/// </summary>
private class TrackedObject
{
/// <summary>The unique identifier for this object</summary>
public ObjectIdentifier Id;
/// <summary>The defintion taht was used to track this object.</summary>
public TrackedObjectDefinition Definition;
/// <summary>The object's JSON data without its children</summary>
public JsonObject Data;
/// <summary>Reference to this object's parent (null for root objects)</summary>
public TrackedObject Parent;
/// <summary>The property name in parent that contains this object</summary>
public string ContainerProperty;
/// <summary>Whether this object is contained in an array (true) or as a direct property (false)</summary>
public bool IsContainedInArray;
/// <summary>The previous sibling element when contained in an array (null if first or not in array)</summary>
public TrackedObject PreviousElement;
/// <summary>The path to this object in the JSON structure</summary>
public string Path;
/// <summary>Child objects belonging to this object</summary>
public LinkedList<TrackedObject> Children = new();
/// <summary>
/// Reconstructs a complete JSON tree from this object and all its children.
/// </summary>
public JsonNode ToJson()
{
var root = Data.DeepClone().AsObject();
foreach ( var child in Children )
{
var pathSegments = child.ContainerProperty.Split( '.' );
var currentObject = root; // Start from the root for each child
// Navigate to the correct container, creating objects as needed
for ( var i = 0; i < pathSegments.Length - 1; i++ )
{
var pathSegment = pathSegments[i];
if ( !currentObject.ContainsKey( pathSegment ) )
{
currentObject[pathSegment] = new JsonObject();
}
currentObject = currentObject[pathSegment].AsObject();
}
// Handle the final path segment
var finalSegment = pathSegments[pathSegments.Length - 1];
if ( child.IsContainedInArray )
{
if ( !currentObject.ContainsKey( finalSegment ) )
{
currentObject[finalSegment] = new JsonArray();
}
var parentArray = currentObject[finalSegment].AsArray();
parentArray.Add( child.ToJson() );
}
else
{
currentObject[finalSegment] = child.ToJson();
}
}
return root;
}
}
private class TrackedObjects
{
public TrackedObject Root;
public Dictionary<ObjectIdentifier, TrackedObject> IdToTrackedObject = new( 128 );
public HashSet<string> TrackedPaths = new( 128 );
}
private static (ObjectIdentifier?, TrackedObjectDefinition) TryGetObjectIdentifier(
JsonObject jsonObject,
string parentType,
IEnumerable<TrackedObjectDefinition> definitions )
{
ObjectIdentifier? bestCandidate = null;
TrackedObjectDefinition bestDefinition = null;
var bestCandidateScore = 0f;
foreach ( var definition in definitions )
{
if ( !definition.AllowedAsRoot && parentType == null )
continue;
if ( !definition.AllowedAsRoot && string.IsNullOrEmpty( definition.ParentType ) )
{
Log.Warning( $"Object definition '{definition.Type}' is not allowed as root, but has no owner type" );
}
if ( parentType != null && string.IsNullOrEmpty( definition.ParentType ) )
continue;
if ( !string.IsNullOrEmpty( definition.ParentType ) && parentType == null && !definition.AllowedAsRoot )
continue;
if ( !string.IsNullOrEmpty( definition.ParentType ) && !definition.ParentType.Equals( parentType, StringComparison.OrdinalIgnoreCase ) && !definition.AllowedAsRoot )
continue;
var defintionScore = definition.MatchScore( jsonObject );
if ( defintionScore == 0f )
continue;
if ( defintionScore > bestCandidateScore )
{
var id = definition.ToId( jsonObject );
// We allow an empty ids only root level objects
if ( id == null && !definition.AllowedAsRoot )
{
Log.Error( $"Object of type '{definition.Type}' does not have a valid id" );
continue;
}
bestCandidate = new ObjectIdentifier
{
Type = definition.Type,
IdValue = id,
};
bestCandidateScore = defintionScore;
bestDefinition = definition;
}
}
return (bestCandidate, bestDefinition);
}
private static TrackedObjects FindTrackedObjectsInJson(
JsonObject root,
HashSet<TrackedObjectDefinition> definitions )
{
var result = new TrackedObjects();
if ( root is null )
{
return result;
}
var clonedRoot = root.DeepClone().AsObject();
TraverseNode( clonedRoot, "", definitions, result, null, null, false );
// Sanitize objects to remove tracked objects
foreach ( var (objId, trackedObj) in result.IdToTrackedObject )
{
if ( trackedObj.Definition.Atomic ) continue;
trackedObj.Data = StripNestedObjects( trackedObj, result.TrackedPaths );
}
return result;
}
private static void TraverseNode(
JsonNode node,
string path,
HashSet<TrackedObjectDefinition> definitions,
TrackedObjects result,
TrackedObject parent,
string containerProperty,
bool containerIsArray )
{
if ( node is JsonObject jsonObject )
{
// Get the parent type if available
string parentType = parent?.Id.Type;
// Try to get an object identifier
var (currentIdentifier, matchedDefintion) = TryGetObjectIdentifier( jsonObject, parentType, definitions );
if ( currentIdentifier.HasValue )
{
result.IdToTrackedObject[currentIdentifier.Value] = new TrackedObject
{
Id = currentIdentifier.Value,
Definition = matchedDefintion,
Data = jsonObject,
Parent = parent,
ContainerProperty = containerProperty,
IsContainedInArray = containerIsArray,
Path = path,
};
parent?.Children.AddLast( result.IdToTrackedObject[currentIdentifier.Value] );
// If parent is null set our root
if ( parent == null )
{
result.Root = result.IdToTrackedObject[currentIdentifier.Value];
}
result.TrackedPaths.Add( path );
if ( matchedDefintion.Atomic )
{
// If the object is self contained we don't need to traverse its children
return;
}
}
// Traverse child properties
foreach ( var (propName, propValue) in jsonObject )
{
var newPath = AppendToPath( path, propName );
var newParent = currentIdentifier.HasValue && result.IdToTrackedObject.ContainsKey( currentIdentifier.Value ) ? result.IdToTrackedObject[currentIdentifier.Value] : parent;
// Reset containerproperty name if we found a tracked object
var newContainerProperty = currentIdentifier.HasValue ? propName : $"{containerProperty}.{propName}";
TraverseNode(
propValue,
newPath,
definitions,
result,
newParent,
newContainerProperty,
false );
}
}
else if ( node is JsonArray jsonArray )
{
TrackedObject previousElement = null;
for ( int i = 0; i < jsonArray.Count; i++ )
{
var item = jsonArray[i];
var childPath = AppendToPath( path, i );
if ( item is JsonObject jsonArrayObject )
{
// Try to get identifier for this object
var (elementId, _) = TryGetObjectIdentifier( jsonArrayObject, parent?.Id.Type, definitions );
// Process this object
TraverseNode( item, childPath, definitions, result, parent, containerProperty, true );
// If we found a valid identifier, update its node with previous element info
if ( elementId.HasValue && result.IdToTrackedObject.TryGetValue( elementId.Value, out var trackedObj ) )
{
result.TrackedPaths.Add( path );
// Set the previous element reference
trackedObj.PreviousElement = previousElement;
// Current becomes previous for next iteration
previousElement = result.IdToTrackedObject[elementId.Value];
}
}
// We only support objects and value arrays
// so don't do anything if array contains values or other arrays
}
}
}
/// <summary>
/// Represents a complete set of changes to be applied to a JSON structure.
/// </summary>
/// <remarks>
/// A patch contains all the operations needed to transform one JSON structure into another
/// while preserving object identity and relationships.
/// </remarks>
internal class Patch
{
/// <summary>
/// Objects that need to be added to the target structure.
/// </summary>
[JsonInclude]
public List<AddedObject> AddedObjects { get; set; } = new List<AddedObject>( 16 );
/// <summary>
/// Objects that need to be removed from the target structure.
/// </summary>
[JsonInclude]
public List<RemovedObject> RemovedObjects { get; set; } = new List<RemovedObject>( 16 );
/// <summary>
/// Property values that need to be changed on existing objects.
/// </summary>
[JsonInclude]
public List<PropertyOverride> PropertyOverrides { get; set; } = new List<PropertyOverride>( 32 );
/// <summary>
/// Objects that need to be moved to a different location in the structure.
/// </summary>
[JsonInclude]
public List<MovedObject> MovedObjects { get; set; } = new List<MovedObject>( 16 );
}
/// <summary>
/// Compares two JSON object trees and calculates the differences between them.
/// </summary>
/// <param name="oldRoot">The original JSON object tree</param>
/// <param name="newRoot">The updated JSON object tree</param>
/// <param name="definitions">Set of definitions for tracked object types in the JSON structure</param>
/// <returns>A Patch object containing all changes needed to transform oldRoot into newRoot</returns>
internal static Patch CalculateDifferences(
JsonObject oldRoot,
JsonObject newRoot,
HashSet<TrackedObjectDefinition> definitions )
{
var patch = new Patch();
// Find objects in old and new JSON structures
var oldObjects = FindTrackedObjectsInJson( oldRoot, definitions );
var newObjects = FindTrackedObjectsInJson( newRoot, definitions );
// Find removed objects
foreach ( var oldObj in oldObjects.IdToTrackedObject.Where( o => o.Value.Parent != null ) )
{
if ( !newObjects.IdToTrackedObject.ContainsKey( oldObj.Key ) )
{
patch.RemovedObjects.Add( new RemovedObject
{
Id = oldObj.Key
} );
}
}
// Find added objects and property overrides
foreach ( var newObj in newObjects.IdToTrackedObject )
{
if ( !oldObjects.IdToTrackedObject.ContainsKey( newObj.Key ) )
{
// Object is in new but not in old
if ( newObj.Value.Parent != null )
{
patch.AddedObjects.Add( new AddedObject
{
Id = newObj.Key,
Parent = newObj.Value.Parent.Id,
ContainerProperty = newObj.Value.ContainerProperty,
IsContainerArray = newObj.Value.IsContainedInArray,
Data = newObj.Value.Data.DeepClone().AsObject(),
PreviousElement = newObj.Value.PreviousElement?.Id
} );
}
}
else
{
// Check for new or modified properties
foreach ( var property in newObj.Value.Data )
{
var oldObjValue = oldObjects.IdToTrackedObject[newObj.Key].Data;
var propName = property.Key;
var newValue = property.Value;
if ( oldObjects.TrackedPaths.Contains( AppendToPath( newObj.Value.Path, propName ) ) || newObjects.TrackedPaths.Contains( AppendToPath( newObj.Value.Path, propName ) ) )
{
// Skip tracked properties
continue;
}
if ( newObj.Value.Definition.Atomic )
{
// Skip property overrides self contained objects
continue;
}
if ( newObj.Value.Definition.IgnoredProperties.Contains( propName ) )
{
continue;
}
if ( oldObjValue.TryGetPropertyValue( propName, out var oldValue ) )
{
// Property exists in both - check for differences
if ( !JsonNode.DeepEquals( oldValue, newValue ) )
{
patch.PropertyOverrides.Add( new PropertyOverride
{
Target = newObj.Key,
Property = propName,
Value = newValue?.DeepClone()
} );
}
}
else if ( newValue != null || oldObjValue != null )
{
// Property is new and not null
patch.PropertyOverrides.Add( new PropertyOverride
{
Target = newObj.Key,
Property = propName,
Value = newValue?.DeepClone()
} );
}
}
// Check if object has moved (different parent or different position in array)
if ( newObj.Value.PreviousElement?.Id != oldObjects.IdToTrackedObject[newObj.Key].PreviousElement?.Id ||
newObj.Value.Parent?.Id != oldObjects.IdToTrackedObject[newObj.Key].Parent?.Id )
{
patch.MovedObjects.Add( new MovedObject
{
Id = newObj.Key,
NewParent = newObj.Value.Parent.Id,
NewContainerProperty = newObj.Value.ContainerProperty,
IsNewContainerArray = newObj.Value.IsContainedInArray,
NewPreviousElement = newObj.Value.PreviousElement?.Id
} );
}
}
}
return patch;
}
private static JsonObject StripNestedObjects(
TrackedObject original,
HashSet<string> trackedPaths )
{
var sanitized = original.Data;
RemoveTrackedObjects( sanitized, original.Path, trackedPaths );
return sanitized;
}
private static void RemoveTrackedObjects(
JsonNode node,
string path,
HashSet<string> trackedPaths )
{
if ( node is JsonObject jsonObject )
{
// Process all properties of the object
foreach ( var property in jsonObject.ToList() )
{
var propName = property.Key;
var propValue = property.Value;
var propPath = AppendToPath( path, propName );
if ( propValue is JsonObject propObject )
{
// Check if the object is tracked
if ( trackedPaths.Contains( propPath ) )
{
jsonObject.Remove( propName );
continue;
}
// Recursively process this object if it's not tracked itself
RemoveTrackedObjects( propObject, propPath, trackedPaths );
}
else if ( propValue is JsonArray propArray )
{
// Check if the array itself is tracked
if ( trackedPaths.Contains( propPath ) )
{
propArray.Clear();
continue;
}
// Check array items (only if containing objects)
for ( int i = propArray.Count - 1; i >= 0; i-- )
{
if ( propArray[i] is JsonObject arrayObj )
{
var itemPath = AppendToPath( propPath, i );
if ( trackedPaths.Contains( itemPath ) )
{
// Remove tracked array items
propArray.RemoveAt( i );
}
else
{
// Recursively process untracked objects in the array
RemoveTrackedObjects( arrayObj, itemPath, trackedPaths );
}
}
}
}
}
}
}
/// <summary>
/// Applies a patch to transform a JSON object tree, with support for partial patch application
/// when the source tree has been modified after the patch was created.
/// </summary>
/// <param name="sourceRoot">The JSON object tree to modify</param>
/// <param name="patch">The patch containing all changes to apply</param>
/// <param name="definitions">Set of definitions for tracked object types</param>
/// <returns>A new JSON object tree with all applicable changes applied</returns>
/// <remarks>
/// Partial patch application semantics:
///
/// Object Removal:
/// - Skipped if object doesn't exist in source
/// - Proceeds if object exists even if parent has changed
///
/// Object Addition:
/// - Only added if parent exists in source
/// - Skipped if parent is missing
///
/// Object Moves:
/// - Requires both object and target parent to exist
/// - Object is removed if target parent doesn't exist
///
/// Property Overrides:
/// - Only applied if target object exists
///
/// Array Ordering:
/// - Best effort based on neighbourhood information (previous element)
/// - Objects without previous elements are placed at start
///
/// Operations are processed in this order: removals, additions, moves,
/// reordering, and finally property overrides.
/// </remarks>
internal static JsonObject ApplyPatch(
JsonObject sourceRoot,
Patch patch,
HashSet<TrackedObjectDefinition> definitions )
{
var sourceTrackedObjects = FindTrackedObjectsInJson( sourceRoot, definitions );
// Removals are easy just nuke them from our object tree
foreach ( var removal in patch.RemovedObjects )
{
var removedObject = sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( removal.Id );
if ( removedObject == null ) continue;
// check if parent still exists
if ( removedObject.Parent != null )
{
removedObject.Parent.Children.Remove( removedObject );
}
sourceTrackedObjects.IdToTrackedObject.Remove( removedObject.Id );
}
// Register all objects that will be added to our tree later
// We need their references to be avialable early
// As we might need to move obejcts into their children
// add objects to the source objects
foreach ( var addition in patch.AddedObjects )
{
sourceTrackedObjects.IdToTrackedObject[addition.Id] = new TrackedObject
{
Id = addition.Id,
Data = addition.Data,
ContainerProperty = addition.ContainerProperty,
IsContainedInArray = addition.IsContainerArray,
};
}
// Second pass to set parents and prev and handle additions
// need a second pass, because we can only start doing this once all references are available
foreach ( var added in patch.AddedObjects )
{
var addedObject = sourceTrackedObjects.IdToTrackedObject[added.Id];
addedObject.Parent = sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( added.Parent );
addedObject.PreviousElement = added.PreviousElement.HasValue ? sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( added.PreviousElement.Value ) : null;
// Add to parent if it still exists
if ( addedObject.Parent != null )
{
addedObject.Parent.Children.AddLast( addedObject );
}
}
// Next handle moves
foreach ( var move in patch.MovedObjects )
{
var movedObject = sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( move.Id );
if ( movedObject == null ) continue;
// If the parent is null, we can't move it
if ( movedObject.Parent == null ) continue;
var newParentObject = sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( move.NewParent );
if ( newParentObject != null )
{
// We can perform the move
movedObject.Parent.Children.Remove( movedObject );
movedObject.Parent = newParentObject;
movedObject.ContainerProperty = move.NewContainerProperty;
movedObject.IsContainedInArray = move.IsNewContainerArray;
movedObject.PreviousElement = move.NewPreviousElement.HasValue ? sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( move.NewPreviousElement.Value ) : null;
movedObject.Parent.Children.AddLast( movedObject );
}
else
{
// Target parent doesn't exist, remove the object entirely
movedObject.Parent.Children.Remove( movedObject );
sourceTrackedObjects.IdToTrackedObject.Remove( movedObject.Id );
}
}
// Try ordering obejcts that have been corerctly added (valid parent)
var objectsRequiringReordering = patch.AddedObjects
.Select( a => sourceTrackedObjects.IdToTrackedObject[a.Id] )
// It is possible that moved items dont exist at all
.Concat( patch.MovedObjects.Select( m => sourceTrackedObjects.IdToTrackedObject.GetValueOrDefault( m.Id ) ) )
.Where( o => o != null && o.Parent != null );
ReorderAddedObjects( objectsRequiringReordering, sourceTrackedObjects );
// Last apply property overrides
foreach ( var propertyOverride in patch.PropertyOverrides )
{
if ( sourceTrackedObjects.IdToTrackedObject.TryGetValue( propertyOverride.Target, out var trackedObj ) )
{
trackedObj.Data[propertyOverride.Property] = propertyOverride.Value?.DeepClone();
}
}
return sourceTrackedObjects.Root.ToJson().AsObject();
}
private static void ReorderAddedObjects( IEnumerable<TrackedObject> addedObjects, TrackedObjects sourceObjects )
{
// Smart people would probably do this in O(n log n)
// We just bruteforce it by adjusting the previous elemetns n times (leading to O(n^2)), so that order within addedObjects doesn't matter.
// Doing ti like this has the benefit of clear code, and performance imapct is negible anyway.
foreach ( var _ in addedObjects )
{
foreach ( var addedObj in addedObjects )
{
var parent = addedObj.Parent;
if ( parent == null ) continue;
if ( addedObj.PreviousElement != null )
{
var prevNode = parent.Children.Find( addedObj.PreviousElement );
if ( prevNode != null )
{
parent.Children.Remove( addedObj );
parent.Children.AddAfter( prevNode, addedObj );
}
}
else
{
parent.Children.Remove( addedObj );
// Best guess is to add it to the front
parent.Children.AddFirst( addedObj );
}
}
}
}
/// <summary>
/// Helper method to append a property name to a path string
/// </summary>
private static string AppendToPath( string path, string property )
{
if ( string.IsNullOrEmpty( path ) )
return property;
return string.Concat( path, ".", property );
}
/// <summary>
/// Helper method to append an array index to a path string
/// </summary>
private static string AppendToPath( string path, int index )
{
if ( string.IsNullOrEmpty( path ) )
return index.ToString();
return string.Concat( path, ".", index.ToString() );
}
}