diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp index 0ba44e4e61..c08f038c8b 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSystemComponent.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -50,6 +51,8 @@ namespace AZ void JsonSystemComponent::Reflect(ReflectContext* reflectContext) { + JsonConfigurableStackSerializer::Reflect(reflectContext); + if (JsonRegistrationContext* jsonContext = azrtti_cast(reflectContext)) { jsonContext->Serializer()->HandlesType(); diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/RegistrationContext.h b/Code/Framework/AzCore/AzCore/Serialization/Json/RegistrationContext.h index 25a3bfedba..f62dd54e71 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/RegistrationContext.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/RegistrationContext.h @@ -8,11 +8,12 @@ #pragma once +#include +#include #include #include #include #include -#include #include namespace AZ @@ -22,6 +23,7 @@ namespace AZ { public: AZ_RTTI(JsonRegistrationContext, "{5A763774-CA8B-4245-A897-A03C503DCD60}", ReflectContext); + AZ_CLASS_ALLOCATOR(JsonRegistrationContext, SystemAllocator, 0); class SerializerBuilder; using SerializerMap = AZStd::unordered_map, AZStd::hash>; diff --git a/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.cpp b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.cpp new file mode 100644 index 0000000000..320219c7e5 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.cpp @@ -0,0 +1,172 @@ +/* + * 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 +#include +#include + +namespace AZ +{ + AZ_CLASS_ALLOCATOR_IMPL(JsonConfigurableStackSerializer, AZ::SystemAllocator, 0); + + JsonSerializationResult::Result JsonConfigurableStackSerializer::Load( + void* outputValue, + [[maybe_unused]] const Uuid& outputValueTypeId, + const rapidjson::Value& inputValue, + JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; // Used remove name conflicts in AzCore in uber builds. + + switch (inputValue.GetType()) + { + case rapidjson::kArrayType: + return LoadFromArray(outputValue, inputValue, context); + case rapidjson::kObjectType: + return LoadFromObject(outputValue, inputValue, context); + + case rapidjson::kNullType: + [[fallthrough]]; + case rapidjson::kFalseType: + [[fallthrough]]; + case rapidjson::kTrueType: + [[fallthrough]]; + case rapidjson::kStringType: + [[fallthrough]]; + case rapidjson::kNumberType: + return context.Report( + JSR::Tasks::ReadField, JSR::Outcomes::Unsupported, + "Unsupported type. Configurable stack values can only be read from arrays or objects."); + default: + return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Unknown, "Unknown json type encountered for string value."); + } + } + + JsonSerializationResult::Result JsonConfigurableStackSerializer::Store( + [[maybe_unused]] rapidjson::Value& outputValue, + [[maybe_unused]] const void* inputValue, + [[maybe_unused]] const void* defaultValue, + [[maybe_unused]] const Uuid& valueTypeId, + JsonSerializerContext& context) + { + return context.Report( + JsonSerializationResult::Tasks::WriteValue, JsonSerializationResult::Outcomes::Unsupported, + "Configuration stacks can not be written out."); + } + + void JsonConfigurableStackSerializer::Reflect(ReflectContext* context) + { + if (JsonRegistrationContext* jsonContext = azrtti_cast(context)) + { + jsonContext->Serializer()->HandlesType(); + } + } + + JsonSerializationResult::Result JsonConfigurableStackSerializer::LoadFromArray( + void* outputValue, const rapidjson::Value& inputValue, JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; // Used remove name conflicts in AzCore in uber builds. + + auto stack = reinterpret_cast(outputValue); + const Uuid& nodeValueType = stack->GetNodeType(); + + JSR::ResultCode result(JSR::Tasks::ReadField); + uint32_t counter = 0; + for (auto& it : inputValue.GetArray()) + { + ScopedContextPath subPath(context, counter); + void* value = stack->AddNode(AZStd::to_string(counter)); + result.Combine(ContinueLoading(value, nodeValueType, it, context)); + counter++; + } + + return context.Report( + result, + result.GetProcessing() != JSR::Processing::Halted ? "Loaded configurable stack from array." + : "Failed to load configurable stack from array."); + } + + JsonSerializationResult::Result JsonConfigurableStackSerializer::LoadFromObject( + void* outputValue, const rapidjson::Value& inputValue, JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; // Used remove name conflicts in AzCore in uber builds. + + auto stack = reinterpret_cast(outputValue); + const Uuid& nodeValueType = stack->GetNodeType(); + + AZStd::queue> + delayedEntries; + JSR::ResultCode result(JSR::Tasks::ReadField); + + auto add = [&](ConfigurableStackInterface::InsertPosition position, rapidjson::Value::ConstMemberIterator target, + rapidjson::Value::ConstMemberIterator it) + { + if (target->value.IsString()) + { + delayedEntries.emplace(position, AZStd::string_view(target->value.GetString(), target->value.GetStringLength()), it); + } + else + { + result.Combine(context.Report( + JSR::Tasks::ReadField, JSR::Outcomes::Skipped, + "Skipped value for the Configurable Stack because the target wasn't a string.")); + } + }; + + // Load all the regular entries into the stack and store any with a before or after binding for + // later inserting. + for (auto it = inputValue.MemberBegin(); it != inputValue.MemberEnd(); ++it) + { + AZStd::string_view name(it->name.GetString(), it->name.GetStringLength()); + ScopedContextPath subPath(context, name); + if (it->value.IsObject()) + { + if (auto target = it->value.FindMember(StackBefore); target != it->value.MemberEnd()) + { + add(ConfigurableStackInterface::InsertPosition::Before, target, it); + continue; + } + if (auto target = it->value.FindMember(StackAfter); target != it->value.MemberEnd()) + { + add(ConfigurableStackInterface::InsertPosition::After, target, it); + continue; + } + } + + void* value = stack->AddNode(name); + result.Combine(ContinueLoading(value, nodeValueType, it->value, context)); + } + + // Insert the entries that have been delayed. + while (!delayedEntries.empty()) + { + auto&& [insertPosition, target, valueStore] = delayedEntries.front(); + AZStd::string_view name(valueStore->name.GetString(), valueStore->name.GetStringLength()); + ScopedContextPath subPath(context, name); + void* value = stack->AddNode(name, target, insertPosition); + if (value != nullptr) + { + result.Combine(ContinueLoading(value, nodeValueType, valueStore->value, context)); + } + else + { + result.Combine(context.Report( + JSR::Tasks::ReadField, JSR::Outcomes::Skipped, + AZStd::string::format( + "Skipped value for the Configurable Stack because the target '%.*s' couldn't be found.", AZ_STRING_ARG(name)))); + } + delayedEntries.pop(); + } + + return context.Report( + result, + result.GetProcessing() != JSR::Processing::Halted ? "Loaded configurable stack from array." + : "Failed to load configurable stack from array."); + } +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.h b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.h new file mode 100644 index 0000000000..afb0eca6e0 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.h @@ -0,0 +1,187 @@ +/* + * 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 +#include +#include +#include +#include +#include +#include + +namespace AZ +{ + //! 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. + + class ReflectContext; + + class ConfigurableStackInterface + { + public: + friend class JsonConfigurableStackSerializer; + + virtual ~ConfigurableStackInterface() = default; + + virtual const TypeId& GetNodeType() const = 0; + + protected: + enum class InsertPosition + { + Before, + After + }; + virtual void* AddNode(AZStd::string name) = 0; + virtual void* AddNode(AZStd::string name, AZStd::string_view target, InsertPosition position) = 0; + }; + + template + class ConfigurableStack final : public ConfigurableStackInterface + { + public: + using NodeValue = AZStd::shared_ptr; + using Node = AZStd::pair; + using NodeContainer = AZStd::vector; + using Iterator = typename NodeContainer::iterator; + using ConstIterator = typename NodeContainer::const_iterator; + + ~ConfigurableStack() override = default; + + const TypeId& GetNodeType() const override; + + Iterator begin(); + Iterator end(); + ConstIterator begin() const; + ConstIterator end() const; + + size_t size() const; + + protected: + void* AddNode(AZStd::string name) override; + void* AddNode(AZStd::string name, AZStd::string_view target, InsertPosition position) override; + + private: + NodeContainer m_nodes; + }; + + AZ_TYPE_INFO_TEMPLATE(ConfigurableStack, "{0A3D2038-6E6A-4EFD-A1B4-F30D947E21B1}", AZ_TYPE_INFO_TYPENAME); + + template + struct SerializeGenericTypeInfo> + { + using ConfigurableStackType = ConfigurableStack; + + class GenericConfigurableStackInfo : public GenericClassInfo + { + public: + AZ_TYPE_INFO(GenericConfigurableStackInfo, "{FC5A9353-D0DE-48F6-81B5-1CB2985C5F65}"); + + GenericConfigurableStackInfo(); + + SerializeContext::ClassData* GetClassData() override; + size_t GetNumTemplatedArguments() override; + const Uuid& GetTemplatedTypeId([[maybe_unused]] size_t element) override; + const Uuid& GetSpecializedTypeId() const override; + const Uuid& GetGenericTypeId() const override; + + void Reflect(SerializeContext* serializeContext) override; + + SerializeContext::ClassData m_classData; + }; + + using ClassInfoType = GenericConfigurableStackInfo; + + static ClassInfoType* GetGenericInfo(); + static const Uuid& GetClassTypeId(); + }; + + class JsonConfigurableStackSerializer : public BaseJsonSerializer + { + public: + AZ_RTTI(JsonConfigurableStackSerializer, "{45A31805-9058-41A9-B1A3-71E2CB4D9237}", BaseJsonSerializer); + AZ_CLASS_ALLOCATOR_DECL; + + JsonSerializationResult::Result Load( + void* outputValue, + const Uuid& outputValueTypeId, + const rapidjson::Value& inputValue, + JsonDeserializerContext& context) override; + + JsonSerializationResult::Result Store( + rapidjson::Value& outputValue, + const void* inputValue, + const void* defaultValue, + const Uuid& valueTypeId, + JsonSerializerContext& context) override; + + static void Reflect(ReflectContext* context); + + private: + JsonSerializationResult::Result LoadFromArray( + void* outputValue, const rapidjson::Value& inputValue, JsonDeserializerContext& context); + JsonSerializationResult::Result LoadFromObject( + void* outputValue, const rapidjson::Value& inputValue, JsonDeserializerContext& context); + + static constexpr const char* StackBefore = "$stack_before"; + static constexpr const char* StackAfter = "$stack_after"; + }; +} // namespace AZ + +#include diff --git a/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.inl b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.inl new file mode 100644 index 0000000000..7cfba21497 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Settings/ConfigurableStack.inl @@ -0,0 +1,149 @@ +/* + * 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 + +namespace AZ +{ + // + // ConfigurableStack + // + + template + const TypeId& ConfigurableStack::GetNodeType() const + { + return azrtti_typeid(); + } + + template + auto ConfigurableStack::begin() -> Iterator + { + return m_nodes.begin(); + } + + template + auto ConfigurableStack::end() -> Iterator + { + return m_nodes.end(); + } + + template + auto ConfigurableStack::begin() const -> ConstIterator + { + return m_nodes.begin(); + } + + template + auto ConfigurableStack::end() const -> ConstIterator + { + return m_nodes.end(); + } + + template + size_t ConfigurableStack::size() const + { + return m_nodes.size(); + } + + template + void* ConfigurableStack::AddNode(AZStd::string name) + { + Node& result = m_nodes.emplace_back(AZStd::move(name)); + return &result.second; + } + + template + void* ConfigurableStack::AddNode(AZStd::string name, AZStd::string_view target, InsertPosition position) + { + auto end = m_nodes.end(); + for (auto it = m_nodes.begin(); it != end; ++it) + { + if (it->first == target) + { + if (position == InsertPosition::After) + { + ++it; + } + auto result = m_nodes.insert(it, Node(AZStd::move(name), {})); + return &result->second; + } + } + return nullptr; + } + + + + // + // SerializeGenericTypeInfo + // + + + template + SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GenericConfigurableStackInfo() + : m_classData{ SerializeContext::ClassData::Create( + "AZ::ConfigurableStack", GetSpecializedTypeId(), Internal::NullFactory::GetInstance(), nullptr, nullptr) } + { + } + + template + SerializeContext::ClassData* SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GetClassData() + { + return &m_classData; + } + + template + size_t SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GetNumTemplatedArguments() + { + return 1; + } + + template + const Uuid& SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GetTemplatedTypeId( + [[maybe_unused]] size_t element) + { + return SerializeGenericTypeInfo::GetClassTypeId(); + } + + template + const Uuid& SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GetSpecializedTypeId() const + { + return azrtti_typeid(); + } + + template + const Uuid& SerializeGenericTypeInfo>::GenericConfigurableStackInfo::GetGenericTypeId() const + { + return TYPEINFO_Uuid(); + } + + template + void SerializeGenericTypeInfo>::GenericConfigurableStackInfo::Reflect(SerializeContext* serializeContext) + { + if (serializeContext) + { + serializeContext->RegisterGenericClassInfo(GetSpecializedTypeId(), this, &AnyTypeInfoConcept::CreateAny); + if (serializeContext->FindClassData(azrtti_typeid>()) == nullptr) + { + serializeContext->RegisterGenericType>(); + } + } + } + + template + auto SerializeGenericTypeInfo>::GetGenericInfo() -> ClassInfoType* + { + return GetCurrentSerializeContextModule().CreateGenericClassInfo(); + } + + template + const Uuid& SerializeGenericTypeInfo>::GetClassTypeId() + { + return GetGenericInfo()->GetClassData()->m_typeId; + } +} // namespace AZ + diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index b7821f1d65..dc2481b4c5 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -560,6 +560,9 @@ set(FILES Serialization/std/VariantReflection.inl Settings/CommandLine.cpp Settings/CommandLine.h + Settings/ConfigurableStack.cpp + Settings/ConfigurableStack.inl + Settings/ConfigurableStack.h Settings/SettingsRegistry.cpp Settings/SettingsRegistry.h Settings/SettingsRegistryConsoleUtils.cpp diff --git a/Code/Framework/AzCore/Tests/Settings/ConfigurableStackTests.cpp b/Code/Framework/AzCore/Tests/Settings/ConfigurableStackTests.cpp new file mode 100644 index 0000000000..0a69ac0053 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Settings/ConfigurableStackTests.cpp @@ -0,0 +1,281 @@ +/* + * 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 +#include +#include +#include +#include +#include + +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(context)) + { + sc->Class()->Field("Value", &ConfigInt::m_value); + } + } + }; + + struct ConfigurableStackTests : public ScopedAllocatorSetupFixture + { + void Reflect(AZ::ReflectContext* context) + { + if (auto sc = azrtti_cast(context)) + { + AZ::JsonSystemComponent::Reflect(sc); + ConfigInt::Reflect(sc); + sc->RegisterGenericType>(); + } + else if (auto jrc = azrtti_cast(context)) + { + AZ::JsonSystemComponent::Reflect(jrc); + } + } + + void SetUp() override + { + ScopedAllocatorSetupFixture::SetUp(); + + m_serializeContext = AZStd::make_unique(); + m_jsonRegistrationContext = AZStd::make_unique(); + + Reflect(m_serializeContext.get()); + Reflect(m_jsonRegistrationContext.get()); + + m_deserializationSettings.m_registrationContext = m_jsonRegistrationContext.get(); + m_deserializationSettings.m_serializeContext = m_serializeContext.get(); + } + + void TearDown() + { + m_jsonRegistrationContext->EnableRemoveReflection(); + Reflect(m_jsonRegistrationContext.get()); + m_jsonRegistrationContext->DisableRemoveReflection(); + + m_serializeContext->EnableRemoveReflection(); + Reflect(m_serializeContext.get()); + m_serializeContext->DisableRemoveReflection(); + + ScopedAllocatorSetupFixture::TearDown(); + } + + void ObjectTest(AZStd::string_view jsonText) + { + AZ::ConfigurableStack 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++; + } + } + + AZStd::unique_ptr m_serializeContext; + AZStd::unique_ptr m_jsonRegistrationContext; + AZ::JsonDeserializerSettings m_deserializationSettings; + }; + + TEST_F(ConfigurableStackTests, DeserializeArray) + { + AZ::ConfigurableStack 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 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 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 diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index 23ee2e3c57..99e97ca3cf 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -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