From cc5791ded3223a9c30af3f7d567da55db7f4d7fc Mon Sep 17 00:00:00 2001 From: Andrew Nolte Date: Thu, 22 Jan 2026 14:39:43 -0500 Subject: [PATCH 1/2] json schema: Add enum value descriptions --- include/rfl/config.hpp | 23 ++++++++ include/rfl/json/schema/Type.hpp | 9 +++- include/rfl/parsing/Parser_enum.hpp | 12 +++++ include/rfl/parsing/schema/Type.hpp | 19 +++++-- src/rfl/avro/to_schema.cpp | 10 ++++ src/rfl/capnproto/to_schema.cpp | 8 +++ src/rfl/json/to_schema.cpp | 19 ++++++- tests/json/test_enum_descriptions.cpp | 76 +++++++++++++++++++++++++++ 8 files changed, 168 insertions(+), 8 deletions(-) create mode 100644 tests/json/test_enum_descriptions.cpp diff --git a/include/rfl/config.hpp b/include/rfl/config.hpp index 9526f2076..1a32dff86 100644 --- a/include/rfl/config.hpp +++ b/include/rfl/config.hpp @@ -1,6 +1,8 @@ #ifndef RFL_CONFIG_HPP_ #define RFL_CONFIG_HPP_ +#include + namespace rfl::config { // To specify a different range for a particular enum type, specialize the @@ -13,6 +15,27 @@ struct enum_range { // static constexpr int max = ...; }; +// To add descriptions to enum values for JSON schema generation, specialize +// the enum_descriptions template for that enum type. +// Example: +// template <> +// struct rfl::config::enum_descriptions { +// static constexpr std::string_view get(MyEnum value) { +// switch (value) { +// case MyEnum::option1: return "Description for option1"; +// case MyEnum::option2: return "Description for option2"; +// default: return ""; +// } +// } +// }; +template +struct enum_descriptions { + // Default implementation returns empty string (no descriptions) + static constexpr std::string_view get(T) { return ""; } + // Set to true in specializations that provide descriptions + static constexpr bool has_descriptions = false; +}; + } // namespace rfl::config #endif diff --git a/include/rfl/json/schema/Type.hpp b/include/rfl/json/schema/Type.hpp index 655566a7f..8ed247928 100644 --- a/include/rfl/json/schema/Type.hpp +++ b/include/rfl/json/schema/Type.hpp @@ -118,6 +118,11 @@ struct Type { std::string pattern{}; }; + struct StringConst { + rfl::Flatten annotations{}; + rfl::Rename<"const", std::string> value{}; + }; + struct StringEnum { Literal<"string"> type{}; rfl::Flatten annotations{}; @@ -148,8 +153,8 @@ struct Type { using ReflectionType = rfl::Variant; + Object, OneOf, Reference, Regex, String, StringConst, + StringEnum, StringMap, Tuple, TypedArray>; const auto& reflection() const { return value; } diff --git a/include/rfl/parsing/Parser_enum.hpp b/include/rfl/parsing/Parser_enum.hpp index dabf89d0a..9c156086f 100644 --- a/include/rfl/parsing/Parser_enum.hpp +++ b/include/rfl/parsing/Parser_enum.hpp @@ -5,6 +5,7 @@ #include #include "../Result.hpp" +#include "../config.hpp" #include "../enums.hpp" #include "../thirdparty/enchantum/enchantum.hpp" #include "../internal/has_reflector.hpp" @@ -92,6 +93,17 @@ struct Parser { return Type{Type::Integer{}}; } else if constexpr (enchantum::is_bitflag) { return Type{Type::String{}}; + } else if constexpr (config::enum_descriptions::has_descriptions) { + // Generate DescribedLiteral for enums with descriptions + auto described = Type::DescribedLiteral{}; + constexpr auto enumerators = get_enumerator_array(); + for (const auto& [name, value] : enumerators) { + auto desc = config::enum_descriptions::get(value); + described.values_.push_back(Type::DescribedLiteral::ValueWithDescription{ + .value_ = std::string(name), + .description_ = std::string(desc)}); + } + return Type{std::move(described)}; } else { return Parser< R, W, diff --git a/include/rfl/parsing/schema/Type.hpp b/include/rfl/parsing/schema/Type.hpp index 142a78280..824daa0dd 100644 --- a/include/rfl/parsing/schema/Type.hpp +++ b/include/rfl/parsing/schema/Type.hpp @@ -9,8 +9,8 @@ #include "../../Object.hpp" #include "../../Ref.hpp" #include "../../Variant.hpp" -#include "ValidationType.hpp" #include "../../common.hpp" +#include "ValidationType.hpp" namespace rfl::parsing::schema { @@ -61,6 +61,14 @@ struct RFL_API Type { std::vector values_; }; + struct DescribedLiteral { + struct ValueWithDescription { + std::string value_; + std::string description_; + }; + std::vector values_; + }; + struct Object { rfl::Object types_; std::shared_ptr additional_properties_; @@ -97,10 +105,11 @@ struct RFL_API Type { }; using VariantType = - rfl::Variant; + rfl::Variant; Type(); diff --git a/src/rfl/avro/to_schema.cpp b/src/rfl/avro/to_schema.cpp index 3213b7869..b5c54dee8 100644 --- a/src/rfl/avro/to_schema.cpp +++ b/src/rfl/avro/to_schema.cpp @@ -101,6 +101,16 @@ schema::Type type_to_avro_schema_type( std::to_string(++(*_num_unnamed)), .symbols = _t.values_}}; + } else if constexpr (std::is_same()) { + auto symbols = std::vector(); + for (const auto& v : _t.values_) { + symbols.push_back(v.value_); + } + return schema::Type{ + .value = schema::Type::Enum{.name = std::string("unnamed_") + + std::to_string(++(*_num_unnamed)), + .symbols = symbols}}; + } else if constexpr (std::is_same()) { auto record = schema::Type::Record{ .name = std::string("unnamed_") + std::to_string(++(*_num_unnamed))}; diff --git a/src/rfl/capnproto/to_schema.cpp b/src/rfl/capnproto/to_schema.cpp index 5a9426c5f..fa4b7dcbd 100644 --- a/src/rfl/capnproto/to_schema.cpp +++ b/src/rfl/capnproto/to_schema.cpp @@ -198,6 +198,14 @@ schema::Type type_to_capnproto_schema_type( return literal_to_capnproto_schema_type(_t, _definitions, _parent, _cnp_types); + } else if constexpr (std::is_same()) { + auto values = std::vector(); + for (const auto& v : _t.values_) { + values.push_back(v.value_); + } + return literal_to_capnproto_schema_type( + Type::Literal{.values_ = values}, _definitions, _parent, _cnp_types); + } else if constexpr (std::is_same()) { return object_to_capnproto_schema_type(_t, _definitions, _parent, _cnp_types); diff --git a/src/rfl/json/to_schema.cpp b/src/rfl/json/to_schema.cpp index 4b852bc30..47c3868a7 100644 --- a/src/rfl/json/to_schema.cpp +++ b/src/rfl/json/to_schema.cpp @@ -40,7 +40,8 @@ bool is_optional(const parsing::schema::Type& _t) { if constexpr (std::is_same_v) { return is_optional(*_v.type_); - } else if constexpr (std::is_same_v) { + } else if constexpr (std::is_same_v) { return is_optional(*_v.type_); } else if constexpr (std::is_same_v) { @@ -250,6 +251,22 @@ schema::Type type_to_json_schema_type(const parsing::schema::Type& _type, return schema::Type{.value = schema::Type::StringEnum{.values = _t.values_}}; + } else if constexpr (std::is_same()) { + // Convert to OneOf with StringConst for each described value + auto one_of = std::vector(); + for (const auto& v : _t.values_) { + one_of.push_back(schema::Type{ + .value = schema::Type::StringConst{ + .annotations = + schema::Type::Annotations{ + .description = + v.description_.empty() + ? std::nullopt + : std::optional(v.description_)}, + .value = v.value_}}); + } + return schema::Type{.value = schema::Type::OneOf{.oneOf = one_of}}; + } else if constexpr (std::is_same()) { auto properties = rfl::Object(); auto required = std::vector(); diff --git a/tests/json/test_enum_descriptions.cpp b/tests/json/test_enum_descriptions.cpp new file mode 100644 index 000000000..ac2a4d7c3 --- /dev/null +++ b/tests/json/test_enum_descriptions.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include + +namespace test_enum_descriptions { + +// Define an enum with descriptions +enum class Color { red, green, blue }; + +// An enum without descriptions for comparison +enum class Size { small, medium, large }; + +struct Config { + Color color; + Size size; +}; + +} // namespace test_enum_descriptions + +// Specialize enum_descriptions to provide descriptions for Color values +template <> +struct rfl::config::enum_descriptions { + static constexpr bool has_descriptions = true; + static constexpr std::string_view get(test_enum_descriptions::Color value) { + switch (value) { + case test_enum_descriptions::Color::red: + return "The color red"; + case test_enum_descriptions::Color::green: + return "The color green"; + case test_enum_descriptions::Color::blue: + return "The color blue"; + default: + return ""; + } + } +}; + +namespace test_enum_descriptions { + +TEST(json, test_enum_descriptions_schema) { + const auto json_schema = rfl::json::to_schema(); + + // The schema should contain oneOf with const/description for Color + EXPECT_TRUE(json_schema.find("\"oneOf\"") != std::string::npos) + << "Expected oneOf for described enum. Schema: " << json_schema; + EXPECT_TRUE(json_schema.find("\"const\":\"red\"") != std::string::npos) + << "Expected const for red. Schema: " << json_schema; + EXPECT_TRUE(json_schema.find("\"description\":\"The color red\"") != + std::string::npos) + << "Expected description for red. Schema: " << json_schema; + EXPECT_TRUE(json_schema.find("\"const\":\"green\"") != std::string::npos) + << "Expected const for green. Schema: " << json_schema; + EXPECT_TRUE(json_schema.find("\"const\":\"blue\"") != std::string::npos) + << "Expected const for blue. Schema: " << json_schema; + + // Size should still use regular enum format + EXPECT_TRUE(json_schema.find("\"enum\":[\"small\",\"medium\",\"large\"]") != + std::string::npos) + << "Expected regular enum for Size. Schema: " << json_schema; +} + +TEST(json, test_enum_descriptions_read_write) { + // Verify that read/write still works correctly with described enums + const Config config{.color = Color::green, .size = Size::medium}; + + const auto json_string = rfl::json::write(config); + EXPECT_EQ(json_string, R"({"color":"green","size":"medium"})"); + + const auto parsed = rfl::json::read(json_string); + EXPECT_TRUE(parsed.has_value()) << "Failed to parse: " << parsed.error().what(); + EXPECT_EQ(parsed.value().color, Color::green); + EXPECT_EQ(parsed.value().size, Size::medium); +} + +} // namespace test_enum_descriptions From 1a2635fd9b9680db1331e0570a9e7698d01cb3ac Mon Sep 17 00:00:00 2001 From: "Dr. Patrick Urbanke" Date: Mon, 16 Mar 2026 17:13:57 +0100 Subject: [PATCH 2/2] Added documentation --- docs/enum_descriptions.md | 113 ++++++++++++++++++++++++++++++++++++++ mkdocs.yaml | 1 + 2 files changed, 114 insertions(+) create mode 100644 docs/enum_descriptions.md diff --git a/docs/enum_descriptions.md b/docs/enum_descriptions.md new file mode 100644 index 000000000..014f38140 --- /dev/null +++ b/docs/enum_descriptions.md @@ -0,0 +1,113 @@ +# Enum descriptions + +reflect-cpp allows you to add descriptions to individual enum values in JSON schemas. This is particularly useful when generating documentation or providing additional context for enum values in API specifications. + +## Basic usage + +To add descriptions to enum values, you need to specialize the `rfl::config::enum_descriptions` template for your enum type: + +```cpp +enum class Color { red, green, blue }; + +template <> +struct rfl::config::enum_descriptions { + static constexpr bool has_descriptions = true; + static constexpr std::string_view get(Color value) { + switch (value) { + case Color::red: + return "The color red"; + case Color::green: + return "The color green"; + case Color::blue: + return "The color blue"; + default: + return ""; + } + } +}; +``` + +When you generate a JSON schema for a struct containing this enum, the descriptions will be included: + +```cpp +struct Config { + Color color; +}; + +const std::string json_schema = rfl::json::to_schema(rfl::json::pretty); +``` + +This will generate a schema where the enum is represented using `oneOf` with `const` and `description` fields: + +```json +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$ref": "#/$defs/Config", + "$defs": { + "Config": { + "type": "object", + "properties": { + "color": { + "oneOf": [ + { + "const": "red", + "description": "The color red" + }, + { + "const": "green", + "description": "The color green" + }, + { + "const": "blue", + "description": "The color blue" + } + ] + } + }, + "required": ["color"] + } + } +} +``` + +## Enums without descriptions + +Enums that do not have `enum_descriptions` specialization will continue to use the standard `enum` format in JSON schemas: + +```cpp +enum class Size { small, medium, large }; + +struct Config { + Size size; +}; +``` + +This will generate: + +```json +{ + "type": "object", + "properties": { + "size": { + "type": "string", + "enum": ["small", "medium", "large"] + } + } +} +``` + +## Serialization and deserialization + +Adding descriptions to enums does not affect serialization or deserialization behavior. The enum values are still serialized as strings and parsed the same way: + +```cpp +const Config config{.color = Color::green}; + +const auto json_string = rfl::json::write(config); +// Result: {"color":"green"} + +const auto parsed = rfl::json::read(json_string); +// Works exactly as before +``` + +The descriptions only affect the JSON schema generation, making it easier for users to understand what each enum value represents. diff --git a/mkdocs.yaml b/mkdocs.yaml index 24e7e1fd8..9401c54d5 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -136,6 +136,7 @@ nav: - Composing validators: composing_validators.md - Size validation: size_validation.md - JSON schema: json_schema.md + - Enum descriptions: enum_descriptions.md - Generic elements: - rfl::Object: object.md - rfl::Generic: generic.md