From cc5791ded3223a9c30af3f7d567da55db7f4d7fc Mon Sep 17 00:00:00 2001 From: Andrew Nolte Date: Thu, 22 Jan 2026 14:39:43 -0500 Subject: [PATCH] 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 9526f207..1a32dff8 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 655566a7..8ed24792 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 dabf89d0..9c156086 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 142a7828..824daa0d 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 3213b786..b5c54dee 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 5a9426c5..fa4b7dcb 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 4b852bc3..47c3868a 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 00000000..ac2a4d7c --- /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