Adding extract_from functionality + unit tests

This commit is contained in:
Francisco Geiman Thiesen
2025-09-26 19:48:16 -07:00
parent 88a1b3e83b
commit 6aa7eea334
2 changed files with 304 additions and 9 deletions
@@ -28,10 +28,10 @@ namespace builder {
// Types we serialize as JSON strings (not as containers)
template <typename T>
concept string_like =
std::is_same_v<remove_cvref_t<T>, std::string> ||
std::is_same_v<remove_cvref_t<T>, std::string_view> ||
std::is_same_v<remove_cvref_t<T>, const char*> ||
std::is_same_v<remove_cvref_t<T>, char*>;
std::is_same_v<std::remove_cvref_t<T>, std::string> ||
std::is_same_v<std::remove_cvref_t<T>, std::string_view> ||
std::is_same_v<std::remove_cvref_t<T>, const char*> ||
std::is_same_v<std::remove_cvref_t<T>, char*>;
// Concept that checks if a type is a container but not a string (because
// strings handling must be handled differently)
// Now uses iterator-based approach for broader container support
@@ -40,7 +40,7 @@ concept container_but_not_string =
std::ranges::input_range<T> && !string_like<T>;
template <class T>
requires(container_but_not_string<T> && !require_custom_serialization<T>)
requires(container_but_not_string<T> && !concepts::string_view_keyed_map<T> && !require_custom_serialization<T>)
constexpr void atom(string_builder &b, const T &t) {
auto it = t.begin();
auto end = t.end();
@@ -258,7 +258,7 @@ void append(string_builder &b, const Z &z) {
// works for container that have begin() and end() iterators
template <class Z>
requires(container_but_not_string<Z> && !require_custom_serialization<Z>)
requires(container_but_not_string<Z> && !concepts::string_view_keyed_map<Z> && !require_custom_serialization<Z>)
void append(string_builder &b, const Z &z) {
auto it = z.begin();
auto end = z.end();
@@ -307,17 +307,88 @@ string_builder& operator<<(string_builder& b, const Z& z) {
append(b, z);
return b;
}
// extract_from: Serialize only specific fields from a struct to JSON
template<internal::fixed_string... FieldNames, typename T>
requires(std::is_class_v<T> && (sizeof...(FieldNames) > 0))
void extract_from(string_builder &b, const T &obj) {
// Helper to check if a field name matches any of the requested fields
auto should_extract = [](std::string_view field_name) constexpr -> bool {
return ((FieldNames.view() == field_name) || ...);
};
b.append('{');
bool first = true;
// Iterate through all members of T using reflection
template for (constexpr auto mem : std::define_static_array(
std::meta::nonstatic_data_members_of(^^T, std::meta::access_context::unchecked()))) {
if constexpr (std::meta::is_public(mem)) {
constexpr std::string_view key = std::define_static_string(std::meta::identifier_of(mem));
// Only serialize this field if it's in our list of requested fields
if constexpr (should_extract(key)) {
if (!first) {
b.append(',');
}
first = false;
// Serialize the key
constexpr auto quoted_key = std::define_static_string(constevalutil::consteval_to_quoted_escaped(std::meta::identifier_of(mem)));
b.append_raw(quoted_key);
b.append(':');
// Serialize the value
atom(b, obj.[:mem:]);
}
}
};
b.append('}');
}
template<internal::fixed_string... FieldNames, typename T>
requires(std::is_class_v<T> && (sizeof...(FieldNames) > 0))
simdjson_warn_unused simdjson_result<std::string> extract_from(const T &obj, size_t initial_capacity = string_builder::DEFAULT_INITIAL_CAPACITY) {
string_builder b(initial_capacity);
extract_from<FieldNames...>(b, obj);
std::string_view s;
if(auto e = b.view().get(s); e) { return e; }
return std::string(s);
}
} // namespace builder
} // namespace SIMDJSON_IMPLEMENTATION
// Alias the function template to 'to' in the global namespace
template <class Z>
simdjson_warn_unused simdjson_result<std::string> to_json(const Z &z, size_t initial_capacity = SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) {
return SIMDJSON_IMPLEMENTATION::builder::to_json_string(z, initial_capacity);
SIMDJSON_IMPLEMENTATION::builder::string_builder b(initial_capacity);
SIMDJSON_IMPLEMENTATION::builder::append(b, z);
std::string_view s;
if(auto e = b.view().get(s); e) { return e; }
return std::string(s);
}
template <class Z>
simdjson_warn_unused simdjson_error to_json(const Z &z, std::string &s, size_t initial_capacity = SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) {
return SIMDJSON_IMPLEMENTATION::builder::to_json(z, s, initial_capacity);
SIMDJSON_IMPLEMENTATION::builder::string_builder b(initial_capacity);
SIMDJSON_IMPLEMENTATION::builder::append(b, z);
std::string_view view;
if(auto e = b.view().get(view); e) { return e; }
s.assign(view);
return SUCCESS;
}
// Global namespace function for extract_from
template<SIMDJSON_IMPLEMENTATION::builder::internal::fixed_string... FieldNames, typename T>
requires(std::is_class_v<T> && (sizeof...(FieldNames) > 0))
simdjson_warn_unused simdjson_result<std::string> extract_from(const T &obj, size_t initial_capacity = SIMDJSON_IMPLEMENTATION::builder::string_builder::DEFAULT_INITIAL_CAPACITY) {
SIMDJSON_IMPLEMENTATION::builder::string_builder b(initial_capacity);
SIMDJSON_IMPLEMENTATION::builder::extract_from<FieldNames...>(b, obj);
std::string_view s;
if(auto e = b.view().get(s); e) { return e; }
return std::string(s);
}
} // namespace simdjson
#endif // SIMDJSON_STATIC_REFLECTION
@@ -513,13 +513,237 @@ namespace builder_tests {
}
bool test_extract_from() {
TEST_START();
#if SIMDJSON_STATIC_REFLECTION
// Test 1: Extract specific fields from Car struct
{
struct Car {
std::string make;
std::string model;
int year;
double price;
bool electric;
};
Car car{"Tesla", "Model 3", 2023, 42000.0, true};
// Extract only make and model
auto json_result = extract_from<"make", "model">(car);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
// Parse back to verify correctness
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
std::string_view make;
std::string_view model;
ASSERT_SUCCESS(doc["make"].get(make));
ASSERT_SUCCESS(doc["model"].get(model));
ASSERT_EQUAL(make, "Tesla");
ASSERT_EQUAL(model, "Model 3");
// Verify excluded fields are not present
auto year_result = doc["year"];
ASSERT_ERROR(year_result.error(), NO_SUCH_FIELD);
auto price_result = doc["price"];
ASSERT_ERROR(price_result.error(), NO_SUCH_FIELD);
auto electric_result = doc["electric"];
ASSERT_ERROR(electric_result.error(), NO_SUCH_FIELD);
}
// Test 2: Extract different field combination
{
struct Car {
std::string make;
std::string model;
int year;
double price;
bool electric;
};
Car car{"Ford", "F-150", 2024, 55000.0, false};
// Extract year and price
auto json_result = extract_from<"year", "price">(car);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
int64_t year;
double price;
ASSERT_SUCCESS(doc["year"].get(year));
ASSERT_SUCCESS(doc["price"].get(price));
ASSERT_EQUAL(year, 2024);
ASSERT_EQUAL(price, 55000.0);
// Verify excluded fields
auto make_result = doc["make"];
ASSERT_ERROR(make_result.error(), NO_SUCH_FIELD);
}
// Test 3: Extract from struct with optional fields
{
struct Person {
std::string name;
int age;
std::optional<std::string> email;
std::optional<std::string> phone;
};
Person person{"John Doe", 30, "john@example.com", std::nullopt};
// Extract name and email
auto json_result = extract_from<"name", "email">(person);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
std::string_view name;
std::string_view email;
ASSERT_SUCCESS(doc["name"].get(name));
ASSERT_SUCCESS(doc["email"].get(email));
ASSERT_EQUAL(name, "John Doe");
ASSERT_EQUAL(email, "john@example.com");
}
// Test 4: Extract with optional that has value
{
struct Person {
std::string name;
int age;
std::optional<std::string> email;
std::optional<std::string> phone;
};
Person person{"Jane Smith", 25, "jane@example.com", "555-1234"};
// Extract name, age, and phone
auto json_result = extract_from<"name", "age", "phone">(person);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
std::string_view name;
int64_t age;
std::string_view phone;
ASSERT_SUCCESS(doc["name"].get(name));
ASSERT_SUCCESS(doc["age"].get(age));
ASSERT_SUCCESS(doc["phone"].get(phone));
ASSERT_EQUAL(name, "Jane Smith");
ASSERT_EQUAL(age, 25);
ASSERT_EQUAL(phone, "555-1234");
// Email should not be present
auto email_result = doc["email"];
ASSERT_ERROR(email_result.error(), NO_SUCH_FIELD);
}
// Test 5: Round-trip test - serialize with extract_from, deserialize with extract_into
{
struct Product {
std::string id;
std::string name;
double price;
int stock;
};
Product original{"P123", "Widget", 19.99, 100};
// Extract specific fields to JSON
auto json_result = extract_from<"id", "name", "price">(original);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
// Parse and extract back
Product restored{"", "", 0.0, 0};
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
ondemand::object obj;
ASSERT_SUCCESS(doc.get_object().get(obj));
auto extract_result = obj.extract_into<"id", "name", "price">(restored);
ASSERT_SUCCESS(extract_result);
// Verify fields match
ASSERT_EQUAL(restored.id, original.id);
ASSERT_EQUAL(restored.name, original.name);
ASSERT_EQUAL(restored.price, original.price);
ASSERT_EQUAL(restored.stock, 0); // Stock should remain at default
}
// Test 6: Extract from nested structs
{
struct Address {
std::string street;
std::string city;
std::string zip;
};
struct Company {
std::string name;
Address headquarters;
int employees;
};
Company company{"TechCorp", {"123 Main St", "San Francisco", "94105"}, 500};
// Extract name and employees only
auto json_result = extract_from<"name", "employees">(company);
ASSERT_SUCCESS(json_result.error());
std::string json = json_result.value();
auto padded = pad(json);
ondemand::parser parser;
auto doc = parser.iterate(padded);
ASSERT_SUCCESS(doc);
std::string_view name;
int64_t employees;
ASSERT_SUCCESS(doc["name"].get(name));
ASSERT_SUCCESS(doc["employees"].get(employees));
ASSERT_EQUAL(name, "TechCorp");
ASSERT_EQUAL(employees, 500);
// headquarters should not be present
auto hq_result = doc["headquarters"];
ASSERT_ERROR(hq_result.error(), NO_SUCH_FIELD);
}
#endif
TEST_SUCCEED();
}
bool run() {
return test_primitive_types() &&
test_string_types() &&
test_optional_types() &&
test_smart_pointer_types() &&
test_container_types() &&
test_extract_into();
test_extract_into() &&
test_extract_from();
}
} // namespace builder_tests