Skip to content

Commit cc5791d

Browse files
committed
json schema: Add enum value descriptions
1 parent b91fd65 commit cc5791d

8 files changed

Lines changed: 168 additions & 8 deletions

File tree

include/rfl/config.hpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#ifndef RFL_CONFIG_HPP_
22
#define RFL_CONFIG_HPP_
33

4+
#include <string_view>
5+
46
namespace rfl::config {
57

68
// To specify a different range for a particular enum type, specialize the
@@ -13,6 +15,27 @@ struct enum_range {
1315
// static constexpr int max = ...;
1416
};
1517

18+
// To add descriptions to enum values for JSON schema generation, specialize
19+
// the enum_descriptions template for that enum type.
20+
// Example:
21+
// template <>
22+
// struct rfl::config::enum_descriptions<MyEnum> {
23+
// static constexpr std::string_view get(MyEnum value) {
24+
// switch (value) {
25+
// case MyEnum::option1: return "Description for option1";
26+
// case MyEnum::option2: return "Description for option2";
27+
// default: return "";
28+
// }
29+
// }
30+
// };
31+
template <typename T>
32+
struct enum_descriptions {
33+
// Default implementation returns empty string (no descriptions)
34+
static constexpr std::string_view get(T) { return ""; }
35+
// Set to true in specializations that provide descriptions
36+
static constexpr bool has_descriptions = false;
37+
};
38+
1639
} // namespace rfl::config
1740

1841
#endif

include/rfl/json/schema/Type.hpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ struct Type {
118118
std::string pattern{};
119119
};
120120

