diff --git a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.cpp b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.cpp new file mode 100644 index 0000000000..4abeeaf07e --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.cpp @@ -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 + * + */ + +#include + +#include + +namespace AZ::DOM +{ + Visitor::Result JsonBackend::ReadFromStringInPlace(AZStd::string& buffer, Visitor* visitor) + { + return Json::VisitSerializedJsonInPlace(buffer, visitor); + } + + Visitor::Result JsonBackend::ReadFromString(AZStd::string_view buffer, Lifetime lifetime, Visitor* visitor) + { + return Json::VisitSerializedJson(buffer, lifetime, visitor); + } + + AZStd::unique_ptr JsonBackend::CreateStreamWriter(AZ::IO::GenericStream* stream) + { + return Json::GetJsonStreamWriter(stream, Json::OutputFormatting::PrettyPrintedJson); + } + + void JsonBackend::Register() + { + if (auto backendRegistry = BackendRegistry::Get()) + { + backendRegistry->RegisterBackend(kName, {kExtension}); + } + } +} diff --git a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.h b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.h new file mode 100644 index 0000000000..5e93ece1de --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonBackend.h @@ -0,0 +1,27 @@ +/* + * 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 + +namespace AZ::DOM +{ + class JsonBackend final : public Backend + { + public: + Visitor::Result ReadFromStringInPlace(AZStd::string& buffer, Visitor* visitor) override; + Visitor::Result ReadFromString(AZStd::string_view buffer, Lifetime lifetime, Visitor* visitor) override; + AZStd::unique_ptr CreateStreamWriter(AZ::IO::GenericStream* stream) override; + + static constexpr const char* kName = "JSON"; + static constexpr const char* kExtension = ".json"; + static void Register(); + }; +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp new file mode 100644 index 0000000000..302fd7bd63 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.cpp @@ -0,0 +1,675 @@ +/* + * 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 +#include +#include +#include +#include +#include + +namespace AZ::DOM::Json +{ + // + // class DocumentWriter + // + // Visitor that produces a rapidjson::Document + class DocumentWriter final : public Visitor + { + public: + VisitorFlags GetVisitorFlags() const override + { + return VisitorFlags::SupportsRawKeys | VisitorFlags::SupportsArrays | VisitorFlags::SupportsObjects; + } + + Result Null() override + { + CurrentValue().SetNull(); + return FinishWrite(); + } + + Result Bool(bool value) override + { + CurrentValue().SetBool(value); + return FinishWrite(); + } + + Result Int64(AZ::s64 value) override + { + CurrentValue().SetInt64(value); + return FinishWrite(); + } + + Result Uint64(AZ::u64 value) override + { + CurrentValue().SetUint64(value); + return FinishWrite(); + } + + Result Double(double value) override + { + CurrentValue().SetDouble(value); + return FinishWrite(); + } + + Result String(AZStd::string_view value, Lifetime lifetime) override + { + if (lifetime == Lifetime::Temporary) + { + CurrentValue().SetString(value.data(), static_cast(value.length()), m_result.GetAllocator()); + } + else + { + CurrentValue().SetString(value.data(), static_cast(value.size())); + } + return FinishWrite(); + } + + Result StartObject() override + { + CurrentValue().SetObject(); + + const bool isObject = true; + m_entryStack.emplace_front(isObject, CurrentValue()); + return VisitorSuccess(); + } + + Result EndObject(AZ::u64 attributeCount) override + { + if (m_entryStack.empty()) + { + return VisitorFailure(VisitorErrorCode::InternalError, "EndObject called without a matching BeginObject call"); + } + + if (!m_entryStack.front().m_isObject) + { + return VisitorFailure(VisitorErrorCode::InternalError, "Expected EndArray and received EndObject instead"); + } + + if (m_entryStack.front().m_entryCount != attributeCount) + { + return FormatVisitorFailure( + VisitorErrorCode::InternalError, "EndObject: Expected %lu attributes but received %lu attributes instead", + attributeCount, m_entryStack.front().m_entryCount); + } + + m_entryStack.pop_front(); + return FinishWrite(); + } + + Result Key(AZ::Name key) override + { + return RawKey(key.GetStringView(), Lifetime::Persistent); + } + + Result RawKey(AZStd::string_view key, Lifetime lifetime) override + { + AZ_Assert(!m_entryStack.empty(), "Attempmted to push a key with no object"); + AZ_Assert(m_entryStack.front().m_isObject, "Attempted to push a key to an array"); + if (lifetime == Lifetime::Persistent) + { + m_entryStack.front().m_key.SetString(key.data(), static_cast(key.size())); + } + else + { + m_entryStack.front().m_key.SetString(key.data(), static_cast(key.size()), m_result.GetAllocator()); + } + return VisitorSuccess(); + } + + Result StartArray() override + { + CurrentValue().SetArray(); + + const bool isObject = false; + m_entryStack.emplace_front(isObject, CurrentValue()); + return VisitorSuccess(); + } + + Result EndArray(AZ::u64 elementCount) override + { + if (m_entryStack.empty()) + { + return VisitorFailure(VisitorErrorCode::InternalError, "EndArray called without a matching BeginArray call"); + } + + if (m_entryStack.front().m_isObject) + { + return VisitorFailure(VisitorErrorCode::InternalError, "Expected EndObject and received EndArray instead"); + } + + if (m_entryStack.front().m_entryCount != elementCount) + { + return FormatVisitorFailure( + VisitorErrorCode::InternalError, "EndArray: Expected %lu elements but received %lu elements instead", elementCount, + m_entryStack.front().m_entryCount); + } + + m_entryStack.pop_front(); + return FinishWrite(); + } + + rapidjson::Document&& TakeDocument() + { + return AZStd::move(m_result); + } + + private: + Result FinishWrite() + { + if (m_entryStack.empty()) + { + return VisitorSuccess(); + } + + // Retrieve the top value of the stack and replace it with a null value + rapidjson::Value value; + m_entryStack.front().m_value.Swap(value); + ++m_entryStack.front().m_entryCount; + + if (m_entryStack.front().m_key.IsString()) + { + m_entryStack.front().m_container.AddMember(m_entryStack.front().m_key.Move(), AZStd::move(value), m_result.GetAllocator()); + m_entryStack.front().m_key.SetNull(); + } + else + { + m_entryStack.front().m_container.PushBack(AZStd::move(value), m_result.GetAllocator()); + } + + return VisitorSuccess(); + } + + rapidjson::Value& CurrentValue() + { + if (m_entryStack.empty()) + { + return m_result; + } + return m_entryStack.front().m_value; + } + + struct ValueInfo + { + ValueInfo(bool isObject, rapidjson::Value& container) + : m_isObject(isObject) + , m_container(container) + { + } + + bool m_isObject; + rapidjson::Value& m_container; + rapidjson::Value m_value; + AZ::u64 m_entryCount = 0; + rapidjson::Value m_key; + }; + + rapidjson::Document m_result; + AZStd::deque m_entryStack; + AZ::u64 m_entryCount = 0; + }; + + // + // class StreamWriter + // + // Visitor that writes to a rapidjson::Writer + template + class StreamWriter : public Visitor + { + public: + StreamWriter(AZ::IO::GenericStream* stream) + : m_streamWriter(stream) + , m_writer(TWriter(m_streamWriter)) + { + } + + VisitorFlags GetVisitorFlags() const override + { + return VisitorFlags::SupportsRawKeys | VisitorFlags::SupportsArrays | VisitorFlags::SupportsObjects; + } + + Result Null() override + { + return CheckWrite(m_writer.Null()); + } + + Result Bool(bool value) override + { + return CheckWrite(m_writer.Bool(value)); + } + + Result Int64(AZ::s64 value) override + { + return CheckWrite(m_writer.Int64(value)); + } + + Result Uint64(AZ::u64 value) override + { + return CheckWrite(m_writer.Uint64(value)); + } + + Result Double(double value) override + { + return CheckWrite(m_writer.Double(value)); + } + + Result String(AZStd::string_view value, Lifetime lifetime) override + { + const bool shouldCopy = lifetime == Lifetime::Temporary; + return CheckWrite(m_writer.String(value.data(), static_cast(value.size()), shouldCopy)); + } + + Result StartObject() override + { + return CheckWrite(m_writer.StartObject()); + } + + Result EndObject(AZ::u64 attributeCount) override + { + return CheckWrite(m_writer.EndObject(static_cast(attributeCount))); + } + + Result Key(AZ::Name key) override + { + return RawKey(key.GetStringView(), Lifetime::Persistent); + } + + Result RawKey(AZStd::string_view key, Lifetime lifetime) override + { + const bool shouldCopy = lifetime == Lifetime::Temporary; + return CheckWrite(m_writer.Key(key.data(), static_cast(key.size()), shouldCopy)); + } + + Result StartArray() override + { + return CheckWrite(m_writer.StartArray()); + } + + Result EndArray(AZ::u64 elementCount) override + { + return CheckWrite(m_writer.EndArray(static_cast(elementCount))); + } + + private: + Result CheckWrite(bool writeSucceeded) + { + if (!writeSucceeded) + { + return VisitorFailure(VisitorErrorCode::InternalError, "Failed to write JSON"); + } + return VisitorSuccess(); + } + + AZ::IO::RapidJSONStreamWriter m_streamWriter; + TWriter m_writer; + }; + + // + // struct JsonReadHandler + // + // Handler for a rapidjson::Reader that translates reads into an AZ::DOM::Visitor + struct JsonReadHandler : public rapidjson::BaseReaderHandler, JsonReadHandler> + { + public: + JsonReadHandler(Visitor* visitor, Lifetime stringLifetime) + : m_visitor(visitor) + , m_stringLifetime(stringLifetime) + , m_outcome(AZ::Success()) + { + } + + bool Null() + { + return CheckResult(m_visitor->Null()); + } + + bool Bool(bool b) + { + return CheckResult(m_visitor->Bool(b)); + } + + bool Int(int i) + { + return CheckResult(m_visitor->Int64(static_cast(i))); + } + + bool Uint(unsigned i) + { + return CheckResult(m_visitor->Uint64(static_cast(i))); + } + + bool Int64(int64_t i) + { + return CheckResult(m_visitor->Int64(i)); + } + + bool Uint64(uint64_t i) + { + return CheckResult(m_visitor->Uint64(i)); + } + + bool Double(double d) + { + return CheckResult(m_visitor->Double(d)); + } + + bool RawNumber([[maybe_unused]] const Ch* str, [[maybe_unused]] rapidjson::SizeType length, [[maybe_unused]] bool copy) + { + AZ_Assert(false, "Raw numbers are unsupported"); + return false; + } + + bool String(const Ch* str, rapidjson::SizeType length, bool copy) + { + Lifetime lifetime = m_stringLifetime; + if (!copy) + { + lifetime = Lifetime::Temporary; + } + return CheckResult(m_visitor->String(AZStd::string_view(str, length), lifetime)); + } + + bool StartObject() + { + return CheckResult(m_visitor->StartObject()); + } + + bool Key(const Ch* str, rapidjson::SizeType length, [[maybe_unused]] bool copy) + { + AZStd::string_view key = AZStd::string_view(str, length); + if (!m_visitor->SupportsRawKeys()) + { + m_visitor->Key(AZ::Name(key)); + } + Lifetime lifetime = m_stringLifetime; + if (!copy) + { + lifetime = Lifetime::Temporary; + } + return CheckResult(m_visitor->RawKey(key, lifetime)); + } + + bool EndObject([[maybe_unused]] rapidjson::SizeType memberCount) + { + return CheckResult(m_visitor->EndObject(memberCount)); + } + + bool StartArray() + { + return CheckResult(m_visitor->StartArray()); + } + + bool EndArray([[maybe_unused]] rapidjson::SizeType elementCount) + { + return CheckResult(m_visitor->EndArray(elementCount)); + } + + Visitor::Result&& TakeOutcome() + { + return AZStd::move(m_outcome); + } + + private: + bool CheckResult(Visitor::Result result) + { + if (!result.IsSuccess()) + { + m_outcome = AZStd::move(result); + return false; + } + return true; + } + + Visitor::Result m_outcome; + Visitor* m_visitor; + Lifetime m_stringLifetime; + }; + + // + // struct AzStringStream + // + // rapidjson stream wrapper for AZStd::string suitable for in-situ parsing + struct AzStringStream + { + using Ch = char; + + AzStringStream(AZStd::string& buffer) + { + m_cursor = buffer.data(); + m_begin = m_cursor; + } + + char Peek() const + { + return *m_cursor; + } + + char Take() + { + return *m_cursor++; + } + + size_t Tell() const + { + return static_cast(m_cursor - m_begin); + } + + char* PutBegin() + { + m_write = m_cursor; + return m_cursor; + } + + void Put(char c) + { + (*m_write++) = c; + } + + void Flush() + { + } + + size_t PutEnd(char* begin) + { + return m_write - begin; + } + + const char* Peek4() const + { + AZ_Assert(false, "Not implemented, encoding is hard-coded to UTF-8"); + } + + char* m_cursor; //!< Current read position. + char* m_write; //!< Current write position. + const char* m_begin; //!< Head of string. + }; + + // + // Serialized JSON util functions + // + AZStd::unique_ptr GetJsonStreamWriter(AZ::IO::GenericStream* stream, OutputFormatting format) + { + if (format == OutputFormatting::MinifiedJson) + { + using WriterType = rapidjson::Writer; + return AZStd::make_unique>(stream); + } + else + { + using WriterType = rapidjson::PrettyWriter; + return AZStd::make_unique>(stream); + } + } + + Visitor::Result VisitSerializedJson(AZStd::string_view buffer, Lifetime lifetime, Visitor* visitor) + { + rapidjson::Reader reader; + rapidjson::MemoryStream stream(buffer.data(), buffer.size()); + JsonReadHandler handler(visitor, lifetime); + + constexpr int flags = rapidjson::kParseCommentsFlag; + reader.Parse(stream, handler); + return handler.TakeOutcome(); + } + + Visitor::Result VisitSerializedJsonInPlace(AZStd::string& buffer, Visitor* visitor) + { + rapidjson::Reader reader; + AzStringStream stream(buffer); + JsonReadHandler handler(visitor, Lifetime::Persistent); + + constexpr int flags = rapidjson::kParseCommentsFlag | rapidjson::kParseInsituFlag; + reader.Parse(stream, handler); + return handler.TakeOutcome(); + } + + // + // In-memory rapidjson util functions + // + AZ::Outcome WriteToRapidJsonDocument(Backend::WriteCallback writeCallback) + { + DocumentWriter writer; + auto result = writeCallback(&writer); + if (!result.IsSuccess()) + { + return AZ::Failure(result.TakeError().FormatVisitorErrorMessage()); + } + return AZ::Success(writer.TakeDocument()); + } + + Visitor::Result VisitRapidJsonValue(const rapidjson::Value& value, Visitor* visitor, Lifetime lifetime) + { + enum class EndMarker + { + EndArray, + EndObject + }; + + // Processing stack consists of values comprised of one of a: + // - rapidjson::Value to process + // - EndMarker denoting the end of an array or object + // - string denoting a key at the beginning of a key/value pair + using Entry = AZStd::variant; + AZStd::stack entryStack; + AZStd::stack entryCountStack; + entryStack.push(&value); + + while (!entryStack.empty()) + { + const auto currentEntry = entryStack.top(); + entryStack.pop(); + + if (AZStd::holds_alternative(currentEntry)) + { + EndMarker marker = AZStd::get(currentEntry); + if (marker == EndMarker::EndArray) + { + visitor->EndArray(entryCountStack.top()); + } + else + { + visitor->EndObject(entryCountStack.top()); + } + entryCountStack.pop(); + continue; + } + + if (AZStd::holds_alternative(currentEntry)) + { + AZStd::string_view key = AZStd::get(currentEntry); + if (visitor->SupportsRawKeys()) + { + visitor->RawKey(key, lifetime); + } + else + { + visitor->Key(AZ::Name(key)); + } + continue; + } + + const rapidjson::Value& currentValue = *AZStd::get(currentEntry); + if (!entryCountStack.empty()) + { + ++entryCountStack.top(); + } + + Visitor::Result result = AZ::Success(); + + switch (currentValue.GetType()) + { + case rapidjson::kNullType: + result = visitor->Null(); + break; + case rapidjson::kFalseType: + result = visitor->Bool(false); + break; + case rapidjson::kTrueType: + result = visitor->Bool(true); + break; + case rapidjson::kObjectType: + entryStack.push(EndMarker::EndObject); + entryCountStack.push(0); + result = visitor->StartObject(); + for (auto it = currentValue.MemberEnd(); it != currentValue.MemberBegin(); --it) + { + auto entry = (it - 1); + const AZStd::string_view key(entry->name.GetString(), static_cast(entry->name.GetStringLength())); + entryStack.push(&entry->value); + entryStack.push(key); + } + break; + case rapidjson::kArrayType: + entryStack.push(EndMarker::EndArray); + entryCountStack.push(0); + result = visitor->StartArray(); + for (auto it = currentValue.End(); it != currentValue.Begin(); --it) + { + auto entry = (it - 1); + entryStack.push(entry); + } + break; + case rapidjson::kStringType: + result = visitor->String( + AZStd::string_view(currentValue.GetString(), static_cast(currentValue.GetStringLength())), lifetime); + break; + case rapidjson::kNumberType: + if (currentValue.IsFloat() || currentValue.IsDouble()) + { + result = visitor->Double(currentValue.GetDouble()); + } + else if (currentValue.IsInt64() || currentValue.IsInt()) + { + result = visitor->Int64(currentValue.GetInt64()); + } + else + { + result = visitor->Uint64(currentValue.GetUint64()); + } + break; + default: + return AZ::Failure(VisitorError(VisitorErrorCode::InvalidData, "Value with invalid type specified")); + } + + if (!result.IsSuccess()) + { + return result; + } + } + + return AZ::Success(); + } +} // namespace AZ::DOM::Json diff --git a/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.h b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.h new file mode 100644 index 0000000000..323e0b880c --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/Backends/JSON/JsonSerializationUtils.h @@ -0,0 +1,60 @@ +/* + * 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::DOM::Json +{ + //! Specifies how JSON should be formatted when serialized. + enum class OutputFormatting + { + MinifiedJson, //!< Formats JSON in compact minified form, focusing on minimizing output size. + PrettyPrintedJson, //!< Formats JSON in a pretty printed form, focusing on legibility to readers. + }; + + //! Creates a Visitor that will write serialized JSON to the specified stream. + //! \param stream The stream the visitor will write to. + //! \param format The format to write in. + //! \return A Visitor that will write to stream when visited. + AZStd::unique_ptr GetJsonStreamWriter( + AZ::IO::GenericStream* stream, OutputFormatting format = OutputFormatting::PrettyPrintedJson); + //! Reads serialized JSON from a string and applies it to a visitor. + //! \param buffer The UTF-8 serialized JSON to read. + //! \param lifetime Specifies the lifetime of the specified buffer. If the string specified by buffer might be deallocated, + //! ensure specify Lifetime::Temporary is specified. + //! \param visitor The visitor to visit with the JSON buffer's contents. + //! \return The aggregate result specifying whether the visitor operations were successful. + Visitor::Result VisitSerializedJson(AZStd::string_view buffer, Lifetime lifetime, Visitor* visitor); + //! Reads serialized JSON from a string in-place and applies it to a visitor. + //! \param buffer The UTF-8 serialized JSON to read. This buffer will be modified as part of the deserialization process to + //! apply null terminators. + //! \param visitor The visitor to visit with the JSON buffer's contents. The strings provided to the visitor will only + //! be valid for the lifetime of buffer. + //! \return The aggregate result specifying whether the visitor operations were successful. + Visitor::Result VisitSerializedJsonInPlace(AZStd::string& buffer, Visitor* visitor); + + //! Takes a visitor specified by a callback and produces a rapidjson::Document. + //! \param writeCallback A callback specifying a visitor to accept to build the resulting document. + //! \return An outcome with either the rapidjson::Document or an error message. + AZ::Outcome WriteToRapidJsonDocument(Backend::WriteCallback writeCallback); + //! Accepts a visitor with the contents of a rapidjson::Value. + //! \param value The rapidjson::Value to apply to visitor. + //! \param visitor The visitor to receive the contents of value. + //! \param lifetime The lifetime to specify for visiting strings. If the rapidjson::Value might be destroyed or changed + //! before the visitor is finished using these values, Lifetime::Temporary should be specified. + //! \return The aggregate result specifying whether the visitor operations were successful. + Visitor::Result VisitRapidJsonValue(const rapidjson::Value& value, Visitor* visitor, Lifetime lifetime); +} // namespace AZ::DOM::Json diff --git a/Code/Framework/AzCore/AzCore/DOM/DomBackend.cpp b/Code/Framework/AzCore/AzCore/DOM/DomBackend.cpp new file mode 100644 index 0000000000..f26e34acdf --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomBackend.cpp @@ -0,0 +1,77 @@ +/* + * 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 "DomBackend.h" + +namespace AZ::DOM +{ + Visitor::Result Backend::ReadFromPath(AZ::IO::PathView pathName, Visitor* visitor, size_t maxFileSize) + { + auto readResult = AZ::Utils::ReadFile(pathName.Native(), maxFileSize); + if (!readResult.IsSuccess()) + { + return AZ::Failure(VisitorError(VisitorErrorCode::InternalError, readResult.TakeError())); + } + + AZStd::string fileContents = readResult.TakeValue(); + return ReadFromString(fileContents, Lifetime::Temporary, visitor); + } + + Visitor::Result Backend::ReadFromStream(AZ::IO::GenericStream* stream, Visitor* visitor, size_t maxSize) + { + size_t length = stream->GetLength(); + if (length > maxSize) + { + return AZ::Failure(VisitorError(VisitorErrorCode::InternalError, "Stream is too large.")); + } + AZStd::string buffer; + buffer.resize(maxSize); + stream->Read(length, buffer.data()); + return ReadFromString(buffer, Lifetime::Temporary, visitor); + } + + Visitor::Result Backend::ReadFromStringInPlace(AZStd::string& buffer, Visitor* visitor) + { + return ReadFromString(buffer, Lifetime::Persistent, visitor); + } + + Visitor::Result Backend::WriteToStream(AZ::IO::GenericStream* stream, WriteCallback callback) + { + AZStd::unique_ptr writer = CreateStreamWriter(stream); + return callback(writer.get()); + } + + Visitor::Result Backend::WriteToPath(AZ::IO::PathView pathName, WriteCallback callback) + { + AZStd::string buffer; + auto serializeResult = WriteToString(buffer, callback); + if (!serializeResult.IsSuccess()) + { + return serializeResult; + } + + auto writeResult = AZ::Utils::WriteFile(buffer, pathName.Native()); + if (!writeResult.IsSuccess()) + { + return AZ::Failure(VisitorError(VisitorErrorCode::InternalError, writeResult.TakeError())); + } + + return AZ::Success(); + } + + Visitor::Result Backend::WriteToString(AZStd::string& buffer, WriteCallback callback) + { + AZ::IO::ByteContainerStream stream{&buffer}; + AZStd::unique_ptr writer = CreateStreamWriter(&stream); + return callback(writer.get()); + } +} diff --git a/Code/Framework/AzCore/AzCore/DOM/DomBackend.h b/Code/Framework/AzCore/AzCore/DOM/DomBackend.h new file mode 100644 index 0000000000..bb5c9e40fe --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomBackend.h @@ -0,0 +1,54 @@ +/* + * 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 + +namespace AZ::DOM +{ + //! Backends are registered centrally and used to transition DOM formats to and from a textual format. + class Backend + { + public: + virtual ~Backend() = default; + + //! Attempt to read this format from the given file into the target Visitor. + Visitor::Result ReadFromPath( + AZ::IO::PathView pathName, Visitor* visitor, size_t maxFileSize = AZStd::numeric_limits::max()); + //! Attempt to read this format from the given stream into the target Visitor. + //! The base implementation reads the stream into memory and calls ReadFromString. + virtual Visitor::Result ReadFromStream( + AZ::IO::GenericStream* stream, Visitor* visitor, size_t maxSize = AZStd::numeric_limits::max()); + //! Attempt to read this format from a mutable string into the target Visitor. This enables some backends to + //! parse without making additional string allocations. + //! This string may be modified and read in place without being copied, so when calling this please ensure that: + //! - The string won't be deallocated until the visitor no longer needs the values and + //! - The string is safe to modify in place. + //! The base implementation simply calls ReadFromString. + virtual Visitor::Result ReadFromStringInPlace(AZStd::string& buffer, Visitor* visitor); + //! Attempt to read this format from an immutable buffer in memory into the target Visitor. + virtual Visitor::Result ReadFromString(AZStd::string_view buffer, Lifetime lifetime, Visitor* visitor) = 0; + + //! Acquire a visitor interface for writing to the target output file. + virtual AZStd::unique_ptr CreateStreamWriter(AZ::IO::GenericStream* stream) = 0; + //! A callback that accepts a Visitor, making DOM calls to inform the serializer, and returns an + //! aggregate error code to indicate whether or not the operation succeeded. + using WriteCallback = AZStd::function; + //! Attempt to write a value to a stream using a write callback. + Visitor::Result WriteToStream(AZ::IO::GenericStream* stream, WriteCallback callback); + //! Attempt to write a value to a file using a write callback. + Visitor::Result WriteToPath(AZ::IO::PathView pathName, WriteCallback callback); + //! Attempt to write a value to a string using a write callback. + Visitor::Result WriteToString(AZStd::string& buffer, WriteCallback callback); + }; +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.cpp b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.cpp new file mode 100644 index 0000000000..c8f4050602 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.cpp @@ -0,0 +1,68 @@ +/* + * 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 + +namespace AZ::DOM +{ + BackendRegistry* BackendRegistry::s_instance = nullptr; + + BackendRegistryInterface* BackendRegistry::Get() + { + return AZ::Interface::Get(); + } + + void BackendRegistry::Create() + { + AZ_Assert(!s_instance, "Attempted to register BackendRegistry when it's already registered"); + s_instance = aznew BackendRegistry; + AZ::Interface::Register(s_instance); + } + + void BackendRegistry::Destroy() + { + AZ_Assert(s_instance, "Attempted to unregister a non-existent BackendRegistry"); + AZ::Interface::Unregister(s_instance); + delete s_instance; + s_instance = nullptr; + } + + AZStd::unique_ptr BackendRegistry::GetBackendByName(AZStd::string_view name) + { + auto backendIterator = m_nameToBackend.find(name); + if (backendIterator != m_nameToBackend.end()) + { + return backendIterator->second(); + } + return nullptr; + } + + AZStd::unique_ptr BackendRegistry::GetBackendForExtension(AZStd::string_view extension) + { + auto extensionIterator = m_extensionToName.find(extension); + if (extensionIterator != m_extensionToName.end()) + { + return GetBackendByName(extensionIterator->second); + } + return nullptr; + } + + void BackendRegistry::RegisterBackendInternal( + BackendRegistryInterface::BackendFactory factory, AZStd::string name, AZStd::vector extensions) + { + AZ_Assert(m_nameToBackend.find(name) == m_nameToBackend.end(), "DOM Backend %s already registered", name.c_str()); + m_nameToBackend.insert({name, factory}); + for (AZStd::string& extension : extensions) + { + AZ_Assert(m_extensionToName.find(extension) == m_extensionToName.end(), "DOM Extensions already registered", extension.c_str()); + m_extensionToName.insert({AZStd::move(extension), name}); + } + } +} diff --git a/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.h b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.h new file mode 100644 index 0000000000..c71e6798fc --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistry.h @@ -0,0 +1,44 @@ +/* + * 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 + +namespace AZ::DOM +{ + //! DOM backend registry implementation. + //! \see BackendRegistryInterface + class BackendRegistry final : public BackendRegistryInterface + { + public: + AZ_RTTI(BackendRegistry, "{95533861-6201-4E45-BD03-097A20850C48}", BackendRegistryInterface); + AZ_CLASS_ALLOCATOR(BackendRegistry, AZ::SystemAllocator, 0); + + AZStd::unique_ptr GetBackendByName(AZStd::string_view name) override; + AZStd::unique_ptr GetBackendForExtension(AZStd::string_view extension) override; + + //! Convenience method, gets the current instance of the BackendRegistry via AZ::Interface. + static BackendRegistryInterface* Get(); + //! Creates a singleton BackendRegistry and registers it via AZ::Interface. + static void Create(); + //! Destroys the singleton BackendRegistry created by Create and unregisters it from AZ::Interface. + static void Destroy(); + + protected: + void RegisterBackendInternal(BackendFactory factory, AZStd::string name, AZStd::vector extensions) override; + + private: + using BackendFactory = AZStd::function()>; + AZStd::unordered_map m_nameToBackend; + AZStd::unordered_map m_extensionToName; + + static BackendRegistry* s_instance; + }; +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistryInterface.h b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistryInterface.h new file mode 100644 index 0000000000..4651f35961 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomBackendRegistryInterface.h @@ -0,0 +1,54 @@ +/* + * 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::DOM +{ + //! Central interface for registering and looking up backend types by file extension. + class BackendRegistryInterface + { + public: + AZ_RTTI(BackendRegistryInterface, "{B72A88AC-DF15-4F92-9489-ABCDEA3D94E6}") + using BackendFactory = AZStd::function()>; + + virtual ~BackendRegistryInterface() = default; + + //! Registers a factory for a given backend. + //! \param name A unique name to identify this backend. + //! \param extensions A set of file extensions this backend supports. + //! Extensions should by the entire file suffix including any dots (.), for example: + //! ".json" or ".tar.gz" + template + void RegisterBackend(AZStd::string name, AZStd::vector extensions); + + //! Looks up a DOM backend based on its name. + virtual AZStd::unique_ptr GetBackendByName(AZStd::string_view name) = 0; + + //! Looks up a DOM backend based on a file extension. + virtual AZStd::unique_ptr GetBackendForExtension(AZStd::string_view extension) = 0; + + protected: + //! Registers a factory for a given backend, called by RegisterBackend. + //! \param factory The factory function to create new backend instances. + //! \param name The unique name to identify this backend. + //! \param extensions A set of file extensions this backend supports. + //! \see RegisterBackend + virtual void RegisterBackendInternal(BackendFactory factory, AZStd::string name, AZStd::vector extensions) = 0; + }; + + template + void BackendRegistryInterface::RegisterBackend(AZStd::string name, AZStd::vector extensions) + { + RegisterBackendInternal([](){ + return AZStd::make_unique(); + }, AZStd::move(name), AZStd::move(extensions)); + } +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h index 584cfce4ed..eb6f20ae1b 100644 --- a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h +++ b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h @@ -17,7 +17,7 @@ namespace AZ::DOM { // // Lifetime enum - // + // //! Specifies the period in which a reference value will still be alive and safe to read. enum class Lifetime { @@ -30,7 +30,7 @@ namespace AZ::DOM // // VisitorErrorCode enum - // + // //! Error code specifying the reason a Visitor operation failed. enum class VisitorErrorCode { @@ -75,7 +75,7 @@ namespace AZ::DOM }; //! A type alias for opaque DOM types that aren't meant to be serializable. - //! /see VisitorInterface::OpaqueValue + //! \see VisitorInterface::OpaqueValue using OpaqueType = AZStd::any; // @@ -116,7 +116,7 @@ namespace AZ::DOM //! - \ref Double: 64 bit double precision float //! - \ref Null: sentinel "empty" type with no value representation //! - \ref String: UTF8 encoded string - //! - \ref Object: an ordered container of key/value pairs where keys are AZ::Names and values may be any DOM type + //! - \ref Object: an ordered container of key/value pairs where keys are \ref AZ::Name and values may be any DOM type //! (including Object) //! - \ref Array: an ordered container of values, in which values are any DOM value type (including Array) //! - \ref Node: a container @@ -144,17 +144,17 @@ namespace AZ::DOM //! Raw (\see VisitorFlags::SupportsRawValues) and opaque values (\see VisitorFlags::SupportsOpaqueValues) //! are disallowed by default, as their handling is intended to be implementation-specific. virtual VisitorFlags GetVisitorFlags() const; - //! /see VisitorFlags::SupportsRawValues + //! \see VisitorFlags::SupportsRawValues bool SupportsRawValues() const; - //! /see VisitorFlags::SupportsRawKeys + //! \see VisitorFlags::SupportsRawKeys bool SupportsRawKeys() const; - //! /see VisitorFlags::SupportsObjects + //! \see VisitorFlags::SupportsObjects bool SupportsObjects() const; - //! /see VisitorFlags::SupportsArrays + //! \see VisitorFlags::SupportsArrays bool SupportsArrays() const; - //! /see VisitorFlags::SupportsNodes + //! \see VisitorFlags::SupportsNodes bool SupportsNodes() const; - //! /see VisitorFlags::SupportsOpaqueValues + //! \see VisitorFlags::SupportsOpaqueValues bool SupportsOpaqueValues() const; //! Operates on an empty null value. @@ -231,6 +231,15 @@ namespace AZ::DOM static Result VisitorFailure(VisitorErrorCode code, AZStd::string additionalInfo); //! Helper method, constructs a failure \ref Result with the specified error. static Result VisitorFailure(VisitorError error); + + //! Helper method, constructs a failure \ref Result with the specified code and supplemental info specified by a format string + //! and its arguments. + template + static Result FormatVisitorFailure(VisitorErrorCode code, TArgs... formatArgs) + { + return VisitorFailure(code, AZStd::string::format(formatArgs...)); + } + //! Helper method, constructs a success \ref Result. static Result VisitorSuccess(); }; diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index a5cc3fdcd4..54f2c9ecca 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -125,8 +125,18 @@ set(FILES Debug/TraceMessagesDrillerBus.h Debug/TraceReflection.cpp Debug/TraceReflection.h + DOM/DomAdapter.h + DOM/DomBackend.cpp + DOM/DomBackend.h + DOM/DomBackendRegistry.cpp + DOM/DomBackendRegistry.h + DOM/DomBackendRegistryInterface.h DOM/DomVisitor.cpp DOM/DomVisitor.h + DOM/Backends/JSON/JsonBackend.cpp + DOM/Backends/JSON/JsonBackend.h + DOM/Backends/JSON/JsonSerializationUtils.cpp + DOM/Backends/JSON/JsonSerializationUtils.h Driller/DefaultStringPool.h Driller/Driller.cpp Driller/Driller.h diff --git a/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp b/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp new file mode 100644 index 0000000000..238802a35f --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomJsonBenchmarks.cpp @@ -0,0 +1,184 @@ +/* + * 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 + * + */ + +#if defined(HAVE_BENCHMARK) + +#include +#include +#include +#include +#include +#include + +namespace Benchmark +{ + class DomJsonBenchmark : public UnitTest::AllocatorsBenchmarkFixture + { + public: + void SetUp([[maybe_unused]] const ::benchmark::State& st) override + { + UnitTest::AllocatorsBenchmarkFixture::SetUp(st); + AZ::NameDictionary::Create(); + } + + void SetUp([[maybe_unused]] ::benchmark::State& st) override + { + UnitTest::AllocatorsBenchmarkFixture::SetUp(st); + AZ::NameDictionary::Create(); + } + + void TearDown([[maybe_unused]] ::benchmark::State& st) override + { + AZ::NameDictionary::Destroy(); + UnitTest::AllocatorsBenchmarkFixture::TearDown(st); + } + + void TearDown([[maybe_unused]] const ::benchmark::State& st) override + { + AZ::NameDictionary::Destroy(); + UnitTest::AllocatorsBenchmarkFixture::TearDown(st); + } + + AZStd::string GenerateDomJsonBenchmarkPayload(int64_t entryCount = 100, int64_t stringTemplateLength = 5) + { + rapidjson::Document document; + document.SetObject(); + + AZStd::string entryTemplate; + while (entryTemplate.size() < static_cast(stringTemplateLength)) + { + entryTemplate += "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor "; + } + entryTemplate.resize(stringTemplateLength); + AZStd::string buffer; + + auto createString = [&](int n) -> rapidjson::Value + { + buffer = AZStd::string::format("#%i %s", n, entryTemplate.c_str()); + return rapidjson::Value(buffer.data(), static_cast(buffer.size()), document.GetAllocator()); + }; + + auto createEntry = [&](int n) -> rapidjson::Value + { + rapidjson::Value entry(rapidjson::kObjectType); + entry.AddMember("string", createString(n), document.GetAllocator()); + entry.AddMember("int", rapidjson::Value(n), document.GetAllocator()); + entry.AddMember("double", rapidjson::Value(static_cast(n) * 0.5), document.GetAllocator()); + entry.AddMember("bool", rapidjson::Value(n % 2 == 0), document.GetAllocator()); + entry.AddMember("null", rapidjson::Value(rapidjson::kNullType), document.GetAllocator()); + return entry; + }; + + auto createArray = [&]() -> rapidjson::Value + { + rapidjson::Value array; + array.SetArray(); + for (int i = 0; i < entryCount; ++i) + { + array.PushBack(createEntry(i), document.GetAllocator()); + } + return array; + }; + + auto createObject = [&]() -> rapidjson::Value + { + rapidjson::Value object; + object.SetObject(); + for (int i = 0; i < entryCount; ++i) + { + buffer = AZStd::string::format("Key%i", i); + rapidjson::Value key; + key.SetString(buffer.data(), static_cast(buffer.length()), document.GetAllocator()); + object.AddMember(key.Move(), createArray(), document.GetAllocator()); + } + return object; + }; + + document.SetObject(); + document.AddMember("entries", createObject(), document.GetAllocator()); + + AZStd::string serializedJson; + auto result = AZ::JsonSerializationUtils::WriteJsonString(document, serializedJson); + AZ_Assert(result.IsSuccess(), "Failed to serialize generated JSON"); + return serializedJson; + } + }; + +// Helper macro for registering JSON benchmarks +#define BENCHMARK_REGISTER_JSON(BaseClass, Method) \ + BENCHMARK_REGISTER_F(BaseClass, Method) \ + ->Args({ 10, 5 }) \ + ->Args({ 10, 500 }) \ + ->Args({ 100, 5 }) \ + ->Args({ 100, 500 }) \ + ->Unit(benchmark::kMillisecond); + + BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDocumentInPlace)(benchmark::State& state) + { + AZ::DOM::JsonBackend backend; + AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + state.PauseTiming(); + AZStd::string payloadCopy = serializedPayload; + state.ResumeTiming(); + + auto result = AZ::DOM::Json::WriteToRapidJsonDocument( + [&](AZ::DOM::Visitor* visitor) + { + return backend.ReadFromStringInPlace(payloadCopy, visitor); + }); + + benchmark::DoNotOptimize(result.GetValue()); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDocumentInPlace) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, DomDeserializeToDocument)(benchmark::State& state) + { + AZ::DOM::JsonBackend backend; + AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + auto result = AZ::DOM::Json::WriteToRapidJsonDocument( + [&](AZ::DOM::Visitor* visitor) + { + return backend.ReadFromString(serializedPayload, AZ::DOM::Lifetime::Temporary, visitor); + }); + + benchmark::DoNotOptimize(result.GetValue()); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, DomDeserializeToDocument) + + BENCHMARK_DEFINE_F(DomJsonBenchmark, JsonUtilsDeserializeToDocument)(benchmark::State& state) + { + AZ::DOM::JsonBackend backend; + AZStd::string serializedPayload = GenerateDomJsonBenchmarkPayload(state.range(0), state.range(1)); + + for (auto _ : state) + { + auto result = AZ::JsonSerializationUtils::ReadJsonString(serializedPayload); + + benchmark::DoNotOptimize(result.GetValue()); + } + + state.SetBytesProcessed(serializedPayload.size() * state.iterations()); + } + BENCHMARK_REGISTER_JSON(DomJsonBenchmark, JsonUtilsDeserializeToDocument) + +#undef BENCHMARK_REGISTER_JSON +} // namespace Benchmark + +#endif // defined(HAVE_BENCHMARK) diff --git a/Code/Framework/AzCore/Tests/DOM/DomJsonTests.cpp b/Code/Framework/AzCore/Tests/DOM/DomJsonTests.cpp new file mode 100644 index 0000000000..e778a449d5 --- /dev/null +++ b/Code/Framework/AzCore/Tests/DOM/DomJsonTests.cpp @@ -0,0 +1,288 @@ +/* + * 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::DOM::Tests +{ + class DomJsonTests : public UnitTest::AllocatorsFixture + { + public: + void SetUp() override + { + UnitTest::AllocatorsFixture::SetUp(); + NameDictionary::Create(); + m_document = AZStd::make_unique(); + } + + void TearDown() override + { + m_document.reset(); + NameDictionary::Destroy(); + UnitTest::AllocatorsFixture::TearDown(); + } + + static bool DeepCompare(const rapidjson::Value& lhs, const rapidjson::Value& rhs) + { + if (lhs.GetType() != rhs.GetType()) + { + return false; + } + + switch (lhs.GetType()) + { + case rapidjson::kNullType: + return true; + case rapidjson::kFalseType: + return true; + case rapidjson::kTrueType: + return true; + case rapidjson::kObjectType: + { + if (lhs.MemberCount() != rhs.MemberCount()) + { + return false; + } + + auto lhsIt = lhs.MemberBegin(); + auto rhsIt = rhs.MemberBegin(); + while (lhsIt != lhs.MemberEnd()) + { + if (lhsIt->name != rhsIt->name) + { + return false; + } + + if (!DeepCompare(lhsIt->value, rhsIt->value)) + { + return false; + } + + ++lhsIt; + ++rhsIt; + } + return true; + } + case rapidjson::kArrayType: + { + if (lhs.Size() != rhs.Size()) + { + return false; + } + + auto lhsIt = lhs.Begin(); + auto rhsIt = rhs.Begin(); + while (lhsIt != lhs.End()) + { + if (!DeepCompare(*lhsIt, *rhsIt)) + { + return false; + } + + ++lhsIt; + ++rhsIt; + } + return true; + } + case rapidjson::kStringType: + return lhs == rhs; + case rapidjson::kNumberType: + return lhs == rhs; + } + + AZ_Assert(false, "Unexpected JSON value type"); + return false; + } + + rapidjson::Value CreateString(const AZStd::string& text) + { + rapidjson::Value key; + key.SetString(text.c_str(), static_cast(text.length()), m_document->GetAllocator()); + return key; + } + + template + void AddValue(const AZStd::string& key, T value) + { + m_document->AddMember(CreateString(key), rapidjson::Value(value), m_document->GetAllocator()); + } + + // Validate round-trip serialization to and from rapidjson::Document and a UTF-8 encoded string + void PerformSerializationChecks() + { + // Generate a canonical serializaed representation of this document using rapidjson + // This will be pretty-printed using the same rapidjson pretty printer we use, so should be binary identical + // to any output generated by the visitor API + AZStd::string canonicalSerializedDocument; + AZ::JsonSerializationUtils::WriteJsonString(*m_document, canonicalSerializedDocument); + + auto visitDocumentFn = [this](AZ::DOM::Visitor* visitor) + { + return Json::VisitRapidJsonValue(*m_document, visitor, Lifetime::Persistent); + }; + + // Document -> Document + { + auto result = Json::WriteToRapidJsonDocument(visitDocumentFn); + EXPECT_TRUE(result.IsSuccess()); + EXPECT_TRUE(DeepCompare(*m_document, result.GetValue())); + } + + // Document -> string + { + AZStd::string serializedDocument; + JsonBackend backend; + auto result = backend.WriteToString(serializedDocument, visitDocumentFn); + EXPECT_TRUE(result.IsSuccess()); + EXPECT_EQ(canonicalSerializedDocument, serializedDocument); + } + + // string -> Document + { + auto result = Json::WriteToRapidJsonDocument( + [&canonicalSerializedDocument](AZ::DOM::Visitor* visitor) + { + JsonBackend backend; + return backend.ReadFromString(canonicalSerializedDocument, AZ::DOM::Lifetime::Temporary, visitor); + }); + EXPECT_TRUE(result.IsSuccess()); + EXPECT_TRUE(DeepCompare(*m_document, result.GetValue())); + } + + // string -> string + { + AZStd::string serializedDocument; + JsonBackend backend; + auto result = backend.WriteToString( + serializedDocument, + [&backend, &canonicalSerializedDocument](AZ::DOM::Visitor* visitor) + { + return backend.ReadFromString(canonicalSerializedDocument, AZ::DOM::Lifetime::Temporary, visitor); + }); + EXPECT_TRUE(result.IsSuccess()); + EXPECT_EQ(canonicalSerializedDocument, serializedDocument); + } + } + + AZStd::unique_ptr m_document; + }; + + TEST_F(DomJsonTests, EmptyArray) + { + m_document->SetArray(); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, SimpleArray) + { + m_document->SetArray(); + for (int i = 0; i < 5; ++i) + { + m_document->PushBack(i, m_document->GetAllocator()); + } + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, NestedArrays) + { + m_document->SetArray(); + for (int j = 0; j < 7; ++j) + { + rapidjson::Value nestedArray(rapidjson::kArrayType); + for (int i = 0; i < 5; ++i) + { + nestedArray.PushBack(i, m_document->GetAllocator()); + } + m_document->PushBack(nestedArray.Move(), m_document->GetAllocator()); + } + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, EmptyObject) + { + m_document->SetObject(); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, SimpleObject) + { + m_document->SetObject(); + for (int i = 0; i < 5; ++i) + { + m_document->AddMember(CreateString(AZStd::string::format("Key%i", i)), rapidjson::Value(i), m_document->GetAllocator()); + } + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, NestedObjects) + { + m_document->SetObject(); + for (int j = 0; j < 7; ++j) + { + rapidjson::Value nestedObject(rapidjson::kObjectType); + for (int i = 0; i < 5; ++i) + { + nestedObject.AddMember(CreateString(AZStd::string::format("Key%i", i)), rapidjson::Value(i), m_document->GetAllocator()); + } + m_document->AddMember(CreateString(AZStd::string::format("Obj%i", j)), nestedObject.Move(), m_document->GetAllocator()); + } + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, Int64) + { + m_document->SetObject(); + AddValue("int64_min", AZStd::numeric_limits::min()); + AddValue("int64_max", AZStd::numeric_limits::max()); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, Uint64) + { + m_document->SetObject(); + AddValue("uint64_min", AZStd::numeric_limits::min()); + AddValue("uint64_max", AZStd::numeric_limits::max()); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, Double) + { + m_document->SetObject(); + AddValue("double_min", AZStd::numeric_limits::min()); + AddValue("double_max", AZStd::numeric_limits::max()); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, Null) + { + m_document->SetObject(); + m_document->AddMember(CreateString("null_value"), rapidjson::Value(rapidjson::kNullType), m_document->GetAllocator()); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, Bool) + { + m_document->SetObject(); + AddValue("true_value", true); + AddValue("false_value", false); + PerformSerializationChecks(); + } + + TEST_F(DomJsonTests, String) + { + m_document->SetObject(); + m_document->AddMember(CreateString("empty_string"), CreateString(""), m_document->GetAllocator()); + m_document->AddMember(CreateString("short_string"), CreateString("test"), m_document->GetAllocator()); + m_document->AddMember(CreateString("long_string"), CreateString("abcdefghijklmnopqrstuvwxyz0123456789"), m_document->GetAllocator()); + PerformSerializationChecks(); + } +} // namespace AZ::DOM::Tests diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index d39595c45e..0fca75e2a8 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -213,6 +213,8 @@ set(FILES AZStd/Variant.cpp AZStd/VariantSerialization.cpp AZStd/VectorAndArray.cpp + DOM/DomJsonTests.cpp + DOM/DomJsonBenchmarks.cpp ) # Prevent the following files from being grouped in UNITY builds