From dc9d1a2f312263058a06c0147ab831ea8eb0996f Mon Sep 17 00:00:00 2001 From: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> Date: Thu, 9 Dec 2021 18:35:18 -0600 Subject: [PATCH] Introduced a Json Serializer for the AzCore mutable path classes Added UnitTest for Json Serialization of AzCore Path types resolves #2477 Signed-off-by: lumberyard-employee-dm <56135373+lumberyard-employee-dm@users.noreply.github.com> --- Code/Framework/AzCore/AzCore/IO/Path/Path.h | 1 + Code/Framework/AzCore/AzCore/IO/Path/Path.inl | 7 + .../AzCore/AzCore/IO/Path/PathReflect.cpp | 14 +- .../Serialization/Json/PathSerializer.cpp | 127 ++++++++++++++++++ .../Serialization/Json/PathSerializer.h | 26 ++++ .../AzCore/AzCore/azcore_files.cmake | 2 + .../Json/PathSerializerTests.cpp | 106 +++++++++++++++ .../AzCore/Tests/azcoretests_files.cmake | 1 + 8 files changed, 280 insertions(+), 4 deletions(-) create mode 100644 Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.cpp create mode 100644 Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.h create mode 100644 Code/Framework/AzCore/Tests/Serialization/Json/PathSerializerTests.cpp diff --git a/Code/Framework/AzCore/AzCore/IO/Path/Path.h b/Code/Framework/AzCore/AzCore/IO/Path/Path.h index 094dee16f2..7f89809294 100644 --- a/Code/Framework/AzCore/AzCore/IO/Path/Path.h +++ b/Code/Framework/AzCore/AzCore/IO/Path/Path.h @@ -484,6 +484,7 @@ namespace AZ::IO // as_posix //! Replicates the behavior of the Python pathlib as_posix method //! by replacing the Windows Path Separator with the Posix Path Seperator + constexpr string_type AsPosix() const; AZStd::string StringAsPosix() const; constexpr AZStd::fixed_string FixedMaxPathStringAsPosix() const noexcept; diff --git a/Code/Framework/AzCore/AzCore/IO/Path/Path.inl b/Code/Framework/AzCore/AzCore/IO/Path/Path.inl index 5ac15fc728..0dc1799528 100644 --- a/Code/Framework/AzCore/AzCore/IO/Path/Path.inl +++ b/Code/Framework/AzCore/AzCore/IO/Path/Path.inl @@ -1043,6 +1043,13 @@ namespace AZ::IO // as_posix // Returns a copy of the path with the path separators converted to PosixPathSeparator template + constexpr auto BasicPath::AsPosix() const -> string_type + { + string_type resultPath(m_path.begin(), m_path.end()); + AZStd::replace(resultPath.begin(), resultPath.end(), WindowsPathSeparator, PosixPathSeparator); + return resultPath; + } + template AZStd::string BasicPath::StringAsPosix() const { AZStd::string resultPath(m_path.begin(), m_path.end()); diff --git a/Code/Framework/AzCore/AzCore/IO/Path/PathReflect.cpp b/Code/Framework/AzCore/AzCore/IO/Path/PathReflect.cpp index 55a991f19f..1d0cdba66c 100644 --- a/Code/Framework/AzCore/AzCore/IO/Path/PathReflect.cpp +++ b/Code/Framework/AzCore/AzCore/IO/Path/PathReflect.cpp @@ -7,6 +7,8 @@ */ #include +#include +#include #include #include @@ -35,10 +37,8 @@ namespace AZ::IO size_t Save(const void* classPtr, IO::GenericStream& stream, bool) override { /// Save paths out using the PosixPathSeparator - PathType path(reinterpret_cast(classPtr)->Native(), AZ::IO::PosixPathSeparator); - path.MakePreferred(); - - return static_cast(stream.Write(path.Native().size(), path.c_str())); + auto posixPathString{ reinterpret_cast(classPtr)->AsPosix() }; + return static_cast(stream.Write(posixPathString.size(), posixPathString.c_str())); } bool Load(void* classPtr, IO::GenericStream& stream, unsigned int, bool) override @@ -73,5 +73,11 @@ namespace AZ::IO AZ::SerializeContext::IDataSerializer::CreateDefaultDeleteDeleter() }) ; } + else if (auto jsonContext = azrtti_cast(context)) + { + jsonContext->Serializer() + ->HandlesType() + ->HandlesType(); + } } } diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.cpp new file mode 100644 index 0000000000..7c057730e8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.cpp @@ -0,0 +1,127 @@ +/* + * 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 + +namespace AZ::JsonPathSerializerInternal +{ + template + static JsonSerializationResult::Result Load(PathType* pathValue, const rapidjson::Value& inputValue, + JsonDeserializerContext& context) + { + namespace JSR = JsonSerializationResult; // Used remove name conflicts in AzCore in uber builds. + + AZ_Assert(pathValue, "Expected a valid pointer to load from json value."); + + switch (inputValue.GetType()) + { + case rapidjson::kArrayType: + case rapidjson::kObjectType: + case rapidjson::kFalseType: + case rapidjson::kTrueType: + case rapidjson::kNumberType: + [[fallthrough]]; + case rapidjson::kNullType: + return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Unsupported, + "Unsupported type. String values can't be read from arrays, objects or null."); + case rapidjson::kStringType: + { + size_t pathLength = inputValue.GetStringLength(); + if (pathLength <= pathValue->Native().max_size()) + { + *pathValue = PathType(AZStd::string_view(inputValue.GetString(), pathLength)).LexicallyNormal(); + return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Success, "Successfully read path."); + } + using UuidString = AZStd::fixed_string; + using ErrorString = AZStd::fixed_string<256>; + return context.Report(JsonSerializationResult::Tasks::ReadField, JSR::Outcomes::Invalid, + ErrorString::format("Json string value is too large to fit within path type %s. It needs to be less than %zu code points", + azrtti_typeid().template ToString().c_str(), pathValue->Native().max_size())); + } + default: + return context.Report(JSR::Tasks::ReadField, JSR::Outcomes::Unknown, "Unknown json type encountered for string value."); + } + } + template + static JsonSerializationResult::Result StoreWithDefault(rapidjson::Value& outputValue, const PathType* pathValue, + const PathType* defaultPathValue, JsonSerializerContext& context) + { + namespace JSR = JsonSerializationResult; // Removes name conflicts in AzCore in uber builds. + + if (context.ShouldKeepDefaults() || defaultPathValue == nullptr || *pathValue != *defaultPathValue) + { + auto posixPathString = pathValue->AsPosix(); + outputValue.SetString(posixPathString.c_str(), aznumeric_caster(posixPathString.size()), context.GetJsonAllocator()); + return context.Report(JSR::Tasks::WriteValue, JSR::Outcomes::Success, "Path successfully stored."); + } + + return context.Report(JSR::Tasks::WriteValue, JSR::Outcomes::DefaultsUsed, "Default Path used."); + } +} + +namespace AZ +{ + AZ_CLASS_ALLOCATOR_IMPL(JsonPathSerializer, SystemAllocator, 0); + + JsonSerializationResult::Result JsonPathSerializer::Load(void* outputValue, const Uuid& outputValueTypeId, + const rapidjson::Value& inputValue, JsonDeserializerContext& context) + { + if (outputValueTypeId == azrtti_typeid()) + { + return JsonPathSerializerInternal::Load(reinterpret_cast(outputValue), inputValue, + context); + } + else if (outputValueTypeId == azrtti_typeid()) + { + return JsonPathSerializerInternal::Load(reinterpret_cast(outputValue), inputValue, + context); + } + + using UuidString = AZStd::fixed_string; + auto errorTypeIdString = outputValueTypeId.ToString(); + AZ_Assert(false, "Unable to serialize json string" + " to a path of type %s", errorTypeIdString.c_str()); + + using ErrorString = AZStd::fixed_string<256>; + return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::TypeMismatch, + ErrorString::format("Output value type ID %s is not a valid Path type", errorTypeIdString.c_str())); + } + + JsonSerializationResult::Result JsonPathSerializer::Store(rapidjson::Value& outputValue, const void* inputValue, + const void* defaultValue, const Uuid& valueTypeId, JsonSerializerContext& context) + { + if (valueTypeId == azrtti_typeid()) + { + return JsonPathSerializerInternal::StoreWithDefault(outputValue, + reinterpret_cast(inputValue), + reinterpret_cast(defaultValue), context); + } + else if (valueTypeId == azrtti_typeid()) + { + return JsonPathSerializerInternal::StoreWithDefault(outputValue, + reinterpret_cast(inputValue), + reinterpret_cast(defaultValue), context); + } + + using UuidString = AZStd::fixed_string; + auto errorTypeIdString = valueTypeId.ToString(); + AZ_Assert(false, "Unable to serialize path type %s to a json string", + errorTypeIdString.c_str()); + + using ErrorString = AZStd::fixed_string<256>; + return context.Report(JsonSerializationResult::Tasks::WriteValue, JsonSerializationResult::Outcomes::TypeMismatch, + ErrorString::format("Input value type ID %s is not a valid Path type", errorTypeIdString.c_str())); + } + +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.h b/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.h new file mode 100644 index 0000000000..609a489ec4 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/PathSerializer.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace AZ +{ + class JsonPathSerializer + : public BaseJsonSerializer + { + public: + AZ_RTTI(JsonPathSerializer, "{F6FBA901-07E0-4F03-A0B6-72A9A6CE1E96}", 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; + }; +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index e8001f7215..0dfce11247 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -533,6 +533,8 @@ set(FILES Serialization/Json/JsonUtils.cpp Serialization/Json/MapSerializer.h Serialization/Json/MapSerializer.cpp + Serialization/Json/PathSerializer.h + Serialization/Json/PathSerializer.cpp Serialization/Json/RegistrationContext.h Serialization/Json/RegistrationContext.cpp Serialization/Json/SmartPointerSerializer.h diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/PathSerializerTests.cpp b/Code/Framework/AzCore/Tests/Serialization/Json/PathSerializerTests.cpp new file mode 100644 index 0000000000..e00a4171a2 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Serialization/Json/PathSerializerTests.cpp @@ -0,0 +1,106 @@ +/* + * 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 JsonSerializationTests +{ + template + class PathTestDescription + : public JsonSerializerConformityTestDescriptor + { + public: + using JsonSerializerConformityTestDescriptor::Reflect; + void Reflect(AZStd::unique_ptr& serializeContext) override + { + AZ::IO::PathReflect(serializeContext.get()); + } + void Reflect(AZStd::unique_ptr& jsonContext) override + { + AZ::IO::PathReflect(jsonContext.get()); + } + AZStd::shared_ptr CreateSerializer() override + { + return AZStd::make_shared(); + } + + AZStd::shared_ptr CreateDefaultInstance() override + { + return AZStd::make_shared(); + } + + AZStd::shared_ptr CreateFullySetInstance() override + { + return AZStd::make_shared("O3DE/Relative/Path"); + } + + AZStd::string_view GetJsonForFullySetInstance() override + { + return R"("O3DE/Relative/Path")"; + } + + void ConfigureFeatures(JsonSerializerConformityTestDescriptorFeatures& features) override + { + features.EnableJsonType(rapidjson::kStringType); + features.m_supportsPartialInitialization = false; + features.m_supportsInjection = false; + } + + bool AreEqual(const PathType& lhs, const PathType& rhs) override + { + return lhs == rhs; + } + }; + + using PathConformityTestTypes = ::testing::Types< + PathTestDescription, + PathTestDescription + >; + INSTANTIATE_TYPED_TEST_CASE_P(Path, JsonSerializerConformityTests, PathConformityTestTypes); + + + class PathSerializerTests + : public BaseJsonSerializerFixture + { + public: + AZStd::unique_ptr m_serializer; + + void SetUp() override + { + BaseJsonSerializerFixture::SetUp(); + m_serializer = AZStd::make_unique(); + } + + void TearDown() override + { + m_serializer.reset(); + BaseJsonSerializerFixture::TearDown(); + } + }; + + TEST_F(PathSerializerTests, LoadingIntoFixedMaxPath_GreaterThanMaxPathLength_Fails) + { + AZ::IO::Path testPath; + // Fill a path greater than the AZ::IO::MaxPathLength in write it to Json + testPath.Native().append(AZ::IO::MaxPathLength + 2, 'a'); + + rapidjson::Value loadPathValue; + AZ::JsonSerializationResult::ResultCode resultCode = m_serializer->Store(loadPathValue, + &testPath, nullptr, azrtti_typeid(), *m_jsonSerializationContext); + EXPECT_EQ(AZ::JsonSerializationResult::Outcomes::Success, resultCode.GetOutcome()); + + AZ::IO::FixedMaxPath resultPath; + AZ::JsonSerializationResult::ResultCode result = m_serializer->Load(&resultPath, azrtti_typeid(), + loadPathValue, *m_jsonDeserializationContext); + EXPECT_GE(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Invalid); + } +} // namespace JsonSerializationTests diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index b7fe113bc6..98f268b61a 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -111,6 +111,7 @@ set(FILES Serialization/Json/MapSerializerTests.cpp Serialization/Json/MathVectorSerializerTests.cpp Serialization/Json/MathMatrixSerializerTests.cpp + Serialization/Json/PathSerializerTests.cpp Serialization/Json/SmartPointerSerializerTests.cpp Serialization/Json/StringSerializerTests.cpp Serialization/Json/TestCases.h