From 6aa7eea334ed97bb12747b589c01bf6ca66b92bb Mon Sep 17 00:00:00 2001 From: Francisco Geiman Thiesen Date: Fri, 26 Sep 2025 19:48:16 -0700 Subject: [PATCH] Adding extract_from functionality + unit tests --- .../simdjson/generic/ondemand/json_builder.h | 87 ++++++- .../static_reflection_comprehensive_tests.cpp | 226 +++++++++++++++++- 2 files changed, 304 insertions(+), 9 deletions(-) diff --git a/include/simdjson/generic/ondemand/json_builder.h b/include/simdjson/generic/ondemand/json_builder.h index 1991bc6ac..8e4d247a2 100644 --- a/include/simdjson/generic/ondemand/json_builder.h +++ b/include/simdjson/generic/ondemand/json_builder.h @@ -28,10 +28,10 @@ namespace builder { // Types we serialize as JSON strings (not as containers) template concept string_like = - std::is_same_v, std::string> || - std::is_same_v, std::string_view> || - std::is_same_v, const char*> || - std::is_same_v, char*>; + std::is_same_v, std::string> || + std::is_same_v, std::string_view> || + std::is_same_v, const char*> || + std::is_same_v, 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 && !string_like; template - requires(container_but_not_string && !require_custom_serialization) + requires(container_but_not_string && !concepts::string_view_keyed_map && !require_custom_serialization) 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 - requires(container_but_not_string && !require_custom_serialization) + requires(container_but_not_string && !concepts::string_view_keyed_map && !require_custom_serialization) 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 + requires(std::is_class_v && (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 + requires(std::is_class_v && (sizeof...(FieldNames) > 0)) +simdjson_warn_unused simdjson_result extract_from(const T &obj, size_t initial_capacity = string_builder::DEFAULT_INITIAL_CAPACITY) { + string_builder b(initial_capacity); + extract_from(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 simdjson_warn_unused simdjson_result 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 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 + requires(std::is_class_v && (sizeof...(FieldNames) > 0)) +simdjson_warn_unused simdjson_result 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(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 diff --git a/tests/builder/static_reflection_comprehensive_tests.cpp b/tests/builder/static_reflection_comprehensive_tests.cpp index 4704887bb..6b7761b1a 100644 --- a/tests/builder/static_reflection_comprehensive_tests.cpp +++ b/tests/builder/static_reflection_comprehensive_tests.cpp @@ -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 email; + std::optional 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 email; + std::optional 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