Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions include/rfl/config.hpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef RFL_CONFIG_HPP_
#define RFL_CONFIG_HPP_

#include <string_view>

namespace rfl::config {

// To specify a different range for a particular enum type, specialize the
Expand All @@ -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<MyEnum> {
// 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 "";
// }
Comment on lines +27 to +28

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using default in a switch over an enum can suppress useful compiler warnings if a new enumerator is added but not handled in the switch. By removing the default case and having a return statement after the switch, you encourage developers to explicitly handle all enum values. The compiler will warn them if they miss one, leading to more maintainable code.

Suggested change
// default: return "";
// }
// }
// return "";

// }
// };
template <typename T>
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
Expand Down
9 changes: 7 additions & 2 deletions include/rfl/json/schema/Type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,11 @@ struct Type {
std::string pattern{};
};

struct StringConst {
std::optional<std::string> description{};
rfl::Rename<"const", std::string> value{};
};

struct StringEnum {
Literal<"string"> type{};
std::optional<std::string> description{};
Expand Down Expand Up @@ -140,8 +145,8 @@ struct Type {
using ReflectionType =
rfl::Variant<AllOf, AnyOf, Boolean, ExclusiveMaximum, ExclusiveMinimum,
FixedSizeTypedArray, Integer, Maximum, Minimum, Number, Null,
Object, OneOf, Reference, Regex, String, StringEnum,
StringMap, Tuple, TypedArray>;
Object, OneOf, Reference, Regex, String, StringConst,
StringEnum, StringMap, Tuple, TypedArray>;

const auto& reflection() const { return value; }

Expand Down
12 changes: 12 additions & 0 deletions include/rfl/parsing/Parser_enum.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include <type_traits>

#include "../Result.hpp"
#include "../config.hpp"
#include "../enums.hpp"
#include "../thirdparty/enchantum/enchantum.hpp"
#include "AreReaderAndWriter.hpp"
Expand Down Expand Up @@ -70,6 +71,17 @@ struct Parser<R, W, T, ProcessorsType> {
return Type{Type::Integer{}};
} else if constexpr (enchantum::is_bitflag<U>) {
return Type{Type::String{}};
} else if constexpr (config::enum_descriptions<U>::has_descriptions) {
// Generate DescribedLiteral for enums with descriptions
auto described = Type::DescribedLiteral{};
constexpr auto enumerators = get_enumerator_array<U>();
for (const auto& [name, value] : enumerators) {
auto desc = config::enum_descriptions<U>::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,
Expand Down
10 changes: 9 additions & 1 deletion include/rfl/parsing/schema/Type.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ struct RFL_API Type {
std::vector<std::string> values_;
};

struct DescribedLiteral {
struct ValueWithDescription {
std::string value_;
std::string description_;
};
std::vector<ValueWithDescription> values_;
};

struct Object {
rfl::Object<Type> types_;
std::shared_ptr<Type> additional_properties_;
Expand Down Expand Up @@ -92,7 +100,7 @@ struct RFL_API Type {

using VariantType =
rfl::Variant<Boolean, Bytestring, Vectorstring, Int32, Int64, UInt32, UInt64, Integer,
Float, Double, String, AnyOf, Description,
Float, Double, String, AnyOf, Description, DescribedLiteral,
FixedSizeTypedArray, Literal, Object, Optional, Reference,
StringMap, Tuple, TypedArray, Validated>;

Expand Down
13 changes: 13 additions & 0 deletions src/rfl/json/to_schema.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<T, Type::DescribedLiteral>()) {
// Convert to OneOf with StringConst for each described value
auto one_of = std::vector<schema::Type>();
for (const auto& v : _t.values_) {
one_of.push_back(schema::Type{
.value = schema::Type::StringConst{
.description = v.description_.empty()
? std::nullopt
: std::optional<std::string>(v.description_),
.value = v.value_}});
}
return schema::Type{.value = schema::Type::OneOf{.oneOf = one_of}};

} else if constexpr (std::is_same<T, Type::Object>()) {
auto properties = rfl::Object<schema::Type>();
auto required = std::vector<std::string>();
Expand Down
76 changes: 76 additions & 0 deletions tests/json/test_enum_descriptions.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include <gtest/gtest.h>
#include <rfl.hpp>
#include <rfl/json.hpp>
#include <string>

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<test_enum_descriptions::Color> {
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 "";
}
Comment on lines +33 to +35

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to my other comment, it's better to avoid default here. This will cause the compiler to issue a warning if a new value is added to the Color enum but not handled in this switch, making the test more robust against future changes.

    }
    return "";

}
};

namespace test_enum_descriptions {

TEST(json, test_enum_descriptions_schema) {
const auto json_schema = rfl::json::to_schema<Config>();

// 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<Config>(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
Loading