mirror of
https://github.com/simdjson/simdjson
synced 2026-06-08 17:27:07 +00:00
Adding extract_from functionality + unit tests
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user