From 19a238e3a25d0b26615dd1f9148c7c6205a0dce8 Mon Sep 17 00:00:00 2001 From: mintel1 Date: Sat, 11 May 2024 16:19:39 +0200 Subject: [PATCH 1/4] First initial commit attempting to get rapidyaml into the build and replacing lots of rapidjson text in the yaml version. Need to recognise ryml headers --- cmake/third_party.cmake | 1 + conanfile.py | 1 + .../src/morpheus/core/serialisation/CMakeLists.txt | 3 +++ .../src/morpheus/core/serialisation/exceptions.cpp | 5 +++++ .../src/morpheus/core/serialisation/exceptions.hpp | 11 +++++++++++ libraries/core/tests/serialisation/CMakeLists.txt | 2 ++ .../core/tests/serialisation/exceptions.tests.cpp | 1 + 7 files changed, 24 insertions(+) diff --git a/cmake/third_party.cmake b/cmake/third_party.cmake index 4a3d77dac..0d75c6356 100644 --- a/cmake/third_party.cmake +++ b/cmake/third_party.cmake @@ -28,6 +28,7 @@ find_package(ctre REQUIRED) find_package(magic_enum REQUIRED) find_package(Microsoft.GSL REQUIRED) find_package(RapidJSON REQUIRED) +find_package(ryml REQUIRED) if(WIN32) find_package(wil REQUIRED) diff --git a/conanfile.py b/conanfile.py index 91a675fcd..acc57a869 100644 --- a/conanfile.py +++ b/conanfile.py @@ -83,6 +83,7 @@ class Morpheus(ConanFile): "magic_enum/0.9.5", "ms-gsl/4.0.0", "rapidjson/cci.20230929", + "rapidyaml/0.5.0", "range-v3/0.12.0", "scnlib/2.0.2", #"zlib/1.2.12" # xapian-core/1.4.19' requires 'zlib/1.2.12' while 'boost/1.81.0' requires 'zlib/1.2.13'. To fix this conflict you need to override the package 'zlib' in your root package. diff --git a/libraries/core/src/morpheus/core/serialisation/CMakeLists.txt b/libraries/core/src/morpheus/core/serialisation/CMakeLists.txt index 6f8061d3b..7ed122a17 100644 --- a/libraries/core/src/morpheus/core/serialisation/CMakeLists.txt +++ b/libraries/core/src/morpheus/core/serialisation/CMakeLists.txt @@ -13,6 +13,8 @@ target_sources(MorpheusCore text_writer.hpp write_serialiser.hpp write_serialiser_decl.hpp + yaml_reader.hpp + yaml_writer.hpp PRIVATE exceptions.cpp json_reader.cpp @@ -22,6 +24,7 @@ target_sources(MorpheusCore target_link_libraries(MorpheusCore PUBLIC rapidjson + ryml::ryml # https://conan.io/center/recipes/rapidyaml?version=0.5.0 ) add_subdirectory(adapters) diff --git a/libraries/core/src/morpheus/core/serialisation/exceptions.cpp b/libraries/core/src/morpheus/core/serialisation/exceptions.cpp index dd8615d31..1b15a0f0f 100644 --- a/libraries/core/src/morpheus/core/serialisation/exceptions.cpp +++ b/libraries/core/src/morpheus/core/serialisation/exceptions.cpp @@ -18,4 +18,9 @@ void throwJsonException(std::string_view message) throw boost::enable_error_info(JsonException(std::string(message))) << ExceptionInfo(MORPHEUS_CURRENT_STACKTRACE); } +void throwYamlException(std::string_view message) +{ + throw boost::enable_error_info(YamlException(std::string(message))) << ExceptionInfo(MORPHEUS_CURRENT_STACKTRACE); +} + } // namespace morpheus::serialisation diff --git a/libraries/core/src/morpheus/core/serialisation/exceptions.hpp b/libraries/core/src/morpheus/core/serialisation/exceptions.hpp index 954ff84e9..0b2022e78 100644 --- a/libraries/core/src/morpheus/core/serialisation/exceptions.hpp +++ b/libraries/core/src/morpheus/core/serialisation/exceptions.hpp @@ -25,10 +25,21 @@ class JsonException : public std::runtime_error using std::runtime_error::runtime_error; }; +/// \class YamlException +/// Exception type to be thrown for errors when parsing YAML. +class YamlException : public std::runtime_error +{ +public: + using std::runtime_error::runtime_error; +}; + /// Throws a std::runtime_error derived binary exception with the attached message. MORPHEUSCORE_EXPORT [[noreturn]] MORPHEUS_FUNCTION_COLD void throwBinaryException(std::string_view message); /// Throws a std::runtime_error derived Json exception with the attached message. MORPHEUSCORE_EXPORT [[noreturn]] MORPHEUS_FUNCTION_COLD void throwJsonException(std::string_view message); +/// Throws a std::runtime_error derived Yaml exception with the attached message. +MORPHEUSCORE_EXPORT [[noreturn]] MORPHEUS_FUNCTION_COLD void throwYamlException(std::string_view message); + } // namespace morpheus::serialisation diff --git a/libraries/core/tests/serialisation/CMakeLists.txt b/libraries/core/tests/serialisation/CMakeLists.txt index 811c904a3..f6b1aa8d2 100644 --- a/libraries/core/tests/serialisation/CMakeLists.txt +++ b/libraries/core/tests/serialisation/CMakeLists.txt @@ -7,6 +7,8 @@ target_sources(MorpheusCoreTests exceptions.tests.cpp json_reader.tests.cpp json_writer.tests.cpp + yaml_reader.tests.cpp + yaml_writer.tests.cpp read_serialiser.tests.cpp write_serialiser.tests.cpp ) diff --git a/libraries/core/tests/serialisation/exceptions.tests.cpp b/libraries/core/tests/serialisation/exceptions.tests.cpp index 45e47299b..33a147f9e 100644 --- a/libraries/core/tests/serialisation/exceptions.tests.cpp +++ b/libraries/core/tests/serialisation/exceptions.tests.cpp @@ -17,6 +17,7 @@ TEST_CASE("Serialisatoin exception helpers", "[morpheus.serialisation.exception. { REQUIRE_THROWS_AS(throwBinaryException("Test binary exception"), BinaryException); REQUIRE_THROWS_AS(throwJsonException("Tesst Json exception"), JsonException); + REQUIRE_THROWS_AS(throwYamlException("Test Yaml exception"), YamlException); } From 1836f2b0ba96ced9d26de155185e303c15d6853b Mon Sep 17 00:00:00 2001 From: mintel1 Date: Sat, 11 May 2024 18:08:37 +0200 Subject: [PATCH 2/4] New yaml serialisation files - first commit --- .../core/serialisation/yaml_reader.hpp | 178 +++++++++ .../core/serialisation/yaml_writer.hpp | 98 +++++ .../tests/serialisation/yaml_reader.tests.cpp | 374 ++++++++++++++++++ .../tests/serialisation/yaml_writer.tests.cpp | 279 +++++++++++++ 4 files changed, 929 insertions(+) create mode 100644 libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp create mode 100644 libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp create mode 100644 libraries/core/tests/serialisation/yaml_reader.tests.cpp create mode 100644 libraries/core/tests/serialisation/yaml_writer.tests.cpp diff --git a/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp b/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp new file mode 100644 index 000000000..c5628bf53 --- /dev/null +++ b/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp @@ -0,0 +1,178 @@ +#pragma once + +#include "morpheus/core/base/assert.hpp" +#include "morpheus/core/base/cold.hpp" +#include "morpheus/core/functional/overload.hpp" +#include "morpheus/core/memory/polymorphic_value.hpp" +#include "morpheus/core/serialisation/exceptions.hpp" + +#include + +//#include +//#include +#include + + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace morpheus::serialisation +{ + +/// \class YamlReader +/// Read in objects from an underlying yaml representation. +class MORPHEUSCORE_EXPORT YamlReader +{ + enum class FundamentalType : std::uint32_t + { + Boolean, + Int64, + Uint64, + Float, + Double, + String + }; + +public: + using OwnedStream = memory::polymorphic_value; + + + + static constexpr bool canBeTextual() { return true; } + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::isTextual() + static constexpr bool isTextual() { return true; } + + /// Yaml reader take in a stream of yaml to extract data members from. + /// \param[in] stream Stream used to read in the yaml source. This must outlive the reader as its held by referece. + explicit YamlReader(OwnedStream stream, bool validate = true); + explicit YamlReader(YamlReader const& rhs); + ~YamlReader(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::beginComposite() + void beginComposite(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::endComposite() + void endComposite(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::beginValue() + void beginValue(std::string_view const key); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::endValue() + void endValue(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::beginSequence() + std::optional beginSequence(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::endSequence() + void endSequence(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::beginNullable() + bool beginNullable(); + + /// \copydoc morpheus::serialisation::concepts::ReaderArchtype::endNullable() + void endNullable(); + + // clang-format off + /// Read a boolean from the serialisation. + template + requires std::is_same_v + T read() + { + auto const [event, next] = getNext(); + MORPHEUS_ASSERT(next->index() == 0); + return std::get(*next); + } + + /// Reads a integral type from the serialisation. + template + requires(not std::is_same_v) + Interger read() + { + auto const [event, next] = getNext(); + return std::visit(functional::Overload{ + [](std::integral auto const value) { return boost::numeric_cast(value); }, + [](auto const) -> Interger { throwYamlException("Unable to convert to integral representation"); } + }, *next); + } + + /// Reads a float or double type from the serialisation. + template + Float read() + { + auto const [event, next] = getNext(); + return std::visit(functional::Overload { + [](std::integral auto const value) { return boost::numeric_cast(value); }, + [](std::floating_point auto const value) + { + if (std::isinf(value)) [[unlikely]] + { + if (value > 0) + return std::numeric_limits::infinity(); + else + return -std::numeric_limits::infinity(); + } + return boost::numeric_cast(value); + }, + [](auto const) -> Float { throwYamlException("Unable to convert to floating point representation"); } + }, *next); + } + + /// Reads a string type from the serialisation. + template + requires std::is_same_v + T read() + { + auto const [event, next] = getNext(); + MORPHEUS_ASSERT(next->index() == magic_enum::enum_integer(FundamentalType::String)); + return std::get(*next); + } + + template + requires std::is_same_v> + T read() + { + return {}; + } + // clang-format on + +private: + enum class Event : std::uint32_t + { + BeginComposite, + EndComposite, + BeginValue, + Value, + EndValue, + BeginSequence, + EndSequence, + }; + + friend struct YamlExtracter; + using FundamentalValue = std::variant; + using PossibleValue = std::optional; + using EventValue = std::tuple; + + [[nodiscard]] EventValue getNext(); + + memory::polymorphic_value mSourceStream; /// Owned input stream containing the Yaml source. + + rapidjson::IStreamWrapper mStream; + rapidjson::Reader mYamlReader; + std::unique_ptr mExtractor; + bool mValidate = true; +}; + +} // namespace morpheus::serialisation diff --git a/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp new file mode 100644 index 000000000..7cf00a9ac --- /dev/null +++ b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "morpheus/core/base/platform.hpp" + +#include +#include + +#include +#include +#include +#include +#include +#include + +namespace morpheus::serialisation +{ + +/// \class YamlWriter +/// Implementes the concept Writer for a streaming JSON writer which writes item by item to the output stream. +class MORPHEUSCORE_EXPORT YamlWriter +{ +public: + static constexpr bool canBeTextual() { return true; } + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::isTextual() + static constexpr bool isTextual() { return true; } + + explicit YamlWriter(std::ostream& stream); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::beginComposite() + void beginComposite(); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::endComposite() + void endComposite(); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::beginValue() + void beginValue(std::string_view const key); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::endValue() + void endValue(); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::beginSequence() + void beginSequence(std::optional size = std::nullopt); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::endSequence() + void endSequence(); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::beginNullable() + void beginNullable(bool const null); + + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::endNullable() + void endNullable(); + + /// Write a boolean to the serialisation. + void write(bool const value); + /// Write a 8-bit unsigned integer to the serialisation. + void write(std::uint8_t const value); + /// Write a 8-bit integer to the serialisation. + void write(std::int8_t const value); + /// Write a 16-bit unsigned integer to the serialisation. + void write(std::uint16_t const value); + /// Write a 16-bit integer to the serialisation. + void write(std::int16_t const value); + /// Write a 32-bit unsigned integer to the serialisation. + void write(std::uint32_t const value); + /// Write a 32-bit integer to the serialisation. + void write(std::int32_t const value); + /// Write a 64-bit unsigned integer to the serialisation. + void write(std::uint64_t const value); + /// Write a 64-bit integer to the serialisation. + void write(std::int64_t const value); + /// Write a float to the serialisation. + void write(float const value); + /// Write a double to the serialisation. + void write(double const value); + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::write(std::string_view const) + void write(std::string_view const value); + /// \copydoc morpheus::serialisation::concepts::WriterArchtype::write(std::span const) + void write(std::span const value); + /// Write a string literal to the serialisation. + template void write(const char(&str)[N]) { write(std::string_view(str, N-1)); } + +private: + template + using RapidYamlWriter = rapidjson::Writer< + OutputStream, + rapidjson::UTF8<>, + rapidjson::UTF8<>, + rapidjson::CrtAllocator, + (rapidjson::kWriteDefaultFlags | rapidjson::kWriteNanAndInfFlag) + >; + + rapidjson::OStreamWrapper mStream; + RapidYamlWriter mYamlWriter; +}; + + +} diff --git a/libraries/core/tests/serialisation/yaml_reader.tests.cpp b/libraries/core/tests/serialisation/yaml_reader.tests.cpp new file mode 100644 index 000000000..3a43a6c33 --- /dev/null +++ b/libraries/core/tests/serialisation/yaml_reader.tests.cpp @@ -0,0 +1,374 @@ +#include "morpheus/core/conformance/format.hpp" +#include "morpheus/core/serialisation/adapters/aggregate.hpp" +#include "morpheus/core/serialisation/adapters/std/chrono.hpp" +#include "morpheus/core/serialisation/adapters/std/monostate.hpp" +#include "morpheus/core/serialisation/adapters/std/optional.hpp" +#include "morpheus/core/serialisation/adapters/std/pair.hpp" +#include "morpheus/core/serialisation/adapters/std/tuple.hpp" +#include "morpheus/core/serialisation/adapters/std/unique_ptr.hpp" +#include "morpheus/core/serialisation/adapters/std/variant.hpp" +#include "morpheus/core/serialisation/adapters/std/vector.hpp" +#include "morpheus/core/serialisation/read_serialiser.hpp" +#include "morpheus/core/serialisation/serialisers.hpp" + +#include + +using namespace Catch; + +namespace morpheus::serialisation +{ + +namespace test { + +struct ISteamCopier +{ + using deleter_type = std::default_delete; + + std::istream* operator()(std::istream const& rhs) const + { + auto ss = std::make_unique(rhs.rdbuf()); + return ss.release(); + } + + std::istringstream* operator()(std::istringstream const& rhs) const + { + auto ss = std::make_unique(rhs.str()); + return ss.release(); + } +}; + +template +T deserialise(std::string_view const value, bool const validate = true) +{ + using namespace memory; + auto strstream = std::make_unique(std::string{value}); + auto iss = polymorphic_value(strstream.release(), ISteamCopier{}); + YamlReadSerialiser serialiser(std::move(iss), validate); + return serialiser.deserialise(); +} + +auto readerFromString(std::string_view const value) +{ + using namespace memory; + auto strstream = std::make_unique(std::string{value}); + auto iss = polymorphic_value(strstream.release(), ISteamCopier{}); + return YamlReader(std::move(iss), false); +} + +} // namespace test + +TEMPLATE_TEST_CASE("Yaml writer can write single native types to underlying text representation", "[morpheus.serialisation.yaml_reader.native]", + bool, std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t, std::uint32_t, std::int64_t, std::uint64_t, float, double) +{ + if constexpr (std::is_integral_v) + { + using Limits = std::numeric_limits; + REQUIRE(test::deserialise(fmt_ns::format("{}", Limits::min())) == Limits::min()); + REQUIRE(test::deserialise(fmt_ns::format("{}", Limits::lowest())) == Limits::lowest()); + REQUIRE(test::deserialise(fmt_ns::format("{}", Limits::max())) == Limits::max()); + + if constexpr (not std::is_same_v) + REQUIRE(test::deserialise(fmt_ns::format("{}", Limits::radix)) == Limits::radix); + } + else if constexpr (std::is_floating_point_v) + { + REQUIRE(test::deserialise("0") == 0); + REQUIRE(test::deserialise("-0") == 0); + REQUIRE(test::deserialise("2.75") == TestType(2.75)); + REQUIRE(test::deserialise("-2.75") == TestType(-2.75)); + REQUIRE(std::isinf(test::deserialise("Infinity"))); + REQUIRE(std::isinf(test::deserialise("-Infinity"))); + REQUIRE(std::isnan(test::deserialise("NaN"))); + } +} + +TEST_CASE("Create and then copy a reader and read from the copied stream", "[morpheus.serialisation.yaml_reader.copy]") +{ + GIVEN("A Yaml stream") + { + std::string_view str(R"("value")"); + + WHEN("Read an single value from the stream") + { + YamlReader reader = test::readerFromString(str); + YamlReader copiedReader(reader); + + THEN("Expect an empty composite in the yaml document") { REQUIRE("value" == copiedReader.read()); } + } + } +} + +TEST_CASE("Yaml reader provides basic reader functionality", "[morpheus.serialisation.yaml_reader.fundamental]") +{ + GIVEN("A Yaml stream") + { + std::string_view str(R"("value")"); + + WHEN("Read an single value from the stream") + { + YamlReader reader = test::readerFromString(str); + + THEN("Expect an empty composite in the yaml document") + { + REQUIRE("value" == reader.read()); + } + } + } + GIVEN("A Yaml stream") + { + std::string_view str(R"({})"); + + WHEN("Read an empty composite from the stream") + { + YamlReader reader = test::readerFromString(str); + + THEN("Expect an empty composite in the json document") + { + reader.beginComposite(); + reader.endComposite(); + } + } + } + GIVEN("A Yaml stream") + { + std::string_view str(R"({"key":"value"})"); + + WHEN("Read a composite of key pair from the stream") + { + YamlReader reader = test::readerFromString(str); + + THEN("Expect an empty composite in the yaml document") + { + reader.beginComposite(); + reader.beginValue("key"); + REQUIRE("value" == reader.read()); + reader.endValue(); + reader.endComposite(); + } + } + } + GIVEN("A Yaml stream") + { + std::string_view str(R"({"x":null})"); + + WHEN("Read a composite of key to null pair from the stream") + { + YamlReader reader = test::readerFromString(str); + + THEN("Expect an empty composite in the yaml document") + { + reader.beginComposite(); + reader.beginValue("x"); + REQUIRE(true == reader.beginNullable()); + reader.endNullable(); + reader.endValue(); + reader.endComposite(); + } + } + } +} + +struct SimpleComposite +{ + int first = 0; + bool second = false; + float third = 0.0f; + std::string forth; + + template + void deserialise(Serialiser& s) + { + first = s.template deserialise("first"); + second = s.template deserialise("second"); + third = s.template deserialise("third"); + forth = s.template deserialise("forth"); + } +}; + +struct ComplexComposite +{ + SimpleComposite first; + float second = 0.0f; + + template + void deserialise(Serialiser& s) + { + first = s.template deserialise("first"); + second = s.template deserialise("second"); + } +}; + + +TEST_CASE("Yaml reader can read simple composite types from underlying test representation", "[morpheus.serialisation.yaml_reader.composite]") +{ + GIVEN("A Yaml reader") + { + auto const simple = test::deserialise(R"({"first":100,"second":true,"third":50,"forth":"example"})"); + STATIC_REQUIRE(concepts::ReadSerialisableInsrusive); + + WHEN("Writing an empty composite") + { + THEN("Expect an empty composite in the yaml document") + { + REQUIRE(simple.first == 100); + REQUIRE(simple.second == true); + REQUIRE(simple.third == 50.0); + REQUIRE(simple.forth == "example"); + } + } + } + GIVEN("A Yaml reader") + { + auto const complex = test::deserialise(R"({"first":{"first":100,"second":true,"third":50,"forth":"example"},"second":3.14})"); + STATIC_REQUIRE(concepts::ReadSerialisableInsrusive); + + WHEN("Writing an empty composite") + { + THEN("Expect an empty composite in the yaml document") + { + REQUIRE(complex.first.first == 100); + REQUIRE(complex.first.second == true); + REQUIRE(complex.first.third == 50.0); + REQUIRE(complex.first.forth == "example"); + REQUIRE(complex.second == Approx(3.14)); + } + } + } +} + +template +struct ContainsType +{ + T value = T(); + + template + void deserialise(Serialiser& s) + { + value = s.template deserialise("value"); + } +}; + +TEST_CASE("Yaml reader raise an error on reading incorrect types", "[morpheus.serialisation.yaml_reader.invalid_values]") +{ + GIVEN("A test type for validating serialition of specific types") + { + using IntegralType = ContainsType; + WHEN("Deserialising from Yaml with a string where a integer is expected") + { + auto const yamlText = R"({"value":100})"; + THEN("Ensure serialisation works with valid input") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE(test::deserialise(yamlText, false).value == 100); + } + } + } + GIVEN("A type serialising a integer") + { + using IntegralType = ContainsType; + WHEN("Deserialising from Yaml with a string where a integer is expected") + { + auto const yamlText = R"({"value":"InvalidValue"})"; + THEN("Expect an exception to be thrown on error to convert a string to a integer") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::deserialise(yamlText, false), ContainsSubstring("Unable to convert to integral")); + } + } + } + GIVEN("A type serialising a float") + { + using FloatType = ContainsType; + WHEN("Deserialising from Yaml with a string where a float is expected") + { + auto const yamlText = R"({"value":"InvalidValue"})"; + THEN("Expect an exception to be thrown on error to convert a string to a float") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::deserialise(yamlText, false), ContainsSubstring("Unable to convert to floating point")); + } + } + } +} + +TEST_CASE("Yaml reader thows an error on invalid yaml input", "[morpheus.serialisation.yaml_reader.invalid_yaml]") +{ + GIVEN("A type serialising a integer") + { + using IntegralType = ContainsType; + WHEN("Deserialising from invalid Yaml") + { + auto const yamlText = R"({"value" @ "AtSymbolIsNotAValidSeperator"})"; + THEN("Expect an exception to be thrown on error to convert a string to a integer") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::deserialise(yamlText, false), ContainsSubstring("Parse error at offset")); + } + } + } +} + +TEST_CASE("Yaml reader can read std types from underlying text representation", "[morpheus.serialisation.yaml_reader.adapters.std]") +{ + SECTION("Chrono types") + { + REQUIRE(test::deserialise(R"("123ns")") == std::chrono::nanoseconds{123}); + REQUIRE(test::deserialise(R"("456us")") == std::chrono::microseconds{456}); + REQUIRE(test::deserialise(R"("789ms")") == std::chrono::milliseconds{789}); + REQUIRE(test::deserialise(R"("123s")") == std::chrono::seconds{123}); + REQUIRE(test::deserialise(R"("58min")") == std::chrono::minutes{58}); + REQUIRE(test::deserialise(R"("24h")") == std::chrono::hours{24}); + REQUIRE(test::deserialise(R"("8d")") == std::chrono::days{8}); + REQUIRE(test::deserialise(R"("12w")") == std::chrono::weeks{12}); + REQUIRE(test::deserialise(R"("100y")") == std::chrono::years{100}); + REQUIRE(test::deserialise(R"("12m")") == std::chrono::months{12}); + } + REQUIRE(test::deserialise(R"({})") == std::monostate{}); + REQUIRE(test::deserialise>(R"(100)") == std::optional{100}); + REQUIRE(test::deserialise>(R"(null)") == std::optional{}); + REQUIRE(test::deserialise>(R"([50,true])") == std::pair{50, true}); + REQUIRE(test::deserialise(R"("Hello")") == std::string("Hello")); + REQUIRE(test::deserialise>(R"([75,true,"Example"])") == std::tuple{75, true, "Example"}); +// REQUIRE(test::deserialise>(R"({"type":"bool","value":true})") == std::variant{true}); + REQUIRE(*test::deserialise>(R"(50)") == 50); +} + +TEST_CASE("Error handling test cases for unexpected errors in the input Yaml stream", "[morpheus.serialisation.yaml_reader.error_handling]") +{ + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::readerFromString("50").beginValue("expected_key"), + ContainsSubstring("BeginComposite expected") && ContainsSubstring("Value encountered")); + REQUIRE_THROWS_WITH(test::readerFromString("[1,2,3]").beginValue("expected_key"), + ContainsSubstring("BeginComposite expected") && ContainsSubstring("BeginSequence encountered")); + REQUIRE_THROWS_WITH(test::readerFromString("{}").beginValue("expected_key"), ContainsSubstring("empty composite")); + + GIVEN("A type which parses a key value pair") + { + using IntegralType = ContainsType; + WHEN("Deserialising from Yaml with an invalid type for the key (i.e. an integer not a string)") + { + auto const yamlText = R"({100:100})"; + THEN("Ensure serialisation captures the error in key type") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::deserialise(yamlText, false), ContainsSubstring("error kParseErrorObjectMissName")); + } + } + } + GIVEN("A type which parses a key value pair") + { + using IntegralType = ContainsType; + WHEN("Deserialising from Yaml with an invalid key") + { + auto const yamlText = R"({"incorrect_key":100})"; + THEN("Ensure serialisation captures the error in key type") + { + using Catch::Matchers::ContainsSubstring; + REQUIRE_THROWS_WITH(test::deserialise(yamlText, false), + ContainsSubstring("Expected key value") && ContainsSubstring("actual key incorrect_key")); + } + } + } +} + +} // namespace morpheus::serialisation diff --git a/libraries/core/tests/serialisation/yaml_writer.tests.cpp b/libraries/core/tests/serialisation/yaml_writer.tests.cpp new file mode 100644 index 000000000..4765bcad7 --- /dev/null +++ b/libraries/core/tests/serialisation/yaml_writer.tests.cpp @@ -0,0 +1,279 @@ +#include "morpheus/core/serialisation/json_writer.hpp" +#include "morpheus/core/conformance/format.hpp" +#include "morpheus/core/serialisation/adapters/aggregate.hpp" +#include "morpheus/core/serialisation/adapters/std/chrono.hpp" +#include "morpheus/core/serialisation/adapters/std/monostate.hpp" +#include "morpheus/core/serialisation/adapters/std/optional.hpp" +#include "morpheus/core/serialisation/adapters/std/pair.hpp" +#include "morpheus/core/serialisation/adapters/std/tuple.hpp" +#include "morpheus/core/serialisation/adapters/std/unique_ptr.hpp" +#include "morpheus/core/serialisation/adapters/std/variant.hpp" +#include "morpheus/core/serialisation/adapters/std/vector.hpp" +#include "morpheus/core/serialisation/serialisers.hpp" +#include "morpheus/core/serialisation/write_serialiser.hpp" + +#include +#include +#include +#include +#include + +using namespace Catch; + +namespace morpheus::serialisation +{ + +namespace test { + +template +std::string serialise(T const& value) +{ + std::ostringstream oss; + JsonWriteSerialiser serialiser{oss}; + serialiser.serialise(value); + return oss.str(); +} + +#if (__cpp_lib_to_chars >= 201611L) +template requires std::is_floating_point_v +T toFloatingPoint(std::string_view value) +{ + T result = 0; + auto [ptr, ec] { std::from_chars(value.data(), value.data() + value.size(), result) }; + + if (ec != std::errc()) + { + throw std::system_error(std::make_error_code(ec)); + } + return result; +} +#endif // (__cpp_lib_to_chars >= 201611L) + +} + +TEMPLATE_TEST_CASE("Json writer can write single native types to underlying text representation", "[morpheus.serialisation.json_writer.native]", + bool, std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t, std::uint32_t, std::int64_t, std::uint64_t, float, double) +{ + if constexpr (std::is_integral_v) + { + REQUIRE(test::serialise(std::numeric_limits::min()) == fmt_ns::format("{}", std::numeric_limits::min())); + REQUIRE(test::serialise(std::numeric_limits::lowest()) == fmt_ns::format("{}", std::numeric_limits::lowest())); + REQUIRE(test::serialise(std::numeric_limits::max()) == fmt_ns::format("{}", std::numeric_limits::max())); + REQUIRE(test::serialise(std::numeric_limits::radix) == fmt_ns::format("{}", std::numeric_limits::radix)); + } + else if constexpr (std::is_floating_point_v) + { + REQUIRE(test::serialise(0) == "0"); + REQUIRE(test::serialise(-0) == "0"); + REQUIRE(test::serialise(TestType(2.75)) == "2.75"); + REQUIRE(test::serialise(TestType(-2.75)) == "-2.75"); + REQUIRE(test::serialise(std::numeric_limits::infinity()) == "Infinity"); + REQUIRE(test::serialise(-std::numeric_limits::infinity()) == "-Infinity"); + REQUIRE(test::serialise(std::numeric_limits::quiet_NaN()) == "NaN"); + REQUIRE(test::serialise(-std::numeric_limits::quiet_NaN()) == "NaN"); + REQUIRE(test::serialise(std::numeric_limits::signaling_NaN()) == "NaN"); + REQUIRE(test::serialise(-std::numeric_limits::signaling_NaN()) == "NaN"); + +#if (__cpp_lib_to_chars >= 201611L) + // RapidJson ouputs "1.401298464324817e-45" vs "1e-45" for float, but SetMaxDecimalPlaces() would effect all non-scientific values so we compare + // against the underling value not string representation. +// REQUIRE(test::toFloatingPoint(test::serialise(std::numeric_limits::denorm_min())) == Approx(std::numeric_limits::denorm_min())); +// REQUIRE(test::toFloatingPoint(test::serialise(std::numeric_limits::denorm_min())) == Approx(std::numeric_limits::denorm_min())); +#endif // (__cpp_lib_to_chars >= 201611L) + } +} + +TEST_CASE("Json writer can write simple composite types to underlying text representation", "[morpheus.serialisation.json_writer.composite]") +{ + GIVEN("A Json writer") + { + std::ostringstream strStream; + JsonWriter writer{ strStream }; + + WHEN("Writing an empty composite") + { + writer.beginComposite(); + writer.endComposite(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == "{}"); + } + } + WHEN("Writing an empty composite") + { + writer.beginComposite(); + writer.beginValue("x"); + writer.beginNullable(true); + writer.endNullable(); + writer.endValue(); + writer.endComposite(); + + THEN("Expect an null composite in the json document") + { + REQUIRE(strStream.str() == R"({"x":null})"); + } + } + WHEN("Writing an simple composite with single value") + { + writer.beginComposite(); + writer.beginValue("key"); + writer.write("value"); + writer.endValue(); + writer.endComposite(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == R"({"key":"value"})"); + } + } + WHEN("Writing an simple composite with multiple values") + { + writer.beginComposite(); + writer.beginValue("1"); + writer.write("A"); + writer.endValue(); + writer.beginValue("2"); + writer.write("B"); + writer.endValue(); + writer.beginValue("3"); + writer.write("C"); + writer.endValue(); + writer.endComposite(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == R"({"1":"A","2":"B","3":"C"})"); + } + } + } +} + +TEST_CASE("Json writer can write simple sequence types to underlying text representation", "[morpheus.serialisation.json_writer.squence]") +{ + GIVEN("A Json writer") + { + std::ostringstream strStream; + JsonWriter writer{ strStream }; + + WHEN("Writing an simple value") + { + writer.write("value"); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == R"("value")"); + } + } + WHEN("Writing an empty sequence") + { + writer.beginSequence(); + writer.endSequence(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == "[]"); + } + } + WHEN("Writing an simple composite with single value") + { + writer.beginSequence(); + writer.write("value"); + writer.endSequence(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == R"(["value"])"); + } + } + WHEN("Writing an simple composite with multiple values") + { + writer.beginSequence(); + writer.write("A"); + writer.write("B"); + writer.write("C"); + writer.endSequence(); + + THEN("Expect an empty composite in the json document") + { + REQUIRE(strStream.str() == R"(["A","B","C"])"); + } + } + } +} + +struct Example +{ + int a = 0; + bool b = true; + std::string c = "Example"; +}; + +template<> +inline constexpr bool delegateAggregateSerialisation = true; + +struct Example2 +{ + int a = 0; + Example b; + bool c = true; +}; + +template<> +inline constexpr bool delegateAggregateSerialisation = true; + +TEST_CASE("Json writer can write simple aggregates types to underlying text representation", "[morpheus.serialisation.json_writer.aggregate]") +{ + GIVEN("An aggregate that opts into serialisation") + { + STATIC_REQUIRE(SerialisableAggregate); + + WHEN("Writing default intialised instance") + { + THEN("Expect the aggregate values serialised as a sequence") + { + REQUIRE(test::serialise(Example{}) == R"([0,true,"Example"])"); + } + } + } + GIVEN("An aggregate containing aggregates that all opt into serialisation") + { + STATIC_REQUIRE(SerialisableAggregate); + + WHEN("Writing default intialised instance") + { + THEN("Expect the aggregate values serialised as a sequence embedding in a sequence") + { + REQUIRE(test::serialise(Example2{}) == R"([0,[0,true,"Example"],true])"); + } + } + } +} + +TEST_CASE("Json writer can write std types to underlying text representation", "[morpheus.serialisation.json_writer.adapters.std]") +{ + SECTION("Chrono types") + { + REQUIRE(test::serialise(std::chrono::nanoseconds{123}) == R"("123ns")"); + REQUIRE(test::serialise(std::chrono::microseconds{456}) == R"("456us")"); + REQUIRE(test::serialise(std::chrono::milliseconds{789}) == R"("789ms")"); + REQUIRE(test::serialise(std::chrono::seconds{123}) == R"("123s")"); + REQUIRE(test::serialise(std::chrono::minutes{58}) == R"("58min")"); + REQUIRE(test::serialise(std::chrono::hours{24}) == R"("24h")"); + REQUIRE(test::serialise(std::chrono::days{8}) == R"("8d")"); + REQUIRE(test::serialise(std::chrono::weeks{12}) == R"("12w")"); + REQUIRE(test::serialise(std::chrono::years{100}) == R"("100y")"); + REQUIRE(test::serialise(std::chrono::months{12}) == R"("12m")"); + } + REQUIRE(test::serialise(std::monostate{}) == R"({})"); + REQUIRE(test::serialise(std::optional{100}) == R"(100)"); + REQUIRE(test::serialise(std::optional{}) == R"(null)"); + REQUIRE(test::serialise(std::pair{50, true}) == R"([50,true])"); + REQUIRE(test::serialise(std::string("Hello")) == R"("Hello")"); + REQUIRE(test::serialise(std::tuple{75, true, "Example"}) == R"([75,true,"Example"])"); + REQUIRE(test::serialise(std::make_unique(123)) == R"(123)"); + REQUIRE(test::serialise(std::variant{true}) == R"({"type":"bool","value":true})"); + REQUIRE(test::serialise(std::vector{1,2,3,4,5}) == R"([1,2,3,4,5])"); +} + +} // namespace morpheus::serialisation From 9b883bfba9bf58c3b490c1a839db8fd650311e10 Mon Sep 17 00:00:00 2001 From: mintel1 Date: Sun, 12 May 2024 10:44:17 +0200 Subject: [PATCH 3/4] Further edits to move from Json code to Yaml --- .../core/serialisation/serialisers.hpp | 4 ++ .../core/serialisation/yaml_reader.hpp | 3 +- .../core/serialisation/yaml_writer.hpp | 11 ++++-- .../tests/serialisation/yaml_reader.tests.cpp | 2 +- .../tests/serialisation/yaml_writer.tests.cpp | 39 ++++++++++--------- 5 files changed, 35 insertions(+), 24 deletions(-) diff --git a/libraries/core/src/morpheus/core/serialisation/serialisers.hpp b/libraries/core/src/morpheus/core/serialisation/serialisers.hpp index 7da21fe71..d9c44c111 100644 --- a/libraries/core/src/morpheus/core/serialisation/serialisers.hpp +++ b/libraries/core/src/morpheus/core/serialisation/serialisers.hpp @@ -8,6 +8,8 @@ #include "morpheus/core/serialisation/serialise.hpp" #include "morpheus/core/serialisation/read_serialiser.hpp" #include "morpheus/core/serialisation/write_serialiser.hpp" +#include "morpheus/core/serialisation/yaml_writer.hpp" +#include "morpheus/core/serialisation/yaml_reader.hpp" namespace morpheus::serialisation { @@ -18,4 +20,6 @@ using BinaryReadSerialiser = ReadSerialiser; using JsonWriteSerialiser = WriteSerialiser; using JsonReadSerialiser = ReadSerialiser; +using YamlWriteSerialiser = WriteSerialiser; +using YamlReadSerialiser = ReadSerialiser; } diff --git a/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp b/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp index c5628bf53..44b5d845c 100644 --- a/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp +++ b/libraries/core/src/morpheus/core/serialisation/yaml_reader.hpp @@ -10,7 +10,8 @@ //#include //#include -#include +#include // details around which header to include are mentioned at + // https://github.com/biojppm/rapidyaml/blob/master/samples/quickstart.cpp #include diff --git a/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp index 7cf00a9ac..780d1ef50 100644 --- a/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp +++ b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp @@ -2,8 +2,12 @@ #include "morpheus/core/base/platform.hpp" -#include -#include +//#include +//#include + +#include // details around which header to include are mentioned at + // https://github.com/biojppm/rapidyaml/blob/master/samples/quickstart.cpp + #include #include @@ -81,6 +85,7 @@ class MORPHEUSCORE_EXPORT YamlWriter template void write(const char(&str)[N]) { write(std::string_view(str, N-1)); } private: + // TODO completely rework this section for ryml calls instead of rapidjson template using RapidYamlWriter = rapidjson::Writer< OutputStream, @@ -89,7 +94,7 @@ class MORPHEUSCORE_EXPORT YamlWriter rapidjson::CrtAllocator, (rapidjson::kWriteDefaultFlags | rapidjson::kWriteNanAndInfFlag) >; - + rapidjson::OStreamWrapper mStream; RapidYamlWriter mYamlWriter; }; diff --git a/libraries/core/tests/serialisation/yaml_reader.tests.cpp b/libraries/core/tests/serialisation/yaml_reader.tests.cpp index 3a43a6c33..d422adbcd 100644 --- a/libraries/core/tests/serialisation/yaml_reader.tests.cpp +++ b/libraries/core/tests/serialisation/yaml_reader.tests.cpp @@ -122,7 +122,7 @@ TEST_CASE("Yaml reader provides basic reader functionality", "[morpheus.serialis { YamlReader reader = test::readerFromString(str); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { reader.beginComposite(); reader.endComposite(); diff --git a/libraries/core/tests/serialisation/yaml_writer.tests.cpp b/libraries/core/tests/serialisation/yaml_writer.tests.cpp index 4765bcad7..6dde84851 100644 --- a/libraries/core/tests/serialisation/yaml_writer.tests.cpp +++ b/libraries/core/tests/serialisation/yaml_writer.tests.cpp @@ -1,4 +1,4 @@ -#include "morpheus/core/serialisation/json_writer.hpp" +#include "morpheus/core/serialisation/yaml_writer.hpp" #include "morpheus/core/conformance/format.hpp" #include "morpheus/core/serialisation/adapters/aggregate.hpp" #include "morpheus/core/serialisation/adapters/std/chrono.hpp" @@ -29,7 +29,7 @@ template std::string serialise(T const& value) { std::ostringstream oss; - JsonWriteSerialiser serialiser{oss}; + YamlWriteSerialiser serialiser{oss}; serialiser.serialise(value); return oss.str(); } @@ -51,7 +51,7 @@ T toFloatingPoint(std::string_view value) } -TEMPLATE_TEST_CASE("Json writer can write single native types to underlying text representation", "[morpheus.serialisation.json_writer.native]", +TEMPLATE_TEST_CASE("Yaml writer can write single native types to underlying text representation", "[morpheus.serialisation.yaml_writer.native]", bool, std::int8_t, std::uint8_t, std::int16_t, std::uint16_t, std::int32_t, std::uint32_t, std::int64_t, std::uint64_t, float, double) { if constexpr (std::is_integral_v) @@ -75,6 +75,7 @@ TEMPLATE_TEST_CASE("Json writer can write single native types to underlying text REQUIRE(test::serialise(-std::numeric_limits::signaling_NaN()) == "NaN"); #if (__cpp_lib_to_chars >= 201611L) +// Check the below for rapiyaml and remove or modify as necessary // RapidJson ouputs "1.401298464324817e-45" vs "1e-45" for float, but SetMaxDecimalPlaces() would effect all non-scientific values so we compare // against the underling value not string representation. // REQUIRE(test::toFloatingPoint(test::serialise(std::numeric_limits::denorm_min())) == Approx(std::numeric_limits::denorm_min())); @@ -83,19 +84,19 @@ TEMPLATE_TEST_CASE("Json writer can write single native types to underlying text } } -TEST_CASE("Json writer can write simple composite types to underlying text representation", "[morpheus.serialisation.json_writer.composite]") +TEST_CASE("Yaml writer can write simple composite types to underlying text representation", "[morpheus.serialisation.yaml_writer.composite]") { - GIVEN("A Json writer") + GIVEN("A Yaml writer") { std::ostringstream strStream; - JsonWriter writer{ strStream }; + YamlWriter writer{ strStream }; WHEN("Writing an empty composite") { writer.beginComposite(); writer.endComposite(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == "{}"); } @@ -109,7 +110,7 @@ TEST_CASE("Json writer can write simple composite types to underlying text repre writer.endValue(); writer.endComposite(); - THEN("Expect an null composite in the json document") + THEN("Expect an null composite in the yaml document") { REQUIRE(strStream.str() == R"({"x":null})"); } @@ -122,7 +123,7 @@ TEST_CASE("Json writer can write simple composite types to underlying text repre writer.endValue(); writer.endComposite(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == R"({"key":"value"})"); } @@ -141,7 +142,7 @@ TEST_CASE("Json writer can write simple composite types to underlying text repre writer.endValue(); writer.endComposite(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == R"({"1":"A","2":"B","3":"C"})"); } @@ -149,18 +150,18 @@ TEST_CASE("Json writer can write simple composite types to underlying text repre } } -TEST_CASE("Json writer can write simple sequence types to underlying text representation", "[morpheus.serialisation.json_writer.squence]") +TEST_CASE("Yaml writer can write simple sequence types to underlying text representation", "[morpheus.serialisation.yaml_writer.squence]") { - GIVEN("A Json writer") + GIVEN("A Yaml writer") { std::ostringstream strStream; - JsonWriter writer{ strStream }; + YamlWriter writer{ strStream }; WHEN("Writing an simple value") { writer.write("value"); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == R"("value")"); } @@ -170,7 +171,7 @@ TEST_CASE("Json writer can write simple sequence types to underlying text repres writer.beginSequence(); writer.endSequence(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == "[]"); } @@ -181,7 +182,7 @@ TEST_CASE("Json writer can write simple sequence types to underlying text repres writer.write("value"); writer.endSequence(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == R"(["value"])"); } @@ -194,7 +195,7 @@ TEST_CASE("Json writer can write simple sequence types to underlying text repres writer.write("C"); writer.endSequence(); - THEN("Expect an empty composite in the json document") + THEN("Expect an empty composite in the yaml document") { REQUIRE(strStream.str() == R"(["A","B","C"])"); } @@ -222,7 +223,7 @@ struct Example2 template<> inline constexpr bool delegateAggregateSerialisation = true; -TEST_CASE("Json writer can write simple aggregates types to underlying text representation", "[morpheus.serialisation.json_writer.aggregate]") +TEST_CASE("Yaml writer can write simple aggregates types to underlying text representation", "[morpheus.serialisation.yaml_writer.aggregate]") { GIVEN("An aggregate that opts into serialisation") { @@ -250,7 +251,7 @@ TEST_CASE("Json writer can write simple aggregates types to underlying text repr } } -TEST_CASE("Json writer can write std types to underlying text representation", "[morpheus.serialisation.json_writer.adapters.std]") +TEST_CASE("Yaml writer can write std types to underlying text representation", "[morpheus.serialisation.yaml_writer.adapters.std]") { SECTION("Chrono types") { From a970c12d0dc693805482384376eadb0476ef9a52 Mon Sep 17 00:00:00 2001 From: mintel1 Date: Sun, 12 May 2024 11:43:12 +0200 Subject: [PATCH 4/4] Another text replace - also testing commits from within VS Code --- libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp index 780d1ef50..b49642c28 100644 --- a/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp +++ b/libraries/core/src/morpheus/core/serialisation/yaml_writer.hpp @@ -20,7 +20,7 @@ namespace morpheus::serialisation { /// \class YamlWriter -/// Implementes the concept Writer for a streaming JSON writer which writes item by item to the output stream. +/// Implementes the concept Writer for a streaming YAML writer which writes item by item to the output stream. class MORPHEUSCORE_EXPORT YamlWriter { public: