From df16e967678b058313866088ccaebe84f5b93ef0 Mon Sep 17 00:00:00 2001 From: Daniel Lemire Date: Fri, 17 Apr 2026 14:20:57 -0400 Subject: [PATCH] fix: ondemand for wildcard matches (#2686) * fixing issue 2684 * simplifying. * update * more fixes * moving example * adding forward declaration --- doc/basics.md | 43 ++-- include/simdjson/generic/ondemand/array-inl.h | 79 ++++---- include/simdjson/generic/ondemand/array.h | 22 +- .../simdjson/generic/ondemand/document-inl.h | 42 +++- include/simdjson/generic/ondemand/document.h | 43 +++- .../simdjson/generic/ondemand/object-inl.h | 44 ++-- include/simdjson/generic/ondemand/object.h | 22 +- include/simdjson/generic/ondemand/value-inl.h | 24 ++- include/simdjson/generic/ondemand/value.h | 30 ++- tests/ondemand/ondemand_readme_examples.cpp | 64 ++++++ tests/ondemand/ondemand_wildcard_tests.cpp | 190 ++++++++++-------- 11 files changed, 388 insertions(+), 215 deletions(-) diff --git a/doc/basics.md b/doc/basics.md index a33e197ac..c4548706d 100644 --- a/doc/basics.md +++ b/doc/basics.md @@ -28,7 +28,7 @@ separate document](https://github.com/simdjson/simdjson/blob/master/doc/builder. - [UTF-8 validation (alone)](#utf-8-validation-alone) - [JSON Pointer](#json-pointer) - [JSONPath](#jsonpath) - * [Using `at_path_with_wildcard` for JSONPath Queries (On-Demand)](#using-at_path_with_wildcard-for-jsonpath-queries-on-demand) + * [Using `for_each_at_path_with_wildcard` for JSONPath Queries (On-Demand)](#using-for_each_at_path_with_wildcard-for-jsonpath-queries-on-demand) + [Example Usage](#example-usage) - [C++20 Ranges Support](#c20-ranges-support) - [Compile-Time JSONPath and JSON Pointer (C++26 Reflection)](#compile-time-jsonpath-and-json-pointer-c26-reflection) @@ -1842,15 +1842,15 @@ int64_t x = obj.at_path("$.c.foo.a[1]"); // 20 x = obj.at_path("$.d.foo2.a.2"); // 30 ``` -## Using `at_path_with_wildcard` for JSONPath Queries (On-Demand) +## Using `for_each_at_path_with_wildcard` for JSONPath Queries (On-Demand) -The `at_path_with_wildcard` function in simdjson extends the JSONPath querying capabilities by supporting wildcard expressions (`*`) in JSON paths. This allows users to retrieve multiple elements from a JSON document in a single query. For example, you can use `$.address.*` to fetch all fields within the `address` object or `$.phoneNumbers[*].numbers[*]` to retrieve all phone numbers across multiple objects in an array. +The `for_each_at_path_with_wildcard` function in simdjson extends the JSONPath querying capabilities by supporting wildcard expressions (`*`) in JSON paths. It calls a user-provided callback for each matching element, avoiding the need to materialize all results into a vector. For example, you can use `$.address.*` to fetch all fields within the `address` object or `$.phoneNumbers[*].numbers[*]` to retrieve all phone numbers across multiple objects in an array. -The `*` wildcard matches all elements at a specific level. For instance, `$.address.*` retrieves all key-value pairs in the `address` object, while `$.*.streetAddress` fetches all `streetAddress` fields across objects at the root level. You can combine wildcards with array indexing. For example, `$.phoneNumbers[*].numbers[1]` retrieves the second number from each `numbers` array in the `phoneNumbers` array. If no elements match the wildcard query, the function returns an empty result. For instance, querying `$.empty_object.*` or `$.empty_array.*` will yield an empty set. +The `*` wildcard matches all elements at a specific level. For instance, `$.address.*` retrieves all key-value pairs in the `address` object, while `$.*.streetAddress` fetches all `streetAddress` fields across objects at the root level. You can combine wildcards with array indexing. For example, `$.phoneNumbers[*].numbers[1]` retrieves the second number from each `numbers` array in the `phoneNumbers` array. If no elements match the wildcard query, the callback is simply never called. For instance, querying `$.empty_object.*` or `$.empty_array.*` will yield no callbacks. ### Example Usage -Here is an example demonstrating the use of `at_path_with_wildcard`: +Here is an example demonstrating the use of `for_each_at_path_with_wildcard`: ```cpp simdjson::padded_string json_string = R"( @@ -1879,27 +1879,22 @@ ondemand::parser parser; ondemand::document doc = parser.iterate(json_string); // Fetch all fields in the address object -std::vector values; -auto error = doc.at_path_with_wildcard("$.address.*").get(values); -if (!error) { - for (auto value : values) { - std::string_view field; - if (value.get(field) == SUCCESS) { - std::cout << field << std::endl; - } - } -} +auto error = doc.for_each_at_path_with_wildcard("$.address.*", + [](ondemand::value value) { + std::string_view field; + if (value.get(field) == SUCCESS) { + std::cout << field << std::endl; + } + }); // Fetch all phone numbers -error = doc.at_path_with_wildcard("$.phoneNumbers[*].numbers[*]").get(values); -if (!error) { - for (auto value : values) { - std::string_view number; - if (value.get(number) == SUCCESS) { - std::cout << number << std::endl; - } - } -} +doc.for_each_at_path_with_wildcard("$.phoneNumbers[*].numbers[*]", + [](ondemand::value value) { + std::string_view number; + if (value.get(number) == SUCCESS) { + std::cout << number << std::endl; + } + }); ``` This function is particularly useful for extracting data from complex JSON structures with nested arrays and objects. By leveraging wildcards, you can simplify your queries and reduce the need for multiple iterations. diff --git a/include/simdjson/generic/ondemand/array-inl.h b/include/simdjson/generic/ondemand/array-inl.h index 2e962cb1e..fe27b21a1 100644 --- a/include/simdjson/generic/ondemand/array-inl.h +++ b/include/simdjson/generic/ondemand/array-inl.h @@ -170,46 +170,34 @@ inline simdjson_result array::at_path(std::string_view json_path) noexcep return at_pointer(json_pointer); } -inline simdjson_result> array::at_path_with_wildcard(std::string_view json_path) noexcept { - std::vector result; - +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +inline error_code array::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { auto result_pair = get_next_key_and_json_path(json_path); std::string_view key = result_pair.first; std::string_view remaining_path = result_pair.second; // Wildcard case - if(key=="*"){ - for(auto element: *this){ - - if(element.error()){ - return element.error(); - } - - if(remaining_path.empty()){ - // Use value_unsafe() because we've already checked for errors above. - // The 'element' is a simdjson_result wrapper, and we need to extract - // the underlying value. value_unsafe() is safe here because error() returned false. - result.push_back(std::move(element).value_unsafe()); - - }else{ - auto nested_result = element.at_path_with_wildcard(remaining_path); - - if(nested_result.error()){ - return nested_result.error(); - } - // Same logic as above. - std::vector nested_matches = std::move(nested_result).value_unsafe(); - - result.insert(result.end(), - std::make_move_iterator(nested_matches.begin()), - std::make_move_iterator(nested_matches.end())); + if (key=="*"){ + for(auto element: *this) { + value val; + SIMDJSON_TRY(element.get(val)); + if (remaining_path.empty()) { + callback(val); + } else { + error_code err = element.for_each_at_path_with_wildcard(remaining_path, callback); + if(err) { return err; } } } - return result; - }else{ + return SUCCESS; + } else { // Specific index case in which we access the element at the given index - size_t idx=0; + size_t idx = 0; - for(char c:key){ + for (char c : key) { if(c < '0' || c > '9'){ return INVALID_JSON_POINTER; } @@ -217,16 +205,13 @@ inline simdjson_result> array::at_path_with_wildcard(std::str } auto element = at(idx); - - if(element.error()){ - return element.error(); - } - - if(remaining_path.empty()){ - result.push_back(std::move(element).value_unsafe()); - return result; - }else{ - return element.at_path_with_wildcard(remaining_path); + value val; + SIMDJSON_TRY(element.get(val)); + if (remaining_path.empty()){ + callback(val); + return SUCCESS; + } else { + return element.for_each_at_path_with_wildcard(remaining_path, callback); } } } @@ -289,9 +274,15 @@ simdjson_inline simdjson_result simdj if (error()) { return error(); } return first.at_path(json_path); } -simdjson_inline simdjson_result> simdjson_result::at_path_with_wildcard(std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code simdjson_result::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { if (error()) { return error(); } - return first.at_path_with_wildcard(json_path); + return first.for_each_at_path_with_wildcard(json_path, std::forward(callback)); } simdjson_inline simdjson_result simdjson_result::raw_json() noexcept { if (error()) { return error(); } diff --git a/include/simdjson/generic/ondemand/array.h b/include/simdjson/generic/ondemand/array.h index 7abd5fafc..303146bcf 100644 --- a/include/simdjson/generic/ondemand/array.h +++ b/include/simdjson/generic/ondemand/array.h @@ -119,13 +119,21 @@ public: inline simdjson_result at_path(std::string_view json_path) noexcept; /** - * Get all values matching the given JSONPath expression with wildcard support. + * Call the provided callback for each value matching the given JSONPath + * expression with wildcard support. * Supports wildcard patterns like "[*]" to match all array elements. * * @param json_path JSONPath expression with wildcards - * @return Vector of values matching the wildcard pattern + * @param callback Function called for each matching value + * @return error_code indicating success or failure */ - inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; /** * Consumes the array and returns a string_view instance corresponding to the @@ -249,7 +257,13 @@ public: simdjson_inline simdjson_result at(size_t index) noexcept; simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; simdjson_inline simdjson_result raw_json() noexcept; #if SIMDJSON_SUPPORTS_CONCEPTS // TODO: move this code into object-inl.h diff --git a/include/simdjson/generic/ondemand/document-inl.h b/include/simdjson/generic/ondemand/document-inl.h index f4627564a..a165b3f89 100644 --- a/include/simdjson/generic/ondemand/document-inl.h +++ b/include/simdjson/generic/ondemand/document-inl.h @@ -365,8 +365,14 @@ simdjson_inline simdjson_result document::at_path(std::string_view json_p } } -simdjson_inline simdjson_result> document::at_path_with_wildcard(std::string_view json_path) noexcept { - rewind(); // Rewind the document each time at_path_with_wildcard is called +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code document::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { + rewind(); // Rewind the document each time for_each_at_path_with_wildcard is called if (json_path.empty()) { return INVALID_JSON_POINTER; } @@ -374,9 +380,9 @@ simdjson_inline simdjson_result> document::at_path_with_wildc SIMDJSON_TRY(type().get(t)); switch (t) { case json_type::array: - return (*this).get_array().at_path_with_wildcard(json_path); + return (*this).get_array().for_each_at_path_with_wildcard(json_path, std::forward(callback)); case json_type::object: - return (*this).get_object().at_path_with_wildcard(json_path); + return (*this).get_object().for_each_at_path_with_wildcard(json_path, std::forward(callback)); default: return INVALID_JSON_POINTER; } @@ -713,9 +719,15 @@ simdjson_inline simdjson_result simdjs return first.at_path(json_path); } -simdjson_inline simdjson_result> simdjson_result::at_path_with_wildcard(std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code simdjson_result::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { if (error()) { return error(); } - return first.at_path_with_wildcard(json_path); + return first.for_each_at_path_with_wildcard(json_path, std::forward(callback)); } #if SIMDJSON_STATIC_REFLECTION @@ -826,7 +838,13 @@ simdjson_inline simdjson_result document_reference::get_number() noexcep simdjson_inline simdjson_result document_reference::raw_json_token() noexcept { return doc->raw_json_token(); } simdjson_inline simdjson_result document_reference::at_pointer(std::string_view json_pointer) noexcept { return doc->at_pointer(json_pointer); } simdjson_inline simdjson_result document_reference::at_path(std::string_view json_path) noexcept { return doc->at_path(json_path); } -simdjson_inline simdjson_result> document_reference::at_path_with_wildcard(std::string_view json_path) noexcept { return doc->at_path_with_wildcard(json_path); } +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code document_reference::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { return doc->for_each_at_path_with_wildcard(json_path, std::forward(callback)); } simdjson_inline simdjson_result document_reference::raw_json() noexcept { return doc->raw_json();} simdjson_inline document_reference::operator document&() const noexcept { return *doc; } #if SIMDJSON_SUPPORTS_CONCEPTS && SIMDJSON_STATIC_REFLECTION @@ -1091,11 +1109,17 @@ simdjson_inline simdjson_result simdjs } return first.at_path(json_path); } -simdjson_inline simdjson_result> simdjson_result::at_path_with_wildcard(std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code simdjson_result::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { if (error()) { return error(); } - return first.at_path_with_wildcard(json_path); + return first.for_each_at_path_with_wildcard(json_path, std::forward(callback)); } #if SIMDJSON_STATIC_REFLECTION template diff --git a/include/simdjson/generic/ondemand/document.h b/include/simdjson/generic/ondemand/document.h index 65f28d1c6..d81488494 100644 --- a/include/simdjson/generic/ondemand/document.h +++ b/include/simdjson/generic/ondemand/document.h @@ -744,21 +744,24 @@ public: simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; /** - * Get all values matching the given JSONPath expression with wildcard support. + * Call the provided callback for each value matching the given JSONPath + * expression with wildcard support. * * Supports wildcard patterns like "$.array[*]" or "$.object.*" to match multiple elements. * - * This method materializes all matching values into a vector. * The document will be consumed after this call. * * @param json_path JSONPath expression with wildcards - * @return Vector of values matching the wildcard pattern, or: - * - INVALID_JSON_POINTER if the JSONPath cannot be parsed - * - NO_SUCH_FIELD if a field does not exist - * - INDEX_OUT_OF_BOUNDS if an array index is out of bounds - * - INCORRECT_TYPE if path traversal encounters wrong type + * @param callback Function called for each matching value + * @return error_code indicating success or failure */ - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; /** * Consumes the document and returns a string_view instance corresponding to the @@ -979,7 +982,13 @@ public: simdjson_inline simdjson_result raw_json_token() noexcept; simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; private: document *doc{nullptr}; @@ -1065,7 +1074,13 @@ public: simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; #if SIMDJSON_STATIC_REFLECTION template requires(std::is_class_v && (sizeof...(FieldNames) > 0)) @@ -1150,7 +1165,13 @@ public: simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; #if SIMDJSON_STATIC_REFLECTION template requires(std::is_class_v && (sizeof...(FieldNames) > 0)) diff --git a/include/simdjson/generic/ondemand/object-inl.h b/include/simdjson/generic/ondemand/object-inl.h index a7ad3e2e8..7b33e9b0e 100644 --- a/include/simdjson/generic/ondemand/object-inl.h +++ b/include/simdjson/generic/ondemand/object-inl.h @@ -177,9 +177,13 @@ inline simdjson_result object::at_path(std::string_view json_path) noexce return at_pointer(json_pointer); } -inline simdjson_result> object::at_path_with_wildcard(std::string_view json_path) noexcept { - std::vector result; - +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +inline error_code object::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { auto result_pair = get_next_key_and_json_path(json_path); std::string_view key = result_pair.first; std::string_view remaining_path = result_pair.second; @@ -189,34 +193,22 @@ inline simdjson_result> object::at_path_with_wildcard(std::st for (auto field : *this) { value val; SIMDJSON_TRY(field.value().get(val)); - if (remaining_path.empty()) { - result.push_back(std::move(val)); + callback(val); } else { - auto nested_result = val.at_path_with_wildcard(remaining_path); - - if (nested_result.error()) { - return nested_result.error(); - } - // Extract and append all nested matches to our result - std::vector nested_vec; - SIMDJSON_TRY(std::move(nested_result).get(nested_vec)); - - result.insert(result.end(), - std::make_move_iterator(nested_vec.begin()), - std::make_move_iterator(nested_vec.end())); + SIMDJSON_TRY(val.for_each_at_path_with_wildcard(remaining_path, callback)); } } - return result; + return SUCCESS; } else { value val; SIMDJSON_TRY(find_field(key).get(val)); if (remaining_path.empty()) { - result.push_back(std::move(val)); - return result; + callback(val); + return SUCCESS; } else { - return val.at_path_with_wildcard(remaining_path); + return val.for_each_at_path_with_wildcard(remaining_path, callback); } } } @@ -347,9 +339,15 @@ simdjson_inline simdjson_result simdjs return first.at_path(json_path); } -simdjson_inline simdjson_result> simdjson_result::at_path_with_wildcard(std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +simdjson_inline error_code simdjson_result::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { if (error()) { return error(); } - return first.at_path_with_wildcard(json_path); + return first.for_each_at_path_with_wildcard(json_path, std::forward(callback)); } inline simdjson_result simdjson_result::reset() noexcept { diff --git a/include/simdjson/generic/ondemand/object.h b/include/simdjson/generic/ondemand/object.h index c2aea7f9c..429894eeb 100644 --- a/include/simdjson/generic/ondemand/object.h +++ b/include/simdjson/generic/ondemand/object.h @@ -172,13 +172,21 @@ public: inline simdjson_result at_path(std::string_view json_path) noexcept; /** - * Get all values matching the given JSONPath expression with wildcard support. + * Call the provided callback for each value matching the given JSONPath + * expression with wildcard support. * Supports wildcard patterns like ".*" to match all object fields. * * @param json_path JSONPath expression with wildcards - * @return Vector of values matching the wildcard pattern + * @param callback Function called for each matching value + * @return error_code indicating success or failure */ - inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; /** * Reset the iterator so that we are pointing back at the @@ -328,7 +336,13 @@ public: simdjson_inline simdjson_result operator[](std::string_view key) && noexcept; simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; inline simdjson_result reset() noexcept; inline simdjson_result is_empty() noexcept; inline simdjson_result count_fields() & noexcept; diff --git a/include/simdjson/generic/ondemand/value-inl.h b/include/simdjson/generic/ondemand/value-inl.h index c458fba8b..28a41f95f 100644 --- a/include/simdjson/generic/ondemand/value-inl.h +++ b/include/simdjson/generic/ondemand/value-inl.h @@ -314,14 +314,20 @@ simdjson_inline simdjson_result value::at_path(std::string_view json_path } } -inline simdjson_result> value::at_path_with_wildcard(std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +inline error_code value::for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept { json_type t; SIMDJSON_TRY(type().get(t)); switch (t) { case json_type::array: - return (*this).get_array().at_path_with_wildcard(json_path); + return (*this).get_array().for_each_at_path_with_wildcard(json_path, std::forward(callback)); case json_type::object: - return (*this).get_object().at_path_with_wildcard(json_path); + return (*this).get_object().for_each_at_path_with_wildcard(json_path, std::forward(callback)); default: return INVALID_JSON_POINTER; } @@ -585,12 +591,18 @@ simdjson_inline simdjson_result simdjs return first.at_path(json_path); } -inline simdjson_result> simdjson_result::at_path_with_wildcard( - std::string_view json_path) noexcept { +#if SIMDJSON_SUPPORTS_CONCEPTS +template + requires std::invocable +#else +template +#endif +inline error_code simdjson_result::for_each_at_path_with_wildcard( + std::string_view json_path, Func&& callback) noexcept { if (error()) { return error(); } - return first.at_path_with_wildcard(json_path); + return first.for_each_at_path_with_wildcard(json_path, std::forward(callback)); } } // namespace simdjson diff --git a/include/simdjson/generic/ondemand/value.h b/include/simdjson/generic/ondemand/value.h index d3fa38e84..a80f8050d 100644 --- a/include/simdjson/generic/ondemand/value.h +++ b/include/simdjson/generic/ondemand/value.h @@ -698,13 +698,21 @@ public: simdjson_inline simdjson_result at_path(std::string_view at_path) noexcept; /** - * Get all values matching the given JSONPath expression with wildcard support. + * Call the provided callback for each value matching the given JSONPath + * expression with wildcard support. * Supports wildcard character (*) for arrays or ".*" for objects. * * @param json_path JSONPath expression with wildcards - * @return Vector of values matching the wildcard pattern + * @param callback Function called for each matching value + * @return error_code indicating success or failure */ - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; protected: /** @@ -897,9 +905,23 @@ public: simdjson_inline simdjson_result current_depth() const noexcept; simdjson_inline simdjson_result at_pointer(std::string_view json_pointer) noexcept; simdjson_inline simdjson_result at_path(std::string_view json_path) noexcept; - simdjson_inline simdjson_result> at_path_with_wildcard(std::string_view json_path) noexcept; +#if SIMDJSON_SUPPORTS_CONCEPTS + template + requires std::invocable +#else + template +#endif + simdjson_inline error_code for_each_at_path_with_wildcard(std::string_view json_path, Func&& callback) noexcept; }; +// Forward-declare explicit specializations so MSVC /permissive- sees them before +// any template instantiation that would resolve element.get(val) to the primary. +template<> simdjson_inline error_code +simdjson_result::get( + SIMDJSON_IMPLEMENTATION::ondemand::value &out) noexcept; +template<> simdjson_inline simdjson_result +simdjson_result::get() noexcept; + } // namespace simdjson #endif // SIMDJSON_GENERIC_ONDEMAND_VALUE_H diff --git a/tests/ondemand/ondemand_readme_examples.cpp b/tests/ondemand/ondemand_readme_examples.cpp index 5a0948334..fed10ee10 100644 --- a/tests/ondemand/ondemand_readme_examples.cpp +++ b/tests/ondemand/ondemand_readme_examples.cpp @@ -205,6 +205,68 @@ simdjson_inline simdjson_result simdjson::ondemand::document::get() & noexc #if SIMDJSON_EXCEPTIONS +// Test matching the doc example in doc/basics.md +// "Using for_each_at_path_with_wildcard for JSONPath Queries (On-Demand)" +bool wildcard_basics_example() { + TEST_START(); + simdjson::padded_string json_string = R"( +{ + "firstName": "John", + "lastName": "doe", + "age": 26, + "address": { + "streetAddress": "naist street", + "city": "Nara", + "postalCode": "630-0192" + }, + "phoneNumbers": [ + { + "type": "iPhone", + "numbers": ["0123-4567-8888", "0123-4567-8788"] + }, + { + "type": "home", + "numbers": ["0123-4567-8910"] + } + ] +})"_padded; + + ondemand::parser parser; + ondemand::document doc = parser.iterate(json_string); + + // Fetch all fields in the address object + std::vector fields; + auto error = doc.for_each_at_path_with_wildcard("$.address.*", + [&](ondemand::value value) { + std::string_view field; + if (value.get(field) == SUCCESS) { + fields.push_back(field); + } + }); + ASSERT_SUCCESS(error); + ASSERT_EQUAL(fields.size(), 3); + ASSERT_EQUAL(fields[0], "naist street"); + ASSERT_EQUAL(fields[1], "Nara"); + ASSERT_EQUAL(fields[2], "630-0192"); + + // Fetch all phone numbers + std::vector numbers; + error = doc.for_each_at_path_with_wildcard("$.phoneNumbers[*].numbers[*]", + [&](ondemand::value value) { + std::string_view number; + if (value.get(number) == SUCCESS) { + numbers.push_back(number); + } + }); + ASSERT_SUCCESS(error); + ASSERT_EQUAL(numbers.size(), 3); + ASSERT_EQUAL(numbers[0], "0123-4567-8888"); + ASSERT_EQUAL(numbers[1], "0123-4567-8788"); + ASSERT_EQUAL(numbers[2], "0123-4567-8910"); + + TEST_SUCCEED(); +} + void main_capture() { padded_string json_padded = "{\"a\":[1,2,3], \"b\": 2, \"c\": \"hello\"}"_padded; std::vector fields; @@ -2041,6 +2103,7 @@ bool value_raw_json_object() { } #endif + bool run() { return true && fatal_error() @@ -2100,6 +2163,7 @@ bool run() { && current_location_no_error() && to_string_example_no_except() #if SIMDJSON_EXCEPTIONS + && wildcard_basics_example() && issue2215() && to_string_example() && raw_string() diff --git a/tests/ondemand/ondemand_wildcard_tests.cpp b/tests/ondemand/ondemand_wildcard_tests.cpp index 6189e1995..09a733de7 100644 --- a/tests/ondemand/ondemand_wildcard_tests.cpp +++ b/tests/ondemand/ondemand_wildcard_tests.cpp @@ -21,18 +21,16 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector titles; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$.books[*].title").get(titles)); + std::vector titles; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$.books[*].title", [&](ondemand::value v) { + std::string_view sv; + if (v.get_string().get(sv) == SUCCESS) { titles.push_back(sv); } + })); ASSERT_EQUAL(titles.size(), 3); - - std::string_view title; - ASSERT_SUCCESS(titles[0].get_string().get(title)); - ASSERT_EQUAL(title, "Book A"); - ASSERT_SUCCESS(titles[1].get_string().get(title)); - ASSERT_EQUAL(title, "Book B"); - ASSERT_SUCCESS(titles[2].get_string().get(title)); - ASSERT_EQUAL(title, "Book C"); + ASSERT_EQUAL(titles[0], "Book A"); + ASSERT_EQUAL(titles[1], "Book B"); + ASSERT_EQUAL(titles[2], "Book C"); TEST_SUCCEED(); } @@ -46,18 +44,16 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector prices; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$.prices[*]").get(prices)); + std::vector prices; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$.prices[*]", [&](ondemand::value v) { + uint64_t p; + if (v.get_uint64().get(p) == SUCCESS) { prices.push_back(p); } + })); ASSERT_EQUAL(prices.size(), 5); - - uint64_t price; - ASSERT_SUCCESS(prices[0].get_uint64().get(price)); - ASSERT_EQUAL(price, 10); - ASSERT_SUCCESS(prices[2].get_uint64().get(price)); - ASSERT_EQUAL(price, 30); - ASSERT_SUCCESS(prices[4].get_uint64().get(price)); - ASSERT_EQUAL(price, 50); + ASSERT_EQUAL(prices[0], 10); + ASSERT_EQUAL(prices[2], 30); + ASSERT_EQUAL(prices[4], 50); TEST_SUCCEED(); } @@ -75,19 +71,14 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector names; - auto error = doc.at_path_with_wildcard("$.users.*.name").get(names); + std::set name_set; + auto error = doc.for_each_at_path_with_wildcard("$.users.*.name", [&](ondemand::value v) { + std::string_view sv; + if (v.get_string().get(sv) == SUCCESS) { name_set.insert(sv); } + }); ASSERT_SUCCESS(error); - ASSERT_EQUAL(names.size(), 3); - - // Verify we got all three names (order may vary) - std::set name_set; - for (auto& name : names) { - std::string_view view; - ASSERT_SUCCESS(name.get_string().get(view)); - name_set.insert(view); - } + ASSERT_EQUAL(name_set.size(), 3); ASSERT_TRUE(name_set.count("Alice") > 0); ASSERT_TRUE(name_set.count("Bob") > 0); ASSERT_TRUE(name_set.count("Charlie") > 0); @@ -117,19 +108,15 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector salaries; - auto error = doc.at_path_with_wildcard("$.departments.*.employees[*].salary").get(salaries); + uint64_t total = 0; + size_t count = 0; + auto error = doc.for_each_at_path_with_wildcard("$.departments.*.employees[*].salary", [&](ondemand::value v) { + uint64_t s; + if (v.get_uint64().get(s) == SUCCESS) { total += s; count++; } + }); ASSERT_SUCCESS(error); - ASSERT_EQUAL(salaries.size(), 4); - - // Check sum of all salaries - uint64_t total = 0; - for (auto& salary : salaries) { - uint64_t value; - ASSERT_SUCCESS(salary.get_uint64().get(value)); - total += value; - } + ASSERT_EQUAL(count, 4); ASSERT_EQUAL(total, 360000); TEST_SUCCEED(); @@ -142,11 +129,12 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector items; - auto error = doc.at_path_with_wildcard("$.items[*].name").get(items); + size_t count = 0; + auto error = doc.for_each_at_path_with_wildcard("$.items[*].name", [&](ondemand::value) { + count++; + }); ASSERT_SUCCESS(error); - - ASSERT_EQUAL(items.size(), 0); + ASSERT_EQUAL(count, 0); TEST_SUCCEED(); } @@ -158,11 +146,12 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector names; - auto error = doc.at_path_with_wildcard("$.users.*.name").get(names); + size_t count = 0; + auto error = doc.for_each_at_path_with_wildcard("$.users.*.name", [&](ondemand::value) { + count++; + }); ASSERT_SUCCESS(error); - - ASSERT_EQUAL(names.size(), 0); + ASSERT_EQUAL(count, 0); TEST_SUCCEED(); } @@ -175,8 +164,7 @@ namespace wildcard_tests { auto doc = parser.iterate(json); // Can't use array wildcard on scalar - std::vector values; - auto error = doc.at_path_with_wildcard("$.data[*]").get(values); + auto error = doc.for_each_at_path_with_wildcard("$.data[*]", [](ondemand::value) {}); ASSERT_ERROR(error, INVALID_JSON_POINTER); TEST_SUCCEED(); @@ -195,12 +183,12 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - // Get all nested arrays - std::vector arrays; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$.matrix[*]").get(arrays)); + size_t count = 0; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$.matrix[*]", [&](ondemand::value) { + count++; + })); - // Verify we got 3 arrays - ASSERT_EQUAL(arrays.size(), 3); + ASSERT_EQUAL(count, 3); TEST_SUCCEED(); } @@ -218,18 +206,16 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector statuses; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$.data[*].info.status").get(statuses)); + std::vector statuses; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$.data[*].info.status", [&](ondemand::value v) { + std::string_view sv; + if (v.get_string().get(sv) == SUCCESS) { statuses.push_back(sv); } + })); ASSERT_EQUAL(statuses.size(), 3); - - std::string_view status; - ASSERT_SUCCESS(statuses[0].get_string().get(status)); - ASSERT_EQUAL(status, "active"); - ASSERT_SUCCESS(statuses[1].get_string().get(status)); - ASSERT_EQUAL(status, "inactive"); - ASSERT_SUCCESS(statuses[2].get_string().get(status)); - ASSERT_EQUAL(status, "active"); + ASSERT_EQUAL(statuses[0], "active"); + ASSERT_EQUAL(statuses[1], "inactive"); + ASSERT_EQUAL(statuses[2], "active"); TEST_SUCCEED(); } @@ -250,13 +236,12 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector values; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$.mixed[*]").get(values)); + size_t count = 0; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$.mixed[*]", [&](ondemand::value) { + count++; + })); - // Just verify we got all 6 elements - ASSERT_EQUAL(values.size(), 6); - - // Don't try to access the values - they've been consumed by the OnDemand API + ASSERT_EQUAL(count, 6); TEST_SUCCEED(); } @@ -275,8 +260,11 @@ namespace wildcard_tests { auto doc = parser.iterate(json); // Third element doesn't have "name" field - std::vector names; - auto error = doc.at_path_with_wildcard("$.data[*].name").get(names); + std::vector names; + auto error = doc.for_each_at_path_with_wildcard("$.data[*].name", [&](ondemand::value v) { + std::string_view sv; + if (v.get_string().get(sv) == SUCCESS) { names.push_back(sv); } + }); // This might error or return partial results if (error) { @@ -301,18 +289,47 @@ namespace wildcard_tests { ondemand::parser parser; auto doc = parser.iterate(json); - std::vector names; - ASSERT_SUCCESS(doc.at_path_with_wildcard("$[*].name").get(names)); + std::vector names; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$[*].name", [&](ondemand::value v) { + std::string_view sv; + if (v.get_string().get(sv) == SUCCESS) { names.push_back(sv); } + })); ASSERT_EQUAL(names.size(), 3); + ASSERT_EQUAL(names[0], "Item 1"); + ASSERT_EQUAL(names[1], "Item 2"); + ASSERT_EQUAL(names[2], "Item 3"); - std::string_view name; - ASSERT_SUCCESS(names[0].get_string().get(name)); - ASSERT_EQUAL(name, "Item 1"); - ASSERT_SUCCESS(names[1].get_string().get(name)); - ASSERT_EQUAL(name, "Item 2"); - ASSERT_SUCCESS(names[2].get_string().get(name)); - ASSERT_EQUAL(name, "Item 3"); + TEST_SUCCEED(); + } + + // https://github.com/simdjson/simdjson/issues/2684 + bool wildcard_raw_json_issue_2684() { + TEST_START(); + auto json = R"([{"tag_meta":{"meta_code":2000211,"meta_value":""},"tag_value":""}])"_padded; + + ondemand::parser parser; + auto doc = parser.iterate(json); + + size_t count = 0; + bool raw_json_ok = true; + ASSERT_SUCCESS(doc.for_each_at_path_with_wildcard("$[*].tag_meta", [&](ondemand::value v) { + count++; + std::string_view raw; + if (v.raw_json().get(raw) != SUCCESS) { + raw_json_ok = false; + return; + } + // raw_json() must not leak sibling fields or outer array delimiters + std::string_view expected = R"({"meta_code":2000211,"meta_value":""})"; + if (raw != expected || raw.find("tag_value") != std::string_view::npos) { + std::cerr << " raw_json() returned: " << raw << std::endl; + raw_json_ok = false; + } + })); + + ASSERT_EQUAL(count, 1); + ASSERT_TRUE(raw_json_ok); TEST_SUCCEED(); } @@ -329,10 +346,11 @@ namespace wildcard_tests { wildcard_with_nested_objects() && mixed_types_in_array() && wildcard_nonexistent_field() && - root_array_wildcard(); + root_array_wildcard() && + wildcard_raw_json_issue_2684(); } } int main(int argc, char *argv[]) { return test_main(argc, argv, wildcard_tests::run); -} \ No newline at end of file +}