diff --git a/Code/Framework/AzCore/AzCore/DOM/DomComparison.cpp b/Code/Framework/AzCore/AzCore/DOM/DomComparison.cpp new file mode 100644 index 0000000000..dc21761670 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomComparison.cpp @@ -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 +#include +#include + +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 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 entriesToCompare; + + AZStd::unordered_set 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; + } +} diff --git a/Code/Framework/AzCore/AzCore/DOM/DomComparison.h b/Code/Framework/AzCore/AzCore/DOM/DomComparison.h new file mode 100644 index 0000000000..f882887cba --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomComparison.h @@ -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 + +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::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 diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPatch.cpp b/Code/Framework/AzCore/AzCore/DOM/DomPatch.cpp index f89cb313e7..dc0bff4e7c 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomPatch.cpp +++ b/Code/Framework/AzCore/AzCore/DOM/DomPatch.cpp @@ -8,8 +8,6 @@ #include #include -#include -#include namespace AZ::Dom { @@ -297,7 +295,7 @@ namespace AZ::Dom } } - AZ::Outcome PatchOperation::GetInverse(Value stateBeforeApplication) const + AZ::Outcome, 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({PatchOperation::ReplaceOperation(m_domPath, *existingValue)}); } } - return AZ::Success(PatchOperation::RemoveOperation(m_domPath)); + return AZ::Success({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({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({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({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({ + PatchOperation::AddOperation(GetSourcePath(), *sourceValue), + PatchOperation::ReplaceOperation(GetDestinationPath(), *destinationValue), + }); } - return AZ::Success(PatchOperation::ReplaceOperation(commonAncestor, *existingValue)); + // Otherwise, just do a move + return AZ::Success({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({*this}); } } return AZ::Failure("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 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 entriesToCompare; - - AZStd::unordered_set 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 diff --git a/Code/Framework/AzCore/AzCore/DOM/DomPatch.h b/Code/Framework/AzCore/AzCore/DOM/DomPatch.h index 40ef725b57..2d591a8079 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomPatch.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomPatch.h @@ -71,7 +71,8 @@ namespace AZ::Dom Value GetDomRepresentation() const; static AZ::Outcome CreateFromDomRepresentation(Value domValue); - AZ::Outcome GetInverse(Value stateBeforeApplication) const; + using InversePatches = AZStd::fixed_vector; + AZ::Outcome, 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::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 diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index ad7d7cbdef..b7821f1d65 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -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 diff --git a/Code/Framework/AzCore/Tests/DOM/DomPatchTests.cpp b/Code/Framework/AzCore/Tests/DOM/DomPatchTests.cpp index a0e4f349a5..b60c09330f 100644 --- a/Code/Framework/AzCore/Tests/DOM/DomPatchTests.cpp +++ b/Code/Framework/AzCore/Tests/DOM/DomPatchTests.cpp @@ -7,6 +7,7 @@ */ #include +#include #include 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());