feat: add json_to_field<T>() helper with if constexpr dispatch (issue #67)
- Add json_to_field<T>() helper in detail namespace for compile-time dispatch - Uses if constexpr(has_json_to_v<T>) to choose between recursive json_to<T>() for custom types and direct .get_to() for primitives - Custom types: calls json_to<T>() and prepends field name to error messages - Primitive types: uses .get_to() directly (no recursion) - Add MixedFieldsStruct test type with both primitive and custom type fields - Add test_json_to_field.sh test suite to verify correct dispatch and error messages
This commit is contained in:
@@ -21,6 +21,25 @@ struct has_json_to : std::false_type {};
|
||||
template<typename T>
|
||||
inline constexpr bool has_json_to_v = has_json_to<T>::value;
|
||||
|
||||
// Helper function for json_to - uses if constexpr to dispatch between
|
||||
// recursive json_to<T>() calls (for custom types) and direct .get_to() calls (for primitives)
|
||||
namespace detail {
|
||||
template<typename T>
|
||||
std::expected<T, std::string> json_to_field(const nlohmann::json& j, const char* field_name) {
|
||||
if constexpr (has_json_to_v<T>) {
|
||||
auto result = json_to<T>(j);
|
||||
if (!result) {
|
||||
return std::unexpected(std::string("field '") + field_name + "': " + result.error());
|
||||
}
|
||||
return result.value();
|
||||
} else {
|
||||
T value;
|
||||
j.get_to(value);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper class template for json_to - enables partial specialization for std::vector<T>
|
||||
// Primary template - default behavior (for types without explicit specialization, uses nlohmann)
|
||||
template<typename T, typename = void>
|
||||
@@ -403,6 +422,40 @@ struct SkillToolParams {
|
||||
};
|
||||
DRH_DEFINE_TYPE_REQUIRED(SkillToolParams, name)
|
||||
|
||||
// Test struct for json_to_field - has both primitive and custom type fields
|
||||
struct MixedFieldsStruct {
|
||||
std::string name;
|
||||
int count;
|
||||
FunctionCall func;
|
||||
};
|
||||
template<>
|
||||
inline std::expected<MixedFieldsStruct, std::string> json_to<MixedFieldsStruct>(const nlohmann::json& j) {
|
||||
MixedFieldsStruct s;
|
||||
if (!j.contains("name")) return std::unexpected("missing required field: name");
|
||||
if (!j.contains("count")) return std::unexpected("missing required field: count");
|
||||
if (!j.contains("func")) return std::unexpected("missing required field: func");
|
||||
|
||||
auto name_result = detail::json_to_field<std::string>(j.at("name"), "name");
|
||||
if (!name_result) return std::unexpected(name_result.error());
|
||||
s.name = std::move(name_result.value());
|
||||
|
||||
auto count_result = detail::json_to_field<int>(j.at("count"), "count");
|
||||
if (!count_result) return std::unexpected(count_result.error());
|
||||
s.count = std::move(count_result.value());
|
||||
|
||||
auto func_result = detail::json_to_field<FunctionCall>(j.at("func"), "func");
|
||||
if (!func_result) return std::unexpected(func_result.error());
|
||||
s.func = std::move(func_result.value());
|
||||
|
||||
return s;
|
||||
}
|
||||
template<> struct has_json_to<MixedFieldsStruct> : std::true_type {};
|
||||
inline void to_json(nlohmann::json& j, const MixedFieldsStruct& s) {
|
||||
j["name"] = s.name;
|
||||
j["count"] = s.count;
|
||||
j["func"] = s.func;
|
||||
}
|
||||
|
||||
// Compile-time tests for has_json_to_v
|
||||
static_assert(!has_json_to_v<std::string>, "std::string should not have json_to specialization");
|
||||
static_assert(!has_json_to_v<int>, "int should not have json_to specialization");
|
||||
|
||||
@@ -458,6 +458,13 @@ int main(int argc, char** argv) {
|
||||
} else {
|
||||
std::cout << "error: " << result.error() << std::endl;
|
||||
}
|
||||
} else if (type_str == "MixedFieldsStruct") {
|
||||
auto result = json_to<MixedFieldsStruct>(j);
|
||||
if (result) {
|
||||
std::cout << "success" << std::endl;
|
||||
} else {
|
||||
std::cout << "error: " << result.error() << std::endl;
|
||||
}
|
||||
} else {
|
||||
std::cout << "error: unknown type " << type_str << std::endl;
|
||||
return 1;
|
||||
|
||||
@@ -50,5 +50,8 @@ add_dr_test(test_json_type_mixed)
|
||||
# Add vector type tests
|
||||
add_dr_test(test_json_type_vector)
|
||||
|
||||
# Add json_to_field tests
|
||||
add_dr_test(test_json_to_field)
|
||||
|
||||
# Add expected error handling tests
|
||||
add_dr_test(test_expected_errors)
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
#!/bin/bash
|
||||
# test_json_to_field.sh - Tests for json_to_field<T>() helper with if constexpr dispatch
|
||||
|
||||
# Test MixedFieldsStruct with all fields (primitives + custom type)
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"name": "test", "count": 42, "func": {"name": "test_func", "arguments": "{}"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: MixedFieldsStruct with all fields - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with missing 'name' field (primitive)
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"count": 42, "func": {"name": "test_func", "arguments": "{}"}}')
|
||||
if [ "${OUTPUT}" != "error: missing required field: name" ]; then
|
||||
echo "FAIL: MixedFieldsStruct missing name - expected 'error: missing required field: name', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with missing 'count' field (primitive)
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"name": "test", "func": {"name": "test_func", "arguments": "{}"}}')
|
||||
if [ "${OUTPUT}" != "error: missing required field: count" ]; then
|
||||
echo "FAIL: MixedFieldsStruct missing count - expected 'error: missing required field: count', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with missing 'func' field (custom type)
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"name": "test", "count": 42}')
|
||||
if [ "${OUTPUT}" != "error: missing required field: func" ]; then
|
||||
echo "FAIL: MixedFieldsStruct missing func - expected 'error: missing required field: func', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with invalid nested FunctionCall (missing 'name')
|
||||
# This verifies that error messages include field name context for nested type failures
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"name": "test", "count": 42, "func": {"arguments": "{}"}}')
|
||||
if [ "${OUTPUT}" != "error: field 'func': missing required field: name" ]; then
|
||||
echo "FAIL: MixedFieldsStruct invalid nested func - expected 'error: field '\''func'\'': missing required field: name', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with invalid nested FunctionCall (missing 'arguments')
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct '{"name": "test", "count": 42, "func": {"name": "test_func"}}')
|
||||
if [ "${OUTPUT}" != "error: field 'func': missing required field: arguments" ]; then
|
||||
echo "FAIL: MixedFieldsStruct invalid nested func missing args - expected 'error: field '\''func'\'': missing required field: arguments', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test MixedFieldsStruct with invalid JSON
|
||||
set +e
|
||||
OUTPUT=$("${DRHARNESS}" -R MixedFieldsStruct 'not valid json' 2>&1) || true
|
||||
set -e
|
||||
if [ "${OUTPUT}" != "error: invalid JSON" ]; then
|
||||
echo "FAIL: MixedFieldsStruct invalid JSON - expected 'error: invalid JSON', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All json_to_field tests passed!"
|
||||
Reference in New Issue
Block a user