Move DOM delta comparison to its own file, enhance inverting moves

Signed-off-by: Nicholas Van Sickle <nvsickle@amazon.com>
monroegm-disable-blank-issue-2
Nicholas Van Sickle 4 years ago
parent 5b8176e99b
commit d1bb5a0543

@ -0,0 +1,178 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/DOM/DomComparison.h>
#include <AzCore/std/containers/queue.h>
#include <AzCore/std/containers/unordered_set.h>
namespace AZ::Dom
{
PatchUndoRedoInfo GenerateHierarchicalDeltaPatch(
const Value& beforeState, const Value& afterState, const DeltaPatchGenerationParameters& params)
{
PatchUndoRedoInfo patches;
auto AddPatch = [&patches](PatchOperation op, PatchOperation inverse)
{
patches.m_forwardPatches.PushBack(AZStd::move(op));
patches.m_inversePatches.PushFront(AZStd::move(inverse));
};
AZStd::function<void(const Path&, const Value&, const Value&)> compareValues;
struct PendingComparison
{
Path m_path;
const Value& m_before;
const Value& m_after;
PendingComparison(Path path, const Value& before, const Value& after)
: m_path(AZStd::move(path))
, m_before(before)
, m_after(after)
{
}
};
AZStd::queue<PendingComparison> entriesToCompare;
AZStd::unordered_set<AZ::Name::Hash> desiredKeys;
auto compareObjects = [&](const Path& path, const Value& before, const Value& after)
{
desiredKeys.clear();
Path subPath = path;
for (auto it = after.MemberBegin(); it != after.MemberEnd(); ++it)
{
desiredKeys.insert(it->first.GetHash());
subPath.Push(it->first);
auto beforeIt = before.FindMember(it->first);
if (beforeIt == before.MemberEnd())
{
AddPatch(PatchOperation::AddOperation(subPath, it->second), PatchOperation::RemoveOperation(subPath));
}
else
{
entriesToCompare.emplace(subPath, beforeIt->second, it->second);
}
subPath.Pop();
}
for (auto it = before.MemberBegin(); it != before.MemberEnd(); ++it)
{
if (!desiredKeys.contains(it->first.GetHash()))
{
subPath.Push(it->first);
AddPatch(PatchOperation::RemoveOperation(subPath), PatchOperation::AddOperation(subPath, it->second));
subPath.Pop();
}
}
};
auto compareArrays = [&](const Path& path, const Value& before, const Value& after)
{
const size_t beforeSize = before.ArraySize();
const size_t afterSize = after.ArraySize();
// If more than replaceThreshold values differ, do a replace operation instead
if (params.m_replaceThreshold != DeltaPatchGenerationParameters::NoReplace)
{
size_t changedValueCount = 0;
const size_t entriesToEnumerate = AZStd::min(beforeSize, afterSize);
for (size_t i = 0; i < entriesToEnumerate; ++i)
{
if (before[i] != after[i])
{
++changedValueCount;
if (changedValueCount >= params.m_replaceThreshold)
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
return;
}
}
}
}
Path subPath = path;
for (size_t i = 0; i < afterSize; ++i)
{
if (i >= beforeSize)
{
subPath.Push(PathEntry(PathEntry::EndOfArrayIndex));
AddPatch(PatchOperation::AddOperation(subPath, after[i]), PatchOperation::RemoveOperation(subPath));
subPath.Pop();
}
else
{
subPath.Push(PathEntry(i));
entriesToCompare.emplace(subPath, before[i], after[i]);
subPath.Pop();
}
}
if (beforeSize > afterSize)
{
subPath.Push(PathEntry(PathEntry::EndOfArrayIndex));
for (size_t i = beforeSize; i > afterSize; --i)
{
AddPatch(PatchOperation::RemoveOperation(subPath), PatchOperation::AddOperation(subPath, before[i - 1]));
}
}
};
auto compareNodes = [&](const Path& path, const Value& before, const Value& after)
{
if (before.GetNodeName() != after.GetNodeName())
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
else
{
compareObjects(path, before, after);
compareArrays(path, before, after);
}
};
compareValues = [&](const Path& path, const Value& before, const Value& after)
{
if (before.GetType() != after.GetType())
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
else if (before == after)
{
// If a shallow comparison succeeds we're pointing to an identical value or container
// and don't need to drill down.
return;
}
else if (before.IsObject())
{
compareObjects(path, before, after);
}
else if (before.IsArray())
{
compareArrays(path, before, after);
}
else if (before.IsNode())
{
compareNodes(path, before, after);
}
else
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
};
entriesToCompare.emplace(Path(), beforeState, afterState);
while (!entriesToCompare.empty())
{
PendingComparison& comparison = entriesToCompare.front();
compareValues(comparison.m_path, comparison.m_before, comparison.m_after);
entriesToCompare.pop();
}
return patches;
}
}

