feat: Add ImageUrl struct and image_url support to ContentPart (#152)
- Add ImageUrl struct with url and detail fields - Extend ContentPart with optional text and image_url fields - Add std::optional serialization/deserialization support - Custom to_json for ContentPart that only serializes relevant fields - Custom json_to<ContentPart> for proper deserialization - Add test_image_url.sh with comprehensive tests - All 30 tests pass
This commit is contained in:
+103
-2
@@ -3,6 +3,7 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <expected>
|
||||
#include <optional>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <regex>
|
||||
@@ -93,6 +94,50 @@ std::expected<T, std::string> json_to(const nlohmann::json& j) {
|
||||
return json_to_impl<T>::apply(j);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// std::optional serialization/deserialization support
|
||||
// ============================================================================
|
||||
|
||||
// Partial specialization for std::optional<T> - always true since we handle it
|
||||
template<typename T>
|
||||
struct has_json_to<std::optional<T>> : std::true_type {};
|
||||
|
||||
// to_json for std::optional - serializes as null when empty, value when present
|
||||
template<typename T>
|
||||
void to_json(nlohmann::json& j, const std::optional<T>& opt) {
|
||||
if (opt.has_value()) {
|
||||
j = opt.value();
|
||||
} else {
|
||||
j = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
// json_to_impl specialization for std::optional<T>
|
||||
// Deserializes null as std::nullopt, otherwise deserializes the inner type
|
||||
template<typename T>
|
||||
struct json_to_impl<std::optional<T>> {
|
||||
static std::expected<std::optional<T>, std::string> apply(const nlohmann::json& j) {
|
||||
if (j.is_null()) {
|
||||
return std::optional<T>{};
|
||||
}
|
||||
if constexpr (has_json_to_v<T>) {
|
||||
auto result = json_to<T>(j);
|
||||
if (!result) {
|
||||
return std::unexpected(result.error());
|
||||
}
|
||||
return std::optional<T>{std::move(result.value())};
|
||||
} else {
|
||||
try {
|
||||
T value;
|
||||
j.get_to(value);
|
||||
return std::optional<T>{std::move(value)};
|
||||
} catch (const nlohmann::json::exception& e) {
|
||||
return std::unexpected(e.what());
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// std::variant serialization/deserialization support
|
||||
// ============================================================================
|
||||
@@ -340,11 +385,67 @@ struct ToolCall {
|
||||
};
|
||||
DRH_DEFINE_TYPE_MIXED(ToolCall, (index), (id, type, function))
|
||||
|
||||
struct ImageUrl {
|
||||
std::string url = "";
|
||||
std::string detail = "auto";
|
||||
};
|
||||
DRH_DEFINE_TYPE_OPTIONAL(ImageUrl, url, detail)
|
||||
|
||||
struct ContentPart {
|
||||
std::string type = "text";
|
||||
std::string text = "";
|
||||
std::optional<std::string> text;
|
||||
std::optional<ImageUrl> image_url;
|
||||
};
|
||||
DRH_DEFINE_TYPE_OPTIONAL(ContentPart, type, text)
|
||||
|
||||
// Custom to_json for ContentPart - only serializes relevant fields based on type
|
||||
inline void to_json(nlohmann::json& j, const ContentPart& cp) {
|
||||
j = nlohmann::json{};
|
||||
j["type"] = cp.type;
|
||||
if (cp.type == "text" && cp.text.has_value()) {
|
||||
j["text"] = cp.text.value();
|
||||
} else if (cp.type == "image_url" && cp.image_url.has_value()) {
|
||||
j["image_url"] = cp.image_url.value();
|
||||
}
|
||||
}
|
||||
|
||||
// Custom json_to for ContentPart
|
||||
template<>
|
||||
inline std::expected<ContentPart, std::string> json_to<ContentPart>(const nlohmann::json& j) {
|
||||
ContentPart cp;
|
||||
|
||||
if (j.contains("type")) {
|
||||
auto type_result = json_to_field<std::string>(j.at("type"), "type");
|
||||
if (!type_result) return std::unexpected(type_result.error());
|
||||
cp.type = std::move(type_result.value());
|
||||
}
|
||||
|
||||
if (j.contains("text") && !j["text"].is_null()) {
|
||||
auto text_result = json_to_field<std::string>(j.at("text"), "text");
|
||||
if (!text_result) return std::unexpected(text_result.error());
|
||||
cp.text = std::move(text_result.value());
|
||||
}
|
||||
|
||||
if (j.contains("image_url") && !j["image_url"].is_null()) {
|
||||
auto url_result = json_to<ImageUrl>(j.at("image_url"));
|
||||
if (!url_result) return std::unexpected("field 'image_url': " + url_result.error());
|
||||
cp.image_url = std::move(url_result.value());
|
||||
}
|
||||
|
||||
return cp;
|
||||
}
|
||||
|
||||
// Register has_json_to<ContentPart> trait
|
||||
template<> struct has_json_to<ContentPart> : std::true_type {};
|
||||
|
||||
// Register ContentPart in json_dispatch_map for -R debug flag
|
||||
inline const auto drh_register_ContentPart = []() {
|
||||
json_dispatch_map["ContentPart"] = [](const nlohmann::json& j) {
|
||||
auto result = json_to<ContentPart>(j);
|
||||
if (result) std::cout << "success" << std::endl;
|
||||
else std::cout << "error: " << result.error() << std::endl;
|
||||
};
|
||||
return 0u;
|
||||
}();
|
||||
|
||||
struct Message {
|
||||
std::string role = "";
|
||||
|
||||
@@ -59,3 +59,6 @@ add_dr_test(test_base64_encode)
|
||||
|
||||
# Add image file reading tests
|
||||
add_dr_test(test_image_flag)
|
||||
|
||||
# Add ImageUrl and ContentPart tests
|
||||
add_dr_test(test_image_url)
|
||||
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/bash
|
||||
# test_image_url.sh - Tests for ImageUrl and ContentPart with image_url support
|
||||
|
||||
# Test ImageUrl deserialization with url and detail fields
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{"url": "data:image/png;base64,abc123", "detail": "auto"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl with url and detail - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ImageUrl deserialization with only url field (detail should default to "auto")
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{"url": "https://example.com/image.png"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl with only url - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ImageUrl deserialization with empty object (should use defaults)
|
||||
OUTPUT=$("${DRHARNESS}" -R ImageUrl '{}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ImageUrl empty object - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with text type
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "text", "text": "Hello world"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with text type - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with text type and only type field (text should be optional)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "text"}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with only type text - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc123", "detail": "auto"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url type - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and minimal image_url (only url)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "https://example.com/image.png"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with minimal image_url - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with empty object (should use defaults)
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart empty object - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and detail "low"
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc", "detail": "low"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url detail low - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test ContentPart with image_url type and detail "high"
|
||||
OUTPUT=$("${DRHARNESS}" -R ContentPart '{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc", "detail": "high"}}')
|
||||
if [ "${OUTPUT}" != "success" ]; then
|
||||
echo "FAIL: ContentPart with image_url detail high - expected 'success', got '${OUTPUT}'"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "All ImageUrl and ContentPart tests passed!"
|
||||
Reference in New Issue
Block a user