Improved the way the Settings Registry can handle stacks/arrays.

The ConfigurableStack makes configuring stacks and arrays through the Settings Registry easier than using direct serialization to a container like AZStd::vector. It does this by using JSON Objects rather than JSON arrays, although arrays are supported for backwards compatibility. Two key words were added:
    - $stack_before : Insert the new entry before the referenced entry. Referencing is done by name.
    - $stack_after  : Insert the new entry after the referenced entry. Referencing is done by name.
to allow inserting new entries at specific locations. An example of a .setreg file at updates existing settings would be:
// Original settings
{
    "Settings in a stack":
    {
        "AnOriginalEntry":
        {
            "MyValue": "hello",
            "ExampleValue": 84
        },
        "TheSecondEntry":
        {
            "MyValue": "world"
        }
    }
}

// Customized settings.
{
    "Settings in a stack":
    {
        // Add a new entry before "AnOriginalEntry" in the original document.
        "NewEntry":
        {
            "$stack_before": "AnOriginalEntry",
            "MyValue": 42
        },
        // Add a second entry after "AnOriginalEntry" in the original document.
        "SecondNewEntry":
        {
            "$stack_after": "AnOriginalEntry",
            "MyValue": "FortyTwo".
        },
        // Update a value in "AnOriginalEntry".
        "AnOriginalEntry":
        {
            "ExampleValue": 42
        },
        // Delete the "TheSecondEntry" from the settings.
        "TheSecondEntry" : null,
    }
}

