Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
a999d07
Added the ToAllCaps preprocessor
liuzicheng1987 Jul 5, 2026
9795dcb
Added missing modification
liuzicheng1987 Jul 5, 2026
5aa0954
Fixed typo
liuzicheng1987 Jul 5, 2026
5de8a6d
Wrote first draft
liuzicheng1987 Jul 5, 2026
4e7f613
First draft for read_object
liuzicheng1987 Jul 7, 2026
7d656be
Fixed exists
liuzicheng1987 Jul 8, 2026
994f2c8
Removed obsolete files
liuzicheng1987 Jul 8, 2026
4bb69bb
Added Writer
liuzicheng1987 Jul 8, 2026
dcf647e
Built my first working test
liuzicheng1987 Jul 9, 2026
4eb0f9d
Added test for nested vars
liuzicheng1987 Jul 9, 2026
b730a41
Better support for arrays
liuzicheng1987 Jul 12, 2026
8b27f17
Make sure that empty arrays are handled properly
liuzicheng1987 Jul 12, 2026
71c82d3
Add more tests
liuzicheng1987 Jul 13, 2026
057d801
Added more tests
liuzicheng1987 Jul 13, 2026
cc0b6d7
Added documentation for environment variables
liuzicheng1987 Jul 18, 2026
ce659dc
Fixed a number of minor issues
liuzicheng1987 Jul 19, 2026
314830c
Only empty if it is nullptr
liuzicheng1987 Jul 19, 2026
4eba9d3
Added an explicit test for maps
liuzicheng1987 Jul 19, 2026
c49c7a3
Merge branch 'main' into f/env
liuzicheng1987 Jul 19, 2026
5f8ae24
Fixed typo
liuzicheng1987 Jul 19, 2026
55bfbe0
Merge branch 'main' into f/env
liuzicheng1987 Jul 20, 2026
6fb2c10
Remove RFL_API
liuzicheng1987 Jul 20, 2026
fa22236
Fixed headers
liuzicheng1987 Jul 20, 2026
d92d1e7
Use generic get_environ()
liuzicheng1987 Jul 20, 2026
802c5dc
Merge branch 'main' into f/env
liuzicheng1987 Jul 20, 2026
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
44 changes: 36 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

