Skip to content

Commit 1857fbb

Browse files
Add the EnumNamesOnly processor
1 parent b1a7da3 commit 1857fbb

8 files changed

Lines changed: 337 additions & 55 deletions

File tree

docs/concepts/processors.md

Lines changed: 71 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Processors
1+
# Processors
22

33
Processors can be used to apply transformations to struct serialization and deserialization.
44

@@ -16,10 +16,10 @@ const auto homer =
1616
.last_name = "Simpson",
1717
.age = 45};
1818

19-
const auto json_string =
19+
const auto json_string =
2020
rfl::json::write<rfl::SnakeCaseToCamelCase>(homer);
2121

22-
const auto homer2 =
22+
const auto homer2 =
2323
rfl::json::read<Person, rfl::SnakeCaseToCamelCase>(json_string).value();
2424
```
2525
@@ -33,27 +33,28 @@ The resulting JSON string looks like this:
3333

3434
reflect-cpp currently supports the following processors:
3535

36-
- `rfl::AddStructName`
37-
- `rfl::AddTagsToVariants`
38-
- `rfl::AddNamespacedTagsToVariants`
39-
- `rfl::AllowRawPtrs`
40-
- `rfl::DefaultIfMissing`
41-
- `rfl::NoExtraFields`
42-
- `rfl::NoFieldNames`
43-
- `rfl::NoOptionals`
44-
- `rfl::UnderlyingEnums`
45-
- `rfl::SnakeCaseToCamelCase`
46-
- `rfl::SnakeCaseToPascalCase`
47-
48-
### `rfl::AddStructName`
36+
- `rfl::AddStructName`
37+
- `rfl::AddTagsToVariants`
38+
- `rfl::AddNamespacedTagsToVariants`
39+
- `rfl::AllowRawPtrs`
40+
- `rfl::DefaultIfMissing`
41+
- `rfl::EnumNamesOnly`
42+
- `rfl::NoExtraFields`
43+
- `rfl::NoFieldNames`
44+
- `rfl::NoOptionals`
45+
- `rfl::UnderlyingEnums`
46+
- `rfl::SnakeCaseToCamelCase`
47+
- `rfl::SnakeCaseToPascalCase`
48+
49+
### `rfl::AddStructName`
4950

5051
It is also possible to add the struct name as an additional field, like this:
5152

5253
```cpp
53-
const auto json_string =
54+
const auto json_string =
5455
rfl::json::write<rfl::AddStructName<"type">>(homer);
5556

56-
const auto homer2 =
57+
const auto homer2 =
5758
rfl::json::read<Person, rfl::AddStructName<"type">>(json_string).value();
5859
```
5960

@@ -63,7 +64,7 @@ The resulting JSON string looks like this:
6364
{"type":"Person","first_name":"Homer","last_name":"Simpson","age":45}
6465
```
6566

66-
### `rfl::AddTagsToVariants`
67+
### `rfl::AddTagsToVariants`
6768

6869
This processor automatically adds tags to variants. Consider the following example:
6970

@@ -150,7 +151,7 @@ const auto msgs = std::vector<Messages>{
150151
Error::Message{.error = "failure", .error_id = 404}
151152
};
152153

153-
// This would cause problems with rfl::AddTagsToVariants because both
154+
// This would cause problems with rfl::AddTagsToVariants because both
154155
// structs have the same name "Message"
155156

156157
// But this works perfectly:
@@ -203,12 +204,12 @@ This generates:
203204

204205
### `rfl::AllowRawPtrs`
205206

206-
By default, reflect-cpp does not allow *reading into* raw pointers, `std::string_view` or `std::span`.
207-
(*Writing from* raw pointers is never a problem.) This is because reading into raw pointers
207+
By default, reflect-cpp does not allow *reading into* raw pointers, `std::string_view` or `std::span`.
208+
(*Writing from* raw pointers is never a problem.) This is because reading into raw pointers
208209
means that the library will allocate memory that the user then has to manually delete. This can lead to misunderstandings and memory leaks.
209210

