From a89724facf1b4f2bc8a3b12fa0d29058f4d91826 Mon Sep 17 00:00:00 2001 From: Charles Cabergs Date: Wed, 7 Oct 2026 14:01:17 +0200 Subject: [PATCH] Add DurationsAsCount preprocessor This allows to serialize std::chrono durations with their count alone instead of the default count and unit. --- docs/content/processors.md | 22 ++++++++++++ docs/content/timestamps.md | 4 +++ include/rfl.hpp | 1 + include/rfl/DurationsAsCount.hpp | 19 +++++++++++ include/rfl/internal/durations_as_count_v.hpp | 32 +++++++++++++++++ include/rfl/parsing/ParserDuration.hpp | 34 +++++++++++++------ tests/json/test_durations_as_count.cpp | 22 ++++++++++++ 7 files changed, 124 insertions(+), 10 deletions(-) create mode 100644 include/rfl/DurationsAsCount.hpp create mode 100644 include/rfl/internal/durations_as_count_v.hpp create mode 100644 tests/json/test_durations_as_count.cpp diff --git a/docs/content/processors.md b/docs/content/processors.md index 63880d7e..25198d83 100644 --- a/docs/content/processors.md +++ b/docs/content/processors.md @@ -38,6 +38,7 @@ reflect-cpp currently supports the following processors: - `rfl::AddNamespacedTagsToVariants` - `rfl::AllowRawPtrs` - `rfl::DefaultIfMissing` +- `rfl::DurationsAsCount` - `rfl::EnumNamesOnly` - `rfl::NoExtraFields` - `rfl::NoFieldNames` @@ -290,6 +291,27 @@ 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::DurationsAsCount` + +By default, `std::chrono::duration` types are written as an object containing +the count and the unit. By passing the processor `rfl::DurationsAsCount`, +they will be written and read as a plain count in the unit of the C++ type: + +```cpp +struct MyStruct { + std::chrono::milliseconds timeout; +}; + +rfl::json::write( + MyStruct{.timeout = std::chrono::milliseconds(1500)}); +``` + +The resulting JSON string looks like this: + +```json +{"timeout":1500} +``` + ### `rfl::EnumNamesOnly` By default, when reading an enum from a string, numeric values are accepted diff --git a/docs/content/timestamps.md b/docs/content/timestamps.md index ca0d47f3..e49ea14a 100644 --- a/docs/content/timestamps.md +++ b/docs/content/timestamps.md @@ -116,3 +116,7 @@ The following units are supported: `std::chrono::nanoseconds`, `std::chrono::mic `std::chrono::milliseconds`, `std::chrono::seconds`, `std::chrono::minutes`, `std::chrono::hours`, `std::chrono::days`, `std::chrono::weeks`, `std::chrono::months`, and `std::chrono::years`. + +To write and read durations as a plain count in the unit of the C++ type +(e.g. `{"duration":10}`), pass the `rfl::DurationsAsCount` processor +(see [processors](../processors)). diff --git a/include/rfl.hpp b/include/rfl.hpp index 68e35588..e3ec7d80 100644 --- a/include/rfl.hpp +++ b/include/rfl.hpp @@ -21,6 +21,7 @@ #include "rfl/DefaultIfMissing.hpp" #include "rfl/DefaultVal.hpp" #include "rfl/Description.hpp" +#include "rfl/DurationsAsCount.hpp" #include "rfl/EnumNamesOnly.hpp" #include "rfl/ExtraFields.hpp" #include "rfl/Field.hpp" diff --git a/include/rfl/DurationsAsCount.hpp b/include/rfl/DurationsAsCount.hpp new file mode 100644 index 00000000..0b8a0e91 --- /dev/null +++ b/include/rfl/DurationsAsCount.hpp @@ -0,0 +1,19 @@ +#ifndef RFL_DURATIONSASCOUNT_HPP_ +#define RFL_DURATIONSASCOUNT_HPP_ + +namespace rfl { + +/// This is a 'fake' processor - it doesn't do much by itself, but its +/// inclusion instructs parsers to write and read std::chrono::duration types +/// as a plain count in the unit of the C++ type, instead of {count, unit}. +struct DurationsAsCount { + public: + template + static auto process(auto&& _named_tuple) { + return _named_tuple; + } +}; + +} // namespace rfl + +#endif diff --git a/include/rfl/internal/durations_as_count_v.hpp b/include/rfl/internal/durations_as_count_v.hpp new file mode 100644 index 00000000..fbe8996e --- /dev/null +++ b/include/rfl/internal/durations_as_count_v.hpp @@ -0,0 +1,32 @@ +#ifndef RFL_INTERNAL_DURATIONSASCOUNT_HPP_ +#define RFL_INTERNAL_DURATIONSASCOUNT_HPP_ + +#include + +#include "../Processors.hpp" +#include "../DurationsAsCount.hpp" + +namespace rfl::internal { + +template +class durations_as_count; + +template +class durations_as_count : public std::false_type {}; + +template <> +class durations_as_count : public std::true_type {}; + +template +struct durations_as_count> { + static constexpr bool value = + (durations_as_count::value || ... || durations_as_count::value); +}; + +template +constexpr bool durations_as_count_v = + durations_as_count>>::value; + +} // namespace rfl::internal + +#endif diff --git a/include/rfl/parsing/ParserDuration.hpp b/include/rfl/parsing/ParserDuration.hpp index 5cb3de02..cc02e91c 100644 --- a/include/rfl/parsing/ParserDuration.hpp +++ b/include/rfl/parsing/ParserDuration.hpp @@ -8,6 +8,7 @@ #include "../Literal.hpp" #include "../Variant.hpp" #include "../always_false.hpp" +#include "../internal/durations_as_count_v.hpp" #include "Parent.hpp" #include "Parser_base.hpp" #include "schema/Type.hpp" @@ -55,13 +56,18 @@ struct ParserDuration { */ static Result read(const R& _r, const InputVarType& _var) noexcept { - return Parser::read(_r, _var) - .and_then(to_duration) - .transform([](auto&& _duration) { - return _duration.visit([](auto&& _d) -> DurationType { - return std::chrono::duration_cast(std::move(_d)); + if constexpr (internal::durations_as_count_v) { + return Parser::read(_r, _var).transform( + [](const Rep _count) { return DurationType(_count); }); + } else { + return Parser::read(_r, _var) + .and_then(to_duration) + .transform([](auto&& _duration) { + return _duration.visit([](auto&& _d) -> DurationType { + return std::chrono::duration_cast(std::move(_d)); + }); }); - }); + } } /** @@ -74,9 +80,13 @@ struct ParserDuration { */ template static void write(const W& _w, const DurationType& _d, const P& _parent) { - const auto r = - RType{.count = static_cast(_d.count()), .unit = make_unit()}; - return Parser::write(_w, r, _parent); + if constexpr (internal::durations_as_count_v) { + Parser::write(_w, _d.count(), _parent); + } else { + const auto r = RType{.count = static_cast(_d.count()), + .unit = make_unit()}; + Parser::write(_w, r, _parent); + } } /** @@ -87,7 +97,11 @@ struct ParserDuration { */ static schema::Type to_schema( std::map* _definitions) { - return Parser::to_schema(_definitions); + if constexpr (internal::durations_as_count_v) { + return Parser::to_schema(_definitions); + } else { + return Parser::to_schema(_definitions); + } } private: diff --git a/tests/json/test_durations_as_count.cpp b/tests/json/test_durations_as_count.cpp new file mode 100644 index 00000000..3154b4c2 --- /dev/null +++ b/tests/json/test_durations_as_count.cpp @@ -0,0 +1,22 @@ +#include +#include +#include + +#include "write_and_read.hpp" + +namespace test_durations_as_count { + +struct TestStruct { + std::chrono::milliseconds ms; + std::chrono::seconds s; + std::chrono::duration d; +}; + +TEST(json, test_durations_as_count) { + const auto test = TestStruct{.ms = std::chrono::milliseconds(1500), + .s = std::chrono::seconds(10), + .d = std::chrono::duration(0.5)}; + write_and_read(test, + R"({"ms":1500,"s":10,"d":0.5})"); +} +} // namespace test_durations_as_count