@ -0,0 +1,37 @@
/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#pragma once
#include <AzCore/DOM/DomPatch.h>
namespace AZ::Dom
{
//! A set of patches for applying a change and doing the inverse operation.
struct PatchUndoRedoInfo
{
Patch m_forwardPatches;
Patch m_inversePatches;
};
//! Parameters for GenerateHierarchicalDeltaPatch.
struct DeltaPatchGenerationParameters
{
static constexpr size_t NoReplace = AZStd::numeric_limits<size_t>::max();
static constexpr size_t AlwaysFullReplace = 0;
//! The threshold of changed values in a node or array which, if exceeded, will cause the generation to create an
//! entire "replace" oepration instead. If set to NoReplace, no replacement will occur.
size_t m_replaceThreshold = 3;
};
//! Generates a set of patches such that m_forwardPatches.Apply(beforeState) shall produce a document equivalent to afterState, and
//! a subsequent m_inversePatches.Apply(beforeState) shall produce the original document. This patch generation strategy does a
//! hierarchical comparison and is not guaranteed to create the minimal set of patches required to transform between the two states.
PatchUndoRedoInfo GenerateHierarchicalDeltaPatch(const Value& beforeState, const Value& afterState, const DeltaPatchGenerationParameters& params = {});
} // namespace AZ::Dom

