diff --git a/README.md b/README.md index f82b555b..e044bd53 100644 --- a/README.md +++ b/README.md @@ -571,6 +571,7 @@ In addition, it supports the following custom containers: - `rfl::Binary`: Used to express numbers in binary format. - `rfl::Box`: Similar to `std::unique_ptr`, but (almost) guaranteed to never be null. - `rfl::Bytestring`: An alias for `std::vector`. Supported by Avro, BSON, Cap'n Proto, CBOR, flexbuffers, msgpack and UBJSON. +- `rfl::Commented`: Allows you to add comments to fields (supported by YAML and XML). - `rfl::Generic`: A catch-all type that can represent (almost) anything. - `rfl::Hex`: Used to express numbers in hex format. - `rfl::Literal`: An explicitly enumerated string. diff --git a/docs/commented.md b/docs/commented.md new file mode 100644 index 00000000..494bceee --- /dev/null +++ b/docs/commented.md @@ -0,0 +1,81 @@ +# `rfl::Commented` + +The `rfl::Commented` wrapper allows you to add comments to fields in your structs. These comments are then serialized in formats that support them, such as YAML and XML. + +Note that `rfl::Commented` is currently unsupported by formats that do not have a standard way of representing comments, such as JSON. Also note that comments are **write-only**: they are ignored during deserialization. + +## Example (YAML) + +In YAML, the comments are added as line comments (`#`): + +```cpp +struct Person { + std::string first_name; + std::string last_name; + rfl::Commented town; +}; + +const auto homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .town = rfl::Commented( + "Springfield", "The town where Homer lives")}; + +const auto yaml_str = rfl::yaml::write(homer); +``` + +This will result in the following YAML: + +```yaml +first_name: Homer +last_name: Simpson +town: Springfield # The town where Homer lives +``` + +## Example (XML) + +In XML, the comments are added as `` blocks after the field: + +```cpp +struct Person { + std::string first_name; + std::string last_name; + rfl::Commented town; +}; + +const auto homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .town = rfl::Commented( + "Springfield", "The town where Homer lives")}; + +const auto xml_str = rfl::xml::write(homer); +``` + +This will result in the following XML: + +```xml + + + Homer + Simpson + Springfield + + +``` + +## API convenience + +`rfl::Commented` provides several ways to access and modify the underlying value and the comment: + +- `.get()`, `.value()`, `operator()()` — access the underlying value (const and non-const overloads). +- `.comment()` — returns an `std::optional` containing the comment. +- `.add_comment(std::string)` — sets or updates the comment. +- `.set(...)`, `operator=(...)` — assign the underlying value. + +Example: + +```cpp +Person p; +p.town = "Springfield"; +p.town.add_comment("The town where Homer lives"); +std::string s = p.town.value(); +``` diff --git a/docs/docs-readme.md b/docs/docs-readme.md index 813753c0..c98e2466 100644 --- a/docs/docs-readme.md +++ b/docs/docs-readme.md @@ -24,6 +24,8 @@ [rfl::Skip](rfl_skip.md) - For skipping fields during serialization and/or deserialization. +[rfl::Commented](commented.md) - For adding comments to your serialized format (only supported by some formats, such as YAML or XML). + [rfl::Result](result.md) - For error handling without exceptions. [Standard containers](standard_containers.md) - Describes how reflect-cpp treats containers in the standard library. diff --git a/include/rfl.hpp b/include/rfl.hpp index b57542b6..e8ba77f8 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -16,6 +16,8 @@ #include "rfl/Binary.hpp" #include "rfl/Box.hpp" #include "rfl/Bytestring.hpp" +#include "rfl/CamelCaseToSnakeCase.hpp" +#include "rfl/Commented.hpp" #include "rfl/DefaultIfMissing.hpp" #include "rfl/DefaultVal.hpp" #include "rfl/Description.hpp" @@ -41,7 +43,6 @@ #include "rfl/Skip.hpp" #include "rfl/SnakeCaseToCamelCase.hpp" #include "rfl/SnakeCaseToPascalCase.hpp" -#include "rfl/CamelCaseToSnakeCase.hpp" #include "rfl/TaggedUnion.hpp" #include "rfl/Timestamp.hpp" #include "rfl/UnderlyingEnums.hpp" diff --git a/include/rfl/Commented.hpp b/include/rfl/Commented.hpp new file mode 100644 index 00000000..156416dd --- /dev/null +++ b/include/rfl/Commented.hpp @@ -0,0 +1,158 @@ +#ifndef RFL_COMMENTED_HPP_ +#define RFL_COMMENTED_HPP_ + +#include +#include +#include +#include + +#include "default.hpp" + +namespace rfl { + +template +struct Commented { + public: + using Type = std::remove_cvref_t; + + Commented() : value_(Type()) {} + + Commented(const Type& _value) : value_(_value) {} + + Commented(Type&& _value) noexcept : value_(std::move(_value)) {} + + Commented(const Type& _value, std::optional _comment) + : comment_(std::move(_comment)), value_(_value) {} + + Commented(Type&& _value, std::optional _comment) noexcept + : comment_(std::move(_comment)), value_(std::move(_value)) {} + + Commented(Commented&& _commented) noexcept = default; + + Commented(const Commented& _commented) = default; + + template + Commented(const Commented& _commented) + : comment_(_commented.comment()), value_(_commented.get()) {} + + template + Commented(Commented&& _commented) noexcept + : comment_(std::move(_commented.comment())), + value_(std::move(_commented.value())) {} + + template + requires(std::is_convertible_v) + Commented(const Commented& _commented) + : comment_(_commented.comment()), value_(_commented.value()) {} + + template + requires(std::is_convertible_v) + Commented(const U& _value) : value_(_value) {} + + template + requires(std::is_convertible_v) + Commented(U&& _value) noexcept : value_(std::forward(_value)) {} + + /// Assigns the underlying object to its default value. + template + requires(std::is_default_constructible_v) + Commented(const Default&) : value_(Type()) {} + + ~Commented() = default; + + /// Sets the comment associated with the field. + void add_comment(std::string _comment) { comment_ = std::move(_comment); } + + /// Returns the comment associated with the field, if any. + const std::optional& comment() const { return comment_; } + + /// Returns the underlying object. + Type& get() { return value_; } + + /// Returns the underlying object. + const Type& get() const { return value_; } + + /// Returns the underlying object. + Type& operator()() { return value_; } + + /// Returns the underlying object. + const Type& operator()() const { return value_; } + + /// Returns the underlying object. + Type& operator*() { return value_; } + + /// Returns the underlying object. + const Type& operator*() const { return value_; } + + /// Assigns the underlying object. + auto& operator=(const Type& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object. + auto& operator=(Type&& _value) noexcept { + value_ = std::move(_value); + return *this; + } + + /// Assigns the underlying object. + template + requires std::is_convertible_v + auto& operator=(const U& _value) { + value_ = _value; + return *this; + } + + /// Assigns the underlying object to its default value. + template + requires std::is_default_constructible_v + auto& operator=(const Default&) { + value_ = Type(); + return *this; + } + + /// Assigns the underlying object. + Commented& operator=(const Commented& _commented) = default; + + /// Assigns the underlying object. + Commented& operator=(Commented&& _commented) = default; + + /// Assigns the underlying object. + template + auto& operator=(const Commented& _commented) { + value_ = _commented.get(); + comment_ = _commented.comment(); + return *this; + } + + /// Assigns the underlying object. + template + auto& operator=(Commented&& _commented) { + value_ = std::move(_commented.value_); + comment_ = std::move(_commented.comment_); + return *this; + } + + /// Assigns the underlying object. + void set(const Type& _value) { value_ = _value; } + + /// Assigns the underlying object. + void set(Type&& _value) { value_ = std::move(_value); } + + /// Returns the underlying object. + Type& value() { return value_; } + + /// Returns the underlying object. + const Type& value() const { return value_; } + + /// The comment associated with the field. + std::optional comment_; + + /// The underlying value. + Type value_; +}; + +} // namespace rfl + +#endif diff --git a/include/rfl/parsing/Parent.hpp b/include/rfl/parsing/Parent.hpp index c8d73be7..8ce99fa4 100644 --- a/include/rfl/parsing/Parent.hpp +++ b/include/rfl/parsing/Parent.hpp @@ -7,6 +7,7 @@ #include "../always_false.hpp" #include "schemaful/IsSchemafulWriter.hpp" #include "supports_attributes.hpp" +#include "supports_comments.hpp" namespace rfl::parsing { @@ -71,6 +72,21 @@ struct Parent { } } + /// Adds a comment to the parent element, if supported by the writer. + template + static void add_comment(const W& _w, std::string_view _comment, + const ParentType& _parent) { + using Type = std::remove_cvref_t; + if constexpr (supports_comments) { + if constexpr (std::is_same()) { + _w.add_comment_to_array(_comment, _parent.arr_); + + } else if constexpr (std::is_same()) { + _w.add_comment_to_object(_comment, _parent.obj_); + } + } + } + // For schemaful formats only. template static auto add_map(const W& _w, const size_t _size, diff --git a/include/rfl/parsing/Parser.hpp b/include/rfl/parsing/Parser.hpp index 4f1cb355..1d9e4d34 100644 --- a/include/rfl/parsing/Parser.hpp +++ b/include/rfl/parsing/Parser.hpp @@ -9,6 +9,7 @@ #include "Parser_box.hpp" #include "Parser_bytestring.hpp" #include "Parser_c_array.hpp" +#include "Parser_commented.hpp" #include "Parser_default.hpp" #include "Parser_default_val.hpp" #include "Parser_duration.hpp" diff --git a/include/rfl/parsing/Parser_commented.hpp b/include/rfl/parsing/Parser_commented.hpp new file mode 100644 index 00000000..4720a5d5 --- /dev/null +++ b/include/rfl/parsing/Parser_commented.hpp @@ -0,0 +1,57 @@ +#ifndef RFL_PARSING_PARSER_COMMENTED_HPP_ +#define RFL_PARSING_PARSER_COMMENTED_HPP_ + +#include +#include +#include + +#include "../Commented.hpp" +#include "../Ref.hpp" +#include "../Result.hpp" +#include "../always_false.hpp" +#include "Parent.hpp" +#include "Parser_base.hpp" +#include "schema/Type.hpp" +#include "schemaful/IsSchemafulReader.hpp" +#include "schemaful/IsSchemafulWriter.hpp" +#include "schemaful/OptionalReader.hpp" +#include "supports_comments.hpp" + +namespace rfl::parsing { + +template + requires AreReaderAndWriter> +struct Parser, ProcessorsType> { + using InputVarType = typename R::InputVarType; + + using ParentType = Parent; + + static Result> read(const R& _r, + const InputVarType& _var) noexcept { + return Parser, ProcessorsType>::read(_r, _var) + .transform([](auto&& _t) { + return Commented(std::forward(_t)); + }); + } + + template + static void write(const W& _w, const Commented& _c, const P& _parent) { + Parser, ProcessorsType>::write(_w, _c.get(), + _parent); + if constexpr (supports_comments) { + if (_c.comment()) { + ParentType::add_comment(_w, *_c.comment(), _parent); + } + } + } + + static schema::Type to_schema( + std::map* _definitions) { + return Parser, ProcessorsType>::to_schema( + _definitions); + } +}; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/parsing/supports_comments.hpp b/include/rfl/parsing/supports_comments.hpp new file mode 100644 index 00000000..a91332b3 --- /dev/null +++ b/include/rfl/parsing/supports_comments.hpp @@ -0,0 +1,25 @@ +#ifndef RFL_PARSING_SUPPORTSCOMMENTS_HPP_ +#define RFL_PARSING_SUPPORTSCOMMENTS_HPP_ + +#include +#include +#include +#include + +#include "../Result.hpp" + +namespace rfl::parsing { + +/// Determines whether a writer supports comments. +template +concept supports_comments = + requires(W w, typename W::OutputArrayType arr, + typename W::OutputObjectType obj, std::string_view comment) { + { w.add_comment_to_array(comment, &arr) } -> std::same_as; + + { w.add_comment_to_object(comment, &obj) } -> std::same_as; + }; + +} // namespace rfl::parsing + +#endif diff --git a/include/rfl/xml/Writer.hpp b/include/rfl/xml/Writer.hpp index e68582ce..6b032ff3 100644 --- a/include/rfl/xml/Writer.hpp +++ b/include/rfl/xml/Writer.hpp @@ -10,8 +10,7 @@ #include "../always_false.hpp" #include "../common.hpp" -namespace rfl { -namespace xml { +namespace rfl::xml { struct RFL_API Writer { struct XMLOutputArray { @@ -59,6 +58,12 @@ struct RFL_API Writer { const size_t _size, OutputObjectType* _parent) const; + void add_comment_to_array(const std::string_view& _comment, + OutputArrayType* _parent) const; + + void add_comment_to_object(const std::string_view& _comment, + OutputObjectType* _parent) const; + OutputObjectType add_object_to_array(const size_t _size, OutputArrayType* _parent) const; @@ -95,7 +100,7 @@ struct RFL_API Writer { template decltype(auto) to_string(const T& _val) const { if constexpr (std::is_same, std::string>()) { - return _val; // Return reference to avoid expensive string copy. + return _val; // Return reference to avoid expensive string copy. } else if constexpr (std::is_same, bool>()) { return _val ? "true" : "false"; } else if constexpr (std::is_floating_point>() || @@ -121,7 +126,6 @@ struct RFL_API Writer { std::string root_name_; }; -} // namespace xml -} // namespace rfl +} // namespace rfl::xml #endif // RFL_XML_WRITER_HPP_ diff --git a/include/rfl/yaml/Writer.hpp b/include/rfl/yaml/Writer.hpp index b5bf0616..943f3b65 100644 --- a/include/rfl/yaml/Writer.hpp +++ b/include/rfl/yaml/Writer.hpp @@ -11,8 +11,7 @@ #include "../always_false.hpp" #include "../common.hpp" -namespace rfl { -namespace yaml { +namespace rfl::yaml { class RFL_API Writer { public: @@ -42,11 +41,17 @@ class RFL_API Writer { } OutputArrayType add_array_to_array(const size_t _size, - OutputArrayType* _parent) const; + OutputArrayType* /*_parent*/) const; OutputArrayType add_array_to_object(const std::string_view& _name, const size_t _size, - OutputObjectType* _parent) const; + OutputObjectType* /*_parent*/) const; + + void add_comment_to_array(const std::string_view& _comment, + OutputArrayType* _parent) const; + + void add_comment_to_object(const std::string_view& _comment, + OutputObjectType* _parent) const; OutputObjectType add_object_to_array(const size_t _size, OutputArrayType* _parent) const; @@ -122,6 +127,8 @@ class RFL_API Writer { OutputArrayType new_array() const; + void new_comment(const std::string_view& _comment) const; + OutputObjectType new_object(const std::string_view& _name) const; OutputObjectType new_object() const; @@ -130,7 +137,6 @@ class RFL_API Writer { const Ref out_; }; -} // namespace yaml -} // namespace rfl +} // namespace rfl::yaml #endif diff --git a/mkdocs.yaml b/mkdocs.yaml index 988c1339..24e7e1fd 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -123,6 +123,7 @@ nav: - rfl::Box and rfl::Ref: rfl_ref.md - rfl::Timestamp: timestamps.md - rfl::Skip: rfl_skip.md + - rfl::Commented: commented.md - rfl::Result: result.md - Standard containers: standard_containers.md - C arrays and inheritance: c_arrays_and_inheritance.md diff --git a/src/rfl/xml/Writer.cpp b/src/rfl/xml/Writer.cpp index 83be3cdc..d46a90e1 100644 --- a/src/rfl/xml/Writer.cpp +++ b/src/rfl/xml/Writer.cpp @@ -38,27 +38,23 @@ Writer::Writer(const Ref& _root, const std::string& _root_name) Writer::~Writer() = default; Writer::OutputArrayType Writer::array_as_root(const size_t /*_size*/) const { - auto node_child = - Ref::make(root_->append_child(root_name_)); + auto node_child = Ref::make(root_->append_child(root_name_)); return OutputArrayType(root_name_, node_child); } Writer::OutputObjectType Writer::object_as_root(const size_t /*_size*/) const { - auto node_child = - Ref::make(root_->append_child(root_name_)); + auto node_child = Ref::make(root_->append_child(root_name_)); return OutputObjectType(node_child); } Writer::OutputVarType Writer::null_as_root() const { - auto node_child = - Ref::make(root_->append_child(root_name_)); + auto node_child = Ref::make(root_->append_child(root_name_)); return OutputVarType(node_child); } Writer::OutputVarType Writer::value_as_root_impl( const std::string& _str) const { - auto node_child = - Ref::make(root_->append_child(root_name_)); + auto node_child = Ref::make(root_->append_child(root_name_)); node_child->append_child(pugi::node_pcdata).set_value(_str); return OutputVarType(node_child); } @@ -74,6 +70,16 @@ Writer::OutputArrayType Writer::add_array_to_object( return OutputArrayType(_name, _parent->node_); } +void Writer::add_comment_to_array(const std::string_view& _comment, + OutputArrayType* _parent) const { + _parent->node_->append_child(pugi::node_comment).set_value(_comment); +} + +void Writer::add_comment_to_object(const std::string_view& _comment, + OutputObjectType* _parent) const { + _parent->node_->append_child(pugi::node_comment).set_value(_comment); +} + Writer::OutputVarType Writer::add_value_to_array_impl( const std::string& _str, OutputArrayType* _parent) const { auto node_child = diff --git a/src/rfl/yaml/Writer.cpp b/src/rfl/yaml/Writer.cpp index a45b144c..eae111e0 100644 --- a/src/rfl/yaml/Writer.cpp +++ b/src/rfl/yaml/Writer.cpp @@ -29,6 +29,16 @@ Writer::OutputArrayType Writer::add_array_to_object( return new_array(_name); } +void Writer::add_comment_to_array(const std::string_view& _comment, + OutputArrayType*) const { + new_comment(_comment); +} + +void Writer::add_comment_to_object(const std::string_view& _comment, + OutputObjectType*) const { + new_comment(_comment); +} + Writer::OutputObjectType Writer::add_object_to_array( const size_t /*_size*/, OutputArrayType* /*_parent*/) const { return new_object(); @@ -68,6 +78,10 @@ Writer::OutputArrayType Writer::new_array() const { return OutputArrayType{}; } +void Writer::new_comment(const std::string_view& _comment) const { + (*out_) << YAML::Comment(std::string(_comment)); +} + Writer::OutputObjectType Writer::new_object( const std::string_view& _name) const { (*out_) << YAML::Key << std::string(_name) << YAML::Value << YAML::BeginMap; diff --git a/tests/xml/test_comment.cpp b/tests/xml/test_comment.cpp new file mode 100644 index 00000000..5416ad53 --- /dev/null +++ b/tests/xml/test_comment.cpp @@ -0,0 +1,32 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_comment { + +struct Person { + std::string first_name; + std::string last_name; + rfl::Commented town; +}; + +TEST(xml, test_comment) { + const auto homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .town = rfl::Commented( + "Springfield", "The town where Homer lives")}; + + const auto xml_str = rfl::xml::write(homer); + + EXPECT_EQ(xml_str, R"( + + Homer + Simpson + Springfield + + +)"); +} +} // namespace test_comment diff --git a/tests/yaml/test_comment.cpp b/tests/yaml/test_comment.cpp new file mode 100644 index 00000000..f57ad7c6 --- /dev/null +++ b/tests/yaml/test_comment.cpp @@ -0,0 +1,27 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_comment { + +struct Person { + std::string first_name; + std::string last_name; + rfl::Commented town; +}; + +TEST(yaml, test_comment) { + const auto homer = Person{.first_name = "Homer", + .last_name = "Simpson", + .town = rfl::Commented( + "Springfield", "The town where Homer lives")}; + + const auto yaml_str = rfl::yaml::write(homer); + + EXPECT_EQ(yaml_str, R"(first_name: Homer +last_name: Simpson +town: Springfield # The town where Homer lives)"); +} +} // namespace test_comment