210-
You might want to consider using some alternatives, such as `std::unique_ptr`, `rfl::Box`,
211-
`std::shared_ptr`, `rfl::Ref` or `std::optional`.
211+
You might want to consider using some alternatives, such as `std::unique_ptr`, `rfl::Box`,
212+
`std::shared_ptr`, `rfl::Ref` or `std::optional`.
212213
But if you absolutely have to use raw pointers, you can pass `rfl::AllowRawPtrs` to `read`:
213214

214215
```cpp
@@ -253,7 +254,7 @@ if(!person.span.empty()) {
253254

254255
The `rfl::DefaultIfMissing` processor is only relevant for reading data. For writing data, it will make no difference.
255256

256-
Usually, when fields are missing in the input data, this will lead to an error
257+
Usually, when fields are missing in the input data, this will lead to an error
257258
(unless they are optional fields).
258259
But if you pass the `rfl::DefaultIfMissing` processor, then missing fields will be
259260
replaced by their default value.
@@ -289,13 +290,44 @@ have gotten had you read the following JSON string:
289290
Because you have not passed a default value to town, the default value
290291
of the type is used instead.
291292

293+
### `rfl::EnumNamesOnly`
294+
295+
By default, when reading an enum from a string, numeric values are accepted
296+
in addition to the declared enumerator names, even when they do not
297+
correspond to a declared enumerator. For instance, given this enum:
298+
299+
```cpp
300+
enum class Color { red = 1, green = 2, blue = 3 };
301+
```
302+
303+
reading `{"color":"2"}` will produce `Color::green`, and even
304+
`{"color":"4"}` will succeed and produce the cast value `4`, although `4`
305+
is not a declared enumerator.
306+
307+
If you want to reject numeric values and only accept the declared
308+
enumerator names, pass the `rfl::EnumNamesOnly` processor to `read`:
309+
310+
```cpp
311+
const auto circle =
312+
rfl::json::read<Circle, rfl::EnumNamesOnly>(json_str);
313+
```
314+
315+
Now, `{"color":"2"}` will lead to an error:
316+
317+
```
318+
Failed to parse field 'color': Invalid enum value: '2'. Must be one of [red, green, blue].
319+
```
320+
321+
This processor only affects reading. Enum values that cannot be matched to
322+
a declared name are still written as their integer representation.
323+
292324
### `rfl::NoExtraFields`
293325

294-
When reading an object and the object contains a field that cannot be
326+
When reading an object and the object contains a field that cannot be
295327
matched to any of the fields in the struct, that field is simply ignored.
296328

297329
However, when `rfl::NoExtraFields` is added to `read`, then such extra fields
298-
will lead to an error.
330+
will lead to an error.
299331

300332
This can be overriden by adding `rfl::ExtraFields` to the struct.
301333

@@ -312,7 +344,7 @@ struct Person {
312344
{"first_name":"Homer","last_name":"Simpson","extra_field":0}
313345
```
314346

315-
If you call `rfl::json::read<Person>(json_string)`, then `extra_field` will
347+
If you call `rfl::json::read<Person>(json_string)`, then `extra_field` will
316348
simply be ignored.
317349

318350
But if you call `rfl::json::read<Person, rfl::NoExtraFields>(json_string)`,
@@ -333,13 +365,13 @@ will not fail, because `extra_field` would be included in `extras`.
333365
334366
### `rfl::NoFieldNames`
335367
336-
We can also remove the field names altogether:
368+
We can also remove the field names altogether:
337369
338370
```cpp
339-
const auto json_string =
371+
const auto json_string =
340372
rfl::json::write<rfl::NoFieldNames>(homer);
341373
342-
const auto homer2 =
374+
const auto homer2 =
343375
rfl::json::read<Person, rfl::NoFieldNames>(json_string).value();
344376
```
345377

@@ -351,19 +383,19 @@ The resulting JSON string looks like this:
351383

352384
This is particularly relevant for binary formats, which do not emphasize readability,
353385
like msgpack or flexbuffers. Removing the field names can reduce the size of the
354-
resulting bytestrings and significantly speed up read and write time,
386+
resulting bytestrings and significantly speed up read and write time,
355387
depending on the dataset.
356388

357389
However, it makes it more difficult to maintain backwards compatability.
358390

