Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 1 addition & 48 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,7 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
- [JSON schema](#json-schema)
- [Enums](#enums)
- [Algebraic data types](#algebraic-data-types)
- [Extra fields](#extra-fields)
- [std::expected](#stdexpected)
- [Extra fields](#extra-fields)
- [Reflective programming](#reflective-programming)
- [Standard Library Integration](#support-for-containers)
- [The team behind reflect-cpp](#the-team-behind-reflect-cpp)
Expand Down Expand Up @@ -510,52 +509,6 @@ This results in the following JSON string:
{"firstName":"Homer","lastName":"Simpson","age":45,"email":"homer@simpson.com","town":"Springfield"}
```

### std::expected

reflect-cpp also supports C++-23's `std::expected`:

```cpp
#include <expected>
#include <rfl/json.hpp>

// A success value is serialized like the value itself:
const std::expected<int, std::string> age = 45;
const std::string json_string = rfl::json::write(age);
// -> 45

// An error is serialized as an object with a single "error" field:
const std::expected<int, std::string> no_age = std::unexpected("unknown age");
const std::string json_string2 = rfl::json::write(no_age);
// -> {"error":"unknown age"}

const auto age2 =
rfl::json::read<std::expected<int, std::string>>(json_string).value();
const auto no_age2 =
rfl::json::read<std::expected<int, std::string>>(json_string2).value();
```

`std::expected` can be used anywhere other types can be used, for example as a
field of a struct or inside a container:

```cpp
struct Person {
std::string first_name;
std::vector<std::expected<int, std::string>> ages;
};

const auto homer =
Person{.first_name = "Homer",
.ages = {42, std::unexpected("unknown age")}};

const std::string json_string3 = rfl::json::write(homer);
// -> {"first_name":"Homer","ages":[42,{"error":"unknown age"}]}
```

`std::expected` requires a standard library that provides the C++-23 feature
(feature-test macro `__cpp_lib_expected`). Note that `std::expected<void, E>` and
`std::expected<T, T>` with an identical value and error type are not supported.
Refer to the [documentation](https://rfl.getml.com/expected) for details.

### Reflective programming

Beyond serialization and deserialization, reflect-cpp also supports reflective programming in general.
Expand Down
110 changes: 71 additions & 39 deletions docs/concepts/processors.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Processors
# Processors

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

Expand All @@ -16,10 +16,10 @@ const auto homer =
.last_name = "Simpson",
.age = 45};

const auto json_string =
const auto json_string =
rfl::json::write<rfl::SnakeCaseToCamelCase>(homer);

const auto homer2 =
const auto homer2 =
rfl::json::read<Person, rfl::SnakeCaseToCamelCase>(json_string).value();
```

Expand All @@ -33,27 +33,28 @@ The resulting JSON string looks like this:

reflect-cpp currently supports the following processors:

- `rfl::AddStructName`
- `rfl::AddTagsToVariants`
- `rfl::AddNamespacedTagsToVariants`
- `rfl::AllowRawPtrs`
- `rfl::DefaultIfMissing`
- `rfl::NoExtraFields`
- `rfl::NoFieldNames`
- `rfl::NoOptionals`
- `rfl::UnderlyingEnums`
- `rfl::SnakeCaseToCamelCase`
- `rfl::SnakeCaseToPascalCase`

### `rfl::AddStructName`
- `rfl::AddStructName`
- `rfl::AddTagsToVariants`
- `rfl::AddNamespacedTagsToVariants`
- `rfl::AllowRawPtrs`
- `rfl::DefaultIfMissing`
- `rfl::EnumNamesOnly`
- `rfl::NoExtraFields`
- `rfl::NoFieldNames`
- `rfl::NoOptionals`
- `rfl::UnderlyingEnums`
- `rfl::SnakeCaseToCamelCase`
- `rfl::SnakeCaseToPascalCase`

### `rfl::AddStructName`

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

```cpp
const auto json_string =
const auto json_string =
rfl::json::write<rfl::AddStructName<"type">>(homer);

const auto homer2 =
const auto homer2 =
rfl::json::read<Person, rfl::AddStructName<"type">>(json_string).value();
```

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

### `rfl::AddTagsToVariants`
### `rfl::AddTagsToVariants`

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

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

// This would cause problems with rfl::AddTagsToVariants because both
// This would cause problems with rfl::AddTagsToVariants because both
// structs have the same name "Message"

// But this works perfectly:
Expand Down Expand Up @@ -203,12 +204,12 @@ This generates:

### `rfl::AllowRawPtrs`

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

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

```cpp
Expand Down Expand Up @@ -253,7 +254,7 @@ if(!person.span.empty()) {

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

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

### `rfl::EnumNamesOnly`

By default, when reading an enum from a string, numeric values are accepted
in addition to the declared enumerator names, even when they do not
correspond to a declared enumerator. For instance, given this enum:

```cpp
enum class Color { red = 1, green = 2, blue = 3 };
```

reading `{"color":"2"}` will produce `Color::green`, and even
`{"color":"4"}` will succeed and produce the cast value `4`, although `4`
is not a declared enumerator.

If you want to reject numeric values and only accept the declared
enumerator names, pass the `rfl::EnumNamesOnly` processor to `read`:

```cpp
const auto circle =
rfl::json::read<Circle, rfl::EnumNamesOnly>(json_str);
```

Now, `{"color":"2"}` will lead to an error:

```
Failed to parse field 'color': Invalid enum value: '2'. Must be one of [red, green, blue].
```

This processor only affects reading. Enum values that cannot be matched to
a declared name are still written as their integer representation.

### `rfl::NoExtraFields`

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

However, when `rfl::NoExtraFields` is added to `read`, then such extra fields
will lead to an error.
will lead to an error.

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

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

If you call `rfl::json::read<Person>(json_string)`, then `extra_field` will
If you call `rfl::json::read<Person>(json_string)`, then `extra_field` will
simply be ignored.

But if you call `rfl::json::read<Person, rfl::NoExtraFields>(json_string)`,
Expand All @@ -333,13 +365,13 @@ will not fail, because `extra_field` would be included in `extras`.

### `rfl::NoFieldNames`

We can also remove the field names altogether:
We can also remove the field names altogether:

```cpp
const auto json_string =
const auto json_string =
rfl::json::write<rfl::NoFieldNames>(homer);

const auto homer2 =
const auto homer2 =
rfl::json::read<Person, rfl::NoFieldNames>(json_string).value();
```

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

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

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

Note that `rfl::NoFieldNames` is not supported for BSON, TOML, XML, or YAML, due
to limitations of these formats.
to limitations of these formats.

### `rfl::NoOptionals`

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

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

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

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

```cpp
const auto json_string =
const auto json_string =
rfl::json::write<rfl::SnakeCaseToPascalCase>(homer);

const auto homer2 =
const auto homer2 =
rfl::json::read<Person, rfl::SnakeCaseToPascalCase>(json_string).value();
```

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

```cpp
const auto json_string =
const auto json_string =
rfl::json::write<rfl::SnakeCaseToCamelCase, rfl::AddStructName<"type">>(homer);

const auto homer2 =
const auto homer2 =
rfl::json::read<Person, rfl::SnakeCaseToCamelCase, rfl::AddStructName<"type">>(json_string).value();
```

Expand Down
16 changes: 16 additions & 0 deletions docs/enums.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,22 @@ This will be represented as follows:

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

## Reading numeric values as enums

When reading, enum values can also be given as numbers. For instance, given this enum:

```cpp
enum class Color { red = 1, green = 2, blue = 3 };
```

reading `{"color":"2"}` will produce `Color::green`.

By default, this also works when the number does not correspond to a
declared enumerator (for instance, `{"color":"4"}` will produce the cast
value `4`). If you want to reject numeric values and only accept the
declared enumerator names, pass the
[`rfl::EnumNamesOnly`](concepts/processors.md) processor to `read`.

## General-purpose enumeration utilities

reflect-cpp also allows you to directly convert between enumerator values and strings:
Expand Down
1 change: 1 addition & 0 deletions include/rfl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "rfl/DefaultIfMissing.hpp"
#include "rfl/DefaultVal.hpp"
#include "rfl/Description.hpp"
#include "rfl/EnumNamesOnly.hpp"
#include "rfl/ExtraFields.hpp"
#include "rfl/Field.hpp"
#include "rfl/Flatten.hpp"
Expand Down
31 changes: 31 additions & 0 deletions include/rfl/EnumNamesOnly.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#ifndef RFL_ENUMNAMESONLY_HPP_
#define RFL_ENUMNAMESONLY_HPP_

namespace rfl {

/// A processor that instructs parsers to accept only the declared names of an
/// enum's enumerators when reading an enum from a string.
/// This is a marker type (doesn't modify data) that changes parser behavior.
/// By default, when reading an enum from a string, numeric values are accepted
/// in addition to the declared enumerator names, even when they do not
/// correspond to a declared enumerator (for instance, reading "4" into an enum
/// with values 1, 2 and 3 will produce the cast value 4).
/// When EnumNamesOnly is added as a processor, numeric values are rejected and
/// only the declared enumerator names will be accepted.
/// Usage: rfl::json::read<MyStruct, EnumNamesOnly>(json_str)
struct EnumNamesOnly {
public:
/// Identity process function - returns the named tuple unchanged.
/// The actual validation happens in the parser, not here.
/// @tparam StructType The struct type being processed
/// @param _named_tuple The named tuple representation of the struct
/// @return The same named tuple (unchanged)
template <class StructType>
static auto process(auto&& _named_tuple) {
return _named_tuple;
}
};

} // namespace rfl

#endif
Loading
Loading