121+
struct StringConst {
122+
rfl::Flatten<Annotations> annotations{};
123+
rfl::Rename<"const", std::string> value{};
124+
};
125+
121126
struct StringEnum {
122127
Literal<"string"> type{};
123128
rfl::Flatten<Annotations> annotations{};
@@ -148,8 +153,8 @@ struct Type {
148153
using ReflectionType =
149154
rfl::Variant<AllOf, AnyOf, Boolean, ExclusiveMaximum, ExclusiveMinimum,
150155
FixedSizeTypedArray, Integer, Maximum, Minimum, Number, Null,
151-
Object, OneOf, Reference, Regex, String, StringEnum,
152-
StringMap, Tuple, TypedArray>;
156+
Object, OneOf, Reference, Regex, String, StringConst,
157+
StringEnum, StringMap, Tuple, TypedArray>;
153158

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

include/rfl/parsing/Parser_enum.hpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <type_traits>
66

77
#include "../Result.hpp"
8+
#include "../config.hpp"
89
#include "../enums.hpp"
910
#include "../thirdparty/enchantum/enchantum.hpp"
1011
#include "../internal/has_reflector.hpp"
@@ -92,6 +93,17 @@ struct Parser<R, W, T, ProcessorsType> {
9293
return Type{Type::Integer{}};
9394
} else if constexpr (enchantum::is_bitflag<U>) {
9495
return Type{Type::String{}};
96+
} else if constexpr (config::enum_descriptions<U>::has_descriptions) {
97+
// Generate DescribedLiteral for enums with descriptions
98+
auto described = Type::DescribedLiteral{};
99+
constexpr auto enumerators = get_enumerator_array<U>();
100+
for (const auto& [name, value] : enumerators) {
101+
auto desc = config::enum_descriptions<U>::get(value);
102+
described.values_.push_back(Type::DescribedLiteral::ValueWithDescription{
103+
.value_ = std::string(name),
104+
.description_ = std::string(desc)});
105+
}
106+
return Type{std::move(described)};
95107
} else {
96108
return Parser<
97109
R, W,

include/rfl/parsing/schema/Type.hpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
#include "../../Object.hpp"
1010
#include "../../Ref.hpp"
1111
#include "../../Variant.hpp"
12-
#include "ValidationType.hpp"
1312
#include "../../common.hpp"
13+
#include "ValidationType.hpp"
1414

1515
namespace rfl::parsing::schema {
1616

@@ -61,6 +61,14 @@ struct RFL_API Type {
6161
std::vector<std::string> values_;
6262
};
6363

64+
struct DescribedLiteral {
65+
struct ValueWithDescription {
66+
std::string value_;
67+
std::string description_;
68+
};
69+
std::vector<ValueWithDescription> values_;
70+
};
71+
6472
struct Object {
6573
rfl::Object<Type> types_;
6674
std::shared_ptr<Type> additional_properties_;
@@ -97,10 +105,11 @@ struct RFL_API Type {
97105
};
98106

99107
using VariantType =
100-
rfl::Variant<Boolean, Bytestring, Vectorstring, Int32, Int64, UInt32, UInt64, Integer,
101-
Float, Double, String, AnyOf, Deprecated, Description,
102-
FixedSizeTypedArray, Literal, Object, Optional, Reference,
103-
StringMap, Tuple, TypedArray, Validated>;
108+
rfl::Variant<Boolean, Bytestring, Vectorstring, Int32, Int64, UInt32,
109+
UInt64, Integer, Float, Double, String, AnyOf, Deprecated,
110+
Description, DescribedLiteral, FixedSizeTypedArray, Literal,
111+
Object, Optional, Reference, StringMap, Tuple, TypedArray,
112+
Validated>;
104113

105114
Type();
106115

src/rfl/avro/to_schema.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,16 @@ schema::Type type_to_avro_schema_type(
101101
std::to_string(++(*_num_unnamed)),
102102
.symbols = _t.values_}};
103103

104+
} else if constexpr (std::is_same<T, Type::DescribedLiteral>()) {
105+
auto symbols = std::vector<std::string>();
106+
for (const auto& v : _t.values_) {
107+
symbols.push_back(v.value_);
108+
}
109+
return schema::Type{
110+
.value = schema::Type::Enum{.name = std::string("unnamed_") +
111+
std::to_string(++(*_num_unnamed)),
112+
.symbols = symbols}};
113+
104114
} else if constexpr (std::is_same<T, Type::Object>()) {
105115
auto record = schema::Type::Record{
106116
.name = std::string("unnamed_") + std::to_string(++(*_num_unnamed))};

src/rfl/capnproto/to_schema.cpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,14 @@ schema::Type type_to_capnproto_schema_type(
198198
return literal_to_capnproto_schema_type(_t, _definitions, _parent,
199199
_cnp_types);
200200

201+
} else if constexpr (std::is_same<T, Type::DescribedLiteral>()) {
202+
auto values = std::vector<std::string>();
203+
for (const auto& v : _t.values_) {
204+
values.push_back(v.value_);
205+
}
206+
return literal_to_capnproto_schema_type(
207+
Type::Literal{.values_ = values}, _definitions, _parent, _cnp_types);
208+
201209
} else if constexpr (std::is_same<T, Type::Object>()) {
202210
return object_to_capnproto_schema_type(_t, _definitions, _parent,
203211
_cnp_types);

src/rfl/json/to_schema.cpp

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ bool is_optional(const parsing::schema::Type& _t) {
4040
if constexpr (std::is_same_v<T, parsing::schema::Type::Deprecated>) {
4141
return is_optional(*_v.type_);
4242

43-
} else if constexpr (std::is_same_v<T, parsing::schema::Type::Description>) {
43+
} else if constexpr (std::is_same_v<T,
44+
parsing::schema::Type::Description>) {
4445
return is_optional(*_v.type_);
4546

4647
} else if constexpr (std::is_same_v<T, parsing::schema::Type::Validated>) {
@@ -250,6 +251,22 @@ schema::Type type_to_json_schema_type(const parsing::schema::Type& _type,
250251
return schema::Type{.value =
251252
schema::Type::StringEnum{.values = _t.values_}};
252253

254+
} else if constexpr (std::is_same<T, Type::DescribedLiteral>()) {
255+
// Convert to OneOf with StringConst for each described value
256+
auto one_of = std::vector<schema::Type>();
257+
for (const auto& v : _t.values_) {
258+
one_of.push_back(schema::Type{
259+
.value = schema::Type::StringConst{
260+
.annotations =
261+
schema::Type::Annotations{
262+
.description =
263+
v.description_.empty()
264+
? std::nullopt
265+
: std::optional<std::string>(v.description_)},
266+
.value = v.value_}});
267+
}
268+
return schema::Type{.value = schema::Type::OneOf{.oneOf = one_of}};
269+
253270
} else if constexpr (std::is_same<T, Type::Object>()) {
254271
auto properties = rfl::Object<schema::Type>();
255272
auto required = std::vector<std::string>();
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
#include <gtest/gtest.h>
2+
#include <rfl.hpp>
3+
#include <rfl/json.hpp>
4+
#include <string>
5+
6+
namespace test_enum_descriptions {
7+
8+
// Define an enum with descriptions
9+
enum class Color { red, green, blue };
10+
11+
// An enum without descriptions for comparison
12+
enum class Size { small, medium, large };
13+
14+
struct Config {
15+
Color color;
16+
Size size;
17+
};
18+
19+
} // namespace test_enum_descriptions
20+
21+
// Specialize enum_descriptions to provide descriptions for Color values
22+
template <>
23+
struct rfl::config::enum_descriptions<test_enum_descriptions::Color> {
24+
static constexpr bool has_descriptions = true;
25+
static constexpr std::string_view get(test_enum_descriptions::Color value) {
26+
switch (value) {
27+
case test_enum_descriptions::Color::red:
28+
return "The color red";
29+
case test_enum_descriptions::Color::green:
30+
return "The color green";
31+
case test_enum_descriptions::Color::blue:
32+
return "The color blue";
33+
default:
34+
return "";
35+
}
36+
}
37+
};
38+
39+
namespace test_enum_descriptions {
40+
41+
TEST(json, test_enum_descriptions_schema) {
42+
const auto json_schema = rfl::json::to_schema<Config>();
43+
44+
// The schema should contain oneOf with const/description for Color
45+
EXPECT_TRUE(json_schema.find("\"oneOf\"") != std::string::npos)
46+
<< "Expected oneOf for described enum. Schema: " << json_schema;
47+
EXPECT_TRUE(json_schema.find("\"const\":\"red\"") != std::string::npos)
48+
<< "Expected const for red. Schema: " << json_schema;
49+
EXPECT_TRUE(json_schema.find("\"description\":\"The color red\"") !=
50+
std::string::npos)
51+
<< "Expected description for red. Schema: " << json_schema;
52+
EXPECT_TRUE(json_schema.find("\"const\":\"green\"") != std::string::npos)
53+
<< "Expected const for green. Schema: " << json_schema;
54+
EXPECT_TRUE(json_schema.find("\"const\":\"blue\"") != std::string::npos)
55+
<< "Expected const for blue. Schema: " << json_schema;
56+
57+
// Size should still use regular enum format
58+
EXPECT_TRUE(json_schema.find("\"enum\":[\"small\",\"medium\",\"large\"]") !=
59+
std::string::npos)
60+
<< "Expected regular enum for Size. Schema: " << json_schema;
61+
}
62+
63+
TEST(json, test_enum_descriptions_read_write) {
64+
// Verify that read/write still works correctly with described enums
65+
const Config config{.color = Color::green, .size = Size::medium};
66+
67+
const auto json_string = rfl::json::write(config);
68+
EXPECT_EQ(json_string, R"({"color":"green","size":"medium"})");
69+
70+
const auto parsed = rfl::json::read<Config>(json_string);
71+
EXPECT_TRUE(parsed.has_value()) << "Failed to parse: " << parsed.error().what();
72+
EXPECT_EQ(parsed.value().color, Color::green);
73+
EXPECT_EQ(parsed.value().size, Size::medium);
74+
}
75+
76+
} // namespace test_enum_descriptions

0 commit comments

Comments
 (0)