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:
2026-05-28 08:56:28 -05:00
parent a245ab571f
commit 4cc328a6fc
4 changed files with 119 additions and 0 deletions
+53
View File
@@ -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");
+7
View File
@@ -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;
+3
View File
@@ -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)
+56
View File
@@ -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!"