The ConfigurableStack uses an AZStd::shared_ptr to store the values. This supports settings up a base class and specifying derived classes in the settings, but requires that the base and derived classes all have a memory allocator associated with them (i.e. by using the "AZ_CLASS_ALLOCATOR" macro) and that the relation of the classes is reflected. Loading a ConfigurableStack can be done using the GetObject call on the SettingsRegistryInterface.

Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com>
This commit is contained in:
AMZN-koppersr
2022-02-10 10:57:16 -08:00
parent f709ba07a9
commit d00436e26a
8 changed files with 817 additions and 1 deletions
@@ -0,0 +1,283 @@
/*
* 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/JSON/document.h>
#include <AzCore/Memory/SystemAllocator.h>
#include <AzCore/Settings/ConfigurableStack.h>
#include <AzCore/Serialization/Json/JsonSerialization.h>
#include <AzCore/Serialization/Json/RegistrationContext.h>
#include <AzCore/Serialization/Json/JsonSystemComponent.h>
#include <AzCore/UnitTest/TestTypes.h>
namespace UnitTest
{
struct ConfigInt
{
AZ_TYPE_INFO(UnitTest::ConfigInt, "{1FAF6E55-7FA4-4FFA-8C41-34F422B8E8AB}");
AZ_CLASS_ALLOCATOR(ConfigInt, AZ::SystemAllocator, 0);
int m_value;
static void Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
sc->Class<ConfigInt>()->Field("Value", &ConfigInt::m_value);
}
}
};
struct ConfigurableStackTests : public AllocatorsFixture
{
void Reflect(AZ::ReflectContext* context)
{
if (auto sc = azrtti_cast<AZ::SerializeContext*>(context))
{
AZ::JsonSystemComponent::Reflect(sc);
ConfigInt::Reflect(sc);
sc->RegisterGenericType<AZ::ConfigurableStack<ConfigInt>>();
}
else if (auto jrc = azrtti_cast<AZ::JsonRegistrationContext*>(context))
{
AZ::JsonSystemComponent::Reflect(jrc);
}
}
void SetUp() override
{
AllocatorsFixture::SetUp();
m_serializeContext = aznew AZ::SerializeContext();
m_jsonRegistrationContext = aznew AZ::JsonRegistrationContext();
Reflect(m_serializeContext);
Reflect(m_jsonRegistrationContext);
m_deserializationSettings.m_registrationContext = m_jsonRegistrationContext;
m_deserializationSettings.m_serializeContext = m_serializeContext;
}
void TearDown()
{
m_jsonRegistrationContext->EnableRemoveReflection();
Reflect(m_jsonRegistrationContext);
m_jsonRegistrationContext->DisableRemoveReflection();
m_serializeContext->EnableRemoveReflection();
Reflect(m_serializeContext);
m_serializeContext->DisableRemoveReflection();
delete m_jsonRegistrationContext;
delete m_serializeContext;
AllocatorsFixture::TearDown();
}
void ObjectTest(AZStd::string_view jsonText)
{
AZ::ConfigurableStack<ConfigInt> stack;
rapidjson::Document document;
document.Parse(jsonText.data(), jsonText.length());
ASSERT_FALSE(document.HasParseError());
AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::Load(stack, document, m_deserializationSettings);
ASSERT_EQ(AZ::JsonSerializationResult::Processing::Completed, result.GetProcessing());
ASSERT_EQ(4, stack.size());
int numberCounter = 0;
int valueCounter = 42;
for (auto& [name, value] : stack)
{
EXPECT_STREQ(AZStd::string::format("Value%i", numberCounter).c_str(), name.c_str());
EXPECT_EQ(valueCounter, value->m_value);
numberCounter++;
valueCounter++;
}
}
AZ::SerializeContext* m_serializeContext;
AZ::JsonRegistrationContext* m_jsonRegistrationContext;
AZ::JsonDeserializerSettings m_deserializationSettings;
};
TEST_F(ConfigurableStackTests, DeserializeArray)
{
AZ::ConfigurableStack<ConfigInt> stack;
rapidjson::Document document;
document.Parse(
R"([
{ "Value": 42 },
{ "Value": 43 },
{ "Value": 44 },
{ "Value": 45 }
])");
ASSERT_FALSE(document.HasParseError());
AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::Load(stack, document, m_deserializationSettings);
ASSERT_EQ(AZ::JsonSerializationResult::Processing::Completed, result.GetProcessing());
ASSERT_EQ(4, stack.size());
int numberCounter = 0;
int valueCounter = 42;
for (auto& [name, value] : stack)
{
EXPECT_STREQ(AZStd::to_string(numberCounter).c_str(), name.c_str());
EXPECT_EQ(valueCounter, value->m_value);
numberCounter++;
valueCounter++;
}
}
TEST_F(ConfigurableStackTests, DeserializeObject)
{
ObjectTest(
R"({
"Value0": { "Value": 42 },
"Value1": { "Value": 43 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 }
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithLateBefore)
{
ObjectTest(
R"({
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 },
"Value1":
{
"$stack_before": "Value2",
"Value": 43
}
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithEarlyBefore)
{
ObjectTest(
R"({
"Value1":
{
"$stack_before": "Value2",
"Value": 43
},
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 }
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithLateAfter)
{
ObjectTest(
R"({
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 },
"Value1":
{
"$stack_after": "Value0",
"Value": 43
}
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithEarlyAfter)
{
ObjectTest(
R"({
"Value1":
{
"$stack_after": "Value0",
"Value": 43
},
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 }
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithBeforeFirst)
{
ObjectTest(
R"({
"Value1": { "Value": 43 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 },
"Value0":
{
"$stack_before": "Value1",
"Value": 42
}
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithInsertAfterLast)
{
ObjectTest(
R"({
"Value3":
{
"$stack_after": "Value2",
"Value": 45
},
"Value0": { "Value": 42 },
"Value1": { "Value": 43 },
"Value2": { "Value": 44 }
})");
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithInvalidTarget)
{
AZ::ConfigurableStack<ConfigInt> stack;
rapidjson::Document document;
document.Parse(
R"({
"Value1":
{
"$stack_after": "airplane",
"Value": 43
},
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 }
})");
ASSERT_FALSE(document.HasParseError());
AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::Load(stack, document, m_deserializationSettings);
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, result.GetProcessing());
EXPECT_EQ(AZ::JsonSerializationResult::Outcomes::PartialSkip, result.GetOutcome());
EXPECT_EQ(3, stack.size());
}
TEST_F(ConfigurableStackTests, DeserializeObjectWithInvalidTargetType)
{
AZ::ConfigurableStack<ConfigInt> stack;
rapidjson::Document document;
document.Parse(
R"({
"Value1":
{
"$stack_after": 42,
"Value": 43
},
"Value0": { "Value": 42 },
"Value2": { "Value": 44 },
"Value3": { "Value": 45 }
})");
ASSERT_FALSE(document.HasParseError());
AZ::JsonSerializationResult::ResultCode result = AZ::JsonSerialization::Load(stack, document, m_deserializationSettings);
EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, result.GetProcessing());
EXPECT_EQ(AZ::JsonSerializationResult::Outcomes::PartialSkip, result.GetOutcome());
EXPECT_EQ(3, stack.size());
}
} // namespace UnitTest
@@ -76,6 +76,7 @@ set(FILES
Name/NameTests.cpp
RTTI/TypeSafeIntegralTests.cpp
Settings/CommandLineTests.cpp
Settings/ConfigurableStackTests.cpp
Settings/SettingsRegistryTests.cpp
Settings/SettingsRegistryConsoleUtilsTests.cpp
Settings/SettingsRegistryMergeUtilsTests.cpp