Add std::variant JSON serialization infrastructure (issue 150)
- Add #include <variant> header - Add json_type_for_type() helper for type-based disambiguation - Add to_json for std::variant that serializes active alternative - Add variant_try_helper_impl for recursive variant deserialization - Add json_to_impl specialization for std::variant - Add TestVariantType (std::variant<std::string, std::vector<FunctionCall>>) for testing variant serialization - Register TestVariantType in json_dispatch_map for -R debug flag - Add compile-time static_assert for TestVariantType - Add test_json_variant.sh test suite - Add test to CMakeLists.txt
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <functional>
|
||||
#include <variant>
|
||||
|
||||
// Forward declaration of template function for JSON deserialization
|
||||
// Specialized implementations are generated by DRH_DEFINE_TYPE_* macros
|
||||
@@ -88,6 +89,78 @@ std::expected<T, std::string> json_to(const nlohmann::json& j) {
|
||||
return json_to_impl<T>::apply(j);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// std::variant serialization/deserialization support
|
||||
// ============================================================================
|
||||
|
||||
// Helper to get the JSON type that a type serializes to
|
||||
// Used for type-based disambiguation during variant deserialization
|
||||
template<typename T>
|
||||
constexpr nlohmann::json::value_type json_type_for_type() {
|
||||
if constexpr (std::is_same_v<std::decay_t<T>, std::string>) {
|
||||
return nlohmann::json::value_t::string;
|
||||
} else if constexpr (requires { typename std::decay_t<T>::value_type; }) {
|
||||
// std::vector<T> or similar container
|
||||
return nlohmann::json::value_t::array;
|
||||
} else if constexpr (std::is_integral_v<std::decay_t<T>>) {
|
||||
return nlohmann::json::value_t::number_integer;
|
||||
} else if constexpr (std::is_floating_point_v<std::decay_t<T>>) {
|
||||
return nlohmann::json::value_t::number_float;
|
||||
} else if constexpr (std::is_same_v<std::decay_t<T>, bool>) {
|
||||
return nlohmann::json::value_t::boolean;
|
||||
} else {
|
||||
// Custom types serialize to objects
|
||||
return nlohmann::json::value_t::object;
|
||||
}
|
||||
}
|
||||
|
||||
// to_json for std::variant - serializes the active alternative
|
||||
template<typename... Alternatives>
|
||||
void to_json(nlohmann::json& j, const std::variant<Alternatives...>& v) {
|
||||
std::visit([&j](const auto& value) {
|
||||
j = value;
|
||||
}, v);
|
||||
}
|
||||
|
||||
// Helper for recursive variant alternative trying
|
||||
// Base case: no more alternatives to try
|
||||
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx>
|
||||
struct variant_try_helper;
|
||||
|
||||
// Primary template: try current alternative, then recurse
|
||||
template<typename VariantType, std::size_t CurrentIdx, std::size_t MaxIdx, typename... Alternatives>
|
||||
struct variant_try_helper_impl {
|
||||
static std::expected<VariantType, std::string> apply(const nlohmann::json& j, nlohmann::json::value_type json_type) {
|
||||
using Alt = std::tuple_element_t<CurrentIdx, std::tuple<Alternatives...>>;
|
||||
|
||||
if (json_type_for_type<Alt>() == json_type) {
|
||||
auto result = json_to<Alt>(j);
|
||||
if (result.has_value()) {
|
||||
return VariantType{std::in_place_index<CurrentIdx>, std::move(result.value())};
|
||||
}
|
||||
// Deserialization failed, but JSON type matched - return error
|
||||
return std::unexpected("variant alternative " + std::to_string(CurrentIdx) + ": " + result.error());
|
||||
}
|
||||
|
||||
// JSON type doesn't match, try next alternative
|
||||
if constexpr (CurrentIdx + 1 < MaxIdx) {
|
||||
return variant_try_helper_impl<VariantType, CurrentIdx + 1, MaxIdx, Alternatives...>::apply(j, json_type);
|
||||
} else {
|
||||
return std::unexpected("no variant alternative matched JSON type");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// json_to_impl specialization for std::variant
|
||||
// Uses type-based disambiguation: matches JSON type to variant alternatives
|
||||
template<typename... Alternatives>
|
||||
struct json_to_impl<std::variant<Alternatives...>> {
|
||||
static std::expected<std::variant<Alternatives...>, std::string> apply(const nlohmann::json& j) {
|
||||
using VariantType = std::variant<Alternatives...>;
|
||||
return variant_try_helper_impl<VariantType, 0, sizeof...(Alternatives), Alternatives...>::apply(j, j.type());
|
||||
}
|
||||
};
|
||||
|
||||
// Helper macros for common registration logic used by all DRH_DEFINE_TYPE_* macros
|
||||
|
||||
// Register has_json_to<Type> trait
|
||||
@@ -238,6 +311,23 @@ inline const auto json_register_vector_FunctionCall = []() {
|
||||
return 0u;
|
||||
}();
|
||||
|
||||
// Test-only variant type for validating variant serialization/deserialization
|
||||
// JSON string → std::string, JSON array → std::vector<FunctionCall>
|
||||
using TestVariantType = std::variant<std::string, std::vector<FunctionCall>>;
|
||||
|
||||
// has_json_to specialization for TestVariantType
|
||||
template<> struct has_json_to<TestVariantType> : std::true_type {};
|
||||
|
||||
// Register TestVariantType in json_dispatch_map for -R debug flag
|
||||
inline const auto drh_register_TestVariantType = []() {
|
||||
json_dispatch_map["TestVariantType"] = [](const nlohmann::json& j) {
|
||||
auto result = json_to<TestVariantType>(j);
|
||||
if (result) std::cout << "success" << std::endl;
|
||||
else std::cout << "error: " << result.error() << std::endl;
|
||||
};
|
||||
return 0u;
|
||||
}();
|
||||
|
||||
struct ToolCall {
|
||||
size_t index;
|
||||
std::string id = "";
|
||||
@@ -408,3 +498,6 @@ DRH_DEFINE_TYPE_MIXED(MixedTypeAllOptionalTest, (), (field1, field2))
|
||||
static_assert(has_json_to_v<MixedTypeSmokeTest>, "MixedTypeSmokeTest should have json_to specialization");
|
||||
static_assert(has_json_to_v<MixedTypeAllRequiredTest>, "MixedTypeAllRequiredTest should have json_to specialization");
|
||||
static_assert(has_json_to_v<MixedTypeAllOptionalTest>, "MixedTypeAllOptionalTest should have json_to specialization");
|
||||
|
||||
// Compile-time test for variant type
|
||||
static_assert(has_json_to_v<TestVariantType>, "TestVariantType should have json_to specialization");
|
||||
|
||||
@@ -48,5 +48,8 @@ add_dr_test(test_json_type_mixed)
|
||||
# Add json_to_field tests
|
||||
add_dr_test(test_json_to_field)
|
||||
|
||||
# Add std::variant JSON serialization tests
|
||||
add_dr_test(test_json_variant)
|
||||
|
||||
# Add expected error handling tests
|
||||
add_dr_test(test_expected_errors)
|
||||
|
||||
Executable
+67
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
# test_json_variant.sh - Tests for std::variant JSON serialization/deserialization
|
||||
|
||||
# Test TestVariantType with string alternative (JSON string)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '"hello world"')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType string alternative - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with vector alternative (JSON array with valid FunctionCall)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test", "arguments": "{}"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType vector alternative - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with empty vector
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType empty vector - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with multiple FunctionCalls in vector
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "func1", "arguments": "{}"}, {"name": "func2", "arguments": "{}"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType multiple items in vector - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (number - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '123')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType number type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (object - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '{"name": "test"}')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType object type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with invalid JSON type (boolean - no matching alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType 'true')
|
||||
if [ "${OUTPUT}" != "error: no variant alternative matched JSON type" ]; then
|
||||
echo "FAIL: TestVariantType boolean type - expected error, got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with empty string (valid string alternative)
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '""')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType empty string - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test TestVariantType with vector containing FunctionCall with only optional fields
|
||||
OUTPUT=$("${DRHARNESS}" -R TestVariantType '[{"name": "test"}]')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: TestVariantType partial FunctionCall - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All variant tests passed!"
|
||||
Reference in New Issue
Block a user