Skip to content

Commit f17023f

Browse files
Added rfl::Commented; resolves #597 (#602)
1 parent 9b57d01 commit f17023f

16 files changed

Lines changed: 452 additions & 20 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,7 @@ In addition, it supports the following custom containers:
571571
- `rfl::Binary`: Used to express numbers in binary format.
572572
- `rfl::Box`: Similar to `std::unique_ptr`, but (almost) guaranteed to never be null.
573573
- `rfl::Bytestring`: An alias for `std::vector<std::byte>`. Supported by Avro, BSON, Cap'n Proto, CBOR, flexbuffers, msgpack and UBJSON.
574+
- `rfl::Commented`: Allows you to add comments to fields (supported by YAML and XML).
574575
- `rfl::Generic`: A catch-all type that can represent (almost) anything.
575576
- `rfl::Hex`: Used to express numbers in hex format.
576577
- `rfl::Literal`: An explicitly enumerated string.

docs/commented.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# `rfl::Commented`
2+
3+
The `rfl::Commented<T>` 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.
4+
5+
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.
6+
7+
## Example (YAML)
8+
9+
In YAML, the comments are added as line comments (`#`):
10+
11+
```cpp
12+
struct Person {
13+
std::string first_name;
14+
std::string last_name;
15+
rfl::Commented<std::string> town;
16+
};
17+
18+
const auto homer = Person{.first_name = "Homer",
19+
.last_name = "Simpson",
20+
.town = rfl::Commented<std::string>(
21+
"Springfield", "The town where Homer lives")};
22+
23+
const auto yaml_str = rfl::yaml::write(homer);
24+
```
25+
26+
This will result in the following YAML:
27+
28+
```yaml
29+
first_name: Homer
30+
last_name: Simpson
31+
town: Springfield # The town where Homer lives
32+
```
33+
34+
## Example (XML)
35+
36+
In XML, the comments are added as `<!-- comment -->` blocks after the field:
37+
38+
```cpp
39+
struct Person {
40+
std::string first_name;
41+
std::string last_name;
42+
rfl::Commented<std::string> town;
43+
};
44+
45+
const auto homer = Person{.first_name = "Homer",
46+
.last_name = "Simpson",
47+
.town = rfl::Commented<std::string>(
48+
"Springfield", "The town where Homer lives")};
49+
50+
const auto xml_str = rfl::xml::write(homer);
51+
```
52+
53+
This will result in the following XML:
54+
55+
```xml
56+
<?xml version="1.0" encoding="UTF-8"?>
57+
<Person>
58+
<first_name>Homer</first_name>
59+
<last_name>Simpson</last_name>
60+
<town>Springfield</town>
61+
<!--The town where Homer lives-->
62+
</Person>
63+
```
64+
65+
## API convenience
66+
67+
`rfl::Commented` provides several ways to access and modify the underlying value and the comment:
68+
69+
- `.get()`, `.value()`, `operator()()` — access the underlying value (const and non-const overloads).
70+
- `.comment()` — returns an `std::optional<std::string>` containing the comment.
71+
- `.add_comment(std::string)` — sets or updates the comment.
72+
- `.set(...)`, `operator=(...)` — assign the underlying value.
73+
74+
Example:
75+
76+
```cpp
77+
Person p;
78+
p.town = "Springfield";
79+
p.town.add_comment("The town where Homer lives");
80+
std::string s = p.town.value();
81+
```

docs/docs-readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424

2525
[rfl::Skip](rfl_skip.md) - For skipping fields during serialization and/or deserialization.
2626

27+
[rfl::Commented](commented.md) - For adding comments to your serialized format (only supported by some formats, such as YAML or XML).
28+
2729
[rfl::Result](result.md) - For error handling without exceptions.
2830

2931
[Standard containers](standard_containers.md) - Describes how reflect-cpp treats containers in the standard library.

include/rfl.hpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
#include "rfl/Binary.hpp"
1717
#include "rfl/Box.hpp"
1818
#include "rfl/Bytestring.hpp"
19+
#include "rfl/CamelCaseToSnakeCase.hpp"
20+
#include "rfl/Commented.hpp"
1921
#include "rfl/DefaultIfMissing.hpp"
2022
#include "rfl/DefaultVal.hpp"
2123
#include "rfl/Description.hpp"
@@ -41,7 +43,6 @@
4143
#include "rfl/Skip.hpp"
4244
#include "rfl/SnakeCaseToCamelCase.hpp"
4345
#include "rfl/SnakeCaseToPascalCase.hpp"
44-
#include "rfl/CamelCaseToSnakeCase.hpp"
4546
#include "rfl/TaggedUnion.hpp"
4647
#include "rfl/Timestamp.hpp"
4748
#include "rfl/UnderlyingEnums.hpp"

include/rfl/Commented.hpp

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
#ifndef RFL_COMMENTED_HPP_
2+
#define RFL_COMMENTED_HPP_
3+
4+
#include <optional>
5+
#include <string>
6+
#include <type_traits>
7+
#include <utility>
8+
9+
#include "default.hpp"
10+
11+
namespace rfl {
12+
13+
template <class T>
14+
struct Commented {
15+
public:
16+
using Type = std::remove_cvref_t<T>;
17+
18+
Commented() : value_(Type()) {}
19+
20+
Commented(const Type& _value) : value_(_value) {}
21+
22+
Commented(Type&& _value) noexcept : value_(std::move(_value)) {}
23+
24+
Commented(const Type& _value, std::optional<std::string> _comment)
25+
: comment_(std::move(_comment)), value_(_value) {}
26+
27+
Commented(Type&& _value, std::optional<std::string> _comment) noexcept
28+
: comment_(std::move(_comment)), value_(std::move(_value)) {}
29+
30+
Commented(Commented&& _commented) noexcept = default;
31+
32+
Commented(const Commented& _commented) = default;
33+
34+
template <class U>
35+
Commented(const Commented<U>& _commented)
36+
: comment_(_commented.comment()), value_(_commented.get()) {}
37+
38+
template <class U>
39+
Commented(Commented<U>&& _commented) noexcept
40+
: comment_(std::move(_commented.comment())),
41+
value_(std::move(_commented.value())) {}
42+
43+
template <class U>
44+
requires(std::is_convertible_v<U, Type>)
45+
Commented(const Commented<U>& _commented)
46+
: comment_(_commented.comment()), value_(_commented.value()) {}
47+
48+
template <class U>
49+
requires(std::is_convertible_v<U, Type>)
50+
Commented(const U& _value) : value_(_value) {}
51+
52+
template <class U>
53+
requires(std::is_convertible_v<U, Type>)
54+
Commented(U&& _value) noexcept : value_(std::forward<U>(_value)) {}
55+
56+
/// Assigns the underlying object to its default value.
57+
template <class U = Type>
58+
requires(std::is_default_constructible_v<U>)
59+
Commented(const Default&) : value_(Type()) {}
60+
61+
~Commented() = default;
62+
63+
/// Sets the comment associated with the field.
64+
void add_comment(std::string _comment) { comment_ = std::move(_comment); }
65+
66+
/// Returns the comment associated with the field, if any.
67+
const std::optional<std::string>& comment() const { return comment_; }
68+
69+
/// Returns the underlying object.
70+
Type& get() { return value_; }
71+
72+
/// Returns the underlying object.
73+
const Type& get() const { return value_; }
74+
75+
/// Returns the underlying object.
76+
Type& operator()() { return value_; }
77+
78+
/// Returns the underlying object.
79+
const Type& operator()() const { return value_; }
80+
81+
/// Returns the underlying object.
82+
Type& operator*() { return value_; }
83+
84+
/// Returns the underlying object.
85+
const Type& operator*() const { return value_; }
86+
87+
/// Assigns the underlying object.
88+
auto& operator=(const Type& _value) {
89+
value_ = _value;
90+
return *this;
91+
}
92+
93+
/// Assigns the underlying object.
94+
auto& operator=(Type&& _value) noexcept {
95+
value_ = std::move(_value);
96+
return *this;
97+
}
98+
99+
/// Assigns the underlying object.
100+
template <class U>
101+
requires std::is_convertible_v<U, Type>
102+
auto& operator=(const U& _value) {
103+
value_ = _value;
104+
return *this;
105+
}
106+
107+
/// Assigns the underlying object to its default value.
108+
template <class U = Type>
109+
requires std::is_default_constructible_v<U>
110+
auto& operator=(const Default&) {
111+
value_ = Type();
112+
return *this;
113+
}
114+
115+
/// Assigns the underlying object.
116+
Commented& operator=(const Commented& _commented) = default;
117+
118+
/// Assigns the underlying object.
119+
Commented& operator=(Commented&& _commented) = default;
120+
121+
/// Assigns the underlying object.
122+
template <class U>
123+
auto& operator=(const Commented<U>& _commented) {
124+
value_ = _commented.get();
125+
comment_ = _commented.comment();
126+
return *this;
127+
}
128+
129+
/// Assigns the underlying object.
130+
template <class U>
131+
auto& operator=(Commented<U>&& _commented) {
132+
value_ = std::move(_commented.value_);
133+
comment_ = std::move(_commented.comment_);
134+
return *this;
135+
}
136+
137+
/// Assigns the underlying object.
138+
void set(const Type& _value) { value_ = _value; }
139+
140+
/// Assigns the underlying object.
141+
void set(Type&& _value) { value_ = std::move(_value); }
142+
143+
/// Returns the underlying object.
144+
Type& value() { return value_; }
145+
146+
/// Returns the underlying object.
147+
const Type& value() const { return value_; }
148+
149+
/// The comment associated with the field.
150+
std::optional<std::string> comment_;
151+
152+
/// The underlying value.
153+
Type value_;
154+
};
155+
156+
} // namespace rfl
157+
158+
#endif

