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 32e0dcfb..392f2b37 100644 --- a/include/rfl/json/schema/Type.hpp +++ b/include/rfl/json/schema/Type.hpp @@ -110,6 +110,11 @@ struct Type { std::string pattern{}; }; + struct StringConst { + std::optional description{}; + rfl::Rename<"const", std::string> value{}; + }; + struct StringEnum { Literal<"string"> type{}; std::optional description{}; @@ -140,8 +145,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 283b94ba..f57bf796 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 "AreReaderAndWriter.hpp" @@ -70,6 +71,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 0ff8e055..7421db98 100644 --- a/include/rfl/parsing/schema/Type.hpp +++ b/include/rfl/parsing/schema/Type.hpp @@ -55,6 +55,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_; @@ -92,7 +100,7 @@ struct RFL_API Type { using VariantType = rfl::Variant; diff --git a/src/rfl/json/to_schema.cpp b/src/rfl/json/to_schema.cpp index d688e08a..2f2fc7b0 100644 --- a/src/rfl/json/to_schema.cpp +++ b/src/rfl/json/to_schema.cpp @@ -237,6 +237,19 @@ 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{ + .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