diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp new file mode 100644 index 0000000000..2c856fb4b0 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.cpp @@ -0,0 +1,254 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace AZ +{ + JsonSerializationResult::ResultCode JsonImportResolver::ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element) + { + using namespace JsonSerializationResult; + + for (auto& path : importPathStack) + { + if (importPath == path) + { + return settings.m_reporting( + AZStd::string::format("'%s' was already imported in this chain. This indicates a cyclic dependency.", importPath.c_str()), + ResultCode(Tasks::Import, Outcomes::Catastrophic), element); + } + } + + importPathStack.push_back(importPath); + AZ::StackedString importElement(AZ::StackedString::Format::JsonPointer); + JsonImportSettings nestedImportSettings; + nestedImportSettings.m_importer = settings.m_importer; + nestedImportSettings.m_reporting = settings.m_reporting; + nestedImportSettings.m_resolveFlags = ImportTracking::Dependencies; + ResultCode result = ResolveImports(jsonDoc, allocator, importPathStack, nestedImportSettings, importElement); + importPathStack.pop_back(); + + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject()) + { + for (auto& field : jsonDoc.GetObject()) + { + if(strncmp(field.name.GetString(), JsonSerialization::ImportDirectiveIdentifier, field.name.GetStringLength()) == 0) + { + const rapidjson::Value& importDirective = field.value; + AZ::IO::FixedMaxPath importAbsPath = importPathStack.back(); + importAbsPath.RemoveFilename(); + AZStd::string importName; + if (importDirective.IsObject()) + { + auto filenameField = importDirective.FindMember("filename"); + if (filenameField != importDirective.MemberEnd()) + { + importName = AZStd::string(filenameField->value.GetString(), filenameField->value.GetStringLength()); + } + } + else + { + importName = AZStd::string(importDirective.GetString(), importDirective.GetStringLength()); + } + importAbsPath.Append(importName); + + rapidjson::Value patch; + ResultCode resolveResult = settings.m_importer->ResolveImport(&jsonDoc, patch, importDirective, importAbsPath, allocator); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + if ((settings.m_resolveFlags & ImportTracking::Imports) == ImportTracking::Imports) + { + rapidjson::Pointer path(element.Get().data(), element.Get().size()); + settings.m_importer->AddImportDirective(path, importName); + } + if ((settings.m_resolveFlags & ImportTracking::Dependencies) == ImportTracking::Dependencies) + { + settings.m_importer->AddImportedFile(importAbsPath.String()); + } + + ResultCode result = ResolveNestedImports(jsonDoc, allocator, importPathStack, settings, importAbsPath, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + settings.m_importer->ApplyPatch(jsonDoc, patch, allocator); + } + else if (field.value.IsObject() || field.value.IsArray()) + { + ScopedStackedString entryName(element, AZStd::string_view(field.name.GetString(), field.name.GetStringLength())); + ResultCode result = ResolveImports(field.value, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + } + else if(jsonDoc.IsArray()) + { + int index = 0; + for (rapidjson::Value::ValueIterator elem = jsonDoc.Begin(); elem != jsonDoc.End(); ++elem, ++index) + { + if (!elem->IsObject() && !elem->IsArray()) + { + continue; + } + ScopedStackedString entryName(element, index); + ResultCode result = ResolveImports(*elem, allocator, importPathStack, settings, element); + if (result.GetOutcome() == Outcomes::Catastrophic) + { + return result; + } + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode JsonImportResolver::RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (jsonDoc.IsObject() || jsonDoc.IsArray()) + { + const BaseJsonImporter::ImportDirectivesList& importDirectives = settings.m_importer->GetImportDirectives(); + for (auto& import : importDirectives) + { + rapidjson::Pointer importPtr = import.first; + rapidjson::Value* currentValue = importPtr.Get(jsonDoc); + + rapidjson::Value importedValue(rapidjson::kObjectType); + importedValue.AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(import.second.c_str()), allocator); + ResultCode resolveResult = JsonSerialization::ResolveImports(importedValue, allocator, settings); + if (resolveResult.GetOutcome() == Outcomes::Catastrophic) + { + return resolveResult; + } + + rapidjson::Value patch; + settings.m_importer->CreatePatch(patch, importedValue, *currentValue, allocator); + settings.m_importer->RestoreImport(currentValue, patch, allocator, import.second); + } + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + auto importedObject = JsonSerializationUtils::ReadJsonFile(importedFilePath.Native()); + if (importedObject.IsSuccess()) + { + rapidjson::Value& importedDoc = importedObject.GetValue(); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + } + else + { + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, const AZStd::string& importFilename) + { + using namespace JsonSerializationResult; + + importPtr->SetObject(); + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + rapidjson::Value importDirective(rapidjson::kObjectType); + importDirective.AddMember(rapidjson::StringRef("filename"), rapidjson::StringRef(importFilename.c_str()), allocator); + importDirective.AddMember(rapidjson::StringRef("patch"), patch, allocator); + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), importDirective, allocator); + } + else + { + importPtr->AddMember(rapidjson::StringRef(JsonSerialization::ImportDirectiveIdentifier), rapidjson::StringRef(importFilename.c_str()), allocator); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator) + { + using namespace JsonSerializationResult; + + if ((patch.IsObject() && patch.MemberCount() > 0) || (patch.IsArray() && !patch.Empty())) + { + return AZ::JsonSerialization::ApplyPatch(target, allocator, patch, JsonMergeApproach::JsonMergePatch); + } + + return ResultCode(Tasks::Import, Outcomes::Success); + } + + JsonSerializationResult::ResultCode BaseJsonImporter::CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator) + { + return JsonSerialization::CreatePatch(patch, allocator, source, target, JsonMergeApproach::JsonMergePatch); + } + + void BaseJsonImporter::AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile) + { + m_importDirectives.emplace_back(jsonPtr, AZStd::move(importFile)); + } + + void BaseJsonImporter::AddImportedFile(AZStd::string importedFile) + { + m_importedFiles.insert(AZStd::move(importedFile)); + } + + const BaseJsonImporter::ImportDirectivesList& BaseJsonImporter::GetImportDirectives() + { + return m_importDirectives; + } + + const BaseJsonImporter::ImportedFilesList& BaseJsonImporter::GetImportedFiles() + { + return m_importedFiles; + } +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h new file mode 100644 index 0000000000..ef53e265d8 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonImporter.h @@ -0,0 +1,108 @@ +/* + * 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 +#include + +namespace AZ +{ + struct JsonImportSettings; + + class BaseJsonImporter + { + public: + AZ_RTTI(BaseJsonImporter, "{7B225807-7B43-430F-8B11-C794DCF5ACA5}"); + + using ImportDirectivesList = AZStd::vector>; + using ImportedFilesList = AZStd::unordered_set; + + virtual JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode RestoreImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator, + const AZStd::string& importFilename); + + virtual JsonSerializationResult::ResultCode ApplyPatch(rapidjson::Value& target, + const rapidjson::Value& patch, rapidjson::Document::AllocatorType& allocator); + + virtual JsonSerializationResult::ResultCode CreatePatch(rapidjson::Value& patch, + const rapidjson::Value& source, const rapidjson::Value& target, + rapidjson::Document::AllocatorType& allocator); + + void AddImportDirective(const rapidjson::Pointer& jsonPtr, AZStd::string importFile); + const ImportDirectivesList& GetImportDirectives(); + + void AddImportedFile(AZStd::string importedFile); + const ImportedFilesList& GetImportedFiles(); + + virtual ~BaseJsonImporter() = default; + + protected: + + ImportDirectivesList m_importDirectives; + ImportedFilesList m_importedFiles; + }; + + enum class ImportTracking : AZ::u8 + { + None = 0, + Dependencies = (1<<0), + Imports = (1<<1), + All = (Dependencies | Imports) + }; + AZ_DEFINE_ENUM_BITWISE_OPERATORS(ImportTracking); + + class JsonImportResolver final + { + public: + + using ImportPathStack = AZStd::vector; + + JsonImportResolver() = delete; + JsonImportResolver& operator=(const JsonImportResolver& rhs) = delete; + JsonImportResolver& operator=(JsonImportResolver&& rhs) = delete; + JsonImportResolver(const JsonImportResolver& rhs) = delete; + JsonImportResolver(JsonImportResolver&& rhs) = delete; + ~JsonImportResolver() = delete; + + static JsonSerializationResult::ResultCode ResolveImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, StackedString& element); + + static JsonSerializationResult::ResultCode RestoreImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + private: + + static JsonSerializationResult::ResultCode ResolveNestedImports(rapidjson::Value& jsonDoc, + rapidjson::Document::AllocatorType& allocator, ImportPathStack& importPathStack, + JsonImportSettings& settings, const AZ::IO::FixedMaxPath& importPath, StackedString& element); + }; + + + struct JsonImportSettings final + { + JsonSerializationResult::JsonIssueCallback m_reporting; + + BaseJsonImporter* m_importer = nullptr; + + ImportTracking m_resolveFlags = ImportTracking::All; + + AZ::IO::FixedMaxPath m_loadedJsonPath; + }; +} // namespace AZ diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp index 45549b8078..a9b2d2fefa 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonMerger.cpp @@ -706,7 +706,7 @@ namespace AZ rapidjson::Value(rapidjson::kNullType), field.value, element, settings); } - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -717,6 +717,10 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } } // Do an extra pass to find all the fields that are removed. @@ -751,7 +755,7 @@ namespace AZ rapidjson::Value value; ResultCode result = CreateMergePatchInternal(value, allocator, rapidjson::Value(rapidjson::kNullType), field.value, element, settings); - if (result.GetOutcome() == Outcomes::Success) + if (result.GetOutcome() == Outcomes::Success || result.GetOutcome() == Outcomes::PartialDefaults) { rapidjson::Value name; name.CopyFrom(field.name, allocator, true); @@ -762,11 +766,20 @@ namespace AZ { return result; } + else + { + resultCode.Combine(result); + } + } + + if (target.MemberCount() == 0) + { + resultCode.Combine(settings.m_reporting("Added empty object to JSON Merge Patch.", + ResultCode(Tasks::CreatePatch, Outcomes::Success), element)); } } patch = AZStd::move(resultValue); - resultCode.Combine(ResultCode(Tasks::CreatePatch, Outcomes::Success)); return resultCode; } else diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp index bc07f684f6..db76e46f2b 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -19,11 +20,6 @@ namespace AZ { - const char* JsonSerialization::TypeIdFieldIdentifier = "$type"; - const char* JsonSerialization::DefaultStringIdentifier = "{}"; - const char* JsonSerialization::KeyFieldIdentifier = "Key"; - const char* JsonSerialization::ValueFieldIdentifier = "Value"; - namespace JsonSerializationInternal { template @@ -394,6 +390,60 @@ namespace AZ } } + JsonSerializationResult::ResultCode JsonSerialization::ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + JsonImportResolver::ImportPathStack importPathStack; + importPathStack.push_back(settings.m_loadedJsonPath); + StackedString element(StackedString::Format::JsonPointer); + + return JsonImportResolver::ResolveImports(jsonDoc, allocator, importPathStack, settings, element); + } + + JsonSerializationResult::ResultCode JsonSerialization::RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings) + { + using namespace JsonSerializationResult; + + if (settings.m_importer == nullptr) + { + AZ_Assert(false, "Importer object needs to be provided"); + return ResultCode(Tasks::Import, Outcomes::Catastrophic); + } + + AZStd::string scratchBuffer; + auto issueReportingCallback = [&scratchBuffer](AZStd::string_view message, ResultCode result, AZStd::string_view target) -> ResultCode + { + return JsonSerialization::DefaultIssueReporter(scratchBuffer, message, result, target); + }; + if (!settings.m_reporting) + { + settings.m_reporting = issueReportingCallback; + } + + settings.m_resolveFlags = ImportTracking::None; + + return JsonImportResolver::RestoreImports(jsonDoc, allocator, settings); + } + JsonSerializationResult::ResultCode JsonSerialization::DefaultIssueReporter(AZStd::string& scratchBuffer, AZStd::string_view message, JsonSerializationResult::ResultCode result, AZStd::string_view path) { diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h index c85847ac78..d961953a1d 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerialization.h @@ -18,6 +18,8 @@ namespace AZ { class BaseJsonSerializer; + + struct JsonImportSettings; enum class JsonMergeApproach { @@ -51,10 +53,11 @@ namespace AZ class JsonSerialization final { public: - static const char* TypeIdFieldIdentifier; - static const char* DefaultStringIdentifier; - static const char* KeyFieldIdentifier; - static const char* ValueFieldIdentifier; + static constexpr const char* TypeIdFieldIdentifier = "$type"; + static constexpr const char* DefaultStringIdentifier = "{}"; + static constexpr const char* KeyFieldIdentifier = "Key"; + static constexpr const char* ValueFieldIdentifier = "Value"; + static constexpr const char* ImportDirectiveIdentifier = "$import"; //! Merges two json values together by applying "patch" to "target" using the selected merge algorithm. //! This version of ApplyPatch is destructive to "target". If the patch can't be correctly applied it will @@ -284,6 +287,22 @@ namespace AZ //! @return An enum containing less, equal or greater. In case of an error, the value for the enum will "error". static JsonSerializerCompareResult Compare(const rapidjson::Value& lhs, const rapidjson::Value& rhs); + //! Resolves all import directives, including nested imports, in the given document. An importer object needs to be passed + //! in through the settings. + //! @param jsonDoc The json document in which to resolve imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are resolved. + static JsonSerializationResult::ResultCode ResolveImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + + //! Restores all import directives that were present in the json document. The same importer object that was + //! passed into ResolveImports through the settings needs to be passed here through settings as well. + //! @param jsonDoc The json document in which to restore imports. + //! @param allocator The allocator associated with the json document. + //! @param settings Additional settings that control the way the imports are restored. + static JsonSerializationResult::ResultCode RestoreImports( + rapidjson::Value& jsonDoc, rapidjson::Document::AllocatorType& allocator, JsonImportSettings& settings); + private: JsonSerialization() = delete; ~JsonSerialization() = delete; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp index 7e84aced7b..822c1c43d5 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.cpp @@ -69,6 +69,9 @@ namespace AZ case Tasks::CreatePatch: target.append("a create patch operation "); break; + case Tasks::Import: + target.append("an import operation"); + break; default: target.append("an unknown operation "); break; diff --git a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h index 8590971a1c..204c40b8ca 100644 --- a/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h +++ b/Code/Framework/AzCore/AzCore/Serialization/Json/JsonSerializationResult.h @@ -32,7 +32,8 @@ namespace AZ ReadField, //!< Task to read a field from JSON to a value. WriteValue, //!< Task to write a value to a JSON field. Merge, //!< Task to merge two JSON values/documents together. - CreatePatch //!< Task to create a patch to transform one value/document to another. + CreatePatch, //!< Task to create a patch to transform one value/document to another. + Import //!< Task to import a JSON document. }; //! Describes how the task was processed. diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index 4d95ddf098..41229429f2 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -522,6 +522,8 @@ set(FILES Serialization/Json/IntSerializer.cpp Serialization/Json/JsonDeserializer.h Serialization/Json/JsonDeserializer.cpp + Serialization/Json/JsonImporter.cpp + Serialization/Json/JsonImporter.h Serialization/Json/JsonMerger.h Serialization/Json/JsonMerger.cpp Serialization/Json/JsonSerialization.h diff --git a/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp new file mode 100644 index 0000000000..5121efa5e3 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Serialization/Json/TestCases_Importing.cpp @@ -0,0 +1,413 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace JsonSerializationTests +{ + class JsonImportingTests; + + class JsonImporterCustom + : public AZ::BaseJsonImporter + { + public: + AZ_RTTI(JsonImporterCustom, "{003F5896-71E0-4A50-A14F-08C319B06AD0}"); + + + AZ::JsonSerializationResult::ResultCode ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, + const AZ::IO::FixedMaxPath& importedFilePath, rapidjson::Document::AllocatorType& allocator) override; + + JsonImporterCustom(JsonImportingTests* tests) + { + testClass = tests; + } + + private: + JsonImportingTests* testClass; + }; + + class JsonImportingTests + : public BaseJsonSerializerFixture + { + public: + void SetUp() override + { + BaseJsonSerializerFixture::SetUp(); + } + + void TearDown() override + { + BaseJsonSerializerFixture::TearDown(); + } + + void GetTestDocument(const AZStd::string& docName, rapidjson::Document& out) + { + const char *objectJson = R"({ + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + })"; + + const char *arrayJson = R"([ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ])"; + + const char *nestedImportJson = R"({ + "desc" : "Nested Import", + "obj" : {"$import" : "object.json"} + })"; + + const char *nestedImportCycle1Json = R"({ + "desc" : "Nested Import Cycle 1", + "obj" : {"$import" : "nested_import_c2.json"} + })"; + + const char *nestedImportCycle2Json = R"({ + "desc" : "Nested Import Cycle 2", + "obj" : {"$import" : "nested_import_c1.json"} + })"; + + if (docName.compare("object.json") == 0) + { + out.Parse(objectJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("array.json") == 0) + { + out.Parse(arrayJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import.json") == 0) + { + out.Parse(nestedImportJson); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c1.json") == 0) + { + out.Parse(nestedImportCycle1Json); + ASSERT_FALSE(out.HasParseError()); + } + else if (docName.compare("nested_import_c2.json") == 0) + { + out.Parse(nestedImportCycle2Json); + ASSERT_FALSE(out.HasParseError()); + } + } + + protected: + void TestImportLoadStore(const char* input, const char* expectedImportedValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + rapidjson::Document expectedOutcome; + expectedOutcome.Parse(expectedImportedValue); + ASSERT_FALSE(expectedOutcome.HasParseError()); + + TestResolveImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutcome.GetObject()); + + rapidjson::Document originalInput; + originalInput.Parse(input); + ASSERT_FALSE(originalInput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), originalInput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestImportCycle(const char* input) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + AZ::JsonSerializationResult::ResultCode result = TestResolveImports(importerObj); + + EXPECT_EQ(result.GetOutcome(), AZ::JsonSerializationResult::Outcomes::Catastrophic); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + void TestInsertNewImport(const char* input, const char* expectedRestoredValue) + { + m_jsonDocument->Parse(input); + ASSERT_FALSE(m_jsonDocument->HasParseError()); + + JsonImporterCustom* importerObj = new JsonImporterCustom(this); + + TestResolveImports(importerObj); + + importerObj->AddImportDirective(rapidjson::Pointer("/object_2"), "object.json"); + + rapidjson::Document expectedOutput; + expectedOutput.Parse(expectedRestoredValue); + ASSERT_FALSE(expectedOutput.HasParseError()); + + TestRestoreImports(importerObj); + + Expect_DocStrEq(m_jsonDocument->GetObject(), expectedOutput.GetObject()); + + m_jsonDocument->SetObject(); + delete importerObj; + } + + AZ::JsonSerializationResult::ResultCode TestResolveImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::ResolveImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + + AZ::JsonSerializationResult::ResultCode TestRestoreImports(JsonImporterCustom* importerObj) + { + AZ::JsonImportSettings settings; + settings.m_importer = importerObj; + + return AZ::JsonSerialization::RestoreImports(m_jsonDocument->GetObject(), m_jsonDocument->GetAllocator(), settings); + } + }; + + AZ::JsonSerializationResult::ResultCode JsonImporterCustom::ResolveImport(rapidjson::Value* importPtr, + rapidjson::Value& patch, const rapidjson::Value& importDirective, const AZ::IO::FixedMaxPath& importedFilePath, + rapidjson::Document::AllocatorType& allocator) + { + AZ::JsonSerializationResult::ResultCode resultCode(AZ::JsonSerializationResult::Tasks::Import); + + rapidjson::Document importedDoc; + testClass->GetTestDocument(importedFilePath.String(), importedDoc); + + if (importDirective.IsObject()) + { + auto patchField = importDirective.FindMember("patch"); + if (patchField != importDirective.MemberEnd()) + { + patch.CopyFrom(patchField->value, allocator); + } + } + + importPtr->CopyFrom(importedDoc, allocator); + + return resultCode; + } + + // Test Cases + + TEST_F(JsonImportingTests, ImportSimpleObjectTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": {"$import" : "object.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleObjectPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_2" : "patched_value" } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object": { + "field_1" : "value_1", + "field_2" : "patched_value", + "field_3" : "value_3" + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": {"$import" : "array.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "value_1" }, + { "element_2" : "value_2" }, + { "element_3" : "value_3" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, ImportSimpleArrayPatchTest) + { + const char* inputFile = R"( + { + "name" : "simple_array_import", + "object": { + "$import" : { + "filename" : "array.json", + "patch" : [ { "element_1" : "patched_value" } ] + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_array_import", + "object": [ + { "element_1" : "patched_value" } + ] + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": {"$import" : "nested_import.json"} + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportPatchTest) + { + const char* inputFile = R"( + { + "name" : "nested_import", + "object": { + "$import" : { + "filename" : "nested_import.json", + "patch" : { "obj" : { "field_3" : "patched_value" } } + } + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "nested_import", + "object": { + "desc" : "Nested Import", + "obj" : { + "field_1" : "value_1", + "field_2" : "value_2", + "field_3" : "patched_value" + } + } + } + )"; + + TestImportLoadStore(inputFile, expectedOutput); + } + + TEST_F(JsonImportingTests, NestedImportCycleTest) + { + const char* inputFile = R"( + { + "name" : "nested_import_cycle", + "object": {"$import" : "nested_import_c1.json"} + } + )"; + + TestImportCycle(inputFile); + } + + TEST_F(JsonImportingTests, InsertNewImportTest) + { + const char* inputFile = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "field_1" : "other_value", + "field_2" : "value_2", + "field_3" : "value_3" + } + } + )"; + + const char* expectedOutput = R"( + { + "name" : "simple_object_import", + "object_1": {"$import" : "object.json"}, + "object_2": { + "$import" : { + "filename" : "object.json", + "patch" : { "field_1" : "other_value" } + } + } + } + )"; + + TestInsertNewImport(inputFile, expectedOutput); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index cc6000209f..d39595c45e 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -121,6 +121,7 @@ set(FILES Serialization/Json/TestCases_Classes.cpp Serialization/Json/TestCases_Compare.cpp Serialization/Json/TestCases_Enum.cpp + Serialization/Json/TestCases_Importing.cpp Serialization/Json/TestCases_Patching.cpp Serialization/Json/TestCases_Pointers.h Serialization/Json/TestCases_Pointers.cpp diff --git a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp index 864bc3f52a..7581986950 100644 --- a/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp +++ b/Code/Framework/AzTest/AzTest/Platform/Mac/Platform_Mac.cpp @@ -7,6 +7,7 @@ */ #include #include +#include #include #include @@ -20,11 +21,16 @@ public: explicit ModuleHandle(const std::string& lib) : m_libHandle(nullptr) { - std::string libext = lib; - if (!AZ::Test::EndsWith(libext, ".dylib")) + AZ::IO::FixedMaxPath libext = AZStd::string_view{ lib.c_str(), lib.size() }; + if (!libext.Stem().Native().starts_with(AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX)) { - libext += ".dylib"; + libext = AZ_TRAIT_OS_DYNAMIC_LIBRARY_PREFIX + libext.Native(); } + if (libext.Extension() != AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION) + { + libext.Native() += AZ_TRAIT_OS_DYNAMIC_LIBRARY_EXTENSION; + } + m_libHandle = dlopen(libext.c_str(), RTLD_NOW); const char* error = dlerror(); if (error)