include/rfl/parsing/Parent.hpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include "../always_false.hpp"
88
#include "schemaful/IsSchemafulWriter.hpp"
99
#include "supports_attributes.hpp"
10+
#include "supports_comments.hpp"
1011

1112
namespace rfl::parsing {
1213

@@ -71,6 +72,21 @@ struct Parent {
7172
}
7273
}
7374

75+
/// Adds a comment to the parent element, if supported by the writer.
76+
template <class ParentType>
77+
static void add_comment(const W& _w, std::string_view _comment,
78+
const ParentType& _parent) {
79+
using Type = std::remove_cvref_t<ParentType>;
80+
if constexpr (supports_comments<W>) {
81+
if constexpr (std::is_same<Type, Array>()) {
82+
_w.add_comment_to_array(_comment, _parent.arr_);
83+
84+
} else if constexpr (std::is_same<Type, Object>()) {
85+
_w.add_comment_to_object(_comment, _parent.obj_);
86+
}
87+
}
88+
}
89+
7490
// For schemaful formats only.
7591
template <class ParentType>
7692
static auto add_map(const W& _w, const size_t _size,

include/rfl/parsing/Parser.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "Parser_box.hpp"
1010
#include "Parser_bytestring.hpp"
1111
#include "Parser_c_array.hpp"
12+
#include "Parser_commented.hpp"
1213
#include "Parser_default.hpp"
1314
#include "Parser_default_val.hpp"
1415
#include "Parser_duration.hpp"
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
#ifndef RFL_PARSING_PARSER_COMMENTED_HPP_
2+
#define RFL_PARSING_PARSER_COMMENTED_HPP_
3+
4+
#include <map>
5+
#include <optional>
6+
#include <type_traits>
7+
8+
#include "../Commented.hpp"
9+
#include "../Ref.hpp"
10+
#include "../Result.hpp"
11+
#include "../always_false.hpp"
12+
#include "Parent.hpp"
13+
#include "Parser_base.hpp"
14+
#include "schema/Type.hpp"
15+
#include "schemaful/IsSchemafulReader.hpp"
16+
#include "schemaful/IsSchemafulWriter.hpp"
17+
#include "schemaful/OptionalReader.hpp"
18+
#include "supports_comments.hpp"
19+
20+
namespace rfl::parsing {
21+
22+
template <class R, class W, class T, class ProcessorsType>
23+
requires AreReaderAndWriter<R, W, Commented<T>>
24+
struct Parser<R, W, Commented<T>, ProcessorsType> {
25+
using InputVarType = typename R::InputVarType;
26+
27+
using ParentType = Parent<W>;
28+
29+
static Result<Commented<T>> read(const R& _r,
30+
const InputVarType& _var) noexcept {
31+
return Parser<R, W, std::remove_cvref_t<T>, ProcessorsType>::read(_r, _var)
32+
.transform([](auto&& _t) {
33+
return Commented<T>(std::forward<decltype(_t)>(_t));
34+
});
35+
}
36+
37+
template <class P>
38+
static void write(const W& _w, const Commented<T>& _c, const P& _parent) {
39+
Parser<R, W, std::remove_cvref_t<T>, ProcessorsType>::write(_w, _c.get(),
40+
_parent);
41+
if constexpr (supports_comments<W>) {
42+
if (_c.comment()) {
43+
ParentType::add_comment(_w, *_c.comment(), _parent);
44+
}
45+
}
46+
}
47+
48+
static schema::Type to_schema(
49+
std::map<std::string, schema::Type>* _definitions) {
50+
return Parser<R, W, std::remove_cvref_t<T>, ProcessorsType>::to_schema(
51+
_definitions);
52+
}
53+
};
54+
55+
} // namespace rfl::parsing
56+
57+
#endif
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#ifndef RFL_PARSING_SUPPORTSCOMMENTS_HPP_
2+
#define RFL_PARSING_SUPPORTSCOMMENTS_HPP_
3+
4+
#include <concepts>
5+
#include <optional>
6+
#include <string>
7+
#include <string_view>
8+
9+
#include "../Result.hpp"
10+
11+
namespace rfl::parsing {
12+
13+
/// Determines whether a writer supports comments.
14+
template <class W>
15+
concept supports_comments =
16+
requires(W w, typename W::OutputArrayType arr,
17+
typename W::OutputObjectType obj, std::string_view comment) {
18+
{ w.add_comment_to_array(comment, &arr) } -> std::same_as<void>;
19+
20+
{ w.add_comment_to_object(comment, &obj) } -> std::same_as<void>;
21+
};
22+
23+
} // namespace rfl::parsing
24+
25+
#endif

0 commit comments

Comments
 (0)