@ -8,8 +8,6 @@
#include <AzCore/DOM/DomPatch.h>
#include <AzCore/DOM/DomUtils.h>
#include <AzCore/std/containers/queue.h>
#include <AzCore/std/containers/unordered_set.h>
namespace AZ::Dom
{
@ -297,7 +295,7 @@ namespace AZ::Dom
}
}
AZ::Outcome<PatchOperation, AZStd::string> PatchOperation::GetInverse(Value stateBeforeApplication) const
AZ::Outcome<AZStd::fixed_vector<PatchOperation, 2>, AZStd::string> PatchOperation::GetInverse(Value stateBeforeApplication) const
{
switch (m_type)
{
@ -310,10 +308,10 @@ namespace AZ::Dom
const Value* existingValue = stateBeforeApplication.FindChild(m_domPath);
if (existingValue != nullptr)
{
return AZ::Success(PatchOperation::ReplaceOperation(m_domPath, *existingValue));
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
}
}
return AZ::Success(PatchOperation::RemoveOperation(m_domPath));
return AZ::Success<InversePatches>({PatchOperation::RemoveOperation(m_domPath)});
}
case Type::Remove:
{
@ -325,7 +323,7 @@ namespace AZ::Dom
m_domPath.AppendToString(errorMessage);
return AZ::Failure(AZStd::move(errorMessage));
}
return AZ::Success(PatchOperation::AddOperation(m_domPath, *existingValue));
return AZ::Success<InversePatches>({PatchOperation::AddOperation(m_domPath, *existingValue)});
}
case Type::Replace:
{
@ -337,7 +335,7 @@ namespace AZ::Dom
m_domPath.AppendToString(errorMessage);
return AZ::Failure(AZStd::move(errorMessage));
}
return AZ::Success(PatchOperation::ReplaceOperation(m_domPath, *existingValue));
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
}
case Type::Copy:
{
@ -349,40 +347,37 @@ namespace AZ::Dom
m_domPath.AppendToString(errorMessage);
return AZ::Failure(AZStd::move(errorMessage));
}
return AZ::Success(PatchOperation::ReplaceOperation(m_domPath, *existingValue));
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
}
case Type::Move:
{
// Move -> Replace, using the common ancestor of the two paths as the replacement
// This is not a minimal inverse, which would be two replace operations at each path
const Path& destPath = m_domPath;
const Path& sourcePath = GetSourcePath();
Path commonAncestor;
for (size_t i = 0; i < destPath.Size() && i < sourcePath.Size(); ++i)
const Value* sourceValue = stateBeforeApplication.FindChild(GetSourcePath());
if (sourceValue == nullptr)
{
if (destPath[i] != sourcePath[i])
{
break;
}
commonAncestor.Push(destPath[i]);
AZStd::string errorMessage = "Unable to invert DOM copy patch, source path not found: ";
m_domPath.AppendToString(errorMessage);
return AZ::Failure(AZStd::move(errorMessage));
}
const Value* existingValue = stateBeforeApplication.FindChild(commonAncestor);
if (existingValue == nullptr)
// If there was a value at the destination path, invert with an add / replace
const Value* destinationValue = stateBeforeApplication.FindChild(GetDestinationPath());
if (destinationValue != nullptr)
{
AZStd::string errorMessage = "Unable to invert DOM copy patch, common ancestor path not found: ";
commonAncestor.AppendToString(errorMessage);
return AZ::Failure(AZStd::move(errorMessage));
InversePatches result({PatchOperation::AddOperation(GetSourcePath(), *sourceValue)});
result.push_back(PatchOperation::ReplaceOperation(GetDestinationPath(), *destinationValue));
return AZ::Success<InversePatches>({
PatchOperation::AddOperation(GetSourcePath(), *sourceValue),
PatchOperation::ReplaceOperation(GetDestinationPath(), *destinationValue),
});
}
return AZ::Success(PatchOperation::ReplaceOperation(commonAncestor, *existingValue));
// Otherwise, just do a move
return AZ::Success<InversePatches>({PatchOperation::MoveOperation(GetDestinationPath(), GetSourcePath())});
}
case Type::Test:
{
// Test -> Test (no change)
// When inverting a sequence of patches, applying them in reverse order should allow the test to continue to succeed
return AZ::Success(*this);
return AZ::Success<InversePatches>({*this});
}
}
return AZ::Failure<AZStd::string>("Unable to invert DOM patch, unknown type specified");
@ -801,168 +796,4 @@ namespace AZ::Dom
{
return PatchOperation(AZStd::move(testPath), PatchOperation::Type::Test, AZStd::move(value));
}
PatchInfo GenerateHierarchicalDeltaPatch(
const Value& beforeState, const Value& afterState, const DeltaPatchGenerationParameters& params)
{
PatchInfo patches;
auto AddPatch = [&patches](PatchOperation op, PatchOperation inverse)
{
patches.m_forwardPatches.PushBack(AZStd::move(op));
patches.m_inversePatches.PushFront(AZStd::move(inverse));
};
AZStd::function<void(const Path&, const Value&, const Value&)> compareValues;
struct PendingComparison
{
Path m_path;
const Value& m_before;
const Value& m_after;
PendingComparison(Path path, const Value& before, const Value& after)
: m_path(AZStd::move(path))
, m_before(before)
, m_after(after)
{
}
};
AZStd::queue<PendingComparison> entriesToCompare;
AZStd::unordered_set<AZ::Name::Hash> desiredKeys;
auto compareObjects = [&](const Path& path, const Value& before, const Value& after)
{
desiredKeys.clear();
Path subPath = path;
for (auto it = after.MemberBegin(); it != after.MemberEnd(); ++it)
{
desiredKeys.insert(it->first.GetHash());
subPath.Push(it->first);
auto beforeIt = before.FindMember(it->first);
if (beforeIt == before.MemberEnd())
{
AddPatch(PatchOperation::AddOperation(subPath, it->second), PatchOperation::RemoveOperation(subPath));
}
else
{
entriesToCompare.emplace(subPath, beforeIt->second, it->second);
}
subPath.Pop();
}
for (auto it = before.MemberBegin(); it != before.MemberEnd(); ++it)
{
if (!desiredKeys.contains(it->first.GetHash()))
{
subPath.Push(it->first);
AddPatch(PatchOperation::RemoveOperation(subPath), PatchOperation::AddOperation(subPath, it->second));
subPath.Pop();
}
}
};
auto compareArrays = [&](const Path& path, const Value& before, const Value& after)
{
const size_t beforeSize = before.ArraySize();
const size_t afterSize = after.ArraySize();
// If more than replaceThreshold values differ, do a replace operation instead
if (params.m_replaceThreshold != DeltaPatchGenerationParameters::NoReplace)
{
size_t changedValueCount = 0;
const size_t entriesToEnumerate = AZStd::min(beforeSize, afterSize);
for (size_t i = 0; i < entriesToEnumerate; ++i)
{
if (before[i] != after[i])
{
++changedValueCount;
if (changedValueCount >= params.m_replaceThreshold)
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
return;
}
}
}
}
Path subPath = path;
for (size_t i = 0; i < afterSize; ++i)
{
if (i >= beforeSize)
{
subPath.Push(PathEntry(PathEntry::EndOfArrayIndex));
AddPatch(PatchOperation::AddOperation(subPath, after[i]), PatchOperation::RemoveOperation(subPath));
subPath.Pop();
}
else
{
subPath.Push(PathEntry(i));
entriesToCompare.emplace(subPath, before[i], after[i]);
subPath.Pop();
}
}
if (beforeSize > afterSize)
{
subPath.Push(PathEntry(PathEntry::EndOfArrayIndex));
for (size_t i = beforeSize; i > afterSize; --i)
{
AddPatch(PatchOperation::RemoveOperation(subPath), PatchOperation::AddOperation(subPath, before[i - 1]));
}
}
};
auto compareNodes = [&](const Path& path, const Value& before, const Value& after)
{
if (before.GetNodeName() != after.GetNodeName())
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
else
{
compareObjects(path, before, after);
compareArrays(path, before, after);
}
};
compareValues = [&](const Path& path, const Value& before, const Value& after)
{
if (before.GetType() != after.GetType())
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
else if (before == after)
{
// If a shallow comparison succeeds we're pointing to an identical value or container
// and don't need to drill down.
return;
}
else if (before.IsObject())
{
compareObjects(path, before, after);
}
else if (before.IsArray())
{
compareArrays(path, before, after);
}
else if (before.IsNode())
{
compareNodes(path, before, after);
}
else
{
AddPatch(PatchOperation::ReplaceOperation(path, after), PatchOperation::ReplaceOperation(path, before));
}
};
entriesToCompare.emplace(Path(), beforeState, afterState);
while (!entriesToCompare.empty())
{
PendingComparison& comparison = entriesToCompare.front();
compareValues(comparison.m_path, comparison.m_before, comparison.m_after);
entriesToCompare.pop();
}
return patches;
}
} // namespace AZ::Dom

