From d15b7c153399b0fb50dd5c90b0192cac31986df4 Mon Sep 17 00:00:00 2001 From: David Morasz Date: Mon, 27 Apr 2026 20:20:23 +0200 Subject: [PATCH 1/6] YAML strings which contain new-lines should be exported as multi-line literals --- include/rfl/yaml/Writer.hpp | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/include/rfl/yaml/Writer.hpp b/include/rfl/yaml/Writer.hpp index 943f3b650..cf9c7c4f5 100644 --- a/include/rfl/yaml/Writer.hpp +++ b/include/rfl/yaml/Writer.hpp @@ -85,10 +85,15 @@ class RFL_API Writer { template OutputVarType insert_value(const std::string_view& _name, const T& _var) const { - if constexpr (std::is_same, std::string>() || - std::is_same, bool>() || - std::is_same, - std::remove_cvref_t>()) { + if constexpr (std::is_same, std::string>()) { + (*out_) << YAML::Key << std::string(_name) << YAML::Value; + if (_var.find('\n') != std::string::npos) { + (*out_) << YAML::Literal; + } + (*out_) << _var; + } else if constexpr (std::is_same, bool>() || + std::is_same, + std::remove_cvref_t>()) { (*out_) << YAML::Key << std::string(_name) << YAML::Value << _var; } else if constexpr (std::is_floating_point>()) { // std::to_string is necessary to ensure that floating point values are @@ -106,10 +111,14 @@ class RFL_API Writer { template OutputVarType insert_value(const T& _var) const { - if constexpr (std::is_same, std::string>() || - std::is_same, bool>() || - std::is_same, - std::remove_cvref_t>()) { + if constexpr (std::is_same, std::string>()) { + if (_var.find('\n') != std::string::npos) { + (*out_) << YAML::Literal; + } + (*out_) << _var; + } else if constexpr (std::is_same, bool>() || + std::is_same, + std::remove_cvref_t>()) { (*out_) << _var; } else if constexpr (std::is_floating_point>()) { // std::to_string is necessary to ensure that floating point values are From fed4cfc6ffc9cb9c3d5db3cdbef15b58828dba38 Mon Sep 17 00:00:00 2001 From: David Morasz Date: Mon, 27 Apr 2026 21:45:10 +0200 Subject: [PATCH 2/6] control YAML multiline string behavior with flags --- include/rfl/yaml/Writer.hpp | 19 ++++++++++++++++--- include/rfl/yaml/write.hpp | 8 ++++---- src/rfl/yaml/Writer.cpp | 2 +- tests/yaml/test_multiline.cpp | 26 ++++++++++++++++++++++++++ tests/yaml/write_and_read.hpp | 6 +++--- 5 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 tests/yaml/test_multiline.cpp diff --git a/include/rfl/yaml/Writer.hpp b/include/rfl/yaml/Writer.hpp index cf9c7c4f5..a37b47f4b 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(); @@ -87,7 +99,7 @@ class RFL_API Writer { const T& _var) const { if constexpr (std::is_same, std::string>()) { (*out_) << YAML::Key << std::string(_name) << YAML::Value; - if (_var.find('\n') != std::string::npos) { + if (flags & string_all_literal || (flags & string_multiline_literal && _var.find('\n') != std::string::npos)) { (*out_) << YAML::Literal; } (*out_) << _var; @@ -112,7 +124,7 @@ class RFL_API Writer { template OutputVarType insert_value(const T& _var) const { if constexpr (std::is_same, std::string>()) { - if (_var.find('\n') != std::string::npos) { + if (flags & string_all_literal || (flags & string_multiline_literal && _var.find('\n') != std::string::npos)) { (*out_) << YAML::Literal; } (*out_) << _var; @@ -144,6 +156,7 @@ class RFL_API Writer { public: const Ref out_; + Flags flags; }; } // namespace rfl::yaml 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..eae3adc28 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..84c6b1e1e --- /dev/null +++ b/tests/yaml/test_multiline.cpp @@ -0,0 +1,26 @@ +#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); +} +} // 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); } From 03c16504c4154d91fb0a07ff76aced1081d6eb00 Mon Sep 17 00:00:00 2001 From: David Morasz Date: Mon, 27 Apr 2026 23:17:11 +0200 Subject: [PATCH 3/6] fix unintentional trailing new-lines when parsing from multiline string literals --- include/rfl/yaml/Reader.hpp | 15 ++++++++++++++- tests/yaml/test_multiline.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/include/rfl/yaml/Reader.hpp b/include/rfl/yaml/Reader.hpp index 505339e29..82490136a 100644 --- a/include/rfl/yaml/Reader.hpp +++ b/include/rfl/yaml/Reader.hpp @@ -76,7 +76,20 @@ 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. + // + // It would be preferable to do this only if input string was stored + // as a multiline string literal, but unfortunately yaml-cpp doesn't + // seem to expose that information. + auto last_non_new_line = result.find_last_not_of("\r\n"); + result = result.substr(0, last_non_new_line + 1); + } + return result; } else if constexpr (std::is_integral>()) { return static_cast(_var.node_.as>()); diff --git a/tests/yaml/test_multiline.cpp b/tests/yaml/test_multiline.cpp index 84c6b1e1e..a4bc2d818 100644 --- a/tests/yaml/test_multiline.cpp +++ b/tests/yaml/test_multiline.cpp @@ -23,4 +23,30 @@ 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 = +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, etc...)" + }; + + const std::string random_yaml( +R"( +normal_string: | + The normal string + + +multiline_string: | + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod + tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, etc... + +)" + ); + + 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 From 143686d01857bf24001c617bcd0dc17c9875cd6e Mon Sep 17 00:00:00 2001 From: David Morasz Date: Mon, 27 Apr 2026 23:26:04 +0200 Subject: [PATCH 4/6] forgot to check last_non_new_line --- include/rfl/yaml/Reader.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/rfl/yaml/Reader.hpp b/include/rfl/yaml/Reader.hpp index 82490136a..fb33ab809 100644 --- a/include/rfl/yaml/Reader.hpp +++ b/include/rfl/yaml/Reader.hpp @@ -87,7 +87,9 @@ struct Reader { // as a multiline string literal, but unfortunately yaml-cpp doesn't // seem to expose that information. auto last_non_new_line = result.find_last_not_of("\r\n"); - result = result.substr(0, last_non_new_line + 1); + if (last_non_new_line != std::string::npos) { + result = result.substr(0, last_non_new_line + 1); + } } return result; From fd5d1c4d13c740c5f75323ede88d1b8f94913302 Mon Sep 17 00:00:00 2001 From: David Morasz Date: Tue, 28 Apr 2026 01:46:12 +0200 Subject: [PATCH 5/6] limit trailing new-line trimming to literal blocks --- include/rfl/yaml/Reader.hpp | 17 +++++++++++------ include/rfl/yaml/read.hpp | 6 +++--- tests/yaml/test_multiline.cpp | 10 +++------- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/include/rfl/yaml/Reader.hpp b/include/rfl/yaml/Reader.hpp index fb33ab809..d964c9f01 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()) { @@ -83,12 +85,12 @@ struct Reader { // multiple re-serialization checks, for this reason we trim trailing // new-lines here. // - // It would be preferable to do this only if input string was stored - // as a multiline string literal, but unfortunately yaml-cpp doesn't - // seem to expose that information. - 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); + // 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; @@ -156,6 +158,9 @@ struct Reader { return error(e.what()); } } + +private: + std::string_view yaml_str; }; } // namespace 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/tests/yaml/test_multiline.cpp b/tests/yaml/test_multiline.cpp index a4bc2d818..e6afa3fab 100644 --- a/tests/yaml/test_multiline.cpp +++ b/tests/yaml/test_multiline.cpp @@ -11,7 +11,7 @@ struct MultilineTestStruct { namespace test_multiline { TEST(yaml, test_multiline) { const auto test = MultilineTestStruct{.normal_string = "The normal string", - .multiline_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 @@ -26,9 +26,7 @@ sunt in culpa qui officia deserunt mollit anim id est laborum.)" TEST(yaml, test_multiline_read) { 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, etc...)" + .multiline_string = "Foobar\n\n" }; const std::string random_yaml( @@ -37,9 +35,7 @@ normal_string: | The normal string -multiline_string: | - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod - tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, etc... +multiline_string: "Foobar\n\n" )" ); From bf5e0150fc5f853240b167b35b9b914a79c24a49 Mon Sep 17 00:00:00 2001 From: David Morasz Date: Tue, 28 Apr 2026 02:11:42 +0200 Subject: [PATCH 6/6] PR feedback (from AI) and naming conventions --- include/rfl/yaml/Reader.hpp | 8 ++++---- include/rfl/yaml/Writer.hpp | 39 +++++++++++++++++++++---------------- src/rfl/yaml/Writer.cpp | 2 +- 3 files changed, 27 insertions(+), 22 deletions(-) diff --git a/include/rfl/yaml/Reader.hpp b/include/rfl/yaml/Reader.hpp index d964c9f01..5bcc68507 100644 --- a/include/rfl/yaml/Reader.hpp +++ b/include/rfl/yaml/Reader.hpp @@ -49,7 +49,7 @@ 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) {} + 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 { @@ -86,7 +86,7 @@ struct Reader { // 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] == '|') { + 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); @@ -159,8 +159,8 @@ struct Reader { } } -private: - std::string_view yaml_str; + private: + std::string_view yaml_str_; }; } // namespace yaml diff --git a/include/rfl/yaml/Writer.hpp b/include/rfl/yaml/Writer.hpp index a37b47f4b..112a105c4 100644 --- a/include/rfl/yaml/Writer.hpp +++ b/include/rfl/yaml/Writer.hpp @@ -94,19 +94,27 @@ class RFL_API Writer { void end_object(OutputObjectType* _obj) const; + private: template - OutputVarType insert_value(const std::string_view& _name, - const T& _var) const { + void insert_literal_block_if_needed(const T& _var) const { if constexpr (std::is_same, std::string>()) { - (*out_) << YAML::Key << std::string(_name) << YAML::Value; - if (flags & string_all_literal || (flags & string_multiline_literal && _var.find('\n') != std::string::npos)) { + 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 { + if constexpr (std::is_same, std::string>() || + std::is_same, bool>() || + std::is_same, + std::remove_cvref_t>()) { + (*out_) << YAML::Key << std::string(_name) << YAML::Value; + insert_literal_block_if_needed(_var); (*out_) << _var; - } else if constexpr (std::is_same, bool>() || - std::is_same, - std::remove_cvref_t>()) { - (*out_) << YAML::Key << std::string(_name) << YAML::Value << _var; } else if constexpr (std::is_floating_point>()) { // std::to_string is necessary to ensure that floating point values are // always written as floats. @@ -123,14 +131,11 @@ class RFL_API Writer { template OutputVarType insert_value(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; - } - (*out_) << _var; - } else if constexpr (std::is_same, bool>() || - std::is_same, - std::remove_cvref_t>()) { + if constexpr (std::is_same, std::string>() || + 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 @@ -156,7 +161,7 @@ class RFL_API Writer { public: const Ref out_; - Flags flags; + Flags flags_; }; } // namespace rfl::yaml diff --git a/src/rfl/yaml/Writer.cpp b/src/rfl/yaml/Writer.cpp index eae3adc28..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, Flags _flags) : out_(_out), flags(_flags) {} +Writer::Writer(const Ref& _out, Flags _flags) : out_(_out), flags_(_flags) {} Writer::~Writer() = default;