diff --git a/include/rfl/yaml/Reader.hpp b/include/rfl/yaml/Reader.hpp index 505339e29..5bcc68507 100644 --- a/include/rfl/yaml/Reader.hpp +++ b/include/rfl/yaml/Reader.hpp @@ -49,6 +49,8 @@ struct Reader { static constexpr bool has_custom_constructor = (requires(InputVarType var) { T::from_yaml_obj(var); }); + Reader(const std::string_view& _yaml_str) noexcept : yaml_str_(_yaml_str) {} + rfl::Result get_field_from_array( const size_t _idx, const InputArrayType& _arr) const noexcept { if (_idx >= _arr.node_.size()) { @@ -76,7 +78,22 @@ struct Reader { if constexpr (std::is_same, std::string>() || std::is_same, bool>() || std::is_floating_point>()) { - return _var.node_.as>(); + auto result = _var.node_.as>(); + if constexpr (std::is_same, std::string>()) { + // In case of multi-line YAML literal strings, yaml-cpp may parse an + // extra new line which is not there intentionally. This may break + // multiple re-serialization checks, for this reason we trim trailing + // new-lines here. + // + // This is only done for literal blocks which doesn't have tags or anchors + if (_var.node_.Tag() == "!" && yaml_str_[_var.node_.Mark().pos] == '|') { + auto last_non_new_line = result.find_last_not_of("\r\n"); + if (last_non_new_line != std::string::npos) { + result = result.substr(0, last_non_new_line + 1); + } + } + } + return result; } else if constexpr (std::is_integral>()) { return static_cast(_var.node_.as>()); @@ -141,6 +158,9 @@ struct Reader { return error(e.what()); } } + + private: + std::string_view yaml_str_; }; } // namespace yaml diff --git a/include/rfl/yaml/Writer.hpp b/include/rfl/yaml/Writer.hpp index 943f3b650..112a105c4 100644 --- a/include/rfl/yaml/Writer.hpp +++ b/include/rfl/yaml/Writer.hpp @@ -15,6 +15,18 @@ namespace rfl::yaml { class RFL_API Writer { public: + enum Flags { + no_flags = 0, + + /// A string value which has at least one new-line character will be written + /// as multiline YAML literal. It costs one call to std::basic_string::find + /// on all string values. + string_multiline_literal = 1, + + /// All string values will be written as multiline YAML literal + string_all_literal = 2 + }; + struct YAMLArray {}; struct YAMLObject {}; @@ -25,7 +37,7 @@ class RFL_API Writer { using OutputObjectType = YAMLObject; using OutputVarType = YAMLVar; - Writer(const Ref& _out); + Writer(const Ref& _out, Flags _flags = no_flags); ~Writer(); @@ -82,6 +94,17 @@ class RFL_API Writer { void end_object(OutputObjectType* _obj) const; + private: + template + void insert_literal_block_if_needed(const T& _var) const { + if constexpr (std::is_same, std::string>()) { + if (flags_ & string_all_literal || (flags_ & string_multiline_literal && _var.find('\n') != std::string::npos)) { + (*out_) << YAML::Literal; + } + } + } + + public: template OutputVarType insert_value(const std::string_view& _name, const T& _var) const { @@ -89,7 +112,9 @@ class RFL_API Writer { std::is_same, bool>() || std::is_same, std::remove_cvref_t>()) { - (*out_) << YAML::Key << std::string(_name) << YAML::Value << _var; + (*out_) << YAML::Key << std::string(_name) << YAML::Value; + insert_literal_block_if_needed(_var); + (*out_) << _var; } else if constexpr (std::is_floating_point>()) { // std::to_string is necessary to ensure that floating point values are // always written as floats. @@ -110,6 +135,7 @@ class RFL_API Writer { std::is_same, bool>() || std::is_same, std::remove_cvref_t>()) { + insert_literal_block_if_needed(_var); (*out_) << _var; } else if constexpr (std::is_floating_point>()) { // std::to_string is necessary to ensure that floating point values are @@ -135,6 +161,7 @@ class RFL_API Writer { public: const Ref out_; + Flags flags_; }; } // namespace rfl::yaml diff --git a/include/rfl/yaml/read.hpp b/include/rfl/yaml/read.hpp index 36a341771..5ba18b9b8 100644 --- a/include/rfl/yaml/read.hpp +++ b/include/rfl/yaml/read.hpp @@ -18,8 +18,8 @@ using InputVarType = typename Reader::InputVarType; /// Parses an object from a YAML var. template -auto read(const InputVarType& _var) { - const auto r = Reader(); +auto read(const InputVarType& _var, const std::string& _yaml_str) { + const auto r = Reader(_yaml_str); using ProcessorsType = Processors; static_assert(!ProcessorsType::no_field_names_, "The NoFieldNames processor is not supported for BSON, XML, " @@ -32,7 +32,7 @@ template Result> read(const std::string& _yaml_str) { try { const auto var = InputVarType(YAML::Load(_yaml_str)); - return read(var); + return read(var, _yaml_str); } catch (std::exception& e) { return error(e.what()); } diff --git a/include/rfl/yaml/write.hpp b/include/rfl/yaml/write.hpp index 48cadc253..86154bdca 100644 --- a/include/rfl/yaml/write.hpp +++ b/include/rfl/yaml/write.hpp @@ -17,11 +17,11 @@ namespace yaml { /// Writes a YAML into an ostream. template -std::ostream& write(const auto& _obj, std::ostream& _stream) { +std::ostream& write(const auto& _obj, std::ostream& _stream, Writer::Flags _flags = Writer::Flags::no_flags) { using T = std::remove_cvref_t; using ParentType = parsing::Parent; const auto out = Ref::make(); - auto w = Writer(out); + auto w = Writer(out, _flags); using ProcessorsType = Processors; static_assert(!ProcessorsType::no_field_names_, "The NoFieldNames processor is not supported for BSON, XML, " @@ -33,11 +33,11 @@ std::ostream& write(const auto& _obj, std::ostream& _stream) { /// Returns a YAML string. template -std::string write(const auto& _obj) { +std::string write(const auto& _obj, Writer::Flags _flags = Writer::Flags::no_flags) { using T = std::remove_cvref_t; using ParentType = parsing::Parent; const auto out = Ref::make(); - auto w = Writer(out); + auto w = Writer(out, _flags); using ProcessorsType = Processors; static_assert(!ProcessorsType::no_field_names_, "The NoFieldNames processor is not supported for BSON, XML, " diff --git a/src/rfl/yaml/Writer.cpp b/src/rfl/yaml/Writer.cpp index eae111e0d..a0d3b8c4c 100644 --- a/src/rfl/yaml/Writer.cpp +++ b/src/rfl/yaml/Writer.cpp @@ -2,7 +2,7 @@ namespace rfl::yaml { -Writer::Writer(const Ref& _out) : out_(_out) {} +Writer::Writer(const Ref& _out, Flags _flags) : out_(_out), flags_(_flags) {} Writer::~Writer() = default; diff --git a/tests/yaml/test_multiline.cpp b/tests/yaml/test_multiline.cpp new file mode 100644 index 000000000..e6afa3fab --- /dev/null +++ b/tests/yaml/test_multiline.cpp @@ -0,0 +1,48 @@ +#include +#include + +#include "write_and_read.hpp" + +struct MultilineTestStruct { + std::string normal_string; + std::string multiline_string; +}; + +namespace test_multiline { +TEST(yaml, test_multiline) { + const auto test = MultilineTestStruct{.normal_string = "The normal string", + .multiline_string = +R"(Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod +tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, +quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo +consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum +dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, +sunt in culpa qui officia deserunt mollit anim id est laborum.)" + }; + + write_and_read(test, rfl::yaml::Writer::string_multiline_literal); + write_and_read(test, rfl::yaml::Writer::string_all_literal); +} + +TEST(yaml, test_multiline_read) { + const auto test = MultilineTestStruct{.normal_string = "The normal string", + .multiline_string = "Foobar\n\n" + }; + + const std::string random_yaml( +R"( +normal_string: | + The normal string + + +multiline_string: "Foobar\n\n" + +)" + ); + + auto read_result = rfl::yaml::read(random_yaml); + EXPECT_TRUE(read_result.has_value()); + EXPECT_EQ(read_result.value().normal_string, test.normal_string); + EXPECT_EQ(read_result.value().multiline_string, test.multiline_string); +} +} // namespace test_multiline diff --git a/tests/yaml/write_and_read.hpp b/tests/yaml/write_and_read.hpp index b01495e61..bda619a4d 100644 --- a/tests/yaml/write_and_read.hpp +++ b/tests/yaml/write_and_read.hpp @@ -6,14 +6,14 @@ #include template -void write_and_read(const auto& _struct) { +void write_and_read(const auto& _struct, rfl::yaml::Writer::Flags _flags = rfl::yaml::Writer::Flags::no_flags) { using T = std::remove_cvref_t; - const auto serialized1 = rfl::yaml::write(_struct); + const auto serialized1 = rfl::yaml::write(_struct, _flags); const auto res = rfl::yaml::read( std::string_view(serialized1.c_str(), serialized1.size())); EXPECT_TRUE(res && true) << "Test failed on read. Error: " << res.error().what(); - const auto serialized2 = rfl::yaml::write(res.value()); + const auto serialized2 = rfl::yaml::write(res.value(), _flags); EXPECT_EQ(serialized1, serialized2); }