@ -71,7 +71,8 @@ namespace AZ::Dom
Value GetDomRepresentation() const;
static AZ::Outcome<PatchOperation, AZStd::string> CreateFromDomRepresentation(Value domValue);
AZ::Outcome<PatchOperation, AZStd::string> GetInverse(Value stateBeforeApplication) const;
using InversePatches = AZStd::fixed_vector<PatchOperation, 2>;
AZ::Outcome<AZStd::fixed_vector<PatchOperation, 2>, AZStd::string> GetInverse(Value stateBeforeApplication) const;
enum class ExistenceCheckFlags : AZ::u8
{
@ -182,27 +183,4 @@ namespace AZ::Dom
private:
OperationsContainer m_operations;
};
//! A set of patches for applying a change and doing the inverse operation (i.e. undoing it).
struct PatchInfo
{
Patch m_forwardPatches;
Patch m_inversePatches;
};
//! Parameters for GenerateHierarchicalDeltaPatch.
struct DeltaPatchGenerationParameters
{
static constexpr size_t NoReplace = AZStd::numeric_limits<size_t>::max();
static constexpr size_t AlwaysFullReplace = 0;
//! The threshold of changed values in a node or array which, if exceeded, will cause the generation to create an
//! entire "replace" oepration instead. If set to NoReplace, no replacement will occur.
size_t m_replaceThreshold = 3;
};
//! Generates a set of patches such that m_forwardPatches.Apply(beforeState) shall produce a document equivalent to afterState, and
//! a subsequent m_inversePatches.Apply(beforeState) shall produce the original document. This patch generation strategy does a
//! hierarchical comparison and is not guaranteed to create the minimal set of patches required to transform between the two states.
PatchInfo GenerateHierarchicalDeltaPatch(const Value& beforeState, const Value& afterState, const DeltaPatchGenerationParameters& params = {});
} // namespace AZ::Dom

@ -128,6 +128,8 @@ set(FILES
DOM/DomValueWriter.h
DOM/DomVisitor.cpp
DOM/DomVisitor.h
DOM/DomComparison.cpp
DOM/DomComparison.h
DOM/Backends/JSON/JsonBackend.h
DOM/Backends/JSON/JsonSerializationUtils.cpp
DOM/Backends/JSON/JsonSerializationUtils.h

@ -7,6 +7,7 @@
*/
#include <AzCore/DOM/DomPatch.h>
#include <AzCore/DOM/DomComparison.h>
#include <Tests/DOM/DomFixtures.h>
namespace AZ::Dom::Tests
@ -45,9 +46,9 @@ namespace AZ::Dom::Tests
DomTestFixture::TearDown();
}
PatchInfo GenerateAndVerifyDelta()
PatchUndoRedoInfo GenerateAndVerifyDelta()
{
PatchInfo info = GenerateHierarchicalDeltaPatch(m_dataset, m_deltaDataset);
PatchUndoRedoInfo info = GenerateHierarchicalDeltaPatch(m_dataset, m_deltaDataset);
auto result = info.m_forwardPatches.Apply(m_dataset);
EXPECT_TRUE(result.IsSuccess());

Loading…
Cancel
Save