359391
Note that `rfl::NoFieldNames` is not supported for BSON, TOML, XML, or YAML, due
360-
to limitations of these formats.
392+
to limitations of these formats.
361393

362394
### `rfl::NoOptionals`
363395

364396
As we have seen in the section on optional fields, when a `std::optional` is
365397
`std::nullopt`, it is usually not written at all. But if you want them to be explicitly
366-
written as `null`, you can use this processor. The same thing applies to `std::shared_ptr` and
398+
written as `null`, you can use this processor. The same thing applies to `std::shared_ptr` and
367399
`std::unique_ptr`.
368400

369401
```cpp
@@ -384,7 +416,7 @@ The resulting JSON string looks like this:
384416
{"first_name":"Homer","last_name":"Simpson","town":null}
385417
```
386418

387-
By default, `rfl::json::read` will accept both `"town":null` and just
419+
By default, `rfl::json::read` will accept both `"town":null` and just
388420
leaving out the field `town`. However, if you want to require the field
389421
`town` to be included, you can add `rfl::NoOptionals` to `read`:
390422

@@ -424,10 +456,10 @@ Please refer to the example above.
424456
If you want `PascalCase` instead of `camelCase`, you can use the appropriate processor:
425457

426458
```cpp
427-
const auto json_string =
459+
const auto json_string =
428460
rfl::json::write<rfl::SnakeCaseToPascalCase>(homer);
429461

430-
const auto homer2 =
462+
const auto homer2 =
431463
rfl::json::read<Person, rfl::SnakeCaseToPascalCase>(json_string).value();
432464
```
433465

@@ -442,10 +474,10 @@ The resulting JSON string looks like this:
442474
You can combine several processors:
443475

444476
```cpp
445-
const auto json_string =
477+
const auto json_string =
446478
rfl::json::write<rfl::SnakeCaseToCamelCase, rfl::AddStructName<"type">>(homer);
447479

448-
const auto homer2 =
480+
const auto homer2 =
449481
rfl::json::read<Person, rfl::SnakeCaseToCamelCase, rfl::AddStructName<"type">>(json_string).value();
450482
```
451483

docs/enums.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,22 @@ This will be represented as follows:
164164

165165
This works, because 16 + 256 + 512 + 1024 + 8192 = 10000. Flag enums are *always* represented in terms of 2^N-numbers.
166166

167+
## Reading numeric values as enums
168+
169+
When reading, enum values can also be given as numbers. For instance, given this enum:
170+
171+
```cpp
172+
enum class Color { red = 1, green = 2, blue = 3 };
173+
```
174+
175+
reading `{"color":"2"}` will produce `Color::green`.
176+
177+
By default, this also works when the number does not correspond to a
178+
declared enumerator (for instance, `{"color":"4"}` will produce the cast
179+
value `4`). If you want to reject numeric values and only accept the
180+
declared enumerator names, pass the
181+
[`rfl::EnumNamesOnly`](concepts/processors.md) processor to `read`.
182+
167183
## General-purpose enumeration utilities
168184
169185
reflect-cpp also allows you to directly convert between enumerator values and strings:

include/rfl.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "rfl/DefaultIfMissing.hpp"
2222
#include "rfl/DefaultVal.hpp"
2323
#include "rfl/Description.hpp"
24+
#include "rfl/EnumNamesOnly.hpp"
2425
#include "rfl/ExtraFields.hpp"
2526
#include "rfl/Field.hpp"
2627
#include "rfl/Flatten.hpp"

