Merge pull request #7093 from aws-lumberyard-dev/nvsickle/DomPatch
Add AZ::Dom::Patch, a Generic DOM analog to JSON patchmonroegm-disable-blank-issue-2
commit
bc3f957270
@ -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
|
||||||
@ -0,0 +1,799 @@
|
|||||||
|
/*
|
||||||
|
* 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/DomPatch.h>
|
||||||
|
#include <AzCore/DOM/DomUtils.h>
|
||||||
|
|
||||||
|
namespace AZ::Dom
|
||||||
|
{
|
||||||
|
PatchOperation::PatchOperation(Path destinationPath, Type type, Value value)
|
||||||
|
: m_domPath(AZStd::move(destinationPath))
|
||||||
|
, m_type(type)
|
||||||
|
, m_value(AZStd::move(value))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOperation(Path destinationPath, Type type, Path sourcePath)
|
||||||
|
: m_domPath(AZStd::move(destinationPath))
|
||||||
|
, m_type(type)
|
||||||
|
, m_value(AZStd::move(sourcePath))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOperation(Path destinationPath, Type type)
|
||||||
|
: m_domPath(AZStd::move(destinationPath))
|
||||||
|
, m_type(type)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchOperation::operator==(const PatchOperation& rhs) const
|
||||||
|
{
|
||||||
|
if (m_type != rhs.m_type)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (m_type)
|
||||||
|
{
|
||||||
|
case Type::Add:
|
||||||
|
return m_domPath == rhs.m_domPath && Utils::DeepCompareIsEqual(GetValue(), rhs.GetValue());
|
||||||
|
case Type::Remove:
|
||||||
|
return m_domPath == rhs.m_domPath;
|
||||||
|
case Type::Replace:
|
||||||
|
return m_domPath == rhs.m_domPath && Utils::DeepCompareIsEqual(GetValue(), rhs.GetValue());
|
||||||
|
case Type::Copy:
|
||||||
|
return m_domPath == rhs.m_domPath && GetSourcePath() == rhs.GetSourcePath();
|
||||||
|
case Type::Move:
|
||||||
|
return m_domPath == rhs.m_domPath && GetSourcePath() == rhs.GetSourcePath();
|
||||||
|
case Type::Test:
|
||||||
|
return m_domPath == rhs.m_domPath && Utils::DeepCompareIsEqual(GetValue(), rhs.GetValue());
|
||||||
|
default:
|
||||||
|
AZ_Assert(false, "PatchOperation::GetDomRepresentation: invalid patch type specified");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PatchOperation::operator!=(const PatchOperation& rhs) const
|
||||||
|
{
|
||||||
|
return !operator==(rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::Type PatchOperation::GetType() const
|
||||||
|
{
|
||||||
|
return m_type;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchOperation::SetType(Type type)
|
||||||
|
{
|
||||||
|
m_type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Path& PatchOperation::GetDestinationPath() const
|
||||||
|
{
|
||||||
|
return m_domPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchOperation::SetDestinationPath(Path path)
|
||||||
|
{
|
||||||
|
m_domPath = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Value& PatchOperation::GetValue() const
|
||||||
|
{
|
||||||
|
return AZStd::get<Value>(m_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchOperation::SetValue(Value value)
|
||||||
|
{
|
||||||
|
m_value = AZStd::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Path& PatchOperation::GetSourcePath() const
|
||||||
|
{
|
||||||
|
return AZStd::get<Path>(m_value);
|
||||||
|
}
|
||||||
|
|
||||||
|
void PatchOperation::SetSourcePath(Path path)
|
||||||
|
{
|
||||||
|
m_value = AZStd::move(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<Value, AZStd::string> PatchOperation::Apply(Value rootElement) const
|
||||||
|
{
|
||||||
|
PatchOutcome outcome = ApplyInPlace(rootElement);
|
||||||
|
if (!outcome.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(outcome.TakeError());
|
||||||
|
}
|
||||||
|
return AZ::Success(AZStd::move(rootElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyInPlace(Value& rootElement) const
|
||||||
|
{
|
||||||
|
switch (m_type)
|
||||||
|
{
|
||||||
|
case Type::Add:
|
||||||
|
return ApplyAdd(rootElement);
|
||||||
|
case Type::Remove:
|
||||||
|
return ApplyRemove(rootElement);
|
||||||
|
case Type::Replace:
|
||||||
|
return ApplyReplace(rootElement);
|
||||||
|
case Type::Copy:
|
||||||
|
return ApplyCopy(rootElement);
|
||||||
|
case Type::Move:
|
||||||
|
return ApplyMove(rootElement);
|
||||||
|
case Type::Test:
|
||||||
|
return ApplyTest(rootElement);
|
||||||
|
}
|
||||||
|
return AZ::Failure<AZStd::string>("Unsupported DOM patch operation specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
Value PatchOperation::GetDomRepresentation() const
|
||||||
|
{
|
||||||
|
Value serializedPatch(Dom::Type::Object);
|
||||||
|
switch (m_type)
|
||||||
|
{
|
||||||
|
case Type::Add:
|
||||||
|
serializedPatch["op"].SetString("add");
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
serializedPatch["value"] = GetValue();
|
||||||
|
break;
|
||||||
|
case Type::Remove:
|
||||||
|
serializedPatch["op"].SetString("remove");
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
break;
|
||||||
|
case Type::Replace:
|
||||||
|
serializedPatch["op"].SetString("replace");
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
serializedPatch["value"] = GetValue();
|
||||||
|
break;
|
||||||
|
case Type::Copy:
|
||||||
|
serializedPatch["op"].SetString("copy");
|
||||||
|
serializedPatch["from"].CopyFromString(GetSourcePath().ToString());
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
break;
|
||||||
|
case Type::Move:
|
||||||
|
serializedPatch["op"].SetString("move");
|
||||||
|
serializedPatch["from"].CopyFromString(GetSourcePath().ToString());
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
break;
|
||||||
|
case Type::Test:
|
||||||
|
serializedPatch["op"].SetString("test");
|
||||||
|
serializedPatch["path"].CopyFromString(GetDestinationPath().ToString());
|
||||||
|
serializedPatch["value"] = GetValue();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
AZ_Assert(false, "PatchOperation::GetDomRepresentation: invalid patch type specified");
|
||||||
|
}
|
||||||
|
return serializedPatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<PatchOperation, AZStd::string> PatchOperation::CreateFromDomRepresentation(Value domValue)
|
||||||
|
{
|
||||||
|
if (!domValue.IsObject())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("PatchOperation failed to load: PatchOperation must be specified as an Object");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto loadField = [&](const char* field, AZStd::optional<Dom::Type> type = {}) -> AZ::Outcome<Value, AZStd::string>
|
||||||
|
{
|
||||||
|
auto it = domValue.FindMember(field);
|
||||||
|
if (it == domValue.MemberEnd())
|
||||||
|
{
|
||||||
|
return AZ::Failure(AZStd::string::format("PatchOperation failed to load: no \"%s\" specified", field));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.has_value() && it->second.GetType() != type)
|
||||||
|
{
|
||||||
|
return AZ::Failure(AZStd::string::format("PatchOperation failed to load: \"%s\" is invalid", field));
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(it->second);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto opLoad = loadField("op", Dom::Type::String);
|
||||||
|
if (!opLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(opLoad.TakeError());
|
||||||
|
}
|
||||||
|
AZStd::string_view op = opLoad.GetValue().GetString();
|
||||||
|
if (op == "add")
|
||||||
|
{
|
||||||
|
auto pathLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!pathLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLoad.TakeError());
|
||||||
|
}
|
||||||
|
auto valueLoad = loadField("value");
|
||||||
|
if (!valueLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(valueLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(PatchOperation::AddOperation(Path(pathLoad.GetValue().GetString()), valueLoad.TakeValue()));
|
||||||
|
}
|
||||||
|
else if (op == "remove")
|
||||||
|
{
|
||||||
|
auto pathLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!pathLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(PatchOperation::RemoveOperation(Path(pathLoad.GetValue().GetString())));
|
||||||
|
}
|
||||||
|
else if (op == "replace")
|
||||||
|
{
|
||||||
|
auto pathLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!pathLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLoad.TakeError());
|
||||||
|
}
|
||||||
|
auto valueLoad = loadField("value");
|
||||||
|
if (!valueLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(valueLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(PatchOperation::ReplaceOperation(Path(pathLoad.GetValue().GetString()), valueLoad.TakeValue()));
|
||||||
|
}
|
||||||
|
else if (op == "copy")
|
||||||
|
{
|
||||||
|
auto destLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!destLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(destLoad.TakeError());
|
||||||
|
}
|
||||||
|
auto sourceLoad = loadField("from", Dom::Type::String);
|
||||||
|
if (!sourceLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(sourceLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(
|
||||||
|
PatchOperation::CopyOperation(Path(destLoad.GetValue().GetString()), Path(sourceLoad.GetValue().GetString())));
|
||||||
|
}
|
||||||
|
else if (op == "move")
|
||||||
|
{
|
||||||
|
auto destLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!destLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(destLoad.TakeError());
|
||||||
|
}
|
||||||
|
auto sourceLoad = loadField("from", Dom::Type::String);
|
||||||
|
if (!sourceLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(sourceLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(
|
||||||
|
PatchOperation::MoveOperation(Path(destLoad.GetValue().GetString()), Path(sourceLoad.GetValue().GetString())));
|
||||||
|
}
|
||||||
|
else if (op == "test")
|
||||||
|
{
|
||||||
|
auto pathLoad = loadField("path", Dom::Type::String);
|
||||||
|
if (!pathLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLoad.TakeError());
|
||||||
|
}
|
||||||
|
auto valueLoad = loadField("value");
|
||||||
|
if (!valueLoad.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(valueLoad.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success(PatchOperation::TestOperation(Path(pathLoad.GetValue().GetString()), valueLoad.TakeValue()));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("PatchOperation failed to create DOM representation: invalid \"op\" specified");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<AZStd::fixed_vector<PatchOperation, 2>, AZStd::string> PatchOperation::GetInverse(Value stateBeforeApplication) const
|
||||||
|
{
|
||||||
|
switch (m_type)
|
||||||
|
{
|
||||||
|
case Type::Add:
|
||||||
|
{
|
||||||
|
// Add -> Replace (if value already existed in an object) otherwise
|
||||||
|
// Add -> Remove
|
||||||
|
if (m_domPath.Size() > 0 && m_domPath[m_domPath.Size() - 1].IsKey())
|
||||||
|
{
|
||||||
|
const Value* existingValue = stateBeforeApplication.FindChild(m_domPath);
|
||||||
|
if (existingValue != nullptr)
|
||||||
|
{
|
||||||
|
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AZ::Success<InversePatches>({PatchOperation::RemoveOperation(m_domPath)});
|
||||||
|
}
|
||||||
|
case Type::Remove:
|
||||||
|
{
|
||||||
|
// Remove -> Add
|
||||||
|
const Value* existingValue = stateBeforeApplication.FindChild(m_domPath);
|
||||||
|
if (existingValue == nullptr)
|
||||||
|
{
|
||||||
|
AZStd::string errorMessage = "Unable to invert DOM remove patch, source path not found: ";
|
||||||
|
m_domPath.AppendToString(errorMessage);
|
||||||
|
return AZ::Failure(AZStd::move(errorMessage));
|
||||||
|
}
|
||||||
|
return AZ::Success<InversePatches>({PatchOperation::AddOperation(m_domPath, *existingValue)});
|
||||||
|
}
|
||||||
|
case Type::Replace:
|
||||||
|
{
|
||||||
|
// Replace -> Replace (with old value)
|
||||||
|
const Value* existingValue = stateBeforeApplication.FindChild(m_domPath);
|
||||||
|
if (existingValue == nullptr)
|
||||||
|
{
|
||||||
|
AZStd::string errorMessage = "Unable to invert DOM replace patch, source path not found: ";
|
||||||
|
m_domPath.AppendToString(errorMessage);
|
||||||
|
return AZ::Failure(AZStd::move(errorMessage));
|
||||||
|
}
|
||||||
|
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
|
||||||
|
}
|
||||||
|
case Type::Copy:
|
||||||
|
{
|
||||||
|
// Copy -> Replace (with old value)
|
||||||
|
const Value* existingValue = stateBeforeApplication.FindChild(m_domPath);
|
||||||
|
if (existingValue == nullptr)
|
||||||
|
{
|
||||||
|
AZStd::string errorMessage = "Unable to invert DOM copy patch, source path not found: ";
|
||||||
|
m_domPath.AppendToString(errorMessage);
|
||||||
|
return AZ::Failure(AZStd::move(errorMessage));
|
||||||
|
}
|
||||||
|
return AZ::Success<InversePatches>({PatchOperation::ReplaceOperation(m_domPath, *existingValue)});
|
||||||
|
}
|
||||||
|
case Type::Move:
|
||||||
|
{
|
||||||
|
const Value* sourceValue = stateBeforeApplication.FindChild(GetSourcePath());
|
||||||
|
if (sourceValue == nullptr)
|
||||||
|
{
|
||||||
|
AZStd::string errorMessage = "Unable to invert DOM copy patch, source path not found: ";
|
||||||
|
m_domPath.AppendToString(errorMessage);
|
||||||
|
return AZ::Failure(AZStd::move(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there was a value at the destination path, invert with an add / replace
|
||||||
|
const Value* destinationValue = stateBeforeApplication.FindChild(GetDestinationPath());
|
||||||
|
if (destinationValue != nullptr)
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 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<InversePatches>({*this});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return AZ::Failure<AZStd::string>("Unable to invert DOM patch, unknown type specified");
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<PatchOperation::PathContext, AZStd::string> PatchOperation::LookupPath(
|
||||||
|
Value& rootElement, const Path& path, ExistenceCheckFlags flags)
|
||||||
|
{
|
||||||
|
const bool verifyFullPath = (flags & ExistenceCheckFlags::VerifyFullPath) != ExistenceCheckFlags::DefaultExistenceCheck;
|
||||||
|
const bool allowEndOfArray = (flags & ExistenceCheckFlags::AllowEndOfArray) != ExistenceCheckFlags::DefaultExistenceCheck;
|
||||||
|
|
||||||
|
Path target = path;
|
||||||
|
if (target.IsEmpty())
|
||||||
|
{
|
||||||
|
Value wrapper(Dom::Type::Array);
|
||||||
|
wrapper.ArrayPushBack(rootElement);
|
||||||
|
return AZ::Success<PathContext>({ wrapper, PathEntry(0) });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifyFullPath || !allowEndOfArray)
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < path.Size(); ++i)
|
||||||
|
{
|
||||||
|
const PathEntry& entry = path[i];
|
||||||
|
if (entry.IsEndOfArray() && (!allowEndOfArray || i != path.Size() - 1))
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Append to array index (\"-\") specified for path that must already exist");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PathEntry destinationIndex = target[target.Size() - 1];
|
||||||
|
target.Pop();
|
||||||
|
|
||||||
|
Value* targetValue = rootElement.FindMutableChild(target);
|
||||||
|
if (targetValue == nullptr)
|
||||||
|
{
|
||||||
|
AZStd::string errorMessage = "Path not found: ";
|
||||||
|
target.AppendToString(errorMessage);
|
||||||
|
return AZ::Failure(AZStd::move(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex.IsIndex() || destinationIndex.IsEndOfArray())
|
||||||
|
{
|
||||||
|
if (!targetValue->IsArray() && !targetValue->IsNode())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Array index specified for a value that is not an array or node");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destinationIndex.IsIndex() && destinationIndex.GetIndex() >= targetValue->ArraySize())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Array index out bounds");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!targetValue->IsObject() && !targetValue->IsNode())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Key specified for a value that is not an object or node");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verifyFullPath)
|
||||||
|
{
|
||||||
|
if (auto it = targetValue->FindMember(destinationIndex.GetKey()); it == targetValue->MemberEnd())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Key not found in container");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success<PathContext>({ *targetValue, AZStd::move(destinationIndex) });
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyAdd(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto pathLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::AllowEndOfArray);
|
||||||
|
if (!pathLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLookup.TakeError());
|
||||||
|
}
|
||||||
|
const PathContext& context = pathLookup.GetValue();
|
||||||
|
const PathEntry& destinationIndex = context.m_key;
|
||||||
|
Value& targetValue = context.m_value;
|
||||||
|
|
||||||
|
if (destinationIndex.IsIndex() || destinationIndex.IsEndOfArray())
|
||||||
|
{
|
||||||
|
if (destinationIndex.IsEndOfArray())
|
||||||
|
{
|
||||||
|
targetValue.ArrayPushBack(GetValue());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const size_t index = destinationIndex.GetIndex();
|
||||||
|
auto& arrayToChange = targetValue.GetMutableArray();
|
||||||
|
arrayToChange.insert(arrayToChange.begin() + index, GetValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
targetValue[destinationIndex] = GetValue();
|
||||||
|
}
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyRemove(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto pathLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::VerifyFullPath | ExistenceCheckFlags::AllowEndOfArray);
|
||||||
|
if (!pathLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLookup.TakeError());
|
||||||
|
}
|
||||||
|
const PathContext& context = pathLookup.GetValue();
|
||||||
|
const PathEntry& destinationIndex = context.m_key;
|
||||||
|
Value& targetValue = context.m_value;
|
||||||
|
|
||||||
|
if (destinationIndex.IsIndex() || destinationIndex.IsEndOfArray())
|
||||||
|
{
|
||||||
|
size_t index = destinationIndex.IsEndOfArray() ? targetValue.ArraySize() - 1 : destinationIndex.GetIndex();
|
||||||
|
targetValue.ArrayErase(targetValue.MutableArrayBegin() + index);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
auto it = targetValue.FindMutableMember(destinationIndex.GetKey());
|
||||||
|
targetValue.EraseMember(it);
|
||||||
|
}
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyReplace(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto pathLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::VerifyFullPath);
|
||||||
|
if (!pathLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
rootElement[m_domPath] = GetValue();
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyCopy(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto sourceLookup = LookupPath(rootElement, GetSourcePath(), ExistenceCheckFlags::VerifyFullPath);
|
||||||
|
if (!sourceLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(sourceLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto destLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::AllowEndOfArray);
|
||||||
|
if (!destLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(destLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
rootElement[m_domPath] = rootElement[GetSourcePath()];
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyMove(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto sourceLookup = LookupPath(rootElement, GetSourcePath(), ExistenceCheckFlags::VerifyFullPath);
|
||||||
|
if (!sourceLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(sourceLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
auto destLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::AllowEndOfArray);
|
||||||
|
if (!destLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(destLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value valueToMove = rootElement[GetSourcePath()];
|
||||||
|
const PathContext& sourceContext = sourceLookup.GetValue();
|
||||||
|
if (sourceContext.m_key.IsEndOfArray())
|
||||||
|
{
|
||||||
|
sourceContext.m_value.ArrayPopBack();
|
||||||
|
}
|
||||||
|
else if (sourceContext.m_key.IsIndex())
|
||||||
|
{
|
||||||
|
sourceContext.m_value.ArrayErase(sourceContext.m_value.MutableArrayBegin() + sourceContext.m_key.GetIndex());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sourceContext.m_value.EraseMember(sourceContext.m_key.GetKey());
|
||||||
|
}
|
||||||
|
|
||||||
|
rootElement[m_domPath] = AZStd::move(valueToMove);
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation::PatchOutcome PatchOperation::ApplyTest(Value& rootElement) const
|
||||||
|
{
|
||||||
|
auto pathLookup = LookupPath(rootElement, m_domPath, ExistenceCheckFlags::VerifyFullPath);
|
||||||
|
if (!pathLookup.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(pathLookup.TakeError());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Utils::DeepCompareIsEqual(rootElement[m_domPath], GetValue()))
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Test failed, values don't match");
|
||||||
|
}
|
||||||
|
|
||||||
|
return AZ::Success();
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace PatchApplicationStrategy
|
||||||
|
{
|
||||||
|
void HaltOnFailure(PatchApplicationState& state)
|
||||||
|
{
|
||||||
|
if (!state.m_outcome.IsSuccess())
|
||||||
|
{
|
||||||
|
state.m_shouldContinue = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void IgnoreFailureAndContinue([[maybe_unused]] PatchApplicationState& state)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
} // namespace PatchApplicationStrategy
|
||||||
|
|
||||||
|
Patch::Patch(AZStd::initializer_list<PatchOperation> init)
|
||||||
|
: m_operations(init)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Patch::operator==(const Patch& rhs) const
|
||||||
|
{
|
||||||
|
if (m_operations.size() != rhs.m_operations.size())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < m_operations.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_operations[i] != rhs.m_operations[i])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Patch::operator!=(const Patch& rhs) const
|
||||||
|
{
|
||||||
|
return !operator==(rhs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Patch::OperationsContainer& Patch::GetOperations() const
|
||||||
|
{
|
||||||
|
return m_operations;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patch::PushBack(PatchOperation op)
|
||||||
|
{
|
||||||
|
m_operations.push_back(AZStd::move(op));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patch::PushFront(PatchOperation op)
|
||||||
|
{
|
||||||
|
m_operations.insert(m_operations.begin(), AZStd::move(op));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patch::Pop()
|
||||||
|
{
|
||||||
|
m_operations.pop_back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void Patch::Clear()
|
||||||
|
{
|
||||||
|
m_operations.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
const PatchOperation& Patch::At(size_t index) const
|
||||||
|
{
|
||||||
|
return m_operations[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Patch::Size() const
|
||||||
|
{
|
||||||
|
return m_operations.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation& Patch::operator[](size_t index)
|
||||||
|
{
|
||||||
|
return m_operations[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
const PatchOperation& Patch::operator[](size_t index) const
|
||||||
|
{
|
||||||
|
return m_operations[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::begin() -> OperationsContainer::iterator
|
||||||
|
{
|
||||||
|
return m_operations.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::end() -> OperationsContainer::iterator
|
||||||
|
{
|
||||||
|
return m_operations.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::begin() const -> OperationsContainer::const_iterator
|
||||||
|
{
|
||||||
|
return m_operations.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::end() const -> OperationsContainer::const_iterator
|
||||||
|
{
|
||||||
|
return m_operations.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::cbegin() const -> OperationsContainer::const_iterator
|
||||||
|
{
|
||||||
|
return m_operations.begin();
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Patch::cend() const -> OperationsContainer::const_iterator
|
||||||
|
{
|
||||||
|
return m_operations.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t Patch::size() const
|
||||||
|
{
|
||||||
|
return m_operations.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<Value, AZStd::string> Patch::Apply(Value rootElement, StrategyFunctor strategy) const
|
||||||
|
{
|
||||||
|
auto result = ApplyInPlace(rootElement, strategy);
|
||||||
|
if (!result.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(result.TakeError());
|
||||||
|
}
|
||||||
|
return AZ::Success(AZStd::move(rootElement));
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<void, AZStd::string> Patch::ApplyInPlace(Value& rootElement, StrategyFunctor strategy) const
|
||||||
|
{
|
||||||
|
PatchApplicationState state;
|
||||||
|
state.m_currentState = &rootElement;
|
||||||
|
state.m_patch = this;
|
||||||
|
|
||||||
|
for (const PatchOperation& operation : m_operations)
|
||||||
|
{
|
||||||
|
state.m_lastOperation = &operation;
|
||||||
|
state.m_outcome = operation.ApplyInPlace(rootElement);
|
||||||
|
strategy(state);
|
||||||
|
if (!state.m_shouldContinue)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return state.m_outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value Patch::GetDomRepresentation() const
|
||||||
|
{
|
||||||
|
Value domValue(Dom::Type::Array);
|
||||||
|
for (const PatchOperation& operation : m_operations)
|
||||||
|
{
|
||||||
|
domValue.ArrayPushBack(operation.GetDomRepresentation());
|
||||||
|
}
|
||||||
|
return domValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AZ::Outcome<Patch, AZStd::string> Patch::CreateFromDomRepresentation(Value domValue)
|
||||||
|
{
|
||||||
|
if (!domValue.IsArray())
|
||||||
|
{
|
||||||
|
return AZ::Failure<AZStd::string>("Patch must be an array");
|
||||||
|
}
|
||||||
|
|
||||||
|
Patch patch;
|
||||||
|
for (auto it = domValue.ArrayBegin(); it != domValue.ArrayEnd(); ++it)
|
||||||
|
{
|
||||||
|
auto operationLoadResult = PatchOperation::CreateFromDomRepresentation(*it);
|
||||||
|
if (!operationLoadResult.IsSuccess())
|
||||||
|
{
|
||||||
|
return AZ::Failure(operationLoadResult.TakeError());
|
||||||
|
}
|
||||||
|
patch.PushBack(operationLoadResult.TakeValue());
|
||||||
|
}
|
||||||
|
return AZ::Success(AZStd::move(patch));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::AddOperation(Path destinationPath, Value value)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(destinationPath), PatchOperation::Type::Add, AZStd::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::RemoveOperation(Path pathToRemove)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(pathToRemove), PatchOperation::Type::Remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::ReplaceOperation(Path destinationPath, Value value)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(destinationPath), PatchOperation::Type::Replace, AZStd::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::CopyOperation(Path destinationPath, Path sourcePath)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(destinationPath), PatchOperation::Type::Copy, AZStd::move(sourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::MoveOperation(Path destinationPath, Path sourcePath)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(destinationPath), PatchOperation::Type::Move, AZStd::move(sourcePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchOperation PatchOperation::TestOperation(Path testPath, Value value)
|
||||||
|
{
|
||||||
|
return PatchOperation(AZStd::move(testPath), PatchOperation::Type::Test, AZStd::move(value));
|
||||||
|
}
|
||||||
|
} // namespace AZ::Dom
|
||||||
@ -0,0 +1,186 @@
|
|||||||
|
/*
|
||||||
|
* 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/DomPath.h>
|
||||||
|
#include <AzCore/DOM/DomValue.h>
|
||||||
|
#include <AzCore/std/containers/deque.h>
|
||||||
|
|
||||||
|
namespace AZ::Dom
|
||||||
|
{
|
||||||
|
//! A patch operation that represents an atomic operation for mutating or validating a Value.
|
||||||
|
//! PatchOperations can be created with helper methods in Patch. /see Patch
|
||||||
|
class PatchOperation final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using PatchOutcome = AZ::Outcome<void, AZStd::string>;
|
||||||
|
|
||||||
|
//! The operation to perform.
|
||||||
|
enum class Type
|
||||||
|
{
|
||||||
|
Add, //!< Inserts or replaces the value at DestinationPath with Value
|
||||||
|
Remove, //!< Removes the entry at DestinationPath
|
||||||
|
Replace, //!< Replaces the value at DestinationPath with Value
|
||||||
|
Copy, //!< Copies the contents of SourcePath to DestinationPath
|
||||||
|
Move, //!< Moves the contents of SourcePath to DestinationPath
|
||||||
|
Test //!< Ensures the contents of DestinationPath match Value or fails, performs no mutations
|
||||||
|
};
|
||||||
|
|
||||||
|
PatchOperation() = default;
|
||||||
|
PatchOperation(const PatchOperation&) = default;
|
||||||
|
PatchOperation(PatchOperation&&) = default;
|
||||||
|
|
||||||
|
PatchOperation(Path destionationPath, Type type, Value value);
|
||||||
|
PatchOperation(Path destionationPath, Type type, Path sourcePath);
|
||||||
|
PatchOperation(Path path, Type type);
|
||||||
|
|
||||||
|
static PatchOperation AddOperation(Path destinationPath, Value value);
|
||||||
|
static PatchOperation RemoveOperation(Path pathToRemove);
|
||||||
|
static PatchOperation ReplaceOperation(Path destinationPath, Value value);
|
||||||
|
static PatchOperation CopyOperation(Path destinationPath, Path sourcePath);
|
||||||
|
static PatchOperation MoveOperation(Path destinationPath, Path sourcePath);
|
||||||
|
static PatchOperation TestOperation(Path testPath, Value value);
|
||||||
|
|
||||||
|
PatchOperation& operator=(const PatchOperation&) = default;
|
||||||
|
PatchOperation& operator=(PatchOperation&&) = default;
|
||||||
|
|
||||||
|
bool operator==(const PatchOperation& rhs) const;
|
||||||
|
bool operator!=(const PatchOperation& rhs) const;
|
||||||
|
|
||||||
|
Type GetType() const;
|
||||||
|
void SetType(Type type);
|
||||||
|
|
||||||
|
const Path& GetDestinationPath() const;
|
||||||
|
void SetDestinationPath(Path path);
|
||||||
|
|
||||||
|
const Value& GetValue() const;
|
||||||
|
void SetValue(Value value);
|
||||||
|
|
||||||
|
const Path& GetSourcePath() const;
|
||||||
|
void SetSourcePath(Path path);
|
||||||
|
|
||||||
|
AZ::Outcome<Value, AZStd::string> Apply(Value rootElement) const;
|
||||||
|
PatchOutcome ApplyInPlace(Value& rootElement) const;
|
||||||
|
|
||||||
|
Value GetDomRepresentation() const;
|
||||||
|
static AZ::Outcome<PatchOperation, AZStd::string> CreateFromDomRepresentation(Value domValue);
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
DefaultExistenceCheck = 0x0,
|
||||||
|
VerifyFullPath = 0x1,
|
||||||
|
AllowEndOfArray = 0x2,
|
||||||
|
};
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct PathContext
|
||||||
|
{
|
||||||
|
Value& m_value;
|
||||||
|
PathEntry m_key;
|
||||||
|
};
|
||||||
|
|
||||||
|
static AZ::Outcome<PathContext, AZStd::string> LookupPath(
|
||||||
|
Value& rootElement, const Path& path, ExistenceCheckFlags existenceCheckFlags = ExistenceCheckFlags::DefaultExistenceCheck);
|
||||||
|
|
||||||
|
PatchOutcome ApplyAdd(Value& rootElement) const;
|
||||||
|
PatchOutcome ApplyRemove(Value& rootElement) const;
|
||||||
|
PatchOutcome ApplyReplace(Value& rootElement) const;
|
||||||
|
PatchOutcome ApplyCopy(Value& rootElement) const;
|
||||||
|
PatchOutcome ApplyMove(Value& rootElement) const;
|
||||||
|
PatchOutcome ApplyTest(Value& rootElement) const;
|
||||||
|
|
||||||
|
AZStd::variant<AZStd::monostate, Value, Path> m_value;
|
||||||
|
Path m_domPath;
|
||||||
|
Type m_type;
|
||||||
|
};
|
||||||
|
|
||||||
|
AZ_DEFINE_ENUM_BITWISE_OPERATORS(PatchOperation::ExistenceCheckFlags);
|
||||||
|
|
||||||
|
class Patch;
|
||||||
|
|
||||||
|
//! The current state of a Patch application operation.
|
||||||
|
struct PatchApplicationState
|
||||||
|
{
|
||||||
|
//! The outcome of the last operation, may be overridden to produce a different failure outcome.
|
||||||
|
PatchOperation::PatchOutcome m_outcome;
|
||||||
|
//! The patch being applied.
|
||||||
|
const Patch* m_patch = nullptr;
|
||||||
|
//! The last operation attempted.
|
||||||
|
const PatchOperation* m_lastOperation = nullptr;
|
||||||
|
//! The current state of the value being patched, will be returned if the patch operation succeeds.
|
||||||
|
Value* m_currentState = nullptr;
|
||||||
|
//! If set to false, the patch operation should halt.
|
||||||
|
bool m_shouldContinue = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace PatchApplicationStrategy
|
||||||
|
{
|
||||||
|
//! The default patching strategy. Applies all operations in a patch, but halts if any one operation fails.
|
||||||
|
void HaltOnFailure(PatchApplicationState& state);
|
||||||
|
//! Patching strategy that attemps to apply all operations in a patch, but ignores operation failures and continues.
|
||||||
|
void IgnoreFailureAndContinue(PatchApplicationState& state);
|
||||||
|
} // namespace PatchApplicationStrategy
|
||||||
|
|
||||||
|
//! A set of operations that can be applied to a Value to produce a new Value.
|
||||||
|
//! \see PatchOperation
|
||||||
|
class Patch final
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using StrategyFunctor = AZStd::function<void(PatchApplicationState&)>;
|
||||||
|
using OperationsContainer = AZStd::deque<PatchOperation>;
|
||||||
|
|
||||||
|
Patch() = default;
|
||||||
|
Patch(const Patch&) = default;
|
||||||
|
Patch(Patch&&) = default;
|
||||||
|
Patch(AZStd::initializer_list<PatchOperation> init);
|
||||||
|
|
||||||
|
template<class InputIterator>
|
||||||
|
Patch(InputIterator first, InputIterator last)
|
||||||
|
: m_operations(first, last)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
Patch& operator=(const Patch&) = default;
|
||||||
|
Patch& operator=(Patch&&) = default;
|
||||||
|
|
||||||
|
bool operator==(const Patch& rhs) const;
|
||||||
|
bool operator!=(const Patch& rhs) const;
|
||||||
|
|
||||||
|
const OperationsContainer& GetOperations() const;
|
||||||
|
void PushBack(PatchOperation op);
|
||||||
|
void PushFront(PatchOperation op);
|
||||||
|
void Pop();
|
||||||
|
void Clear();
|
||||||
|
const PatchOperation& At(size_t index) const;
|
||||||
|
size_t Size() const;
|
||||||
|
|
||||||
|
PatchOperation& operator[](size_t index);
|
||||||
|
const PatchOperation& operator[](size_t index) const;
|
||||||
|
|
||||||
|
OperationsContainer::iterator begin();
|
||||||
|
OperationsContainer::iterator end();
|
||||||
|
OperationsContainer::const_iterator begin() const;
|
||||||
|
OperationsContainer::const_iterator end() const;
|
||||||
|
OperationsContainer::const_iterator cbegin() const;
|
||||||
|
OperationsContainer::const_iterator cend() const;
|
||||||
|
size_t size() const;
|
||||||
|
|
||||||
|
AZ::Outcome<Value, AZStd::string> Apply(Value rootElement, StrategyFunctor strategy = PatchApplicationStrategy::HaltOnFailure) const;
|
||||||
|
AZ::Outcome<void, AZStd::string> ApplyInPlace(Value& rootElement, StrategyFunctor strategy = PatchApplicationStrategy::HaltOnFailure) const;
|
||||||
|
|
||||||
|
Value GetDomRepresentation() const;
|
||||||
|
static AZ::Outcome<Patch, AZStd::string> CreateFromDomRepresentation(Value domValue);
|
||||||
|
|
||||||
|
private:
|
||||||
|
OperationsContainer m_operations;
|
||||||
|
};
|
||||||
|
} // namespace AZ::Dom
|
||||||
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* 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/DomPatch.h>
|
||||||
|
#include <AzCore/DOM/DomUtils.h>
|
||||||
|
#include <AzCore/DOM/DomValue.h>
|
||||||
|
#include <AzCore/DOM/DomComparison.h>
|
||||||
|
#include <AzCore/Name/NameDictionary.h>
|
||||||
|
#include <AzCore/UnitTest/TestTypes.h>
|
||||||
|
#include <Tests/DOM/DomFixtures.h>
|
||||||
|
|
||||||
|
namespace AZ::Dom::Benchmark
|
||||||
|
{
|
||||||
|
class DomPatchBenchmark : public Tests::DomBenchmarkFixture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void TearDownHarness() override
|
||||||
|
{
|
||||||
|
m_before = {};
|
||||||
|
m_after = {};
|
||||||
|
Tests::DomBenchmarkFixture::TearDownHarness();
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleReplace(benchmark::State& state, bool deepCopy, bool apply)
|
||||||
|
{
|
||||||
|
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
|
||||||
|
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
|
||||||
|
m_after["entries"]["Key0"] = Value("replacement string", true);
|
||||||
|
|
||||||
|
RunBenchmarkInternal(state, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void TopLevelReplace(benchmark::State& state, bool apply)
|
||||||
|
{
|
||||||
|
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
|
||||||
|
m_after = Value(Type::Object);
|
||||||
|
m_after["UnrelatedKey"] = Value(42);
|
||||||
|
|
||||||
|
RunBenchmarkInternal(state, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void KeyRemove(benchmark::State& state, bool deepCopy, bool apply)
|
||||||
|
{
|
||||||
|
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
|
||||||
|
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
|
||||||
|
m_after["entries"].RemoveMember("Key1");
|
||||||
|
|
||||||
|
RunBenchmarkInternal(state, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArrayAppend(benchmark::State& state, bool deepCopy, bool apply)
|
||||||
|
{
|
||||||
|
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
|
||||||
|
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
|
||||||
|
m_after["entries"]["Key2"].ArrayPushBack(Value(0));
|
||||||
|
|
||||||
|
RunBenchmarkInternal(state, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ArrayPrepend(benchmark::State& state, bool deepCopy, bool apply)
|
||||||
|
{
|
||||||
|
m_before = GenerateDomBenchmarkPayload(state.range(0), state.range(1));
|
||||||
|
m_after = deepCopy ? Utils::DeepCopy(m_before) : m_before;
|
||||||
|
auto& arr = m_after["entries"]["Key2"].GetMutableArray();
|
||||||
|
arr.insert(arr.begin(), Value(42));
|
||||||
|
|
||||||
|
RunBenchmarkInternal(state, apply);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void RunBenchmarkInternal(benchmark::State& state, bool apply)
|
||||||
|
{
|
||||||
|
if (apply)
|
||||||
|
{
|
||||||
|
auto patchInfo = GenerateHierarchicalDeltaPatch(m_before, m_after);
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto patchResult = patchInfo.m_forwardPatches.Apply(m_before);
|
||||||
|
benchmark::DoNotOptimize(patchResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (auto _ : state)
|
||||||
|
{
|
||||||
|
auto patchInfo = GenerateHierarchicalDeltaPatch(m_before, m_after);
|
||||||
|
benchmark::DoNotOptimize(patchInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.SetItemsProcessed(state.iterations());
|
||||||
|
}
|
||||||
|
|
||||||
|
Value m_before;
|
||||||
|
Value m_after;
|
||||||
|
};
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_ShallowCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
SimpleReplace(state, false, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_ShallowCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_DeepCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
SimpleReplace(state, true, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_SimpleReplace_DeepCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_TopLevelReplace)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
TopLevelReplace(state, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_TopLevelReplace)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_ShallowCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
KeyRemove(state, false, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_ShallowCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_DeepCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
KeyRemove(state, true, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_KeyRemove_DeepCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_ShallowCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
ArrayAppend(state, false, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_ShallowCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_DeepCopy)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
ArrayAppend(state, true, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayAppend_DeepCopy)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Generate_ArrayPrepend)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
ArrayPrepend(state, true, false);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Generate_ArrayPrepend)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_SimpleReplace)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
SimpleReplace(state, true, true);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_SimpleReplace)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_TopLevelReplace)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
TopLevelReplace(state, true);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_TopLevelReplace)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_KeyRemove)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
KeyRemove(state, true, true);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_KeyRemove)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_ArrayAppend)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
ArrayAppend(state, true, true);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_ArrayAppend)
|
||||||
|
|
||||||
|
BENCHMARK_DEFINE_F(DomPatchBenchmark, AzDomPatch_Apply_ArrayPrepend)(benchmark::State& state)
|
||||||
|
{
|
||||||
|
ArrayPrepend(state, true, true);
|
||||||
|
}
|
||||||
|
DOM_REGISTER_SERIALIZATION_BENCHMARK_MS(DomPatchBenchmark, AzDomPatch_Apply_ArrayPrepend)
|
||||||
|
} // namespace AZ::Dom::Benchmark
|
||||||
@ -0,0 +1,563 @@
|
|||||||
|
/*
|
||||||
|
* 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/DomPatch.h>
|
||||||
|
#include <AzCore/DOM/DomComparison.h>
|
||||||
|
#include <Tests/DOM/DomFixtures.h>
|
||||||
|
|
||||||
|
namespace AZ::Dom::Tests
|
||||||
|
{
|
||||||
|
class DomPatchTests : public DomTestFixture
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
void SetUp() override
|
||||||
|
{
|
||||||
|
DomTestFixture::SetUp();
|
||||||
|
|
||||||
|
m_dataset = Value(Type::Object);
|
||||||
|
m_dataset["arr"].SetArray();
|
||||||
|
|
||||||
|
m_dataset["node"].SetNode("SomeNode");
|
||||||
|
m_dataset["node"]["int"] = 5;
|
||||||
|
m_dataset["node"]["null"] = Value();
|
||||||
|
|
||||||
|
for (int i = 0; i < 5; ++i)
|
||||||
|
{
|
||||||
|
m_dataset["arr"].ArrayPushBack(Value(i));
|
||||||
|
m_dataset["node"].ArrayPushBack(Value(i * 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_dataset["obj"].SetObject();
|
||||||
|
m_dataset["obj"]["foo"] = true;
|
||||||
|
m_dataset["obj"]["bar"] = false;
|
||||||
|
|
||||||
|
m_deltaDataset = m_dataset;
|
||||||
|
}
|
||||||
|
|
||||||
|
void TearDown() override
|
||||||
|
{
|
||||||
|
m_dataset = m_deltaDataset = Value();
|
||||||
|
|
||||||
|
DomTestFixture::TearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
PatchUndoRedoInfo GenerateAndVerifyDelta()
|
||||||
|
{
|
||||||
|
PatchUndoRedoInfo info = GenerateHierarchicalDeltaPatch(m_dataset, m_deltaDataset);
|
||||||
|
|
||||||
|
auto result = info.m_forwardPatches.Apply(m_dataset);
|
||||||
|
EXPECT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue(), m_deltaDataset));
|
||||||
|
|
||||||
|
result = info.m_inversePatches.Apply(result.GetValue());
|
||||||
|
EXPECT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue(), m_dataset));
|
||||||
|
|
||||||
|
// Verify serialization of the patches
|
||||||
|
auto VerifySerialization = [](const Patch& patch)
|
||||||
|
{
|
||||||
|
Value serializedPatch = patch.GetDomRepresentation();
|
||||||
|
auto deserializePatchResult = Patch::CreateFromDomRepresentation(serializedPatch);
|
||||||
|
EXPECT_TRUE(deserializePatchResult.IsSuccess());
|
||||||
|
EXPECT_EQ(deserializePatchResult.GetValue(), patch);
|
||||||
|
};
|
||||||
|
VerifySerialization(info.m_forwardPatches);
|
||||||
|
VerifySerialization(info.m_inversePatches);
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
Value m_dataset;
|
||||||
|
Value m_deltaDataset;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_InsertInObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/obj/baz");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_ReplaceInObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/obj/foo");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(false));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetBool(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_InsertObjectKeyInArray_Fails)
|
||||||
|
{
|
||||||
|
Path p("/arr/key");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(999));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_AppendInArray_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/arr/-");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()["arr"][5].GetInt64(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_InsertKeyInNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/attr");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(500));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetInt64(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_ReplaceIndexInNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/0");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_AppendInNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/-");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()["node"][5].GetInt64(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, AddOperation_InvalidPath_Fails)
|
||||||
|
{
|
||||||
|
Path p("/non/existent/path");
|
||||||
|
PatchOperation op = PatchOperation::AddOperation(p, Value(0));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/obj/foo");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_FALSE(result.GetValue()["obj"].HasMember("foo"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_RemoveIndexFromArray_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/arr/0");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()["arr"].ArraySize(), 4);
|
||||||
|
EXPECT_EQ(result.GetValue()["arr"][0].GetInt64(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_PopArray_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/arr/-");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
EXPECT_EQ(result.GetValue()["arr"].ArraySize(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/int");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_FALSE(result.GetValue()["node"].HasMember("int"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_RemoveIndexFromNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/1");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()["node"].ArraySize(), 4);
|
||||||
|
EXPECT_EQ(result.GetValue()["node"][1].GetInt64(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_PopIndexFromNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/-");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()["node"].ArraySize(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_RemoveKeyFromArray_Fails)
|
||||||
|
{
|
||||||
|
Path p("/arr/foo");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, RemoveOperation_InvalidPath_Fails)
|
||||||
|
{
|
||||||
|
Path p("/non/existent/path");
|
||||||
|
PatchOperation op = PatchOperation::RemoveOperation(p);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_InsertInObject_Fails)
|
||||||
|
{
|
||||||
|
Path p("/obj/baz");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_ReplaceInObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/obj/foo");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(false));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetBool(), false);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_InsertObjectKeyInArray_Fails)
|
||||||
|
{
|
||||||
|
Path p("/arr/key");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(999));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_AppendInArray_Fails)
|
||||||
|
{
|
||||||
|
Path p("/arr/-");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_InsertKeyInNode_Fails)
|
||||||
|
{
|
||||||
|
Path p("/node/attr");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(500));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_ReplaceIndexInNode_Succeeds)
|
||||||
|
{
|
||||||
|
Path p("/node/0");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_EQ(result.GetValue()[p].GetInt64(), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_AppendInNode_Fails)
|
||||||
|
{
|
||||||
|
Path p("/node/-");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(42));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, ReplaceOperation_InvalidPath_Fails)
|
||||||
|
{
|
||||||
|
Path p("/non/existent/path");
|
||||||
|
PatchOperation op = PatchOperation::ReplaceOperation(p, Value(0));
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_ArrayToObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/obj/arr");
|
||||||
|
Path src("/arr");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_ObjectToArrayInRange_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/arr/0");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_ObjectToArrayOutOfRange_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/arr/5");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_ObjectToNodeChildInRange_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/node/0");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(result.GetValue()[src], result.GetValue()[dest]));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_ObjectToNodeChildOutOfRange_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/node/5");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_InvalidSourcePath_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/node/0");
|
||||||
|
Path src("/invalid/path");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, CopyOperation_InvalidDestinationPath_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/invalid/path");
|
||||||
|
Path src("/arr/0");
|
||||||
|
PatchOperation op = PatchOperation::CopyOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_ArrayToObject_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/obj/arr");
|
||||||
|
Path src("/arr");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_FALSE(result.GetValue().HasMember("arr"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_ObjectToArrayInRange_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/arr/0");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_FALSE(result.GetValue().HasMember("obj"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_ObjectToArrayOutOfRange_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/arr/5");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_ObjectToNodeChildInRange_Succeeds)
|
||||||
|
{
|
||||||
|
Path dest("/node/0");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
EXPECT_TRUE(Utils::DeepCompareIsEqual(m_dataset[src], result.GetValue()[dest]));
|
||||||
|
EXPECT_FALSE(result.GetValue().HasMember("obj"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_ObjectToNodeChildOutOfRange_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/node/5");
|
||||||
|
Path src("/obj");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_InvalidSourcePath_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/node/0");
|
||||||
|
Path src("/invalid/path");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, MoveOperation_InvalidDestinationPath_Fails)
|
||||||
|
{
|
||||||
|
Path dest("/invalid/path");
|
||||||
|
Path src("/arr/0");
|
||||||
|
PatchOperation op = PatchOperation::MoveOperation(dest, src);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestCorrectValue_Succeeds)
|
||||||
|
{
|
||||||
|
Path path("/arr/1");
|
||||||
|
Value value(1);
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestIncorrectValue_Fails)
|
||||||
|
{
|
||||||
|
Path path("/arr/1");
|
||||||
|
Value value(55);
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestCorrectComplexValue_Succeeds)
|
||||||
|
{
|
||||||
|
Path path;
|
||||||
|
Value value = m_dataset;
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_TRUE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestIncorrectComplexValue_Fails)
|
||||||
|
{
|
||||||
|
Path path;
|
||||||
|
Value value = m_dataset;
|
||||||
|
value["arr"][4] = 9;
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestInvalidPath_Fails)
|
||||||
|
{
|
||||||
|
Path path("/invalid/path");
|
||||||
|
Value value;
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestOperation_TestInsertArrayPath_Fails)
|
||||||
|
{
|
||||||
|
Path path("/arr/-");
|
||||||
|
Value value(4);
|
||||||
|
PatchOperation op = PatchOperation::TestOperation(path, value);
|
||||||
|
auto result = op.Apply(m_dataset);
|
||||||
|
ASSERT_FALSE(result.IsSuccess());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_ReplaceArrayValue)
|
||||||
|
{
|
||||||
|
m_deltaDataset["arr"][0] = 5;
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_AppendArrayValue)
|
||||||
|
{
|
||||||
|
m_deltaDataset["arr"].ArrayPushBack(Value(7));
|
||||||
|
auto result = GenerateAndVerifyDelta();
|
||||||
|
|
||||||
|
// Ensure the generated patch uses the array append operation
|
||||||
|
ASSERT_EQ(result.m_forwardPatches.Size(), 1);
|
||||||
|
EXPECT_TRUE(result.m_forwardPatches[0].GetDestinationPath()[1].IsEndOfArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_AppendArrayValues)
|
||||||
|
{
|
||||||
|
m_deltaDataset["arr"].ArrayPushBack(Value(7));
|
||||||
|
m_deltaDataset["arr"].ArrayPushBack(Value(8));
|
||||||
|
m_deltaDataset["arr"].ArrayPushBack(Value(9));
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_InsertArrayValue)
|
||||||
|
{
|
||||||
|
auto& arr = m_deltaDataset["arr"].GetMutableArray();
|
||||||
|
arr.insert(arr.begin(), Value(42));
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_InsertObjectKey)
|
||||||
|
{
|
||||||
|
m_deltaDataset["obj"]["newKey"].CopyFromString("test");
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_DeleteObjectKey)
|
||||||
|
{
|
||||||
|
m_deltaDataset["obj"].RemoveMember("foo");
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_AppendNodeValues)
|
||||||
|
{
|
||||||
|
m_deltaDataset["node"].ArrayPushBack(Value(7));
|
||||||
|
m_deltaDataset["node"].ArrayPushBack(Value(8));
|
||||||
|
m_deltaDataset["node"].ArrayPushBack(Value(9));
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_InsertNodeValue)
|
||||||
|
{
|
||||||
|
auto& node = m_deltaDataset["node"].GetMutableNode();
|
||||||
|
node.GetChildren().insert(node.GetChildren().begin(), Value(42));
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_InsertNodeKey)
|
||||||
|
{
|
||||||
|
m_deltaDataset["node"]["newKey"].CopyFromString("test");
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_DeleteNodeKey)
|
||||||
|
{
|
||||||
|
m_deltaDataset["node"].RemoveMember("int");
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_RenameNode)
|
||||||
|
{
|
||||||
|
m_deltaDataset["node"].SetNodeName("RenamedNode");
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_F(DomPatchTests, TestPatch_ReplaceRoot)
|
||||||
|
{
|
||||||
|
m_deltaDataset = Value(Type::Array);
|
||||||
|
m_deltaDataset.ArrayPushBack(Value(2));
|
||||||
|
m_deltaDataset.ArrayPushBack(Value(4));
|
||||||
|
m_deltaDataset.ArrayPushBack(Value(6));
|
||||||
|
GenerateAndVerifyDelta();
|
||||||
|
}
|
||||||
|
} // namespace AZ::Dom::Tests
|
||||||
Loading…
Reference in New Issue