**reflect-cpp** is a C++-20 library for **fast serialization, deserialization and validation** using reflection, similar to [pydantic](https://github.com/pydantic/pydantic) in Python, [serde](https://github.com/serde-rs) in Rust, [encoding](https://github.com/golang/go/tree/master/src/encoding) in Go or [aeson](https://github.com/haskell/aeson/tree/master) in Haskell.

Moreover, reflect-cpp is the basis for [sqlgen](https://github.com/getml/sqlgen), a **modern, type-safe ORM and SQL query generator** for C++20, inspired by Python's SQLAlchemy/SQLModel and Rust's Diesel. It provides a fluent, composable interface for database operations with compile-time type checking and SQL injection protection.
Moreover, reflect-cpp is the basis for [sqlgen](https://github.com/getml/sqlgen), a **modern, type-safe ORM and SQL query generator** for C++20, inspired by Python's SQLAlchemy/SQLModel and Rust's Diesel. It provides a fluent, composable interface for database operations with compile-time type checking and SQL injection protection.

reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boilerplate code and increase code safety. Together, they enable reliable and efficient ETL pipelines.

Expand Down Expand Up @@ -45,14 +45,14 @@ reflect-cpp and sqlgen fill important gaps in C++ development. They reduce boile
- [Algebraic data types](#algebraic-data-types)
- [Extra fields](#extra-fields)
- [Reflective programming](#reflective-programming)
- [Standard Library Integration](#support-for-containers)
- [Standard Library Integration](#support-for-containers)
- [The team behind reflect-cpp](#the-team-behind-reflect-cpp)
- [License](#license)

### More in our [documentation](https://rfl.getml.com):
- [Installation ↗](https://rfl.getml.com/install/#option-2-compilation-using-cmake)
- [Benchmarks ↗](https://rfl.getml.com/benchmarks)
- [How to contribute ↗](https://rfl.getml.com/contributing)
- [How to contribute ↗](https://rfl.getml.com/contributing)
- [Compiling and running the tests ↗](https://rfl.getml.com/contributing/#compiling-and-running-the-tests)


Expand All @@ -70,6 +70,8 @@ The following table lists the serialization formats currently supported by refle
| BSON | [libbson](https://github.com/mongodb/mongo-c-driver) | >= 1.25.1 | Apache 2.0 | JSON-like binary format |
| Cap'n Proto | [capnproto](https://capnproto.org) | >= 1.0.2 | MIT | Schemaful binary format |
| CBOR | [jsoncons](https://github.com/danielaparker/jsoncons)| >= 0.176.0 | BSL 1.0 | JSON-like binary format |
| cli | *(none)* | *(none)* | MIT | Command line interface |
| env | *(none)* | *(none)* | MIT | Environment variables |
| Cereal | [Cereal](https://uscilab.github.io/cereal/) | >= 1.3.2 | BSD | C++ serialization library with multiple formats |
| CSV | [Apache Arrow](https://arrow.apache.org/) | >= 21.0.0 | Apache 2.0 | Tabular textual format |
| flexbuffers | [flatbuffers](https://github.com/google/flatbuffers) | >= 23.5.26 | Apache 2.0 | Schema-less version of flatbuffers, binary format |
Expand Down Expand Up @@ -119,9 +121,9 @@ The resulting JSON string looks like this:
You can transform the field names from `snake_case` to `camelCase` like this:

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

Expand Down Expand Up @@ -150,7 +152,7 @@ last_name: Simpson
age: 45
```

This will work for just about any example in the entire documentation
This will work for just about any example in the entire documentation
and any of the following formats, except where explicitly noted otherwise:

```cpp
Expand Down Expand Up @@ -300,6 +302,32 @@ This will resulting CSV will look like this:
"Homer","Simpson","Springfield",1987-04-19,45,"homer@simpson.com"
```

### Environment variables

reflect-cpp can also read from and write to environment variables using `rfl::env::read` and `rfl::env::write`:

```cpp
#include <rfl/env.hpp>

struct Config {
std::string host;
int port;
bool verbose;
};

const auto config = Config{.host = "localhost", .port = 8080, .verbose = true};

rfl::env::write(config);
// Sets HOST=localhost, PORT=8080, VERBOSE=true

const auto config2 = rfl::env::read<Config>().value();
```

Nested structs are flattened with `_` as a separator (e.g., `DATABASE_HOST`).
Arrays use `_INDEX` suffix (e.g., `TAGS_0`, `TAGS_1`).
Enums are serialized via their enumerator name (e.g., `"green"` for `Color::green`).
All field names are converted to uppercase by default.

### CLI argument parsing

reflect-cpp can also parse command-line arguments directly into structs using `rfl::cli::read`:
Expand Down Expand Up @@ -404,7 +432,7 @@ struct Item {
Color color;
};

const auto item = Item{.pos_x = 2.0,
const auto item = Item{.pos_x = 2.0,
.pos_y = 3.0,
.shape = Shape::square,
.color = Color::red | Color::blue};
Expand Down Expand Up @@ -616,7 +644,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<std::byte>`. Supported by Avro, BSON, Cap'n Proto, CBOR, flexbuffers, msgpack and UBJSON.
- `rfl::Bytestring`: An alias for `std::vector<std::byte>`. 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.
Expand Down
124 changes: 124 additions & 0 deletions docs/supported_formats/env.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Environment Variables

For environment variable support, you must also include the header `<rfl/env.hpp>`.

Environment variables provide a flat, hierarchical key-value store for configuration. This module serializes nested structs into environment variable names using `_` as a separator (e.g., `DATABASE_HOST`) and parses them back.

All field names are converted to uppercase by default.

## Reading and writing

Suppose you have a struct like this:

```cpp
struct Settings {
std::string host;
int port;
double rate;
bool verbose;
};
```

You can serialize to environment variables like this:

```cpp
const auto settings = Settings{...};
rfl::env::write(settings);
```

You can parse from environment variables like this:

```cpp
const rfl::Result<Settings> result = rfl::env::read<Settings>();
```

## Nested structs

Nested structs are serialized using `_` as a separator between field names:

```cpp
struct DatabaseSettings {
std::string host;
int port;
};

struct Settings {
std::string host;
DatabaseSettings database;
};
```

This produces the following environment variables:

- `HOST` — the value of `host`
- `DATABASE_HOST` — the value of `database.host`
- `DATABASE_PORT` — the value of `database.port`

## Arrays

Arrays are serialized with `_INDEX` suffix on the field name:

```cpp
struct Settings {
std::string host;
std::vector<std::string> tags;
};
```

This produces:

- `HOST` — the value of `host`
- `TAGS_0` — the first element
- `TAGS_1` — the second element
- ...

## Enums

Enums are serialized using `rfl::enum_to_string()`, which produces the enumerator name (e.g., `"green"` for `Color::green`). If the enumerator has no name, the underlying integer value is used.

```cpp
enum class Color { red, green, blue };

struct Circle {
float radius;
Color color;
};
```

`color = Color::green` produces `COLOR=green`.

### Flag enums

Flag enums (detected via `enchantum::is_bitflag`) are serialized by joining matching flag names with `|`:

```cpp
enum class Permissions { read = 1, write = 2, execute = 4 };

inline Permissions operator|(Permissions a, Permissions b) {
return static_cast<Permissions>(static_cast<int>(a) | static_cast<int>(b));
}
```

`permissions = Permissions::read | Permissions::write` produces `PERMISSIONS=read|write`.

## Tagged unions

Tagged unions work as expected, with the discriminant field serialized as a regular field:

```cpp
using Shapes = rfl::TaggedUnion<"shape", Circle, Square, Rectangle>;
```

This produces a `SHAPE` field with the variant name (e.g., `SHAPE=RECTANGLE`).

## Processors

You can pass processors to `read` and `write` to customize behavior. By default, `ToAllCaps` is applied, which converts all field names to uppercase. You can add additional processors to the template parameter list:

```cpp
rfl::env::write<SomeProcessor>(settings);
```

## Custom constructors

Custom constructors are not supported for environment variable parsing.
1 change: 1 addition & 0 deletions include/rfl.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
#include "rfl/SnakeCaseToPascalCase.hpp"
#include "rfl/TaggedUnion.hpp"
#include "rfl/Timestamp.hpp"
#include "rfl/ToAllCaps.hpp"
#include "rfl/UnderlyingEnums.hpp"
#include "rfl/Validator.hpp"
#include "rfl/Variant.hpp"
Expand Down
44 changes: 44 additions & 0 deletions include/rfl/ToAllCaps.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#ifndef RFL_TOALLCAPS_HPP_
#define RFL_TOALLCAPS_HPP_

#include "Field.hpp"
#include "internal/is_rename.hpp"
#include "internal/transform_case.hpp"

namespace rfl {

struct ToAllCaps {
public:
/// Replaces all instances of field names with all uppercase letters.
/// A named tuple is a tuple-like structure where each element has a name/key.
/// @tparam StructType The type of the struct being processed
/// @param _named_tuple The named tuple containing the struct's fields
/// @return A new named tuple with field names converted to all uppercase
template <class StructType>
static auto process(const auto& _named_tuple) {
return _named_tuple.transform([]<class FieldType>(const FieldType& _f) {
if constexpr (FieldType::name() != "xml_content" &&
!internal::is_rename_v<typename FieldType::Type>) {
return handle_one_field(_f);
} else {
return _f;
}
});
}

private:
/// Applies the all-uppercase transformation to a single field.
/// @tparam FieldType The type of the field being transformed
/// @param _f The field to transform
/// @return A new field with the transformed name and the same value
template <class FieldType>
static auto handle_one_field(const FieldType& _f) {
using NewFieldType = Field<internal::to_all_caps<FieldType::name_>(),
typename FieldType::Type>;
return NewFieldType(_f.value());
}
};

} // namespace rfl

#endif
11 changes: 11 additions & 0 deletions include/rfl/env.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#ifndef RFL_ENV_HPP_
#define RFL_ENV_HPP_

#include "../rfl.hpp"
#include "env/Parser.hpp"
#include "env/Reader.hpp"
#include "env/Writer.hpp"
#include "env/read.hpp"
#include "env/write.hpp"

#endif
48 changes: 48 additions & 0 deletions include/rfl/env/Parser.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#ifndef RFL_ENV_PARSER_HPP_
#define RFL_ENV_PARSER_HPP_

#include "../NamedTuple.hpp"
#include "../internal/no_optionals_v.hpp"
#include "../parsing/AreReaderAndWriter.hpp"
#include "../parsing/NamedTupleParser.hpp"
#include "../parsing/Parser_base.hpp"
#include "Reader.hpp"
#include "Writer.hpp"

namespace rfl::parsing {

/// Specializes the Parser template for NamedTuple types within the env
/// namespace. This specialization inherits from NamedTupleParser to handle
/// serialization and deserialization of NamedTuple structures into/from
/// environment variables.
///
/// Key configuration:
/// - _ignore_empty_containers: true (empty containers are ignored during
/// parsing)
/// - _all_required: determined by no_optionals_v<ProcessorsType> (fields are
/// required if the processors do not contain the NoOptional preprocessor)
/// - _no_field_names: false (field names are preserved in the output)
///
/// The AreReaderAndWriter concept ensures that env::Reader and env::Writer can
/// handle NamedTuple<FieldTypes...> types.
template <class ProcessorsType, class... FieldTypes>
requires AreReaderAndWriter<env::Reader, env::Writer,
NamedTuple<FieldTypes...>>
struct Parser<env::Reader, env::Writer, NamedTuple<FieldTypes...>,
ProcessorsType>
: public NamedTupleParser<
env::Reader, env::Writer,
/*_ignore_empty_containers=*/true,
/*_all_required=*/internal::no_optionals_v<ProcessorsType>,
/*_no_field_names_=*/false, ProcessorsType, FieldTypes...> {};

} // namespace rfl::parsing

namespace rfl::env {

template <class T, class ProcessorsType>
using Parser = parsing::Parser<Reader, Writer, T, ProcessorsType>;

} // namespace rfl::env

#endif
Loading
Loading