include/rfl/EnumNamesOnly.hpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
#ifndef RFL_ENUMNAMESONLY_HPP_
2+
#define RFL_ENUMNAMESONLY_HPP_
3+
4+
namespace rfl {
5+
6+
/// A processor that instructs parsers to accept only the declared names of an
7+
/// enum's enumerators when reading an enum from a string.
8+
/// This is a marker type (doesn't modify data) that changes parser behavior.
9+
/// By default, when reading an enum from a string, numeric values are accepted
10+
/// in addition to the declared enumerator names, even when they do not
11+
/// correspond to a declared enumerator (for instance, reading "4" into an enum
12+
/// with values 1, 2 and 3 will produce the cast value 4).
13+
/// When EnumNamesOnly is added as a processor, numeric values are rejected and
14+
/// only the declared enumerator names will be accepted.
15+
/// Usage: rfl::json::read<MyStruct, EnumNamesOnly>(json_str)
16+
struct EnumNamesOnly {
17+
public:
18+
/// Identity process function - returns the named tuple unchanged.
19+
/// The actual validation happens in the parser, not here.
20+
/// @tparam StructType The struct type being processed
21+
/// @param _named_tuple The named tuple representation of the struct
22+
/// @return The same named tuple (unchanged)
23+
template <class StructType>
24+
static auto process(auto&& _named_tuple) {
25+
return _named_tuple;
26+
}
27+
};
28+
29+
} // namespace rfl
30+
31+
#endif

include/rfl/enums.hpp

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -81,28 +81,44 @@ std::string enum_to_string(const EnumType _enum) {
8181
}
8282

8383
// Converts a string to a value of the given enum type.
84-
template <class EnumType>
84+
//
85+
// By default, numeric values are accepted in addition to the declared
86+
// enumerator names, even when they do not correspond to a declared
87+
// enumerator (for instance, reading "4" into an enum with values 1, 2 and 3
88+
// will produce the cast value 4). Pass enum_names_only = true (or use the
89+
// EnumNamesOnly processor with a parser) to accept only the declared
90+
// enumerator names instead.
91+
template <class EnumType, bool enum_names_only = false>
8592
Result<EnumType> string_to_enum(const std::string& _str) {
93+
const auto make_error_msg = [&](const auto& name) {
94+
std::string msg = "Invalid enum value: '";
95+
msg += name;
96+
msg += "'. Must be one of [";
97+
const char* sep = "";
98+
for (const auto& p : get_enumerator_array<EnumType>()) {
99+
msg += sep;
100+
msg += p.first;
101+
sep = ", ";
102+
}
103+
msg += "].";
104+
return error(msg);
105+
};
106+
86107
const auto cast_numbers_or_names =
87-
[](const std::string& name) -> Result<EnumType> {
108+
[&](const std::string& name) -> Result<EnumType> {
88109
const auto r = internal::enums::from_string<EnumType>(name);
89110
if (r) {
90111
return *r;
91112
}
92-
try {
93-
return static_cast<EnumType>(std::stoi(name));
94-
} catch (std::exception& exp) {
95-
std::string msg = "Invalid enum value: '";
96-
msg += name;
97-
msg += "'. Must be one of [";
98-
const char* sep = "";
99-
for (const auto& p : get_enumerator_array<EnumType>()) {
100-
msg += sep;
101-
msg += p.first;
102-
sep = ", ";
113+
if constexpr (enum_names_only) {
114+
return make_error_msg(name);
115+
} else {
116+
try {
117+
const auto val = std::stoi(name);
118+
return static_cast<EnumType>(val);
119+
} catch (std::exception& exp) {
120+
return make_error_msg(name);
103121
}
104-
msg += "].";
105-
return error(msg);
106122
}
107123
};
108124

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
#ifndef RFL_INTERNAL_ENUMNAMESONLY_HPP_
2+
#define RFL_INTERNAL_ENUMNAMESONLY_HPP_
3+
4+
#include <type_traits>
5+
6+
#include "../EnumNamesOnly.hpp"
7+
#include "../Processors.hpp"
8+
9+
namespace rfl::internal {
10+
11+
template <class T>
12+
class enum_names_only;
13+
14+
template <class T>
15+
class enum_names_only : public std::false_type {};
16+
17+
template <>
18+
class enum_names_only<EnumNamesOnly> : public std::true_type {};
19+
20+
template <class Head, class... Tail>
21+
struct enum_names_only<Processors<Head, Tail...>> {
22+
static constexpr bool value =
23+
(enum_names_only<Head>::value || ... || enum_names_only<Tail>::value);
24+
};
25+
26+
template <class T>
27+
constexpr bool enum_names_only_v =
28+
enum_names_only<std::remove_cvref_t<std::remove_pointer_t<T>>>::value;
29+
30+
} // namespace rfl::internal
31+
32+
#endif

0 commit comments

Comments
 (0)