From 22fc278f877bb00773d1916e25321a59acc3c040 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 09:31:13 -0300 Subject: [PATCH 01/13] [WIP] Prototype a simple JSON Path implementation Signed-off-by: Juan Cruz Viotti --- .github/workflows/website-build.yml | 1 + .github/workflows/website-deploy.yml | 1 + CMakeLists.txt | 9 + config.cmake.in | 14 + src/core/jsonpath/CMakeLists.txt | 18 + src/core/jsonpath/evaluator.h | 635 ++++++++++ src/core/jsonpath/grammar.h | 143 +++ .../include/sourcemeta/core/jsonpath.h | 139 +++ .../include/sourcemeta/core/jsonpath_error.h | 43 + src/core/jsonpath/jsonpath.cc | 96 ++ src/core/jsonpath/parser.h | 1065 +++++++++++++++++ test/jsonpath/CMakeLists.txt | 30 + test/jsonpath/jsonpath_evaluate_test.cc | 216 ++++ test/jsonpath/jsonpath_filter_test.cc | 282 +++++ test/jsonpath/jsonpath_normalize_test.cc | 137 +++ test/jsonpath/jsonpath_parse_test.cc | 270 +++++ test/jsonpath/jsonpath_suite.cc | 117 ++ test/packaging/find_package/CMakeLists.txt | 1 + test/packaging/find_package/hello.cc | 1 + 19 files changed, 3218 insertions(+) create mode 100644 src/core/jsonpath/CMakeLists.txt create mode 100644 src/core/jsonpath/evaluator.h create mode 100644 src/core/jsonpath/grammar.h create mode 100644 src/core/jsonpath/include/sourcemeta/core/jsonpath.h create mode 100644 src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h create mode 100644 src/core/jsonpath/jsonpath.cc create mode 100644 src/core/jsonpath/parser.h create mode 100644 test/jsonpath/CMakeLists.txt create mode 100644 test/jsonpath/jsonpath_evaluate_test.cc create mode 100644 test/jsonpath/jsonpath_filter_test.cc create mode 100644 test/jsonpath/jsonpath_normalize_test.cc create mode 100644 test/jsonpath/jsonpath_parse_test.cc create mode 100644 test/jsonpath/jsonpath_suite.cc diff --git a/.github/workflows/website-build.yml b/.github/workflows/website-build.yml index c70fe5c916..9701b0a948 100644 --- a/.github/workflows/website-build.yml +++ b/.github/workflows/website-build.yml @@ -43,6 +43,7 @@ jobs: -DSOURCEMETA_CORE_JSON:BOOL=OFF -DSOURCEMETA_CORE_JSONL:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF + -DSOURCEMETA_CORE_JSONPATH:BOOL=OFF -DSOURCEMETA_CORE_JSONLD:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF diff --git a/.github/workflows/website-deploy.yml b/.github/workflows/website-deploy.yml index d4cf68a754..4aa5acdf31 100644 --- a/.github/workflows/website-deploy.yml +++ b/.github/workflows/website-deploy.yml @@ -53,6 +53,7 @@ jobs: -DSOURCEMETA_CORE_JSON:BOOL=OFF -DSOURCEMETA_CORE_JSONL:BOOL=OFF -DSOURCEMETA_CORE_JSONPOINTER:BOOL=OFF + -DSOURCEMETA_CORE_JSONPATH:BOOL=OFF -DSOURCEMETA_CORE_JSONLD:BOOL=OFF -DSOURCEMETA_CORE_YAML:BOOL=OFF -DSOURCEMETA_CORE_JSONRPC:BOOL=OFF diff --git a/CMakeLists.txt b/CMakeLists.txt index 928273ed82..72e2641097 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,7 @@ option(SOURCEMETA_CORE_URI "Build the Sourcemeta Core URI library" ON) option(SOURCEMETA_CORE_URITEMPLATE "Build the Sourcemeta Core URI Template library" ON) option(SOURCEMETA_CORE_JSON "Build the Sourcemeta Core JSON library" ON) option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer library" ON) +option(SOURCEMETA_CORE_JSONPATH "Build the Sourcemeta Core JSON Path library" ON) option(SOURCEMETA_CORE_JSONLD "Build the Sourcemeta Core JSON-LD library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) @@ -183,6 +184,10 @@ if(SOURCEMETA_CORE_JSONPOINTER) add_subdirectory(src/core/jsonpointer) endif() +if(SOURCEMETA_CORE_JSONPATH) + add_subdirectory(src/core/jsonpath) +endif() + if(SOURCEMETA_CORE_JSONLD) add_subdirectory(src/core/jsonld) endif() @@ -348,6 +353,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/jsonpointer) endif() + if(SOURCEMETA_CORE_JSONPATH) + add_subdirectory(test/jsonpath) + endif() + if(SOURCEMETA_CORE_JSONLD) add_subdirectory(test/jsonld) endif() diff --git a/config.cmake.in b/config.cmake.in index 4bcd0ab16d..d2661ede65 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -24,6 +24,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS json) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonl) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) + list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpath) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonld) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonrpc) @@ -145,6 +146,19 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") + elseif(component STREQUAL "jsonpath") + find_dependency(PCRE2 CONFIG) + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_regex.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_ip.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_numeric.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_unicode.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_json.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_text.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpointer.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_jsonpath.cmake") elseif(component STREQUAL "jsonld") find_dependency(PCRE2 CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_preprocessor.cmake") diff --git a/src/core/jsonpath/CMakeLists.txt b/src/core/jsonpath/CMakeLists.txt new file mode 100644 index 0000000000..36860bdcf8 --- /dev/null +++ b/src/core/jsonpath/CMakeLists.txt @@ -0,0 +1,18 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonpath + PRIVATE_HEADERS error.h + SOURCES jsonpath.cc grammar.h parser.h evaluator.h) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jsonpath) +endif() + +target_link_libraries(sourcemeta_core_jsonpath PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jsonpath PUBLIC + sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_core_jsonpath PRIVATE + sourcemeta::core::regex) +target_link_libraries(sourcemeta_core_jsonpath PRIVATE + sourcemeta::core::text) +target_link_libraries(sourcemeta_core_jsonpath PRIVATE + sourcemeta::core::unicode) diff --git a/src/core/jsonpath/evaluator.h b/src/core/jsonpath/evaluator.h new file mode 100644 index 0000000000..fb1264f196 --- /dev/null +++ b/src/core/jsonpath/evaluator.h @@ -0,0 +1,635 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_EVALUATOR_H_ +#define SOURCEMETA_CORE_JSONPATH_EVALUATOR_H_ + +#include +#include +#include +#include +#include + +#include "grammar.h" + +#include // assert +#include // std::size_t +#include // std::int64_t +#include // std::cref +#include // std::optional +#include // std::get, std::holds_alternative, std::monostate, std::variant +#include // std::vector + +namespace sourcemeta::core { + +namespace { + +struct EvaluationState { + const JSON *root; + const JSONPath::Callback *callback; + WeakPointer location; +}; + +// The value of a comparable during evaluation: absent, borrowed from the +// document or the compiled query, or computed by a function +using FilterValue = std::variant; + +auto filter_value_pointer(const FilterValue &value) -> const JSON * { + switch (value.index()) { + case 0: + return nullptr; + case 1: + return *std::get_if(&value); + default: + return std::get_if(&value); + } +} + +auto filter_matches(const FilterExpression &expression, const JSON &candidate, + const JSON &root) -> bool; + +// RFC 9535 Section 2.3.4.2.2: slice expression bounds against a concrete +// array length, where an omitted start or end defaults according to the +// sign of the step and a zero step selects nothing +inline auto slice_bounds(const SelectorSlice &slice, const std::int64_t size, + std::int64_t &lower, std::int64_t &upper) -> bool { + if (slice.step == 0) { + return false; + } + + const auto start{slice.start.has_value() ? slice.start.value() + : (slice.step > 0 ? 0 : size - 1)}; + const auto end{slice.end.has_value() ? slice.end.value() + : (slice.step > 0 ? size : -size - 1)}; + const auto normalized_start{start >= 0 ? start : size + start}; + const auto normalized_end{end >= 0 ? end : size + end}; + if (slice.step > 0) { + lower = std::min(std::max(normalized_start, std::int64_t{0}), size); + upper = std::min(std::max(normalized_end, std::int64_t{0}), size); + } else { + upper = std::min(std::max(normalized_start, std::int64_t{-1}), size - 1); + lower = std::min(std::max(normalized_end, std::int64_t{-1}), size - 1); + } + + return true; +} + +// Walk the nodes selected by a filter query without tracking locations. The +// visitor returns whether to continue, and the traversal reports whether it +// ran to completion without the visitor stopping it +template +auto filter_query_visit(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + const JSON &root, const Visitor &visitor) -> bool { + if (cursor == segments.size()) { + return visitor(current); + } + + const auto &segment{segments[cursor]}; + for (const auto &selector : segment.selectors) { + switch (static_cast(selector.index())) { + case SelectorKind::Name: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr && !filter_query_visit(segments, cursor + 1, + *child, root, visitor)) { + return false; + } + } + + break; + } + case SelectorKind::Wildcard: + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (!filter_query_visit(segments, cursor + 1, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (!filter_query_visit(segments, cursor + 1, element, root, + visitor)) { + return false; + } + } + } + + break; + case SelectorKind::Index: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size && + !filter_query_visit(segments, cursor + 1, + current.at(static_cast(index)), + root, visitor)) { + return false; + } + } + + break; + } + case SelectorKind::Slice: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + if (!filter_query_visit( + segments, cursor + 1, + current.at(static_cast(index)), root, + visitor)) { + return false; + } + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + if (!filter_query_visit( + segments, cursor + 1, + current.at(static_cast(index)), root, + visitor)) { + return false; + } + } + } + } + + break; + } + case SelectorKind::Filter: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (filter_matches(entry->expression, member.second, root) && + !filter_query_visit(segments, cursor + 1, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (filter_matches(entry->expression, element, root) && + !filter_query_visit(segments, cursor + 1, element, root, + visitor)) { + return false; + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (!filter_query_visit(segments, cursor, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (!filter_query_visit(segments, cursor, element, root, visitor)) { + return false; + } + } + } + } + + return true; +} + +// RFC 9535 Section 2.3.5.1: a singular query "produces a nodelist containing +// at most one node", so it resolves directly without traversal +inline auto resolve_singular(const FilterQuery &query, const JSON &candidate, + const JSON &root) -> const JSON * { + const auto *current{query.relative ? &candidate : &root}; + for (const auto &segment : query.segments) { + assert(!segment.descendant); + assert(segment.selectors.size() == 1); + const auto &selector{segment.selectors.front()}; + if (std::holds_alternative(selector)) { + const auto *entry{std::get_if(&selector)}; + current = current->is_object() ? current->try_at(entry->name, entry->hash) + : nullptr; + } else { + const auto *entry{std::get_if(&selector)}; + if (current->is_array()) { + const auto size{static_cast(current->as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + current = index >= 0 && index < size + ? ¤t->at(static_cast(index)) + : nullptr; + } else { + current = nullptr; + } + } + + if (current == nullptr) { + return nullptr; + } + } + + return current; +} + +auto evaluate_value_function(const FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> FilterValue; + +inline auto evaluate_operand(const FilterOperand &operand, + const JSON &candidate, const JSON &root) + -> FilterValue { + if (std::holds_alternative(operand)) { + return FilterValue{&std::get(operand)}; + } + + if (std::holds_alternative(operand)) { + const auto *result{ + resolve_singular(std::get(operand), candidate, root)}; + return result == nullptr ? FilterValue{} : FilterValue{result}; + } + + return evaluate_value_function( + *std::get>(operand), candidate, root); +} + +inline auto evaluate_value_function(const FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> FilterValue { + switch (call.function) { + // RFC 9535 Section 2.4.4: the length function counts string characters, + // array elements, or object members, and is nothing for anything else + case FilterFunctionName::Length: { + const auto value{ + evaluate_operand(call.arguments.front(), candidate, root)}; + const auto *target{filter_value_pointer(value)}; + if (target == nullptr) { + return FilterValue{}; + } + + if (target->is_string()) { + return FilterValue{JSON{static_cast( + utf8_codepoint_count(target->to_string()))}}; + } + + if (target->is_array()) { + return FilterValue{ + JSON{static_cast(target->as_array().size())}}; + } + + if (target->is_object()) { + return FilterValue{ + JSON{static_cast(target->as_object().size())}}; + } + + return FilterValue{}; + } + // RFC 9535 Section 2.4.5: the count function yields the number of nodes + case FilterFunctionName::Count: { + const auto &query{std::get(call.arguments.front())}; + std::int64_t count{0}; + filter_query_visit(query.segments, 0, query.relative ? candidate : root, + root, [&count](const JSON &) -> bool { + count += 1; + return true; + }); + return FilterValue{JSON{count}}; + } + // RFC 9535 Section 2.4.8: the value function yields the value of a + // single node and nothing otherwise + case FilterFunctionName::Value: { + const auto &query{std::get(call.arguments.front())}; + const JSON *single{nullptr}; + std::size_t count{0}; + filter_query_visit(query.segments, 0, query.relative ? candidate : root, + root, [&single, &count](const JSON &node) -> bool { + single = &node; + count += 1; + return count < 2; + }); + return count == 1 ? FilterValue{single} : FilterValue{}; + } + default: + assert(false); + return FilterValue{}; + } +} + +// RFC 9535 Sections 2.4.6 and 2.4.7: the match function considers the whole +// input while the search function considers any substring, and any argument +// mismatch yields a false outcome rather than an error +inline auto evaluate_logical_function(const FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> bool { + const auto input{evaluate_operand(call.arguments.front(), candidate, root)}; + const auto *subject{filter_value_pointer(input)}; + if (subject == nullptr || !subject->is_string()) { + return false; + } + + if (std::holds_alternative(call.arguments.back()) && + std::get(call.arguments.back()).is_string()) { + return call.compiled.has_value() && + matches(call.compiled.value(), subject->to_string()); + } + + const auto pattern{evaluate_operand(call.arguments.back(), candidate, root)}; + const auto *expression{filter_value_pointer(pattern)}; + if (expression == nullptr || !expression->is_string()) { + return false; + } + + const auto compiled{to_regex(expression->to_string(), + call.function == FilterFunctionName::Match + ? RegexDialect::IRegexp + : RegexDialect::IRegexpSearch)}; + return compiled.has_value() && + matches(compiled.value(), subject->to_string()); +} + +// RFC 9535 Section 2.3.5.2.2: equality is deep JSON equality where numbers +// compare mathematically, and an absent value equals only an absent value +inline auto filter_equals(const JSON *left, const JSON *right) -> bool { + if (left == nullptr || right == nullptr) { + return left == nullptr && right == nullptr; + } + + return *left == *right; +} + +// RFC 9535 Section 2.3.5.2.2: ordering applies only when both sides are +// numbers or both sides are strings +inline auto filter_less(const JSON *left, const JSON *right) -> bool { + if (left == nullptr || right == nullptr) { + return false; + } + + const bool left_number{left->is_integer() || left->is_real()}; + const bool right_number{right->is_integer() || right->is_real()}; + if (left_number && right_number) { + if (left->is_integer() && right->is_integer()) { + return left->to_integer() < right->to_integer(); + } + + const auto left_value{left->is_integer() + ? static_cast(left->to_integer()) + : left->to_real()}; + const auto right_value{right->is_integer() + ? static_cast(right->to_integer()) + : right->to_real()}; + return left_value < right_value; + } + + if (left->is_string() && right->is_string()) { + return left->to_string() < right->to_string(); + } + + return false; +} + +inline auto filter_compare(const FilterComparison &comparison, + const JSON &candidate, const JSON &root) -> bool { + const auto left{evaluate_operand(comparison.left, candidate, root)}; + const auto right{evaluate_operand(comparison.right, candidate, root)}; + const auto *left_value{filter_value_pointer(left)}; + const auto *right_value{filter_value_pointer(right)}; + switch (comparison.operation) { + case FilterComparisonOperator::Equal: + return filter_equals(left_value, right_value); + case FilterComparisonOperator::NotEqual: + return !filter_equals(left_value, right_value); + case FilterComparisonOperator::Less: + return filter_less(left_value, right_value); + case FilterComparisonOperator::LessEqual: + return filter_less(left_value, right_value) || + filter_equals(left_value, right_value); + case FilterComparisonOperator::Greater: + return filter_less(right_value, left_value); + case FilterComparisonOperator::GreaterEqual: + return filter_less(right_value, left_value) || + filter_equals(left_value, right_value); + } + + assert(false); + return false; +} + +inline auto filter_matches(const FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool { + if (std::holds_alternative(expression.value)) { + return filter_compare(std::get(expression.value), + candidate, root); + } + + if (std::holds_alternative(expression.value)) { + const auto &test{std::get(expression.value)}; + bool result{false}; + if (std::holds_alternative(test.subject)) { + const auto &query{std::get(test.subject)}; + result = !filter_query_visit(query.segments, 0, + query.relative ? candidate : root, root, + [](const JSON &) -> bool { return false; }); + } else { + result = evaluate_logical_function( + *std::get>(test.subject), + candidate, root); + } + + return test.negated ? !result : result; + } + + if (std::holds_alternative(expression.value)) { + const auto &conjunction{std::get(expression.value)}; + for (const auto &child : conjunction.children) { + if (!filter_matches(child, candidate, root)) { + return false; + } + } + + return true; + } + + if (std::holds_alternative(expression.value)) { + const auto &disjunction{std::get(expression.value)}; + for (const auto &child : disjunction.children) { + if (filter_matches(child, candidate, root)) { + return true; + } + } + + return false; + } + + return !filter_matches(*std::get(expression.value).child, + candidate, root); +} + +auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void; + +inline auto evaluate_selectors(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + for (const auto &selector : segments[cursor].selectors) { + switch (static_cast(selector.index())) { + case SelectorKind::Name: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + state.location.emplace_back(std::cref(entry->name), entry->hash); + evaluate_segments(segments, cursor + 1, *child, state); + state.location.pop_back(); + } + } + + break; + } + case SelectorKind::Wildcard: + if (current.is_object()) { + for (const auto &member : current.as_object()) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_segments(segments, cursor + 1, member.second, state); + state.location.pop_back(); + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + state.location.emplace_back(index); + evaluate_segments(segments, cursor + 1, current.at(index), state); + state.location.pop_back(); + } + } + + break; + case SelectorKind::Index: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } + + break; + } + case SelectorKind::Slice: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } + } + + break; + } + case SelectorKind::Filter: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (filter_matches(entry->expression, member.second, *state.root)) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_segments(segments, cursor + 1, member.second, state); + state.location.pop_back(); + } + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + if (filter_matches(entry->expression, current.at(index), + *state.root)) { + state.location.emplace_back(index); + evaluate_segments(segments, cursor + 1, current.at(index), state); + state.location.pop_back(); + } + } + } + + break; + } + } + } +} + +// RFC 9535 Section 2.5.2.2: a descendant segment visits the input node and +// every descendant in depth-first document order, applying its selectors at +// each visited node +inline auto evaluate_descendant(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + evaluate_selectors(segments, cursor, current, state); + if (current.is_object()) { + for (const auto &member : current.as_object()) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_descendant(segments, cursor, member.second, state); + state.location.pop_back(); + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + state.location.emplace_back(index); + evaluate_descendant(segments, cursor, current.at(index), state); + state.location.pop_back(); + } + } +} + +inline auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + if (cursor == segments.size()) { + (*state.callback)(current, state.location); + return; + } + + if (segments[cursor].descendant) { + evaluate_descendant(segments, cursor, current, state); + } else { + evaluate_selectors(segments, cursor, current, state); + } +} + +} // namespace + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/jsonpath/grammar.h b/src/core/jsonpath/grammar.h new file mode 100644 index 0000000000..d8306cfa32 --- /dev/null +++ b/src/core/jsonpath/grammar.h @@ -0,0 +1,143 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ +#define SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ + +#include +#include + +#include // std::int64_t, std::uint8_t +#include // std::unique_ptr +#include // std::optional +#include // std::variant +#include // std::vector + +namespace sourcemeta::core { + +struct JSONPathSegment; +struct FilterFunctionCall; + +// RFC 9535 Section 2.3.1: "A name selector '' selects at most one +// object member value". The decoded member name is owned here rather than +// viewed from the query source, as string literal escapes decode at parse +// time and result locations need a durable string to reference. The hash is +// precomputed once so evaluation resolves members without hashing +struct SelectorName { + JSON::String name; + JSON::Object::hash_type hash; +}; + +struct SelectorWildcard {}; + +// RFC 9535 Section 2.3.3: "A non-negative index-selector applies to arrays +// only" and "a negative index-selector counts from the array end" +struct SelectorIndex { + std::int64_t index; +}; + +// RFC 9535 Section 2.3.4.2.2: the default start and end depend on the sign +// of the step, so their absence must be preserved +struct SelectorSlice { + std::optional start; + std::optional end; + std::int64_t step; +}; + +// RFC 9535 Section 2.3.5.1: a filter query embedded in a logical expression, +// evaluated relative to the candidate node or to the query root. Whether the +// query is singular is determined at parse time, as comparisons and value +// typed function arguments only admit singular queries +struct FilterQuery { + bool relative; + std::vector segments; + bool singular; +}; + +enum class FilterFunctionName : std::uint8_t { + Length, + Count, + Match, + Search, + Value +}; + +enum class FilterComparisonOperator : std::uint8_t { + Equal, + NotEqual, + Less, + LessEqual, + Greater, + GreaterEqual +}; + +// A comparable or function argument per RFC 9535 Sections 2.3.5.1 and 2.4.3: +// a literal, a query, or a nested function call +using FilterOperand = + std::variant>; + +struct FilterFunctionCall { + FilterFunctionName function; + std::vector arguments; + // A string literal pattern of the match and search functions compiles at + // parse time, and stays empty when the pattern is not a valid RFC 9485 + // expression, which evaluates as a false outcome rather than an error + std::optional compiled; +}; + +struct FilterComparison { + FilterOperand left; + FilterComparisonOperator operation; + FilterOperand right; +}; + +struct FilterTest { + bool negated; + std::variant> subject; +}; + +struct FilterExpression; + +struct FilterConjunction { + std::vector children; +}; + +struct FilterDisjunction { + std::vector children; +}; + +struct FilterNegation { + std::unique_ptr child; +}; + +struct FilterExpression { + std::variant + value; +}; + +struct SelectorFilter { + FilterExpression expression; +}; + +using Selector = std::variant; + +// For fast internal dispatching. It must stay in sync with the variant above +enum class SelectorKind : std::uint8_t { + Name = 0, + Wildcard, + Index, + Slice, + Filter +}; + +struct JSONPathSegment { + bool descendant; + std::vector selectors; +}; + +struct JSONPathQuery { + std::vector segments; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h new file mode 100644 index 0000000000..ddc35281bb --- /dev/null +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -0,0 +1,139 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_H_ +#define SOURCEMETA_CORE_JSONPATH_H_ + +#ifndef SOURCEMETA_CORE_JSONPATH_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include + +#include // std::function +#include // std::unique_ptr +#include // std::vector + +/// @defgroup jsonpath JSONPath +/// @brief A strict RFC 9535 JSONPath implementation. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif + +// The opaque compiled representation of a query +struct JSONPathQuery; + +/// @ingroup jsonpath +/// A parsed RFC 9535 JSONPath query that can be evaluated many times. +class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { +public: + /// A single query result, pairing a value with its location + struct Node { + /// The matched value within the queried document + const JSON *value; + /// The location of the matched value within the queried document + WeakPointer location; + }; + + /// The callback invoked for every query result node + using Callback = + std::function; + + /// Parse a query expression, throwing on invalid input. For example: + /// + /// ```cpp + /// #include + /// + /// const sourcemeta::core::JSONPath path{"$.store.book[0].title"}; + /// ``` + explicit JSONPath(const JSON::StringView expression); + + /// Evaluate the query against a document, invoking the callback once per + /// result node. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON document{ + /// sourcemeta::core::parse_json("{ \"foo\": [ 1, 2 ] }")}; + /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; + /// path.evaluate(document, [](const auto &value, const auto &location) { + /// assert(value.is_integer()); + /// assert(location.size() == 2); + /// }); + /// ``` + auto evaluate(const JSON &document, const Callback &callback) const -> void; + + /// Evaluate the query against a document, materializing the result + /// nodelist. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON document{ + /// sourcemeta::core::parse_json("{ \"foo\": [ 1, 2 ] }")}; + /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; + /// const auto nodes{path.evaluate(document)}; + /// assert(nodes.size() == 1); + /// assert(nodes.front().value->is_integer()); + /// ``` + [[nodiscard]] auto evaluate(const JSON &document) const -> std::vector; + + /// Serialize a location into the RFC 9535 normalized path form. For + /// example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON document{ + /// sourcemeta::core::parse_json("{ \"foo\": [ 1, 2 ] }")}; + /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; + /// const auto nodes{path.evaluate(document)}; + /// assert(sourcemeta::core::JSONPath::normalize( + /// nodes.front().location) == "$['foo'][0]"); + /// ``` + [[nodiscard]] static auto normalize(const WeakPointer &location) + -> JSON::String; + + /// Destruct a query + ~JSONPath(); + + /// Move construct a query + JSONPath(JSONPath &&other) noexcept; + + /// Move assign a query + auto operator=(JSONPath &&other) noexcept -> JSONPath &; + + // To avoid mistakes + JSONPath(const JSONPath &) = delete; + auto operator=(const JSONPath &) -> JSONPath & = delete; + +private: + std::unique_ptr internal_; +}; + +#if defined(_MSC_VER) +#pragma warning(pop) +#endif + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h new file mode 100644 index 0000000000..057fbd5a88 --- /dev/null +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h @@ -0,0 +1,43 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_ERROR_H_ +#define SOURCEMETA_CORE_JSONPATH_ERROR_H_ + +#ifndef SOURCEMETA_CORE_JSONPATH_EXPORT +#include +#endif + +#include // std::uint64_t +#include // std::exception + +namespace sourcemeta::core { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup jsonpath +/// An error that represents a JSONPath parsing failure +class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPathParseError + : public std::exception { +public: + /// Construct an error given the column number where parsing failed + JSONPathParseError(const std::uint64_t column) : column_{column} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return "The input is not a valid JSON Path query"; + } + + /// Get the column number of the error + [[nodiscard]] auto column() const noexcept -> std::uint64_t { + return this->column_; + } + +private: + std::uint64_t column_; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc new file mode 100644 index 0000000000..daaf3a3882 --- /dev/null +++ b/src/core/jsonpath/jsonpath.cc @@ -0,0 +1,96 @@ +#include + +#include "evaluator.h" +#include "grammar.h" +#include "parser.h" + +#include // std::make_unique +#include // std::to_string +#include // std::move +#include // std::vector + +namespace sourcemeta::core { + +JSONPath::JSONPath(const JSON::StringView expression) + : internal_{std::make_unique(parse_jsonpath(expression))} {} + +JSONPath::~JSONPath() = default; +JSONPath::JSONPath(JSONPath &&other) noexcept = default; +auto JSONPath::operator=(JSONPath &&other) noexcept -> JSONPath & = default; + +auto JSONPath::evaluate(const JSON &document, const Callback &callback) const + -> void { + EvaluationState state{ + .root = &document, .callback = &callback, .location = WeakPointer{}}; + evaluate_segments(this->internal_->segments, 0, document, state); +} + +auto JSONPath::evaluate(const JSON &document) const -> std::vector { + std::vector result; + this->evaluate( + document, + [&result](const JSON &value, const WeakPointer &location) -> void { + result.push_back({.value = &value, .location = location}); + }); + return result; +} + +auto JSONPath::normalize(const WeakPointer &location) -> JSON::String { + JSON::String result{"$"}; + for (const auto &token : location) { + if (token.is_property()) { + result += "['"; + for (const char character : token.to_property()) { + // RFC 9535 Section 2.7: "normal-escapable" covers the apostrophe, + // the backslash, and the control characters with a two character + // JSON escape, while "normal-hexchar" covers the remaining control + // characters in lowercase hexadecimal form + switch (character) { + case '\x08': + result += "\\b"; + break; + case '\x0C': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + case '\'': + result += "\\'"; + break; + case '\\': + result += "\\\\"; + break; + default: + if (static_cast(character) < 0x20) { + constexpr JSON::StringView hexadecimal{"0123456789abcdef"}; + result += "\\u00"; + result += + hexadecimal[(static_cast(character) >> 4) & + 0xF]; + result += + hexadecimal[static_cast(character) & 0xF]; + } else { + result += character; + } + } + } + + result += "']"; + } else { + result += '['; + result += std::to_string(token.to_index()); + result += ']'; + } + } + + return result; +} + +} // namespace sourcemeta::core diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h new file mode 100644 index 0000000000..53284dc886 --- /dev/null +++ b/src/core/jsonpath/parser.h @@ -0,0 +1,1065 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_PARSER_H_ +#define SOURCEMETA_CORE_JSONPATH_PARSER_H_ + +#include +#include +#include +#include +#include + +#include "grammar.h" + +#include // assert +#include // std::from_chars +#include // std::size_t +#include // std::int64_t, std::uint32_t +#include // std::strtod +#include // std::make_unique, std::unique_ptr +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::errc +#include // std::move +#include // std::get, std::holds_alternative + +namespace sourcemeta::core { + +namespace { + +// RFC 9535 Section 2.1: "an I-JSON number ... within the range of exact +// values" bounds every int production +constexpr std::int64_t JSONPATH_MAXIMUM_INTEGER{9007199254740991}; + +class JSONPathParser { +public: + JSONPathParser(const std::string_view input) : input_{input} {} + + auto parse() -> JSONPathQuery { + // jsonpath-query = root-identifier segments + if (this->at_end() || this->peek() != '$') { + this->fail(); + } + + this->position_ += 1; + JSONPathQuery result; + bool singular{true}; + result.segments = this->parse_segments(singular); + if (!this->at_end()) { + this->fail(); + } + + return result; + } + +private: + [[noreturn]] auto fail() const -> void { + throw JSONPathParseError{this->position_ + 1}; + } + + [[nodiscard]] auto at_end() const -> bool { + return this->position_ >= this->input_.size(); + } + + [[nodiscard]] auto peek() const -> char { + assert(!this->at_end()); + return this->input_[this->position_]; + } + + static auto is_digit(const char character) -> bool { + return character >= '0' && character <= '9'; + } + + static auto is_lower_alpha(const char character) -> bool { + return character >= 'a' && character <= 'z'; + } + + static auto is_alpha(const char character) -> bool { + return (character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z'); + } + + // S = *( %x20 / %x09 / %x0A / %x0D ) + auto skip_whitespace() -> bool { + bool consumed{false}; + while (!this->at_end()) { + const char character{this->peek()}; + if (character == ' ' || character == '\t' || character == '\n' || + character == '\r') { + this->position_ += 1; + consumed = true; + } else { + break; + } + } + + return consumed; + } + + // Copy one full UTF-8 encoded code point, rejecting malformed sequences + auto copy_character(JSON::String &output) -> void { + const auto lead{static_cast(this->peek())}; + if (lead < 0x80) { + output += static_cast(lead); + this->position_ += 1; + return; + } + + const auto size{utf8_lead_byte_size(lead)}; + if (size == 0 || this->position_ + size > this->input_.size()) { + this->fail(); + } + + for (std::size_t offset{1}; offset < size; ++offset) { + if (!is_utf8_continuation(static_cast( + this->input_[this->position_ + offset]))) { + this->fail(); + } + } + + output.append(this->input_.substr(this->position_, size)); + this->position_ += size; + } + + // segments = *(S segment) + auto parse_segments(bool &singular) -> std::vector { + std::vector segments; + while (true) { + const auto saved{this->position_}; + this->skip_whitespace(); + if (this->at_end() || (this->peek() != '.' && this->peek() != '[')) { + this->position_ = saved; + break; + } + + segments.push_back(this->parse_segment(singular)); + } + + return segments; + } + + // segment = child-segment / descendant-segment + auto parse_segment(bool &singular) -> JSONPathSegment { + JSONPathSegment segment; + segment.descendant = false; + if (this->peek() == '[') { + this->parse_bracketed_selection(segment, singular); + return segment; + } + + this->position_ += 1; + if (!this->at_end() && this->peek() == '.') { + // descendant-segment = ".." (bracketed-selection / wildcard-selector / + // member-name-shorthand) + this->position_ += 1; + segment.descendant = true; + singular = false; + if (this->at_end()) { + this->fail(); + } + + if (this->peek() == '[') { + this->parse_bracketed_selection(segment, singular); + } else if (this->peek() == '*') { + this->position_ += 1; + segment.selectors.emplace_back(SelectorWildcard{}); + } else { + segment.selectors.emplace_back(this->parse_shorthand_name()); + } + + return segment; + } + + // child-segment = bracketed-selection / ("." (wildcard-selector / + // member-name-shorthand)) + if (this->at_end()) { + this->fail(); + } + + if (this->peek() == '*') { + this->position_ += 1; + singular = false; + segment.selectors.emplace_back(SelectorWildcard{}); + } else { + segment.selectors.emplace_back(this->parse_shorthand_name()); + } + + return segment; + } + + // bracketed-selection = "[" S selector *(S "," S selector) S "]" + // Whitespace and multiple selectors disqualify the enclosing query from + // the stricter singular-query grammar of RFC 9535 Section 2.3.5.1 + auto parse_bracketed_selection(JSONPathSegment &segment, bool &singular) + -> void { + this->position_ += 1; + bool internal_whitespace{this->skip_whitespace()}; + segment.selectors.push_back(this->parse_selector(singular)); + while (true) { + internal_whitespace = this->skip_whitespace() || internal_whitespace; + if (this->at_end()) { + this->fail(); + } + + if (this->peek() == ',') { + this->position_ += 1; + this->skip_whitespace(); + segment.selectors.push_back(this->parse_selector(singular)); + } else if (this->peek() == ']') { + this->position_ += 1; + break; + } else { + this->fail(); + } + } + + if (segment.selectors.size() > 1 || internal_whitespace) { + singular = false; + } + } + + // selector = name-selector / wildcard-selector / slice-selector / + // index-selector / filter-selector + auto parse_selector(bool &singular) -> Selector { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == '"' || character == '\'') { + auto name{this->parse_string_literal()}; + const auto hash{JSON::Object::hash(name)}; + return SelectorName{.name = std::move(name), .hash = hash}; + } + + if (character == '*') { + this->position_ += 1; + singular = false; + return SelectorWildcard{}; + } + + if (character == '?') { + // filter-selector = "?" S logical-expr + this->position_ += 1; + singular = false; + this->skip_whitespace(); + return SelectorFilter{this->parse_logical_or()}; + } + + if (character == ':') { + singular = false; + return this->parse_slice(std::nullopt); + } + + if (character == '-' || is_digit(character)) { + const auto value{this->parse_integer()}; + // slice-selector = [start S] ":" S [end S] [":" [S step ]] + const auto saved{this->position_}; + this->skip_whitespace(); + if (!this->at_end() && this->peek() == ':') { + singular = false; + return this->parse_slice(value); + } + + this->position_ = saved; + return SelectorIndex{value}; + } + + this->fail(); + } + + auto parse_slice(const std::optional start) -> SelectorSlice { + SelectorSlice slice; + slice.start = start; + slice.step = 1; + assert(this->peek() == ':'); + this->position_ += 1; + this->skip_whitespace(); + if (!this->at_end() && (this->peek() == '-' || is_digit(this->peek()))) { + slice.end = this->parse_integer(); + this->skip_whitespace(); + } + + if (!this->at_end() && this->peek() == ':') { + this->position_ += 1; + const auto saved{this->position_}; + this->skip_whitespace(); + if (!this->at_end() && (this->peek() == '-' || is_digit(this->peek()))) { + slice.step = this->parse_integer(); + } else { + this->position_ = saved; + } + } + + return slice; + } + + // int = "0" / (["-"] DIGIT1 *DIGIT) + auto parse_integer() -> std::int64_t { + const auto begin{this->position_}; + if (this->peek() == '-') { + this->position_ += 1; + } + + if (this->at_end() || !is_digit(this->peek())) { + this->fail(); + } + + if (this->peek() == '0') { + this->position_ += 1; + if (!this->at_end() && is_digit(this->peek())) { + this->fail(); + } + + if (this->input_[begin] == '-') { + this->fail(); + } + + return 0; + } + + while (!this->at_end() && is_digit(this->peek())) { + this->position_ += 1; + } + + std::int64_t value{0}; + const auto outcome{std::from_chars(this->input_.data() + begin, + this->input_.data() + this->position_, + value)}; + if (outcome.ec != std::errc{} || value > JSONPATH_MAXIMUM_INTEGER || + value < -JSONPATH_MAXIMUM_INTEGER) { + this->fail(); + } + + return value; + } + + // string-literal = %x22 *double-quoted %x22 / %x27 *single-quoted %x27 + auto parse_string_literal() -> JSON::String { + const char quote{this->peek()}; + this->position_ += 1; + JSON::String result; + while (true) { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == quote) { + this->position_ += 1; + return result; + } + + if (character == '\\') { + this->position_ += 1; + this->parse_string_escape(result, quote); + continue; + } + + // unescaped starts at %x20, so raw control characters are not allowed + if (static_cast(character) < 0x20) { + this->fail(); + } + + this->copy_character(result); + } + } + + // escapable = %x62 / %x66 / %x6E / %x72 / %x74 / "/" / "\" / (%x75 hexchar) + // plus the quote of the enclosing kind + auto parse_string_escape(JSON::String &result, const char quote) -> void { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + switch (character) { + case 'b': + result += '\x08'; + this->position_ += 1; + return; + case 'f': + result += '\x0C'; + this->position_ += 1; + return; + case 'n': + result += '\n'; + this->position_ += 1; + return; + case 'r': + result += '\r'; + this->position_ += 1; + return; + case 't': + result += '\t'; + this->position_ += 1; + return; + case '/': + result += '/'; + this->position_ += 1; + return; + case '\\': + result += '\\'; + this->position_ += 1; + return; + case 'u': { + this->position_ += 1; + const auto code{this->parse_hex_quad()}; + // hexchar = non-surrogate / (high-surrogate "\" %x75 low-surrogate) + if (code >= 0xDC00 && code <= 0xDFFF) { + this->fail(); + } + + if (code >= 0xD800 && code <= 0xDBFF) { + if (this->at_end() || this->peek() != '\\') { + this->fail(); + } + + this->position_ += 1; + if (this->at_end() || this->peek() != 'u') { + this->fail(); + } + + this->position_ += 1; + const auto low{this->parse_hex_quad()}; + if (low < 0xDC00 || low > 0xDFFF) { + this->fail(); + } + + const auto combined{0x10000 + ((code - 0xD800) << 10) + + (low - 0xDC00)}; + codepoint_to_utf8(static_cast(combined), result); + return; + } + + codepoint_to_utf8(static_cast(code), result); + return; + } + default: + if (character == quote) { + result += quote; + this->position_ += 1; + return; + } + + this->fail(); + } + } + + auto parse_hex_quad() -> std::uint32_t { + std::uint32_t result{0}; + for (std::size_t count{0}; count < 4; ++count) { + if (this->at_end()) { + this->fail(); + } + + const auto value{hex_digit_value(this->peek())}; + if (value < 0) { + this->fail(); + } + + result = (result << 4) | static_cast(value); + this->position_ += 1; + } + + return result; + } + + // member-name-shorthand = name-first *name-char + auto parse_shorthand_name() -> SelectorName { + JSON::String name; + if (this->at_end()) { + this->fail(); + } + + const auto first{static_cast(this->peek())}; + if (first == '_' || is_alpha(static_cast(first))) { + name += static_cast(first); + this->position_ += 1; + } else if (first >= 0x80) { + this->copy_character(name); + } else { + this->fail(); + } + + while (!this->at_end()) { + const auto next{static_cast(this->peek())}; + if (next == '_' || is_alpha(static_cast(next)) || + is_digit(static_cast(next))) { + name += static_cast(next); + this->position_ += 1; + } else if (next >= 0x80) { + this->copy_character(name); + } else { + break; + } + } + + const auto hash{JSON::Object::hash(name)}; + return SelectorName{.name = std::move(name), .hash = hash}; + } + + // logical-or-expr = logical-and-expr *(S "||" S logical-and-expr) + auto parse_logical_or() -> FilterExpression { + std::vector children; + children.push_back(this->parse_logical_and()); + while (true) { + const auto saved{this->position_}; + this->skip_whitespace(); + if (this->position_ + 1 < this->input_.size() && + this->input_[this->position_] == '|' && + this->input_[this->position_ + 1] == '|') { + this->position_ += 2; + this->skip_whitespace(); + children.push_back(this->parse_logical_and()); + } else { + this->position_ = saved; + break; + } + } + + if (children.size() == 1) { + return std::move(children.front()); + } + + return FilterExpression{FilterDisjunction{std::move(children)}}; + } + + // logical-and-expr = basic-expr *(S "&&" S basic-expr) + auto parse_logical_and() -> FilterExpression { + std::vector children; + children.push_back(this->parse_basic_expression()); + while (true) { + const auto saved{this->position_}; + this->skip_whitespace(); + if (this->position_ + 1 < this->input_.size() && + this->input_[this->position_] == '&' && + this->input_[this->position_ + 1] == '&') { + this->position_ += 2; + this->skip_whitespace(); + children.push_back(this->parse_basic_expression()); + } else { + this->position_ = saved; + break; + } + } + + if (children.size() == 1) { + return std::move(children.front()); + } + + return FilterExpression{FilterConjunction{std::move(children)}}; + } + + // basic-expr = paren-expr / comparison-expr / test-expr + auto parse_basic_expression() -> FilterExpression { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == '!') { + this->position_ += 1; + this->skip_whitespace(); + if (this->at_end()) { + this->fail(); + } + + if (this->peek() == '(') { + auto inner{this->parse_parenthesized()}; + return FilterExpression{FilterNegation{ + std::make_unique(std::move(inner))}}; + } + + return FilterExpression{this->parse_test(true)}; + } + + if (character == '(') { + return this->parse_parenthesized(); + } + + return this->parse_comparison_or_test(); + } + + // paren-expr = [logical-not-op S] "(" S logical-expr S ")" + auto parse_parenthesized() -> FilterExpression { + assert(this->peek() == '('); + this->position_ += 1; + this->skip_whitespace(); + auto inner{this->parse_logical_or()}; + this->skip_whitespace(); + if (this->at_end() || this->peek() != ')') { + this->fail(); + } + + this->position_ += 1; + return inner; + } + + // test-expr = [logical-not-op S] (filter-query / function-expr) + auto parse_test(const bool negated) -> FilterTest { + if (this->peek() == '@' || this->peek() == '$') { + bool singular{true}; + auto query{this->parse_filter_query(singular)}; + return FilterTest{.negated = negated, .subject = std::move(query)}; + } + + if (is_lower_alpha(this->peek())) { + auto call{this->parse_function()}; + // RFC 9535 Section 2.4.3: a test expression admits functions whose + // declared result is of logical or nodes type only + if (call->function != FilterFunctionName::Match && + call->function != FilterFunctionName::Search) { + this->fail(); + } + + return FilterTest{.negated = negated, .subject = std::move(call)}; + } + + this->fail(); + } + + [[nodiscard]] auto comparison_operator_ahead() const -> bool { + if (this->at_end()) { + return false; + } + + const char character{this->input_[this->position_]}; + if (character == '<' || character == '>') { + return true; + } + + return (character == '=' || character == '!') && + this->position_ + 1 < this->input_.size() && + this->input_[this->position_ + 1] == '='; + } + + // comparison-op = "==" / "!=" / "<=" / ">=" / "<" / ">" + auto parse_comparison_operator() -> FilterComparisonOperator { + const char character{this->peek()}; + if (character == '=' || character == '!') { + this->position_ += 1; + if (this->at_end() || this->peek() != '=') { + this->fail(); + } + + this->position_ += 1; + return character == '=' ? FilterComparisonOperator::Equal + : FilterComparisonOperator::NotEqual; + } + + if (character == '<' || character == '>') { + this->position_ += 1; + const bool inclusive{!this->at_end() && this->peek() == '='}; + if (inclusive) { + this->position_ += 1; + } + + if (character == '<') { + return inclusive ? FilterComparisonOperator::LessEqual + : FilterComparisonOperator::Less; + } + + return inclusive ? FilterComparisonOperator::GreaterEqual + : FilterComparisonOperator::Greater; + } + + this->fail(); + } + + auto parse_comparison_or_test() -> FilterExpression { + const char character{this->peek()}; + if (character == '@' || character == '$') { + bool singular{true}; + auto query{this->parse_filter_query(singular)}; + const auto saved{this->position_}; + this->skip_whitespace(); + if (this->comparison_operator_ahead()) { + // RFC 9535 Section 2.3.5.1: "comparable = literal / singular-query / + // function-expr" + if (!singular) { + this->fail(); + } + + const auto operation{this->parse_comparison_operator()}; + this->skip_whitespace(); + auto right{this->parse_comparable()}; + return FilterExpression{ + FilterComparison{.left = FilterOperand{std::move(query)}, + .operation = operation, + .right = std::move(right)}}; + } + + this->position_ = saved; + return FilterExpression{ + FilterTest{.negated = false, .subject = std::move(query)}}; + } + + if (character == '"' || character == '\'' || character == '-' || + is_digit(character)) { + auto literal{this->parse_filter_literal()}; + this->skip_whitespace(); + if (!this->comparison_operator_ahead()) { + this->fail(); + } + + const auto operation{this->parse_comparison_operator()}; + this->skip_whitespace(); + auto right{this->parse_comparable()}; + return FilterExpression{ + FilterComparison{.left = FilterOperand{std::move(literal)}, + .operation = operation, + .right = std::move(right)}}; + } + + if (is_lower_alpha(character)) { + auto operand{this->parse_keyword_or_function()}; + const auto saved{this->position_}; + this->skip_whitespace(); + if (this->comparison_operator_ahead()) { + if (std::holds_alternative>( + operand) && + !is_value_type_function( + *std::get>(operand))) { + this->fail(); + } + + const auto operation{this->parse_comparison_operator()}; + this->skip_whitespace(); + auto right{this->parse_comparable()}; + return FilterExpression{FilterComparison{.left = std::move(operand), + .operation = operation, + .right = std::move(right)}}; + } + + this->position_ = saved; + if (!std::holds_alternative>( + operand)) { + // A literal is not a valid test expression + this->fail(); + } + + auto call{ + std::move(std::get>(operand))}; + if (call->function != FilterFunctionName::Match && + call->function != FilterFunctionName::Search) { + this->fail(); + } + + return FilterExpression{ + FilterTest{.negated = false, .subject = std::move(call)}}; + } + + this->fail(); + } + + static auto is_value_type_function(const FilterFunctionCall &call) -> bool { + return call.function == FilterFunctionName::Length || + call.function == FilterFunctionName::Count || + call.function == FilterFunctionName::Value; + } + + // comparable = literal / singular-query / function-expr + auto parse_comparable() -> FilterOperand { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == '@' || character == '$') { + bool singular{true}; + auto query{this->parse_filter_query(singular)}; + if (!singular) { + this->fail(); + } + + return FilterOperand{std::move(query)}; + } + + if (character == '"' || character == '\'' || character == '-' || + is_digit(character)) { + return FilterOperand{this->parse_filter_literal()}; + } + + if (is_lower_alpha(character)) { + auto operand{this->parse_keyword_or_function()}; + if (std::holds_alternative>( + operand) && + !is_value_type_function( + *std::get>(operand))) { + this->fail(); + } + + return operand; + } + + this->fail(); + } + + // literal = number / string-literal / true / false / null + auto parse_filter_literal() -> JSON { + const char character{this->peek()}; + if (character == '"' || character == '\'') { + return JSON{this->parse_string_literal()}; + } + + return this->parse_number(); + } + + // number = (int / "-0") [ frac ] [ exp ] + auto parse_number() -> JSON { + const auto begin{this->position_}; + if (this->peek() == '-') { + this->position_ += 1; + } + + if (this->at_end() || !is_digit(this->peek())) { + this->fail(); + } + + if (this->peek() == '0') { + this->position_ += 1; + if (!this->at_end() && is_digit(this->peek())) { + this->fail(); + } + } else { + while (!this->at_end() && is_digit(this->peek())) { + this->position_ += 1; + } + } + + bool real{false}; + // frac = "." 1*DIGIT + if (!this->at_end() && this->peek() == '.') { + real = true; + this->position_ += 1; + if (this->at_end() || !is_digit(this->peek())) { + this->fail(); + } + + while (!this->at_end() && is_digit(this->peek())) { + this->position_ += 1; + } + } + + // exp = "e" [ "-" / "+" ] 1*DIGIT + if (!this->at_end() && (this->peek() == 'e' || this->peek() == 'E')) { + real = true; + this->position_ += 1; + if (!this->at_end() && (this->peek() == '-' || this->peek() == '+')) { + this->position_ += 1; + } + + if (this->at_end() || !is_digit(this->peek())) { + this->fail(); + } + + while (!this->at_end() && is_digit(this->peek())) { + this->position_ += 1; + } + } + + const auto text{this->input_.substr(begin, this->position_ - begin)}; + if (!real) { + std::int64_t value{0}; + const auto outcome{ + std::from_chars(text.data(), text.data() + text.size(), value)}; + if (outcome.ec != std::errc{} || value > JSONPATH_MAXIMUM_INTEGER || + value < -JSONPATH_MAXIMUM_INTEGER) { + this->fail(); + } + + return JSON{value}; + } + + const std::string buffer{text}; + return JSON{std::strtod(buffer.c_str(), nullptr)}; + } + + // filter-query = rel-query / jsonpath-query + auto parse_filter_query(bool &singular) -> FilterQuery { + FilterQuery query; + query.relative = this->peek() == '@'; + this->position_ += 1; + query.segments = this->parse_segments(singular); + query.singular = singular; + return query; + } + + auto parse_identifier() -> std::string_view { + const auto begin{this->position_}; + while (!this->at_end() && (is_lower_alpha(this->peek()) || + is_digit(this->peek()) || this->peek() == '_')) { + this->position_ += 1; + } + + return this->input_.substr(begin, this->position_ - begin); + } + + auto parse_keyword_or_function() -> FilterOperand { + const auto saved{this->position_}; + const auto identifier{this->parse_identifier()}; + if (!this->at_end() && this->peek() == '(') { + this->position_ = saved; + return FilterOperand{this->parse_function()}; + } + + if (identifier == "true") { + return FilterOperand{JSON{true}}; + } + + if (identifier == "false") { + return FilterOperand{JSON{false}}; + } + + if (identifier == "null") { + return FilterOperand{JSON{nullptr}}; + } + + this->fail(); + } + + // function-expr = function-name "(" S [function-argument + // *(S "," S function-argument)] S ")" + auto parse_function() -> std::unique_ptr { + const auto identifier{this->parse_identifier()}; + auto call{std::make_unique()}; + if (identifier == "length") { + call->function = FilterFunctionName::Length; + } else if (identifier == "count") { + call->function = FilterFunctionName::Count; + } else if (identifier == "match") { + call->function = FilterFunctionName::Match; + } else if (identifier == "search") { + call->function = FilterFunctionName::Search; + } else if (identifier == "value") { + call->function = FilterFunctionName::Value; + } else { + this->fail(); + } + + if (this->at_end() || this->peek() != '(') { + this->fail(); + } + + this->position_ += 1; + this->skip_whitespace(); + if (this->at_end()) { + this->fail(); + } + + if (this->peek() != ')') { + while (true) { + call->arguments.push_back(this->parse_function_argument()); + this->skip_whitespace(); + if (!this->at_end() && this->peek() == ',') { + this->position_ += 1; + this->skip_whitespace(); + } else { + break; + } + } + } + + if (this->at_end() || this->peek() != ')') { + this->fail(); + } + + this->position_ += 1; + this->check_function(*call); + return call; + } + + auto parse_function_argument() -> FilterOperand { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == '@' || character == '$') { + bool singular{true}; + return FilterOperand{this->parse_filter_query(singular)}; + } + + if (character == '"' || character == '\'' || character == '-' || + is_digit(character)) { + return FilterOperand{this->parse_filter_literal()}; + } + + if (is_lower_alpha(character)) { + return this->parse_keyword_or_function(); + } + + // The grammar also admits a logical expression argument, yet none of + // the RFC 9535 functions declares a parameter of logical type, so such + // an argument can never be well-typed per Section 2.4.3 + this->fail(); + } + + [[nodiscard]] static auto is_value_type_operand(const FilterOperand &operand) + -> bool { + if (std::holds_alternative(operand)) { + return true; + } + + if (std::holds_alternative(operand)) { + return std::get(operand).singular; + } + + return is_value_type_function( + *std::get>(operand)); + } + + // RFC 9535 Section 2.4.3: "the function's arguments need to be well-typed + // against the declared parameter types" + auto check_function(FilterFunctionCall &call) -> void { + switch (call.function) { + case FilterFunctionName::Length: + if (call.arguments.size() != 1 || + !is_value_type_operand(call.arguments.front())) { + this->fail(); + } + + return; + case FilterFunctionName::Count: + case FilterFunctionName::Value: + if (call.arguments.size() != 1 || + !std::holds_alternative(call.arguments.front())) { + this->fail(); + } + + return; + case FilterFunctionName::Match: + case FilterFunctionName::Search: + if (call.arguments.size() != 2 || + !is_value_type_operand(call.arguments.front()) || + !is_value_type_operand(call.arguments.back())) { + this->fail(); + } + + if (std::holds_alternative(call.arguments.back()) && + std::get(call.arguments.back()).is_string()) { + call.compiled = + to_regex(std::get(call.arguments.back()).to_string(), + call.function == FilterFunctionName::Match + ? RegexDialect::IRegexp + : RegexDialect::IRegexpSearch); + } + + return; + } + } + + std::string_view input_; + std::size_t position_{0}; +}; + +} // namespace + +inline auto parse_jsonpath(const std::string_view input) -> JSONPathQuery { + JSONPathParser parser{input}; + return parser.parse(); +} + +} // namespace sourcemeta::core + +#endif diff --git a/test/jsonpath/CMakeLists.txt b/test/jsonpath/CMakeLists.txt new file mode 100644 index 0000000000..6b7e868e3e --- /dev/null +++ b/test/jsonpath/CMakeLists.txt @@ -0,0 +1,30 @@ +sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME jsonpath + SOURCES + jsonpath_parse_test.cc + jsonpath_evaluate_test.cc + jsonpath_filter_test.cc + jsonpath_normalize_test.cc) + +target_link_libraries(sourcemeta_core_jsonpath_unit + PRIVATE sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jsonpath_unit + PRIVATE sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_core_jsonpath_unit + PRIVATE sourcemeta::core::jsonpath) + +if(MSVC) + target_compile_options(sourcemeta_core_jsonpath_unit PRIVATE /utf-8) +endif() + +# JSONPath Compliance Test Suite +# See https://github.com/jsonpath-standard/jsonpath-compliance-test-suite +sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME jsonpath_suite + SOURCES jsonpath_suite.cc) +target_compile_definitions(sourcemeta_core_jsonpath_suite_unit + PRIVATE JSONPATH_SUITE_PATH="${PROJECT_SOURCE_DIR}/vendor/jsonpath-compliance-test-suite") +target_link_libraries(sourcemeta_core_jsonpath_suite_unit + PRIVATE sourcemeta::core::json) +target_link_libraries(sourcemeta_core_jsonpath_suite_unit + PRIVATE sourcemeta::core::jsonpointer) +target_link_libraries(sourcemeta_core_jsonpath_suite_unit + PRIVATE sourcemeta::core::jsonpath) diff --git a/test/jsonpath/jsonpath_evaluate_test.cc b/test/jsonpath/jsonpath_evaluate_test.cc new file mode 100644 index 0000000000..e4d5c2f786 --- /dev/null +++ b/test/jsonpath/jsonpath_evaluate_test.cc @@ -0,0 +1,216 @@ +#include +#include +#include + +TEST(jsonpath_evaluate_root) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(*nodes.at(0).value == document); +} + +TEST(jsonpath_evaluate_name) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "a": 1, "b": 2 })JSON")}; + const sourcemeta::core::JSONPath path{"$.b"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_TRUE(nodes.at(0).value->is_integer()); + EXPECT_EQ(nodes.at(0).value->to_integer(), 2); +} + +TEST(jsonpath_evaluate_name_absent) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.b"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_name_on_array) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON")}; + const sourcemeta::core::JSONPath path{"$.a"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_nested_names) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "a": { "b": { "c": 42 } } })JSON")}; + const sourcemeta::core::JSONPath path{"$.a.b.c"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 42); +} + +TEST(jsonpath_evaluate_index) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 20); +} + +TEST(jsonpath_evaluate_index_negative) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[-1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 30); +} + +TEST(jsonpath_evaluate_index_out_of_bounds) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[3]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_index_on_object) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "0": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$[0]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_wildcard_object) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "a": 1, "b": 2 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(nodes.at(1).value->to_integer(), 2); +} + +TEST(jsonpath_evaluate_wildcard_array) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 10, 20 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[*]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 10); + EXPECT_EQ(nodes.at(1).value->to_integer(), 20); +} + +TEST(jsonpath_evaluate_wildcard_scalar) { + const auto document{sourcemeta::core::parse_json("42")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_slice_forward) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 0, 1, 2, 3, 4, 5 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[1:5:2]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); + EXPECT_EQ(nodes.at(1).value->to_integer(), 3); +} + +TEST(jsonpath_evaluate_slice_negative_step) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[::-1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(nodes.at(0).value->to_integer(), 2); + EXPECT_EQ(nodes.at(1).value->to_integer(), 1); + EXPECT_EQ(nodes.at(2).value->to_integer(), 0); +} + +TEST(jsonpath_evaluate_slice_zero_step) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[::0]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_evaluate_slice_negative_bounds) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 0, 1, 2, 3, 4 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[-3:-1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_integer(), 2); + EXPECT_EQ(nodes.at(1).value->to_integer(), 3); +} + +TEST(jsonpath_evaluate_multiple_selectors_duplicates) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 10, 20 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[0, 0, -2]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(nodes.at(0).value->to_integer(), 10); + EXPECT_EQ(nodes.at(1).value->to_integer(), 10); + EXPECT_EQ(nodes.at(2).value->to_integer(), 10); +} + +TEST(jsonpath_evaluate_descendant_names) { + const auto document{sourcemeta::core::parse_json( + R"JSON({ "a": { "a": 1 }, "b": [ { "a": 2 } ] })JSON")}; + const sourcemeta::core::JSONPath path{"$..a"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_TRUE(nodes.at(0).value->is_object()); + EXPECT_EQ(nodes.at(1).value->to_integer(), 1); + EXPECT_EQ(nodes.at(2).value->to_integer(), 2); +} + +TEST(jsonpath_evaluate_descendant_wildcard) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "a": [ 1 ] })JSON")}; + const sourcemeta::core::JSONPath path{"$..*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_TRUE(nodes.at(0).value->is_array()); + EXPECT_EQ(nodes.at(1).value->to_integer(), 1); +} + +TEST(jsonpath_evaluate_descendant_then_child) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "x": { "y": { "z": 1 } } })JSON")}; + const sourcemeta::core::JSONPath path{"$..y.z"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); +} + +TEST(jsonpath_evaluate_callback_matches_vector) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 2, 3 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[*]"}; + std::size_t count{0}; + path.evaluate(document, [&count](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &) { + EXPECT_TRUE(value.is_integer()); + count += 1; + }); + EXPECT_EQ(count, 3); +} + +TEST(jsonpath_evaluate_unicode_name) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "á": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.á"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); +} + +TEST(jsonpath_evaluate_escaped_name_decodes) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "A": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$['\\u0041']"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); +} + +TEST(jsonpath_evaluate_move_construction) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + sourcemeta::core::JSONPath original{"$.a"}; + const sourcemeta::core::JSONPath moved{std::move(original)}; + const auto nodes{moved.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc new file mode 100644 index 0000000000..5d595ece7c --- /dev/null +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -0,0 +1,282 @@ +#include +#include +#include + +TEST(jsonpath_filter_existence) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "b": 2 }, { "a": null } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_existence_null_is_present) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": null } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_negated_existence) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "b": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?!@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_equals_integer) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "a": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == 2]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_equals_integer_and_real) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 1 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == 1.0]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_equals_deep) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": [ 1, 2 ] }, { "a": [ 1, 3 ] }, { "a": [ 1, 2 ] } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == $[0].a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_equals_absent_both_sides) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "b": 1 }, { "c": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.x == @.y]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_equals_absent_one_side) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": null } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == @.b]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_not_equals) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "a": 2 }, {} ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a != 1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_less_than_numbers) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "a": 2.5 }, { "a": 3 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a < 3]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_less_than_strings) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "apple" }, { "a": "banana" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a < 'b']"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_less_than_mixed_types) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "a": "1" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a < '2']"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_less_equal_absent) { + const auto document{sourcemeta::core::parse_json(R"JSON([ {} ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a <= @.b]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_boolean_literals) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": true }, { "a": false } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == true]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_null_literal) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": null }, { "a": 0 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a == null]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_conjunction) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1, "b": 2 }, { "a": 1 }, { "b": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a && @.b]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_disjunction) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "b": 2 }, { "c": 3 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a || @.b]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_negated_parenthesized) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "b": 2 }, { "a": 1, "b": 2 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?!(@.a && @.b)]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_on_object_members) { + const auto document{sourcemeta::core::parse_json( + R"JSON({ "x": { "a": 1 }, "y": { "b": 2 } })JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_absolute_query) { + const auto document{sourcemeta::core::parse_json( + R"JSON({ "limit": 2, "values": [ 1, 2, 3 ] })JSON")}; + const sourcemeta::core::JSONPath path{"$.values[?@ <= $.limit]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_nested_filter) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": [ { "b": 1 } ] }, { "a": [ { "c": 2 } ] } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a[?@.b]]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_length_string) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "ab" }, { "a": "abc" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?length(@.a) == 2]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_length_counts_code_points) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": "héllo" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?length(@.a) == 5]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_length_array_and_object) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": [ 1, 2, 3 ] }, { "a": { "x": 1 } } ])JSON")}; + const sourcemeta::core::JSONPath first{"$[?length(@.a) == 3]"}; + EXPECT_EQ(first.evaluate(document).size(), 1); + const sourcemeta::core::JSONPath second{"$[?length(@.a) == 1]"}; + EXPECT_EQ(second.evaluate(document).size(), 1); +} + +TEST(jsonpath_filter_length_of_number_is_nothing) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 5 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?length(@.a) == @.b]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_count) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": [ 1, 2 ] }, { "a": [ 1 ] } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?count(@.a[*]) == 2]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_value_single_node) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": [ { "b": 7 } ] }, { "a": [ { "b": 7 }, { "b": 7 } ] } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?value(@..b) == 7]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_match_full) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "ab" }, { "a": "xabx" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, 'ab')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_search_substring) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "ab" }, { "a": "xabx" }, { "a": "xx" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?search(@.a, 'ab')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} + +TEST(jsonpath_filter_match_non_string_input) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": 1 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, 'a.*')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_match_invalid_pattern) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": "x" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, '\\\\d')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 0); +} + +TEST(jsonpath_filter_match_dynamic_pattern) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "ab", "p": "a." }, { "a": "ab", "p": "b." } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, @.p)]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_match_dot_excludes_line_feed) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": "x\ny" }, { "a": "xzy" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, 'x.y')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_match_dot_accepts_line_separator) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ { "a": "x\u2028y" } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@.a, 'x.y')]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_current_node_comparison) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 5, 10 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@ > 4]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); +} diff --git a/test/jsonpath/jsonpath_normalize_test.cc b/test/jsonpath/jsonpath_normalize_test.cc new file mode 100644 index 0000000000..1f70084b6f --- /dev/null +++ b/test/jsonpath/jsonpath_normalize_test.cc @@ -0,0 +1,137 @@ +#include +#include +#include + +TEST(jsonpath_normalize_root) { + const auto document{sourcemeta::core::parse_json(R"JSON(1)JSON")}; + const sourcemeta::core::JSONPath path{"$"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$"); +} + +TEST(jsonpath_normalize_property_and_index) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "a": [ { "b": 1 } ] })JSON")}; + const sourcemeta::core::JSONPath path{"$..b"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['a'][0]['b']"); +} + +TEST(jsonpath_normalize_single_quote_escaped) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a'b": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['a\\'b']"); +} + +TEST(jsonpath_normalize_backslash_escaped) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a\\b": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['a\\\\b']"); +} + +TEST(jsonpath_normalize_double_quote_not_escaped) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a\"b": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['a\"b']"); +} + +TEST(jsonpath_normalize_two_character_escapes) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "\b\f\n\r\t": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['\\b\\f\\n\\r\\t']"); +} + +TEST(jsonpath_normalize_control_hexadecimal) { + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "\u000b\u0000\u001f": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['\\u000b\\u0000\\u001f']"); +} + +TEST(jsonpath_normalize_non_ascii_verbatim) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "á🤔": 1 })JSON")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['á🤔']"); +} + +TEST(jsonpath_normalize_deep_indexes) { + const auto document{sourcemeta::core::parse_json(R"JSON([ [ [ 1 ] ] ])JSON")}; + const sourcemeta::core::JSONPath path{"$[0][0][0]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$[0][0][0]"); +} + +TEST(jsonpath_normalize_negative_index_resolved) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[-1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$[2]"); +} + +TEST(jsonpath_normalize_slice_negative_step) { + const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; + const sourcemeta::core::JSONPath path{"$[::-1]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$[2]"); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), + "$[1]"); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(2).location), + "$[0]"); +} + +TEST(jsonpath_normalize_descendant_document_order) { + const auto document{sourcemeta::core::parse_json( + R"JSON({ "a": { "a": 1 }, "b": [ { "a": 2 } ] })JSON")}; + const sourcemeta::core::JSONPath path{"$..a"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 3); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['a']"); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), + "$['a']['a']"); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(2).location), + "$['b'][0]['a']"); +} + +TEST(jsonpath_normalize_filter_locations) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1 }, { "b": 2 }, { "a": null } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$[0]"); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), + "$[2]"); +} + +TEST(jsonpath_normalize_filter_object_member_location) { + const auto document{sourcemeta::core::parse_json( + R"JSON({ "x": { "a": 1 }, "y": { "b": 2 } })JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a]"}; + const auto nodes{path.evaluate(document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), + "$['x']"); +} diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc new file mode 100644 index 0000000000..81576298af --- /dev/null +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -0,0 +1,270 @@ +#include +#include + +#define EXPECT_JSONPATH_PARSE_ERROR(input, expected_column) \ + try { \ + const sourcemeta::core::JSONPath path{input}; \ + FAIL(); \ + } catch (const sourcemeta::core::JSONPathParseError &error) { \ + EXPECT_EQ(error.column(), expected_column); \ + } catch (...) { \ + FAIL(); \ + } + +#define EXPECT_JSONPATH_VALID(input) \ + { \ + const sourcemeta::core::JSONPath path{input}; \ + EXPECT_TRUE(true); \ + } + +TEST(jsonpath_parse_root_only) { EXPECT_JSONPATH_VALID("$"); } + +TEST(jsonpath_parse_shorthand_name) { EXPECT_JSONPATH_VALID("$.foo"); } + +TEST(jsonpath_parse_shorthand_chain) { EXPECT_JSONPATH_VALID("$.foo.bar"); } + +TEST(jsonpath_parse_shorthand_underscore) { EXPECT_JSONPATH_VALID("$._foo"); } + +TEST(jsonpath_parse_shorthand_non_ascii) { EXPECT_JSONPATH_VALID("$.á"); } + +TEST(jsonpath_parse_bracket_single_quoted) { EXPECT_JSONPATH_VALID("$['a']"); } + +TEST(jsonpath_parse_bracket_double_quoted) { + EXPECT_JSONPATH_VALID("$[\"a\"]"); +} + +TEST(jsonpath_parse_bracket_index) { EXPECT_JSONPATH_VALID("$[0]"); } + +TEST(jsonpath_parse_bracket_negative_index) { EXPECT_JSONPATH_VALID("$[-1]"); } + +TEST(jsonpath_parse_wildcard_shorthand) { EXPECT_JSONPATH_VALID("$.*"); } + +TEST(jsonpath_parse_wildcard_bracket) { EXPECT_JSONPATH_VALID("$[*]"); } + +TEST(jsonpath_parse_descendant_shorthand) { EXPECT_JSONPATH_VALID("$..foo"); } + +TEST(jsonpath_parse_descendant_wildcard) { EXPECT_JSONPATH_VALID("$..*"); } + +TEST(jsonpath_parse_descendant_bracket) { EXPECT_JSONPATH_VALID("$..[0]"); } + +TEST(jsonpath_parse_slice_full) { EXPECT_JSONPATH_VALID("$[1:5:2]"); } + +TEST(jsonpath_parse_slice_no_step) { EXPECT_JSONPATH_VALID("$[1:5]"); } + +TEST(jsonpath_parse_slice_open_ended) { EXPECT_JSONPATH_VALID("$[1:]"); } + +TEST(jsonpath_parse_slice_open_start) { EXPECT_JSONPATH_VALID("$[:5]"); } + +TEST(jsonpath_parse_slice_bare_colon) { EXPECT_JSONPATH_VALID("$[:]"); } + +TEST(jsonpath_parse_slice_negative_step) { EXPECT_JSONPATH_VALID("$[::-1]"); } + +TEST(jsonpath_parse_multiple_selectors) { + EXPECT_JSONPATH_VALID("$['a', 'b', 1]"); +} + +TEST(jsonpath_parse_filter_existence) { EXPECT_JSONPATH_VALID("$[?@.a]"); } + +TEST(jsonpath_parse_filter_comparison) { + EXPECT_JSONPATH_VALID("$[?@.a == 1]"); +} + +TEST(jsonpath_parse_filter_logical) { + EXPECT_JSONPATH_VALID("$[?@.a && (@.b || !@.c)]"); +} + +TEST(jsonpath_parse_filter_functions) { + EXPECT_JSONPATH_VALID("$[?length(@.a) >= 2 && match(@.b, 'x.*')]"); +} + +TEST(jsonpath_parse_filter_nested_function) { + EXPECT_JSONPATH_VALID("$[?length(value($..a)) == 3]"); +} + +TEST(jsonpath_parse_name_with_escapes) { + EXPECT_JSONPATH_VALID("$['\\n\\t\\\\\\'']"); +} + +TEST(jsonpath_parse_name_with_unicode_escape) { + EXPECT_JSONPATH_VALID("$['\\u0041']"); +} + +TEST(jsonpath_parse_name_with_surrogate_pair) { + EXPECT_JSONPATH_VALID("$['\\uD834\\uDD1E']"); +} + +TEST(jsonpath_parse_whitespace_between_segments) { + EXPECT_JSONPATH_VALID("$ .foo"); +} + +TEST(jsonpath_parse_whitespace_in_brackets) { + EXPECT_JSONPATH_VALID("$[ 'a' , 1 ]"); +} + +TEST(jsonpath_parse_empty) { EXPECT_JSONPATH_PARSE_ERROR("", 1); } + +TEST(jsonpath_parse_missing_root) { EXPECT_JSONPATH_PARSE_ERROR(".foo", 1); } + +TEST(jsonpath_parse_double_root) { EXPECT_JSONPATH_PARSE_ERROR("$$", 2); } + +TEST(jsonpath_parse_trailing_garbage) { + EXPECT_JSONPATH_PARSE_ERROR("$.a!", 4); +} + +TEST(jsonpath_parse_trailing_whitespace) { + EXPECT_JSONPATH_PARSE_ERROR("$.a ", 4); +} + +TEST(jsonpath_parse_trailing_dot) { EXPECT_JSONPATH_PARSE_ERROR("$.", 3); } + +TEST(jsonpath_parse_dot_before_bracket) { + EXPECT_JSONPATH_PARSE_ERROR("$.['a']", 3); +} + +TEST(jsonpath_parse_bare_descendant) { EXPECT_JSONPATH_PARSE_ERROR("$..", 4); } + +TEST(jsonpath_parse_triple_dot) { EXPECT_JSONPATH_PARSE_ERROR("$...foo", 4); } + +TEST(jsonpath_parse_shorthand_starting_digit) { + EXPECT_JSONPATH_PARSE_ERROR("$.1foo", 3); +} + +TEST(jsonpath_parse_whitespace_inside_shorthand) { + EXPECT_JSONPATH_PARSE_ERROR("$. foo", 3); +} + +TEST(jsonpath_parse_unterminated_bracket) { + EXPECT_JSONPATH_PARSE_ERROR("$['a'", 6); +} + +TEST(jsonpath_parse_empty_bracket) { EXPECT_JSONPATH_PARSE_ERROR("$[]", 3); } + +TEST(jsonpath_parse_unterminated_string) { + EXPECT_JSONPATH_PARSE_ERROR("$['a]", 6); +} + +TEST(jsonpath_parse_raw_control_in_string) { + EXPECT_JSONPATH_PARSE_ERROR("$['\n']", 4); +} + +TEST(jsonpath_parse_invalid_escape) { + EXPECT_JSONPATH_PARSE_ERROR("$['\\x']", 5); +} + +TEST(jsonpath_parse_wrong_quote_escape) { + EXPECT_JSONPATH_PARSE_ERROR("$[\"\\'\"]", 5); +} + +TEST(jsonpath_parse_lone_high_surrogate) { + EXPECT_JSONPATH_PARSE_ERROR("$['\\uD834']", 10); +} + +TEST(jsonpath_parse_lone_low_surrogate) { + EXPECT_JSONPATH_PARSE_ERROR("$['\\uDD1E']", 10); +} + +TEST(jsonpath_parse_index_leading_zero) { + EXPECT_JSONPATH_PARSE_ERROR("$[01]", 4); +} + +TEST(jsonpath_parse_index_negative_zero) { + EXPECT_JSONPATH_PARSE_ERROR("$[-0]", 5); +} + +TEST(jsonpath_parse_index_too_large) { + EXPECT_JSONPATH_PARSE_ERROR("$[9007199254740992]", 19); +} + +TEST(jsonpath_parse_index_fractional) { + EXPECT_JSONPATH_PARSE_ERROR("$[1.5]", 4); +} + +TEST(jsonpath_parse_slice_step_leading_zero) { + EXPECT_JSONPATH_PARSE_ERROR("$[1:2:03]", 8); +} + +TEST(jsonpath_parse_filter_empty) { EXPECT_JSONPATH_PARSE_ERROR("$[?]", 4); } + +TEST(jsonpath_parse_filter_literal_alone) { + EXPECT_JSONPATH_PARSE_ERROR("$[?42]", 6); +} + +TEST(jsonpath_parse_filter_single_equal) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a = 1]", 8); +} + +TEST(jsonpath_parse_filter_non_singular_comparison) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@.* == 1]", 8); +} + +TEST(jsonpath_parse_filter_descendant_comparison) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@..a == 1]", 9); +} + +TEST(jsonpath_parse_filter_unknown_function) { + EXPECT_JSONPATH_PARSE_ERROR("$[?foo(@.a)]", 7); +} + +TEST(jsonpath_parse_filter_length_no_arguments) { + EXPECT_JSONPATH_PARSE_ERROR("$[?length() == 1]", 12); +} + +TEST(jsonpath_parse_filter_length_two_arguments) { + EXPECT_JSONPATH_PARSE_ERROR("$[?length(@.a, @.b) == 1]", 20); +} + +TEST(jsonpath_parse_filter_length_non_singular_argument) { + EXPECT_JSONPATH_PARSE_ERROR("$[?length(@.*) == 1]", 15); +} + +TEST(jsonpath_parse_filter_count_literal_argument) { + EXPECT_JSONPATH_PARSE_ERROR("$[?count(1) == 1]", 12); +} + +TEST(jsonpath_parse_filter_count_as_test) { + EXPECT_JSONPATH_PARSE_ERROR("$[?count(@..*)]", 15); +} + +TEST(jsonpath_parse_filter_length_as_test) { + EXPECT_JSONPATH_PARSE_ERROR("$[?length(@.a)]", 15); +} + +TEST(jsonpath_parse_filter_match_compared) { + EXPECT_JSONPATH_PARSE_ERROR("$[?match(@.a, 'x') == true]", 20); +} + +TEST(jsonpath_parse_filter_match_one_argument) { + EXPECT_JSONPATH_PARSE_ERROR("$[?match(@.a)]", 14); +} + +TEST(jsonpath_parse_filter_value_as_test) { + EXPECT_JSONPATH_PARSE_ERROR("$[?value(@..a)]", 15); +} + +TEST(jsonpath_parse_filter_number_leading_zero) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == 01]", 12); +} + +TEST(jsonpath_parse_filter_number_bare_fraction) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == 1.]", 13); +} + +TEST(jsonpath_parse_filter_number_bare_exponent) { + EXPECT_JSONPATH_PARSE_ERROR("$[?@.a == 1e]", 13); +} + +TEST(jsonpath_parse_filter_unterminated_parenthesis) { + EXPECT_JSONPATH_PARSE_ERROR("$[?(@.a]", 8); +} + +TEST(jsonpath_parse_filter_number_negative_zero_allowed) { + EXPECT_JSONPATH_VALID("$[?@.a == -0]"); +} + +TEST(jsonpath_parse_filter_number_real_allowed) { + EXPECT_JSONPATH_VALID("$[?@.a == 1.5e-2]"); +} + +TEST(jsonpath_parse_invalid_selector_character) { + EXPECT_JSONPATH_PARSE_ERROR("$[!]", 3); +} diff --git a/test/jsonpath/jsonpath_suite.cc b/test/jsonpath/jsonpath_suite.cc new file mode 100644 index 0000000000..08e85bd2cd --- /dev/null +++ b/test/jsonpath/jsonpath_suite.cc @@ -0,0 +1,117 @@ +#include +#include +#include + +#include // std::sort +#include // std::size_t +#include // std::filesystem +#include // std::string +#include // std::vector + +namespace { + +auto values_match(const std::vector &nodes, + const sourcemeta::core::JSON &expected) -> bool { + const auto &array{expected.as_array()}; + if (nodes.size() != array.size()) { + return false; + } + + for (std::size_t index = 0; index < nodes.size(); ++index) { + if (!(*nodes[index].value == expected.at(index))) { + return false; + } + } + + return true; +} + +auto paths_match(const std::vector &nodes, + const sourcemeta::core::JSON &expected) -> bool { + const auto &array{expected.as_array()}; + if (nodes.size() != array.size()) { + return false; + } + + for (std::size_t index = 0; index < nodes.size(); ++index) { + if (sourcemeta::core::JSONPath::normalize(nodes[index].location) != + expected.at(index).to_string()) { + return false; + } + } + + return true; +} + +auto run_jsonpath_test_case(const sourcemeta::core::JSON &test_case) -> void { + const auto &selector{test_case.at("selector").to_string()}; + const auto *invalid{test_case.try_at("invalid_selector")}; + if (invalid != nullptr && invalid->is_boolean() && invalid->to_boolean()) { + try { + const sourcemeta::core::JSONPath path{selector}; + FAIL(); + } catch (const sourcemeta::core::JSONPathParseError &error) { + // An invalid selector is expected to be rejected + EXPECT_TRUE(error.column() > 0); + } + + return; + } + + const sourcemeta::core::JSONPath path{selector}; + const auto nodes{path.evaluate(test_case.at("document"))}; + const auto *result{test_case.try_at("result")}; + if (result != nullptr) { + EXPECT_TRUE(values_match(nodes, *result)); + const auto *result_paths{test_case.try_at("result_paths")}; + if (result_paths != nullptr) { + EXPECT_TRUE(paths_match(nodes, *result_paths)); + } + + return; + } + + // Queries whose result order legitimately varies enumerate every + // acceptable outcome, with paths aligned by index when present + const auto &results{test_case.at("results")}; + const auto *results_paths{test_case.try_at("results_paths")}; + bool matched{false}; + for (std::size_t index = 0; index < results.as_array().size(); ++index) { + if (values_match(nodes, results.at(index)) && + (results_paths == nullptr || + paths_match(nodes, results_paths->at(index)))) { + matched = true; + break; + } + } + + EXPECT_TRUE(matched); +} + +} // namespace + +auto main(int argc, char **argv) -> int { + const std::filesystem::path base{std::filesystem::path{JSONPATH_SUITE_PATH} / + "tests"}; + std::vector files; + for (const auto &entry : + std::filesystem::recursive_directory_iterator{base}) { + if (entry.is_regular_file() && entry.path().extension() == ".json") { + files.push_back(entry.path()); + } + } + + std::sort(files.begin(), files.end()); + for (const auto &file : files) { + const auto document{sourcemeta::core::read_json(file)}; + auto prefix{std::filesystem::relative(file, base)}; + prefix.replace_extension(); + for (const auto &test_case : document.at("tests").as_array()) { + sourcemeta::core::test_register( + prefix.generic_string() + ": " + test_case.at("name").to_string(), + [test_case]() -> void { run_jsonpath_test_case(test_case); }); + } + } + + return sourcemeta::core::test_run(argc, argv); +} diff --git a/test/packaging/find_package/CMakeLists.txt b/test/packaging/find_package/CMakeLists.txt index 276e49400a..e3ba2130a8 100644 --- a/test/packaging/find_package/CMakeLists.txt +++ b/test/packaging/find_package/CMakeLists.txt @@ -23,6 +23,7 @@ target_link_libraries(core_hello PRIVATE sourcemeta::core::uri) target_link_libraries(core_hello PRIVATE sourcemeta::core::uritemplate) target_link_libraries(core_hello PRIVATE sourcemeta::core::json) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonpointer) +target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonpath) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonld) target_link_libraries(core_hello PRIVATE sourcemeta::core::jsonl) target_link_libraries(core_hello PRIVATE sourcemeta::core::yaml) diff --git a/test/packaging/find_package/hello.cc b/test/packaging/find_package/hello.cc index 84c78342c0..778f91bbc6 100644 --- a/test/packaging/find_package/hello.cc +++ b/test/packaging/find_package/hello.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include From 9c2cdc002d64dbe10eca29287aa787f0cca1c854 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 10:53:07 -0300 Subject: [PATCH 02/13] Simpler Signed-off-by: Juan Cruz Viotti --- src/core/jsonpath/CMakeLists.txt | 2 +- src/core/jsonpath/evaluator.h | 30 +-- src/core/jsonpath/grammar.h | 154 ++---------- .../include/sourcemeta/core/jsonpath.h | 233 ++++++++++++++---- src/core/jsonpath/jsonpath.cc | 23 +- src/core/jsonpath/parser.h | 89 ++++--- test/jsonpath/jsonpath_evaluate_test.cc | 76 ++++-- test/jsonpath/jsonpath_filter_test.cc | 94 ++++--- test/jsonpath/jsonpath_normalize_test.cc | 50 ++-- test/jsonpath/jsonpath_suite.cc | 24 +- 10 files changed, 438 insertions(+), 337 deletions(-) diff --git a/src/core/jsonpath/CMakeLists.txt b/src/core/jsonpath/CMakeLists.txt index 36860bdcf8..5fd7fa44cd 100644 --- a/src/core/jsonpath/CMakeLists.txt +++ b/src/core/jsonpath/CMakeLists.txt @@ -10,7 +10,7 @@ target_link_libraries(sourcemeta_core_jsonpath PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_core_jsonpath PUBLIC sourcemeta::core::jsonpointer) -target_link_libraries(sourcemeta_core_jsonpath PRIVATE +target_link_libraries(sourcemeta_core_jsonpath PUBLIC sourcemeta::core::regex) target_link_libraries(sourcemeta_core_jsonpath PRIVATE sourcemeta::core::text) diff --git a/src/core/jsonpath/evaluator.h b/src/core/jsonpath/evaluator.h index fb1264f196..3783632fec 100644 --- a/src/core/jsonpath/evaluator.h +++ b/src/core/jsonpath/evaluator.h @@ -250,18 +250,18 @@ auto evaluate_value_function(const FilterFunctionCall &call, inline auto evaluate_operand(const FilterOperand &operand, const JSON &candidate, const JSON &root) -> FilterValue { - if (std::holds_alternative(operand)) { - return FilterValue{&std::get(operand)}; + if (std::holds_alternative(operand.value)) { + return FilterValue{&std::get(operand.value)}; } - if (std::holds_alternative(operand)) { - const auto *result{ - resolve_singular(std::get(operand), candidate, root)}; + if (std::holds_alternative(operand.value)) { + const auto *result{resolve_singular(std::get(operand.value), + candidate, root)}; return result == nullptr ? FilterValue{} : FilterValue{result}; } - return evaluate_value_function( - *std::get>(operand), candidate, root); + return evaluate_value_function(std::get(operand.value), + candidate, root); } inline auto evaluate_value_function(const FilterFunctionCall &call, @@ -297,7 +297,7 @@ inline auto evaluate_value_function(const FilterFunctionCall &call, } // RFC 9535 Section 2.4.5: the count function yields the number of nodes case FilterFunctionName::Count: { - const auto &query{std::get(call.arguments.front())}; + const auto &query{std::get(call.arguments.front().value)}; std::int64_t count{0}; filter_query_visit(query.segments, 0, query.relative ? candidate : root, root, [&count](const JSON &) -> bool { @@ -309,7 +309,7 @@ inline auto evaluate_value_function(const FilterFunctionCall &call, // RFC 9535 Section 2.4.8: the value function yields the value of a // single node and nothing otherwise case FilterFunctionName::Value: { - const auto &query{std::get(call.arguments.front())}; + const auto &query{std::get(call.arguments.front().value)}; const JSON *single{nullptr}; std::size_t count{0}; filter_query_visit(query.segments, 0, query.relative ? candidate : root, @@ -338,8 +338,8 @@ inline auto evaluate_logical_function(const FilterFunctionCall &call, return false; } - if (std::holds_alternative(call.arguments.back()) && - std::get(call.arguments.back()).is_string()) { + if (std::holds_alternative(call.arguments.back().value) && + std::get(call.arguments.back().value).is_string()) { return call.compiled.has_value() && matches(call.compiled.value(), subject->to_string()); } @@ -442,8 +442,7 @@ inline auto filter_matches(const FilterExpression &expression, [](const JSON &) -> bool { return false; }); } else { result = evaluate_logical_function( - *std::get>(test.subject), - candidate, root); + std::get(test.subject), candidate, root); } return test.negated ? !result : result; @@ -471,8 +470,9 @@ inline auto filter_matches(const FilterExpression &expression, return false; } - return !filter_matches(*std::get(expression.value).child, - candidate, root); + return !filter_matches( + std::get(expression.value).children.front(), candidate, + root); } auto evaluate_segments(const std::vector &segments, diff --git a/src/core/jsonpath/grammar.h b/src/core/jsonpath/grammar.h index d8306cfa32..fc7a21ca34 100644 --- a/src/core/jsonpath/grammar.h +++ b/src/core/jsonpath/grammar.h @@ -1,142 +1,30 @@ #ifndef SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ #define SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ -#include -#include - -#include // std::int64_t, std::uint8_t -#include // std::unique_ptr -#include // std::optional -#include // std::variant -#include // std::vector +#include namespace sourcemeta::core { -struct JSONPathSegment; -struct FilterFunctionCall; - -// RFC 9535 Section 2.3.1: "A name selector '' selects at most one -// object member value". The decoded member name is owned here rather than -// viewed from the query source, as string literal escapes decode at parse -// time and result locations need a durable string to reference. The hash is -// precomputed once so evaluation resolves members without hashing -struct SelectorName { - JSON::String name; - JSON::Object::hash_type hash; -}; - -struct SelectorWildcard {}; - -// RFC 9535 Section 2.3.3: "A non-negative index-selector applies to arrays -// only" and "a negative index-selector counts from the array end" -struct SelectorIndex { - std::int64_t index; -}; - -// RFC 9535 Section 2.3.4.2.2: the default start and end depend on the sign -// of the step, so their absence must be preserved -struct SelectorSlice { - std::optional start; - std::optional end; - std::int64_t step; -}; - -// RFC 9535 Section 2.3.5.1: a filter query embedded in a logical expression, -// evaluated relative to the candidate node or to the query root. Whether the -// query is singular is determined at parse time, as comparisons and value -// typed function arguments only admit singular queries -struct FilterQuery { - bool relative; - std::vector segments; - bool singular; -}; - -enum class FilterFunctionName : std::uint8_t { - Length, - Count, - Match, - Search, - Value -}; - -enum class FilterComparisonOperator : std::uint8_t { - Equal, - NotEqual, - Less, - LessEqual, - Greater, - GreaterEqual -}; - -// A comparable or function argument per RFC 9535 Sections 2.3.5.1 and 2.4.3: -// a literal, a query, or a nested function call -using FilterOperand = - std::variant>; - -struct FilterFunctionCall { - FilterFunctionName function; - std::vector arguments; - // A string literal pattern of the match and search functions compiles at - // parse time, and stays empty when the pattern is not a valid RFC 9485 - // expression, which evaluates as a false outcome rather than an error - std::optional compiled; -}; - -struct FilterComparison { - FilterOperand left; - FilterComparisonOperator operation; - FilterOperand right; -}; - -struct FilterTest { - bool negated; - std::variant> subject; -}; - -struct FilterExpression; - -struct FilterConjunction { - std::vector children; -}; - -struct FilterDisjunction { - std::vector children; -}; - -struct FilterNegation { - std::unique_ptr child; -}; - -struct FilterExpression { - std::variant - value; -}; - -struct SelectorFilter { - FilterExpression expression; -}; - -using Selector = std::variant; - -// For fast internal dispatching. It must stay in sync with the variant above -enum class SelectorKind : std::uint8_t { - Name = 0, - Wildcard, - Index, - Slice, - Filter -}; - -struct JSONPathSegment { - bool descendant; - std::vector selectors; -}; - -struct JSONPathQuery { - std::vector segments; -}; +using JSONPathQuery = JSONPath::Query; +using JSONPathSegment = JSONPath::Segment; +using Selector = JSONPath::Selector; +using SelectorKind = JSONPath::SelectorKind; +using SelectorName = JSONPath::SelectorName; +using SelectorWildcard = JSONPath::SelectorWildcard; +using SelectorIndex = JSONPath::SelectorIndex; +using SelectorSlice = JSONPath::SelectorSlice; +using SelectorFilter = JSONPath::SelectorFilter; +using FilterQuery = JSONPath::FilterQuery; +using FilterOperand = JSONPath::FilterOperand; +using FilterFunctionName = JSONPath::FilterFunctionName; +using FilterFunctionCall = JSONPath::FilterFunctionCall; +using FilterComparisonOperator = JSONPath::FilterComparisonOperator; +using FilterComparison = JSONPath::FilterComparison; +using FilterTest = JSONPath::FilterTest; +using FilterExpression = JSONPath::FilterExpression; +using FilterConjunction = JSONPath::FilterConjunction; +using FilterDisjunction = JSONPath::FilterDisjunction; +using FilterNegation = JSONPath::FilterNegation; } // namespace sourcemeta::core diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h index ddc35281bb..36d3dc5ee0 100644 --- a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -11,9 +11,12 @@ #include #include +#include +#include // std::int64_t, std::uint8_t #include // std::function -#include // std::unique_ptr +#include // std::optional +#include // std::variant #include // std::vector /// @defgroup jsonpath JSONPath @@ -35,25 +38,193 @@ namespace sourcemeta::core { #pragma warning(disable : 4251) #endif -// The opaque compiled representation of a query -struct JSONPathQuery; - /// @ingroup jsonpath /// A parsed RFC 9535 JSONPath query that can be evaluated many times. class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { public: - /// A single query result, pairing a value with its location - struct Node { - /// The matched value within the queried document - const JSON *value; - /// The location of the matched value within the queried document - WeakPointer location; - }; - /// The callback invoked for every query result node using Callback = std::function; + /// A selector that matches a single object member by name + struct SelectorName { + /// The decoded member name + JSON::String name; + /// The precomputed object key hash of the member name + JSON::Object::hash_type hash; + }; + + /// A selector that matches every member or element of a node + struct SelectorWildcard {}; + + /// A selector that matches a single array element by position + struct SelectorIndex { + /// The element position, where a negative value counts from the end + std::int64_t index; + }; + + /// A selector that matches a range of array elements + struct SelectorSlice { + /// The position the range starts at, if any + std::optional start; + /// The position the range stops before, if any + std::optional end; + /// The distance between selected positions + std::int64_t step; + }; + + /// The function extensions that filter expressions can invoke + enum class FilterFunctionName : std::uint8_t { + /// The length of a string, array, or object value + Length, + /// The number of nodes a query selects + Count, + /// Whether a string entirely matches a regular expression + Match, + /// Whether a string contains a match of a regular expression + Search, + /// The value of the single node a query selects + Value + }; + + /// The operators that filter comparisons can use + enum class FilterComparisonOperator : std::uint8_t { + /// Both sides are equal + Equal, + /// Both sides are not equal + NotEqual, + /// The left side orders before the right side + Less, + /// The left side orders before or equals the right side + LessEqual, + /// The right side orders before the left side + Greater, + /// The right side orders before or equals the left side + GreaterEqual + }; + +#if !defined(DOXYGEN) + // Required by the recursive grammar and defined further below + struct Segment; +#endif + + /// A query embedded in a filter expression + struct FilterQuery { + /// Whether the query starts at the candidate node instead of the root + bool relative; + /// The segments of the query + std::vector segments; + /// Whether the query selects at most one node + bool singular; + }; + +#if !defined(DOXYGEN) + // Required by the recursive grammar and defined further below + struct FilterOperand; +#endif + + /// A function invocation within a filter expression + struct FilterFunctionCall { + /// The function to invoke + FilterFunctionName function; + /// The arguments to invoke the function with + std::vector arguments; + /// The compiled form of a constant regular expression argument, which + /// stays empty when the pattern is not a valid RFC 9485 expression + std::optional compiled; + }; + + /// A comparison side or function argument within a filter expression + struct FilterOperand { + /// The literal, query, or function invocation this operand stands for + std::variant value; + }; + + /// A comparison between two filter operands + struct FilterComparison { + /// The left side of the comparison + FilterOperand left; + /// The operator to compare with + FilterComparisonOperator operation; + /// The right side of the comparison + FilterOperand right; + }; + + /// An existence or function test within a filter expression + struct FilterTest { + /// Whether the outcome of the test is negated + bool negated; + /// The query or function invocation under test + std::variant subject; + }; + +#if !defined(DOXYGEN) + // Required by the recursive grammar and defined further below + struct FilterExpression; +#endif + + /// A conjunction of filter expressions + struct FilterConjunction { + /// The expressions that must all hold + std::vector children; + }; + + /// A disjunction of filter expressions + struct FilterDisjunction { + /// The expressions of which at least one must hold + std::vector children; + }; + + /// A negated parenthesized filter expression + struct FilterNegation { + /// The single expression whose outcome is negated + std::vector children; + }; + + /// A logical expression within a filter selector + struct FilterExpression { + /// The comparison, test, or combination this expression stands for + std::variant + value; + }; + + /// A selector that matches the members or elements a logical expression + /// holds for + struct SelectorFilter { + /// The logical expression to apply + FilterExpression expression; + }; + + /// A single selector within a query segment + using Selector = std::variant; + +#if !defined(DOXYGEN) + // For fast internal dispatching. It must stay in sync with the variant above + enum class SelectorKind : std::uint8_t { + Name = 0, + Wildcard, + Index, + Slice, + Filter + }; +#endif + + /// A step in a query + struct Segment { + /// Whether the segment also applies to every descendant of its input + bool descendant; + /// The selectors the segment applies + std::vector selectors; + }; + + /// The compiled representation of a whole query + struct Query { + /// The segments of the query + std::vector segments; + }; + /// Parse a query expression, throwing on invalid input. For example: /// /// ```cpp @@ -80,22 +251,6 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { /// ``` auto evaluate(const JSON &document, const Callback &callback) const -> void; - /// Evaluate the query against a document, materializing the result - /// nodelist. For example: - /// - /// ```cpp - /// #include - /// #include - /// - /// const sourcemeta::core::JSON document{ - /// sourcemeta::core::parse_json("{ \"foo\": [ 1, 2 ] }")}; - /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; - /// const auto nodes{path.evaluate(document)}; - /// assert(nodes.size() == 1); - /// assert(nodes.front().value->is_integer()); - /// ``` - [[nodiscard]] auto evaluate(const JSON &document) const -> std::vector; - /// Serialize a location into the RFC 9535 normalized path form. For /// example: /// @@ -106,28 +261,16 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { /// const sourcemeta::core::JSON document{ /// sourcemeta::core::parse_json("{ \"foo\": [ 1, 2 ] }")}; /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; - /// const auto nodes{path.evaluate(document)}; - /// assert(sourcemeta::core::JSONPath::normalize( - /// nodes.front().location) == "$['foo'][0]"); + /// path.evaluate(document, [](const auto &value, const auto &location) { + /// assert(sourcemeta::core::JSONPath::normalize(location) == + /// "$['foo'][0]"); + /// }); /// ``` [[nodiscard]] static auto normalize(const WeakPointer &location) -> JSON::String; - /// Destruct a query - ~JSONPath(); - - /// Move construct a query - JSONPath(JSONPath &&other) noexcept; - - /// Move assign a query - auto operator=(JSONPath &&other) noexcept -> JSONPath &; - - // To avoid mistakes - JSONPath(const JSONPath &) = delete; - auto operator=(const JSONPath &) -> JSONPath & = delete; - private: - std::unique_ptr internal_; + Query query_; }; #if defined(_MSC_VER) diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index daaf3a3882..2d08dc0181 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -4,35 +4,18 @@ #include "grammar.h" #include "parser.h" -#include // std::make_unique -#include // std::to_string -#include // std::move -#include // std::vector +#include // std::to_string namespace sourcemeta::core { JSONPath::JSONPath(const JSON::StringView expression) - : internal_{std::make_unique(parse_jsonpath(expression))} {} - -JSONPath::~JSONPath() = default; -JSONPath::JSONPath(JSONPath &&other) noexcept = default; -auto JSONPath::operator=(JSONPath &&other) noexcept -> JSONPath & = default; + : query_{parse_jsonpath(expression)} {} auto JSONPath::evaluate(const JSON &document, const Callback &callback) const -> void { EvaluationState state{ .root = &document, .callback = &callback, .location = WeakPointer{}}; - evaluate_segments(this->internal_->segments, 0, document, state); -} - -auto JSONPath::evaluate(const JSON &document) const -> std::vector { - std::vector result; - this->evaluate( - document, - [&result](const JSON &value, const WeakPointer &location) -> void { - result.push_back({.value = &value, .location = location}); - }); - return result; + evaluate_segments(this->query_.segments, 0, document, state); } auto JSONPath::normalize(const WeakPointer &location) -> JSON::String { diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index 53284dc886..2757345a1a 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -14,7 +14,6 @@ #include // std::size_t #include // std::int64_t, std::uint32_t #include // std::strtod -#include // std::make_unique, std::unique_ptr #include // std::optional, std::nullopt #include // std::string #include // std::string_view @@ -565,9 +564,10 @@ class JSONPathParser { } if (this->peek() == '(') { - auto inner{this->parse_parenthesized()}; - return FilterExpression{FilterNegation{ - std::make_unique(std::move(inner))}}; + std::vector children; + children.push_back(this->parse_parenthesized()); + return FilterExpression{ + FilterNegation{.children = std::move(children)}}; } return FilterExpression{this->parse_test(true)}; @@ -607,8 +607,8 @@ class JSONPathParser { auto call{this->parse_function()}; // RFC 9535 Section 2.4.3: a test expression admits functions whose // declared result is of logical or nodes type only - if (call->function != FilterFunctionName::Match && - call->function != FilterFunctionName::Search) { + if (call.function != FilterFunctionName::Match && + call.function != FilterFunctionName::Search) { this->fail(); } @@ -684,7 +684,7 @@ class JSONPathParser { this->skip_whitespace(); auto right{this->parse_comparable()}; return FilterExpression{ - FilterComparison{.left = FilterOperand{std::move(query)}, + FilterComparison{.left = FilterOperand{.value = std::move(query)}, .operation = operation, .right = std::move(right)}}; } @@ -706,7 +706,7 @@ class JSONPathParser { this->skip_whitespace(); auto right{this->parse_comparable()}; return FilterExpression{ - FilterComparison{.left = FilterOperand{std::move(literal)}, + FilterComparison{.left = FilterOperand{.value = std::move(literal)}, .operation = operation, .right = std::move(right)}}; } @@ -716,10 +716,9 @@ class JSONPathParser { const auto saved{this->position_}; this->skip_whitespace(); if (this->comparison_operator_ahead()) { - if (std::holds_alternative>( - operand) && + if (std::holds_alternative(operand.value) && !is_value_type_function( - *std::get>(operand))) { + std::get(operand.value))) { this->fail(); } @@ -732,16 +731,14 @@ class JSONPathParser { } this->position_ = saved; - if (!std::holds_alternative>( - operand)) { + if (!std::holds_alternative(operand.value)) { // A literal is not a valid test expression this->fail(); } - auto call{ - std::move(std::get>(operand))}; - if (call->function != FilterFunctionName::Match && - call->function != FilterFunctionName::Search) { + auto call{std::move(std::get(operand.value))}; + if (call.function != FilterFunctionName::Match && + call.function != FilterFunctionName::Search) { this->fail(); } @@ -772,20 +769,19 @@ class JSONPathParser { this->fail(); } - return FilterOperand{std::move(query)}; + return FilterOperand{.value = std::move(query)}; } if (character == '"' || character == '\'' || character == '-' || is_digit(character)) { - return FilterOperand{this->parse_filter_literal()}; + return FilterOperand{.value = this->parse_filter_literal()}; } if (is_lower_alpha(character)) { auto operand{this->parse_keyword_or_function()}; - if (std::holds_alternative>( - operand) && + if (std::holds_alternative(operand.value) && !is_value_type_function( - *std::get>(operand))) { + std::get(operand.value))) { this->fail(); } @@ -900,19 +896,19 @@ class JSONPathParser { const auto identifier{this->parse_identifier()}; if (!this->at_end() && this->peek() == '(') { this->position_ = saved; - return FilterOperand{this->parse_function()}; + return FilterOperand{.value = this->parse_function()}; } if (identifier == "true") { - return FilterOperand{JSON{true}}; + return FilterOperand{.value = JSON{true}}; } if (identifier == "false") { - return FilterOperand{JSON{false}}; + return FilterOperand{.value = JSON{false}}; } if (identifier == "null") { - return FilterOperand{JSON{nullptr}}; + return FilterOperand{.value = JSON{nullptr}}; } this->fail(); @@ -920,19 +916,19 @@ class JSONPathParser { // function-expr = function-name "(" S [function-argument // *(S "," S function-argument)] S ")" - auto parse_function() -> std::unique_ptr { + auto parse_function() -> FilterFunctionCall { const auto identifier{this->parse_identifier()}; - auto call{std::make_unique()}; + FilterFunctionCall call; if (identifier == "length") { - call->function = FilterFunctionName::Length; + call.function = FilterFunctionName::Length; } else if (identifier == "count") { - call->function = FilterFunctionName::Count; + call.function = FilterFunctionName::Count; } else if (identifier == "match") { - call->function = FilterFunctionName::Match; + call.function = FilterFunctionName::Match; } else if (identifier == "search") { - call->function = FilterFunctionName::Search; + call.function = FilterFunctionName::Search; } else if (identifier == "value") { - call->function = FilterFunctionName::Value; + call.function = FilterFunctionName::Value; } else { this->fail(); } @@ -949,7 +945,7 @@ class JSONPathParser { if (this->peek() != ')') { while (true) { - call->arguments.push_back(this->parse_function_argument()); + call.arguments.push_back(this->parse_function_argument()); this->skip_whitespace(); if (!this->at_end() && this->peek() == ',') { this->position_ += 1; @@ -965,7 +961,7 @@ class JSONPathParser { } this->position_ += 1; - this->check_function(*call); + this->check_function(call); return call; } @@ -977,12 +973,12 @@ class JSONPathParser { const char character{this->peek()}; if (character == '@' || character == '$') { bool singular{true}; - return FilterOperand{this->parse_filter_query(singular)}; + return FilterOperand{.value = this->parse_filter_query(singular)}; } if (character == '"' || character == '\'' || character == '-' || is_digit(character)) { - return FilterOperand{this->parse_filter_literal()}; + return FilterOperand{.value = this->parse_filter_literal()}; } if (is_lower_alpha(character)) { @@ -997,16 +993,15 @@ class JSONPathParser { [[nodiscard]] static auto is_value_type_operand(const FilterOperand &operand) -> bool { - if (std::holds_alternative(operand)) { + if (std::holds_alternative(operand.value)) { return true; } - if (std::holds_alternative(operand)) { - return std::get(operand).singular; + if (std::holds_alternative(operand.value)) { + return std::get(operand.value).singular; } - return is_value_type_function( - *std::get>(operand)); + return is_value_type_function(std::get(operand.value)); } // RFC 9535 Section 2.4.3: "the function's arguments need to be well-typed @@ -1022,8 +1017,8 @@ class JSONPathParser { return; case FilterFunctionName::Count: case FilterFunctionName::Value: - if (call.arguments.size() != 1 || - !std::holds_alternative(call.arguments.front())) { + if (call.arguments.size() != 1 || !std::holds_alternative( + call.arguments.front().value)) { this->fail(); } @@ -1036,10 +1031,10 @@ class JSONPathParser { this->fail(); } - if (std::holds_alternative(call.arguments.back()) && - std::get(call.arguments.back()).is_string()) { + if (std::holds_alternative(call.arguments.back().value) && + std::get(call.arguments.back().value).is_string()) { call.compiled = - to_regex(std::get(call.arguments.back()).to_string(), + to_regex(std::get(call.arguments.back().value).to_string(), call.function == FilterFunctionName::Match ? RegexDialect::IRegexp : RegexDialect::IRegexpSearch); diff --git a/test/jsonpath/jsonpath_evaluate_test.cc b/test/jsonpath/jsonpath_evaluate_test.cc index e4d5c2f786..08bb16f0a1 100644 --- a/test/jsonpath/jsonpath_evaluate_test.cc +++ b/test/jsonpath/jsonpath_evaluate_test.cc @@ -2,10 +2,32 @@ #include #include +namespace { + +struct ResultNode { + const sourcemeta::core::JSON *value; + sourcemeta::core::WeakPointer location; +}; + +auto evaluate_nodes(const sourcemeta::core::JSONPath &path, + const sourcemeta::core::JSON &document) + -> std::vector { + std::vector result; + path.evaluate( + document, + [&result](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &location) -> void { + result.push_back({&value, location}); + }); + return result; +} + +} // namespace + TEST(jsonpath_evaluate_root) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_TRUE(*nodes.at(0).value == document); } @@ -14,7 +36,7 @@ TEST(jsonpath_evaluate_name) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": 1, "b": 2 })JSON")}; const sourcemeta::core::JSONPath path{"$.b"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_TRUE(nodes.at(0).value->is_integer()); EXPECT_EQ(nodes.at(0).value->to_integer(), 2); @@ -23,14 +45,14 @@ TEST(jsonpath_evaluate_name) { TEST(jsonpath_evaluate_name_absent) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.b"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } TEST(jsonpath_evaluate_name_on_array) { const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 2 ])JSON")}; const sourcemeta::core::JSONPath path{"$.a"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -38,7 +60,7 @@ TEST(jsonpath_evaluate_nested_names) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": { "b": { "c": 42 } } })JSON")}; const sourcemeta::core::JSONPath path{"$.a.b.c"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 42); } @@ -47,7 +69,7 @@ TEST(jsonpath_evaluate_index) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; const sourcemeta::core::JSONPath path{"$[1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 20); } @@ -56,7 +78,7 @@ TEST(jsonpath_evaluate_index_negative) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; const sourcemeta::core::JSONPath path{"$[-1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 30); } @@ -65,14 +87,14 @@ TEST(jsonpath_evaluate_index_out_of_bounds) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; const sourcemeta::core::JSONPath path{"$[3]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } TEST(jsonpath_evaluate_index_on_object) { const auto document{sourcemeta::core::parse_json(R"JSON({ "0": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$[0]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -80,7 +102,7 @@ TEST(jsonpath_evaluate_wildcard_object) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": 1, "b": 2 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_EQ(nodes.at(0).value->to_integer(), 1); EXPECT_EQ(nodes.at(1).value->to_integer(), 2); @@ -89,7 +111,7 @@ TEST(jsonpath_evaluate_wildcard_object) { TEST(jsonpath_evaluate_wildcard_array) { const auto document{sourcemeta::core::parse_json(R"JSON([ 10, 20 ])JSON")}; const sourcemeta::core::JSONPath path{"$[*]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_EQ(nodes.at(0).value->to_integer(), 10); EXPECT_EQ(nodes.at(1).value->to_integer(), 20); @@ -98,7 +120,7 @@ TEST(jsonpath_evaluate_wildcard_array) { TEST(jsonpath_evaluate_wildcard_scalar) { const auto document{sourcemeta::core::parse_json("42")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -106,7 +128,7 @@ TEST(jsonpath_evaluate_slice_forward) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 0, 1, 2, 3, 4, 5 ])JSON")}; const sourcemeta::core::JSONPath path{"$[1:5:2]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_EQ(nodes.at(0).value->to_integer(), 1); EXPECT_EQ(nodes.at(1).value->to_integer(), 3); @@ -115,7 +137,7 @@ TEST(jsonpath_evaluate_slice_forward) { TEST(jsonpath_evaluate_slice_negative_step) { const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; const sourcemeta::core::JSONPath path{"$[::-1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 3); EXPECT_EQ(nodes.at(0).value->to_integer(), 2); EXPECT_EQ(nodes.at(1).value->to_integer(), 1); @@ -125,7 +147,7 @@ TEST(jsonpath_evaluate_slice_negative_step) { TEST(jsonpath_evaluate_slice_zero_step) { const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; const sourcemeta::core::JSONPath path{"$[::0]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -133,7 +155,7 @@ TEST(jsonpath_evaluate_slice_negative_bounds) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 0, 1, 2, 3, 4 ])JSON")}; const sourcemeta::core::JSONPath path{"$[-3:-1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_EQ(nodes.at(0).value->to_integer(), 2); EXPECT_EQ(nodes.at(1).value->to_integer(), 3); @@ -142,7 +164,7 @@ TEST(jsonpath_evaluate_slice_negative_bounds) { TEST(jsonpath_evaluate_multiple_selectors_duplicates) { const auto document{sourcemeta::core::parse_json(R"JSON([ 10, 20 ])JSON")}; const sourcemeta::core::JSONPath path{"$[0, 0, -2]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 3); EXPECT_EQ(nodes.at(0).value->to_integer(), 10); EXPECT_EQ(nodes.at(1).value->to_integer(), 10); @@ -153,7 +175,7 @@ TEST(jsonpath_evaluate_descendant_names) { const auto document{sourcemeta::core::parse_json( R"JSON({ "a": { "a": 1 }, "b": [ { "a": 2 } ] })JSON")}; const sourcemeta::core::JSONPath path{"$..a"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 3); EXPECT_TRUE(nodes.at(0).value->is_object()); EXPECT_EQ(nodes.at(1).value->to_integer(), 1); @@ -164,7 +186,7 @@ TEST(jsonpath_evaluate_descendant_wildcard) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": [ 1 ] })JSON")}; const sourcemeta::core::JSONPath path{"$..*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_TRUE(nodes.at(0).value->is_array()); EXPECT_EQ(nodes.at(1).value->to_integer(), 1); @@ -174,7 +196,7 @@ TEST(jsonpath_evaluate_descendant_then_child) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "x": { "y": { "z": 1 } } })JSON")}; const sourcemeta::core::JSONPath path{"$..y.z"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 1); } @@ -194,7 +216,7 @@ TEST(jsonpath_evaluate_callback_matches_vector) { TEST(jsonpath_evaluate_unicode_name) { const auto document{sourcemeta::core::parse_json(R"JSON({ "á": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.á"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 1); } @@ -202,15 +224,23 @@ TEST(jsonpath_evaluate_unicode_name) { TEST(jsonpath_evaluate_escaped_name_decodes) { const auto document{sourcemeta::core::parse_json(R"JSON({ "A": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$['\\u0041']"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(nodes.at(0).value->to_integer(), 1); } +TEST(jsonpath_evaluate_copy_construction) { + const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; + const sourcemeta::core::JSONPath original{"$.a"}; + const sourcemeta::core::JSONPath copy{original}; + EXPECT_EQ(evaluate_nodes(original, document).size(), 1); + EXPECT_EQ(evaluate_nodes(copy, document).size(), 1); +} + TEST(jsonpath_evaluate_move_construction) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; sourcemeta::core::JSONPath original{"$.a"}; const sourcemeta::core::JSONPath moved{std::move(original)}; - const auto nodes{moved.evaluate(document)}; + const auto nodes{evaluate_nodes(moved, document)}; EXPECT_EQ(nodes.size(), 1); } diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index 5d595ece7c..bd24810105 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -2,11 +2,33 @@ #include #include +namespace { + +struct ResultNode { + const sourcemeta::core::JSON *value; + sourcemeta::core::WeakPointer location; +}; + +auto evaluate_nodes(const sourcemeta::core::JSONPath &path, + const sourcemeta::core::JSON &document) + -> std::vector { + std::vector result; + path.evaluate( + document, + [&result](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &location) -> void { + result.push_back({&value, location}); + }); + return result; +} + +} // namespace + TEST(jsonpath_filter_existence) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "b": 2 }, { "a": null } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -14,7 +36,7 @@ TEST(jsonpath_filter_existence_null_is_present) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": null } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -22,7 +44,7 @@ TEST(jsonpath_filter_negated_existence) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "b": 2 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?!@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -30,7 +52,7 @@ TEST(jsonpath_filter_equals_integer) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "a": 2 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == 2]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -38,7 +60,7 @@ TEST(jsonpath_filter_equals_integer_and_real) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 1 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == 1.0]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -46,7 +68,7 @@ TEST(jsonpath_filter_equals_deep) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": [ 1, 2 ] }, { "a": [ 1, 3 ] }, { "a": [ 1, 2 ] } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == $[0].a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -54,7 +76,7 @@ TEST(jsonpath_filter_equals_absent_both_sides) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "b": 1 }, { "c": 2 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.x == @.y]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -62,7 +84,7 @@ TEST(jsonpath_filter_equals_absent_one_side) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": null } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == @.b]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -70,7 +92,7 @@ TEST(jsonpath_filter_not_equals) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "a": 2 }, {} ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a != 1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -78,7 +100,7 @@ TEST(jsonpath_filter_less_than_numbers) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "a": 2.5 }, { "a": 3 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a < 3]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -86,7 +108,7 @@ TEST(jsonpath_filter_less_than_strings) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "apple" }, { "a": "banana" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a < 'b']"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -94,14 +116,14 @@ TEST(jsonpath_filter_less_than_mixed_types) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 1 }, { "a": "1" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a < '2']"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } TEST(jsonpath_filter_less_equal_absent) { const auto document{sourcemeta::core::parse_json(R"JSON([ {} ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a <= @.b]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -109,7 +131,7 @@ TEST(jsonpath_filter_boolean_literals) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": true }, { "a": false } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == true]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -117,7 +139,7 @@ TEST(jsonpath_filter_null_literal) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": null }, { "a": 0 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a == null]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -125,7 +147,7 @@ TEST(jsonpath_filter_conjunction) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1, "b": 2 }, { "a": 1 }, { "b": 2 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a && @.b]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -133,7 +155,7 @@ TEST(jsonpath_filter_disjunction) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "b": 2 }, { "c": 3 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a || @.b]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -141,7 +163,7 @@ TEST(jsonpath_filter_negated_parenthesized) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "b": 2 }, { "a": 1, "b": 2 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?!(@.a && @.b)]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -149,7 +171,7 @@ TEST(jsonpath_filter_on_object_members) { const auto document{sourcemeta::core::parse_json( R"JSON({ "x": { "a": 1 }, "y": { "b": 2 } })JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -157,7 +179,7 @@ TEST(jsonpath_filter_absolute_query) { const auto document{sourcemeta::core::parse_json( R"JSON({ "limit": 2, "values": [ 1, 2, 3 ] })JSON")}; const sourcemeta::core::JSONPath path{"$.values[?@ <= $.limit]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -165,7 +187,7 @@ TEST(jsonpath_filter_nested_filter) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": [ { "b": 1 } ] }, { "a": [ { "c": 2 } ] } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a[?@.b]]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -173,7 +195,7 @@ TEST(jsonpath_filter_length_string) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "ab" }, { "a": "abc" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?length(@.a) == 2]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -181,7 +203,7 @@ TEST(jsonpath_filter_length_counts_code_points) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": "héllo" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?length(@.a) == 5]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -189,16 +211,16 @@ TEST(jsonpath_filter_length_array_and_object) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": [ 1, 2, 3 ] }, { "a": { "x": 1 } } ])JSON")}; const sourcemeta::core::JSONPath first{"$[?length(@.a) == 3]"}; - EXPECT_EQ(first.evaluate(document).size(), 1); + EXPECT_EQ(evaluate_nodes(first, document).size(), 1); const sourcemeta::core::JSONPath second{"$[?length(@.a) == 1]"}; - EXPECT_EQ(second.evaluate(document).size(), 1); + EXPECT_EQ(evaluate_nodes(second, document).size(), 1); } TEST(jsonpath_filter_length_of_number_is_nothing) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 5 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?length(@.a) == @.b]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -206,7 +228,7 @@ TEST(jsonpath_filter_count) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": [ 1, 2 ] }, { "a": [ 1 ] } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?count(@.a[*]) == 2]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -214,7 +236,7 @@ TEST(jsonpath_filter_value_single_node) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": [ { "b": 7 } ] }, { "a": [ { "b": 7 }, { "b": 7 } ] } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?value(@..b) == 7]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -222,7 +244,7 @@ TEST(jsonpath_filter_match_full) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "ab" }, { "a": "xabx" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, 'ab')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -230,7 +252,7 @@ TEST(jsonpath_filter_search_substring) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "ab" }, { "a": "xabx" }, { "a": "xx" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?search(@.a, 'ab')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } @@ -238,7 +260,7 @@ TEST(jsonpath_filter_match_non_string_input) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": 1 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, 'a.*')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -246,7 +268,7 @@ TEST(jsonpath_filter_match_invalid_pattern) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": "x" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, '\\\\d')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 0); } @@ -254,7 +276,7 @@ TEST(jsonpath_filter_match_dynamic_pattern) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "ab", "p": "a." }, { "a": "ab", "p": "b." } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, @.p)]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -262,7 +284,7 @@ TEST(jsonpath_filter_match_dot_excludes_line_feed) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": "x\ny" }, { "a": "xzy" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, 'x.y')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } @@ -270,13 +292,13 @@ TEST(jsonpath_filter_match_dot_accepts_line_separator) { const auto document{ sourcemeta::core::parse_json(R"JSON([ { "a": "x\u2028y" } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?match(@.a, 'x.y')]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); } TEST(jsonpath_filter_current_node_comparison) { const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 5, 10 ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@ > 4]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); } diff --git a/test/jsonpath/jsonpath_normalize_test.cc b/test/jsonpath/jsonpath_normalize_test.cc index 1f70084b6f..f5288ce01a 100644 --- a/test/jsonpath/jsonpath_normalize_test.cc +++ b/test/jsonpath/jsonpath_normalize_test.cc @@ -2,10 +2,32 @@ #include #include +namespace { + +struct ResultNode { + const sourcemeta::core::JSON *value; + sourcemeta::core::WeakPointer location; +}; + +auto evaluate_nodes(const sourcemeta::core::JSONPath &path, + const sourcemeta::core::JSON &document) + -> std::vector { + std::vector result; + path.evaluate( + document, + [&result](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &location) -> void { + result.push_back({&value, location}); + }); + return result; +} + +} // namespace + TEST(jsonpath_normalize_root) { const auto document{sourcemeta::core::parse_json(R"JSON(1)JSON")}; const sourcemeta::core::JSONPath path{"$"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$"); } @@ -13,7 +35,7 @@ TEST(jsonpath_normalize_property_and_index) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": [ { "b": 1 } ] })JSON")}; const sourcemeta::core::JSONPath path{"$..b"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['a'][0]['b']"); @@ -22,7 +44,7 @@ TEST(jsonpath_normalize_property_and_index) { TEST(jsonpath_normalize_single_quote_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a'b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['a\\'b']"); } @@ -30,7 +52,7 @@ TEST(jsonpath_normalize_single_quote_escaped) { TEST(jsonpath_normalize_backslash_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a\\b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['a\\\\b']"); } @@ -38,7 +60,7 @@ TEST(jsonpath_normalize_backslash_escaped) { TEST(jsonpath_normalize_double_quote_not_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a\"b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['a\"b']"); } @@ -47,7 +69,7 @@ TEST(jsonpath_normalize_two_character_escapes) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "\b\f\n\r\t": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['\\b\\f\\n\\r\\t']"); } @@ -56,7 +78,7 @@ TEST(jsonpath_normalize_control_hexadecimal) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "\u000b\u0000\u001f": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['\\u000b\\u0000\\u001f']"); } @@ -64,7 +86,7 @@ TEST(jsonpath_normalize_control_hexadecimal) { TEST(jsonpath_normalize_non_ascii_verbatim) { const auto document{sourcemeta::core::parse_json(R"JSON({ "á🤔": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['á🤔']"); } @@ -72,7 +94,7 @@ TEST(jsonpath_normalize_non_ascii_verbatim) { TEST(jsonpath_normalize_deep_indexes) { const auto document{sourcemeta::core::parse_json(R"JSON([ [ [ 1 ] ] ])JSON")}; const sourcemeta::core::JSONPath path{"$[0][0][0]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$[0][0][0]"); } @@ -81,7 +103,7 @@ TEST(jsonpath_normalize_negative_index_resolved) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; const sourcemeta::core::JSONPath path{"$[-1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$[2]"); @@ -90,7 +112,7 @@ TEST(jsonpath_normalize_negative_index_resolved) { TEST(jsonpath_normalize_slice_negative_step) { const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; const sourcemeta::core::JSONPath path{"$[::-1]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 3); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$[2]"); @@ -104,7 +126,7 @@ TEST(jsonpath_normalize_descendant_document_order) { const auto document{sourcemeta::core::parse_json( R"JSON({ "a": { "a": 1 }, "b": [ { "a": 2 } ] })JSON")}; const sourcemeta::core::JSONPath path{"$..a"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 3); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['a']"); @@ -118,7 +140,7 @@ TEST(jsonpath_normalize_filter_locations) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "b": 2 }, { "a": null } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 2); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$[0]"); @@ -130,7 +152,7 @@ TEST(jsonpath_normalize_filter_object_member_location) { const auto document{sourcemeta::core::parse_json( R"JSON({ "x": { "a": 1 }, "y": { "b": 2 } })JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{path.evaluate(document)}; + const auto nodes{evaluate_nodes(path, document)}; EXPECT_EQ(nodes.size(), 1); EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$['x']"); diff --git a/test/jsonpath/jsonpath_suite.cc b/test/jsonpath/jsonpath_suite.cc index 08e85bd2cd..10496164f9 100644 --- a/test/jsonpath/jsonpath_suite.cc +++ b/test/jsonpath/jsonpath_suite.cc @@ -10,7 +10,25 @@ namespace { -auto values_match(const std::vector &nodes, +struct ResultNode { + const sourcemeta::core::JSON *value; + sourcemeta::core::WeakPointer location; +}; + +auto evaluate_nodes(const sourcemeta::core::JSONPath &path, + const sourcemeta::core::JSON &document) + -> std::vector { + std::vector result; + path.evaluate( + document, + [&result](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &location) -> void { + result.push_back({&value, location}); + }); + return result; +} + +auto values_match(const std::vector &nodes, const sourcemeta::core::JSON &expected) -> bool { const auto &array{expected.as_array()}; if (nodes.size() != array.size()) { @@ -26,7 +44,7 @@ auto values_match(const std::vector &nodes, return true; } -auto paths_match(const std::vector &nodes, +auto paths_match(const std::vector &nodes, const sourcemeta::core::JSON &expected) -> bool { const auto &array{expected.as_array()}; if (nodes.size() != array.size()) { @@ -59,7 +77,7 @@ auto run_jsonpath_test_case(const sourcemeta::core::JSON &test_case) -> void { } const sourcemeta::core::JSONPath path{selector}; - const auto nodes{path.evaluate(test_case.at("document"))}; + const auto nodes{evaluate_nodes(path, test_case.at("document"))}; const auto *result{test_case.try_at("result")}; if (result != nullptr) { EXPECT_TRUE(values_match(nodes, *result)); From 1cef6116ca0a9b651fc01e95f40b576ba79070fe Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:02:38 -0300 Subject: [PATCH 03/13] Simpler Signed-off-by: Juan Cruz Viotti --- src/core/jsonpath/CMakeLists.txt | 2 +- src/core/jsonpath/evaluator.h | 635 ------------------------------- src/core/jsonpath/grammar.h | 31 -- src/core/jsonpath/jsonpath.cc | 633 +++++++++++++++++++++++++++++- src/core/jsonpath/parser.h | 226 +++++------ 5 files changed, 750 insertions(+), 777 deletions(-) delete mode 100644 src/core/jsonpath/evaluator.h delete mode 100644 src/core/jsonpath/grammar.h diff --git a/src/core/jsonpath/CMakeLists.txt b/src/core/jsonpath/CMakeLists.txt index 5fd7fa44cd..86a23de371 100644 --- a/src/core/jsonpath/CMakeLists.txt +++ b/src/core/jsonpath/CMakeLists.txt @@ -1,6 +1,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonpath PRIVATE_HEADERS error.h - SOURCES jsonpath.cc grammar.h parser.h evaluator.h) + SOURCES jsonpath.cc parser.h) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jsonpath) diff --git a/src/core/jsonpath/evaluator.h b/src/core/jsonpath/evaluator.h deleted file mode 100644 index 3783632fec..0000000000 --- a/src/core/jsonpath/evaluator.h +++ /dev/null @@ -1,635 +0,0 @@ -#ifndef SOURCEMETA_CORE_JSONPATH_EVALUATOR_H_ -#define SOURCEMETA_CORE_JSONPATH_EVALUATOR_H_ - -#include -#include -#include -#include -#include - -#include "grammar.h" - -#include // assert -#include // std::size_t -#include // std::int64_t -#include // std::cref -#include // std::optional -#include // std::get, std::holds_alternative, std::monostate, std::variant -#include // std::vector - -namespace sourcemeta::core { - -namespace { - -struct EvaluationState { - const JSON *root; - const JSONPath::Callback *callback; - WeakPointer location; -}; - -// The value of a comparable during evaluation: absent, borrowed from the -// document or the compiled query, or computed by a function -using FilterValue = std::variant; - -auto filter_value_pointer(const FilterValue &value) -> const JSON * { - switch (value.index()) { - case 0: - return nullptr; - case 1: - return *std::get_if(&value); - default: - return std::get_if(&value); - } -} - -auto filter_matches(const FilterExpression &expression, const JSON &candidate, - const JSON &root) -> bool; - -// RFC 9535 Section 2.3.4.2.2: slice expression bounds against a concrete -// array length, where an omitted start or end defaults according to the -// sign of the step and a zero step selects nothing -inline auto slice_bounds(const SelectorSlice &slice, const std::int64_t size, - std::int64_t &lower, std::int64_t &upper) -> bool { - if (slice.step == 0) { - return false; - } - - const auto start{slice.start.has_value() ? slice.start.value() - : (slice.step > 0 ? 0 : size - 1)}; - const auto end{slice.end.has_value() ? slice.end.value() - : (slice.step > 0 ? size : -size - 1)}; - const auto normalized_start{start >= 0 ? start : size + start}; - const auto normalized_end{end >= 0 ? end : size + end}; - if (slice.step > 0) { - lower = std::min(std::max(normalized_start, std::int64_t{0}), size); - upper = std::min(std::max(normalized_end, std::int64_t{0}), size); - } else { - upper = std::min(std::max(normalized_start, std::int64_t{-1}), size - 1); - lower = std::min(std::max(normalized_end, std::int64_t{-1}), size - 1); - } - - return true; -} - -// Walk the nodes selected by a filter query without tracking locations. The -// visitor returns whether to continue, and the traversal reports whether it -// ran to completion without the visitor stopping it -template -auto filter_query_visit(const std::vector &segments, - const std::size_t cursor, const JSON ¤t, - const JSON &root, const Visitor &visitor) -> bool { - if (cursor == segments.size()) { - return visitor(current); - } - - const auto &segment{segments[cursor]}; - for (const auto &selector : segment.selectors) { - switch (static_cast(selector.index())) { - case SelectorKind::Name: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - const auto *child{current.try_at(entry->name, entry->hash)}; - if (child != nullptr && !filter_query_visit(segments, cursor + 1, - *child, root, visitor)) { - return false; - } - } - - break; - } - case SelectorKind::Wildcard: - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (!filter_query_visit(segments, cursor + 1, member.second, root, - visitor)) { - return false; - } - } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (!filter_query_visit(segments, cursor + 1, element, root, - visitor)) { - return false; - } - } - } - - break; - case SelectorKind::Index: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; - const auto index{entry->index < 0 ? entry->index + size - : entry->index}; - if (index >= 0 && index < size && - !filter_query_visit(segments, cursor + 1, - current.at(static_cast(index)), - root, visitor)) { - return false; - } - } - - break; - } - case SelectorKind::Slice: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; - std::int64_t lower{0}; - std::int64_t upper{0}; - if (!slice_bounds(*entry, size, lower, upper)) { - break; - } - - if (entry->step > 0) { - for (auto index{lower}; index < upper; index += entry->step) { - if (!filter_query_visit( - segments, cursor + 1, - current.at(static_cast(index)), root, - visitor)) { - return false; - } - } - } else { - for (auto index{upper}; index > lower; index += entry->step) { - if (!filter_query_visit( - segments, cursor + 1, - current.at(static_cast(index)), root, - visitor)) { - return false; - } - } - } - } - - break; - } - case SelectorKind::Filter: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (filter_matches(entry->expression, member.second, root) && - !filter_query_visit(segments, cursor + 1, member.second, root, - visitor)) { - return false; - } - } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (filter_matches(entry->expression, element, root) && - !filter_query_visit(segments, cursor + 1, element, root, - visitor)) { - return false; - } - } - } - - break; - } - } - } - - if (segment.descendant) { - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (!filter_query_visit(segments, cursor, member.second, root, - visitor)) { - return false; - } - } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (!filter_query_visit(segments, cursor, element, root, visitor)) { - return false; - } - } - } - } - - return true; -} - -// RFC 9535 Section 2.3.5.1: a singular query "produces a nodelist containing -// at most one node", so it resolves directly without traversal -inline auto resolve_singular(const FilterQuery &query, const JSON &candidate, - const JSON &root) -> const JSON * { - const auto *current{query.relative ? &candidate : &root}; - for (const auto &segment : query.segments) { - assert(!segment.descendant); - assert(segment.selectors.size() == 1); - const auto &selector{segment.selectors.front()}; - if (std::holds_alternative(selector)) { - const auto *entry{std::get_if(&selector)}; - current = current->is_object() ? current->try_at(entry->name, entry->hash) - : nullptr; - } else { - const auto *entry{std::get_if(&selector)}; - if (current->is_array()) { - const auto size{static_cast(current->as_array().size())}; - const auto index{entry->index < 0 ? entry->index + size : entry->index}; - current = index >= 0 && index < size - ? ¤t->at(static_cast(index)) - : nullptr; - } else { - current = nullptr; - } - } - - if (current == nullptr) { - return nullptr; - } - } - - return current; -} - -auto evaluate_value_function(const FilterFunctionCall &call, - const JSON &candidate, const JSON &root) - -> FilterValue; - -inline auto evaluate_operand(const FilterOperand &operand, - const JSON &candidate, const JSON &root) - -> FilterValue { - if (std::holds_alternative(operand.value)) { - return FilterValue{&std::get(operand.value)}; - } - - if (std::holds_alternative(operand.value)) { - const auto *result{resolve_singular(std::get(operand.value), - candidate, root)}; - return result == nullptr ? FilterValue{} : FilterValue{result}; - } - - return evaluate_value_function(std::get(operand.value), - candidate, root); -} - -inline auto evaluate_value_function(const FilterFunctionCall &call, - const JSON &candidate, const JSON &root) - -> FilterValue { - switch (call.function) { - // RFC 9535 Section 2.4.4: the length function counts string characters, - // array elements, or object members, and is nothing for anything else - case FilterFunctionName::Length: { - const auto value{ - evaluate_operand(call.arguments.front(), candidate, root)}; - const auto *target{filter_value_pointer(value)}; - if (target == nullptr) { - return FilterValue{}; - } - - if (target->is_string()) { - return FilterValue{JSON{static_cast( - utf8_codepoint_count(target->to_string()))}}; - } - - if (target->is_array()) { - return FilterValue{ - JSON{static_cast(target->as_array().size())}}; - } - - if (target->is_object()) { - return FilterValue{ - JSON{static_cast(target->as_object().size())}}; - } - - return FilterValue{}; - } - // RFC 9535 Section 2.4.5: the count function yields the number of nodes - case FilterFunctionName::Count: { - const auto &query{std::get(call.arguments.front().value)}; - std::int64_t count{0}; - filter_query_visit(query.segments, 0, query.relative ? candidate : root, - root, [&count](const JSON &) -> bool { - count += 1; - return true; - }); - return FilterValue{JSON{count}}; - } - // RFC 9535 Section 2.4.8: the value function yields the value of a - // single node and nothing otherwise - case FilterFunctionName::Value: { - const auto &query{std::get(call.arguments.front().value)}; - const JSON *single{nullptr}; - std::size_t count{0}; - filter_query_visit(query.segments, 0, query.relative ? candidate : root, - root, [&single, &count](const JSON &node) -> bool { - single = &node; - count += 1; - return count < 2; - }); - return count == 1 ? FilterValue{single} : FilterValue{}; - } - default: - assert(false); - return FilterValue{}; - } -} - -// RFC 9535 Sections 2.4.6 and 2.4.7: the match function considers the whole -// input while the search function considers any substring, and any argument -// mismatch yields a false outcome rather than an error -inline auto evaluate_logical_function(const FilterFunctionCall &call, - const JSON &candidate, const JSON &root) - -> bool { - const auto input{evaluate_operand(call.arguments.front(), candidate, root)}; - const auto *subject{filter_value_pointer(input)}; - if (subject == nullptr || !subject->is_string()) { - return false; - } - - if (std::holds_alternative(call.arguments.back().value) && - std::get(call.arguments.back().value).is_string()) { - return call.compiled.has_value() && - matches(call.compiled.value(), subject->to_string()); - } - - const auto pattern{evaluate_operand(call.arguments.back(), candidate, root)}; - const auto *expression{filter_value_pointer(pattern)}; - if (expression == nullptr || !expression->is_string()) { - return false; - } - - const auto compiled{to_regex(expression->to_string(), - call.function == FilterFunctionName::Match - ? RegexDialect::IRegexp - : RegexDialect::IRegexpSearch)}; - return compiled.has_value() && - matches(compiled.value(), subject->to_string()); -} - -// RFC 9535 Section 2.3.5.2.2: equality is deep JSON equality where numbers -// compare mathematically, and an absent value equals only an absent value -inline auto filter_equals(const JSON *left, const JSON *right) -> bool { - if (left == nullptr || right == nullptr) { - return left == nullptr && right == nullptr; - } - - return *left == *right; -} - -// RFC 9535 Section 2.3.5.2.2: ordering applies only when both sides are -// numbers or both sides are strings -inline auto filter_less(const JSON *left, const JSON *right) -> bool { - if (left == nullptr || right == nullptr) { - return false; - } - - const bool left_number{left->is_integer() || left->is_real()}; - const bool right_number{right->is_integer() || right->is_real()}; - if (left_number && right_number) { - if (left->is_integer() && right->is_integer()) { - return left->to_integer() < right->to_integer(); - } - - const auto left_value{left->is_integer() - ? static_cast(left->to_integer()) - : left->to_real()}; - const auto right_value{right->is_integer() - ? static_cast(right->to_integer()) - : right->to_real()}; - return left_value < right_value; - } - - if (left->is_string() && right->is_string()) { - return left->to_string() < right->to_string(); - } - - return false; -} - -inline auto filter_compare(const FilterComparison &comparison, - const JSON &candidate, const JSON &root) -> bool { - const auto left{evaluate_operand(comparison.left, candidate, root)}; - const auto right{evaluate_operand(comparison.right, candidate, root)}; - const auto *left_value{filter_value_pointer(left)}; - const auto *right_value{filter_value_pointer(right)}; - switch (comparison.operation) { - case FilterComparisonOperator::Equal: - return filter_equals(left_value, right_value); - case FilterComparisonOperator::NotEqual: - return !filter_equals(left_value, right_value); - case FilterComparisonOperator::Less: - return filter_less(left_value, right_value); - case FilterComparisonOperator::LessEqual: - return filter_less(left_value, right_value) || - filter_equals(left_value, right_value); - case FilterComparisonOperator::Greater: - return filter_less(right_value, left_value); - case FilterComparisonOperator::GreaterEqual: - return filter_less(right_value, left_value) || - filter_equals(left_value, right_value); - } - - assert(false); - return false; -} - -inline auto filter_matches(const FilterExpression &expression, - const JSON &candidate, const JSON &root) -> bool { - if (std::holds_alternative(expression.value)) { - return filter_compare(std::get(expression.value), - candidate, root); - } - - if (std::holds_alternative(expression.value)) { - const auto &test{std::get(expression.value)}; - bool result{false}; - if (std::holds_alternative(test.subject)) { - const auto &query{std::get(test.subject)}; - result = !filter_query_visit(query.segments, 0, - query.relative ? candidate : root, root, - [](const JSON &) -> bool { return false; }); - } else { - result = evaluate_logical_function( - std::get(test.subject), candidate, root); - } - - return test.negated ? !result : result; - } - - if (std::holds_alternative(expression.value)) { - const auto &conjunction{std::get(expression.value)}; - for (const auto &child : conjunction.children) { - if (!filter_matches(child, candidate, root)) { - return false; - } - } - - return true; - } - - if (std::holds_alternative(expression.value)) { - const auto &disjunction{std::get(expression.value)}; - for (const auto &child : disjunction.children) { - if (filter_matches(child, candidate, root)) { - return true; - } - } - - return false; - } - - return !filter_matches( - std::get(expression.value).children.front(), candidate, - root); -} - -auto evaluate_segments(const std::vector &segments, - const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void; - -inline auto evaluate_selectors(const std::vector &segments, - const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { - for (const auto &selector : segments[cursor].selectors) { - switch (static_cast(selector.index())) { - case SelectorKind::Name: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - const auto *child{current.try_at(entry->name, entry->hash)}; - if (child != nullptr) { - state.location.emplace_back(std::cref(entry->name), entry->hash); - evaluate_segments(segments, cursor + 1, *child, state); - state.location.pop_back(); - } - } - - break; - } - case SelectorKind::Wildcard: - if (current.is_object()) { - for (const auto &member : current.as_object()) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_segments(segments, cursor + 1, member.second, state); - state.location.pop_back(); - } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - state.location.emplace_back(index); - evaluate_segments(segments, cursor + 1, current.at(index), state); - state.location.pop_back(); - } - } - - break; - case SelectorKind::Index: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; - const auto index{entry->index < 0 ? entry->index + size - : entry->index}; - if (index >= 0 && index < size) { - state.location.emplace_back(static_cast(index)); - evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), - state); - state.location.pop_back(); - } - } - - break; - } - case SelectorKind::Slice: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; - std::int64_t lower{0}; - std::int64_t upper{0}; - if (!slice_bounds(*entry, size, lower, upper)) { - break; - } - - if (entry->step > 0) { - for (auto index{lower}; index < upper; index += entry->step) { - state.location.emplace_back(static_cast(index)); - evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), - state); - state.location.pop_back(); - } - } else { - for (auto index{upper}; index > lower; index += entry->step) { - state.location.emplace_back(static_cast(index)); - evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), - state); - state.location.pop_back(); - } - } - } - - break; - } - case SelectorKind::Filter: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (filter_matches(entry->expression, member.second, *state.root)) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_segments(segments, cursor + 1, member.second, state); - state.location.pop_back(); - } - } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - if (filter_matches(entry->expression, current.at(index), - *state.root)) { - state.location.emplace_back(index); - evaluate_segments(segments, cursor + 1, current.at(index), state); - state.location.pop_back(); - } - } - } - - break; - } - } - } -} - -// RFC 9535 Section 2.5.2.2: a descendant segment visits the input node and -// every descendant in depth-first document order, applying its selectors at -// each visited node -inline auto evaluate_descendant(const std::vector &segments, - const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { - evaluate_selectors(segments, cursor, current, state); - if (current.is_object()) { - for (const auto &member : current.as_object()) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_descendant(segments, cursor, member.second, state); - state.location.pop_back(); - } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - state.location.emplace_back(index); - evaluate_descendant(segments, cursor, current.at(index), state); - state.location.pop_back(); - } - } -} - -inline auto evaluate_segments(const std::vector &segments, - const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { - if (cursor == segments.size()) { - (*state.callback)(current, state.location); - return; - } - - if (segments[cursor].descendant) { - evaluate_descendant(segments, cursor, current, state); - } else { - evaluate_selectors(segments, cursor, current, state); - } -} - -} // namespace - -} // namespace sourcemeta::core - -#endif diff --git a/src/core/jsonpath/grammar.h b/src/core/jsonpath/grammar.h deleted file mode 100644 index fc7a21ca34..0000000000 --- a/src/core/jsonpath/grammar.h +++ /dev/null @@ -1,31 +0,0 @@ -#ifndef SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ -#define SOURCEMETA_CORE_JSONPATH_GRAMMAR_H_ - -#include - -namespace sourcemeta::core { - -using JSONPathQuery = JSONPath::Query; -using JSONPathSegment = JSONPath::Segment; -using Selector = JSONPath::Selector; -using SelectorKind = JSONPath::SelectorKind; -using SelectorName = JSONPath::SelectorName; -using SelectorWildcard = JSONPath::SelectorWildcard; -using SelectorIndex = JSONPath::SelectorIndex; -using SelectorSlice = JSONPath::SelectorSlice; -using SelectorFilter = JSONPath::SelectorFilter; -using FilterQuery = JSONPath::FilterQuery; -using FilterOperand = JSONPath::FilterOperand; -using FilterFunctionName = JSONPath::FilterFunctionName; -using FilterFunctionCall = JSONPath::FilterFunctionCall; -using FilterComparisonOperator = JSONPath::FilterComparisonOperator; -using FilterComparison = JSONPath::FilterComparison; -using FilterTest = JSONPath::FilterTest; -using FilterExpression = JSONPath::FilterExpression; -using FilterConjunction = JSONPath::FilterConjunction; -using FilterDisjunction = JSONPath::FilterDisjunction; -using FilterNegation = JSONPath::FilterNegation; - -} // namespace sourcemeta::core - -#endif diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index 2d08dc0181..a6d2536985 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -1,13 +1,640 @@ #include +#include -#include "evaluator.h" -#include "grammar.h" #include "parser.h" -#include // std::to_string +#include // std::max, std::min +#include // assert +#include // std::size_t +#include // std::int64_t +#include // std::cref +#include // std::optional +#include // std::to_string +#include // std::get, std::get_if, std::holds_alternative, std::monostate, std::variant +#include // std::vector namespace sourcemeta::core { +namespace { + +struct EvaluationState { + const JSON *root; + const JSONPath::Callback *callback; + WeakPointer location; +}; + +// The value of a comparable during evaluation: absent, borrowed from the +// document or the compiled query, or computed by a function +using FilterValue = std::variant; + +auto filter_value_pointer(const FilterValue &value) -> const JSON * { + switch (value.index()) { + case 0: + return nullptr; + case 1: + return *std::get_if(&value); + default: + return std::get_if(&value); + } +} + +auto filter_matches(const JSONPath::FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool; + +// RFC 9535 Section 2.3.4.2.2: slice expression bounds against a concrete +// array length, where an omitted start or end defaults according to the +// sign of the step and a zero step selects nothing +inline auto slice_bounds(const JSONPath::SelectorSlice &slice, + const std::int64_t size, std::int64_t &lower, + std::int64_t &upper) -> bool { + if (slice.step == 0) { + return false; + } + + const auto start{slice.start.has_value() ? slice.start.value() + : (slice.step > 0 ? 0 : size - 1)}; + const auto end{slice.end.has_value() ? slice.end.value() + : (slice.step > 0 ? size : -size - 1)}; + const auto normalized_start{start >= 0 ? start : size + start}; + const auto normalized_end{end >= 0 ? end : size + end}; + if (slice.step > 0) { + lower = std::min(std::max(normalized_start, std::int64_t{0}), size); + upper = std::min(std::max(normalized_end, std::int64_t{0}), size); + } else { + upper = std::min(std::max(normalized_start, std::int64_t{-1}), size - 1); + lower = std::min(std::max(normalized_end, std::int64_t{-1}), size - 1); + } + + return true; +} + +// Walk the nodes selected by a filter query without tracking locations. The +// visitor returns whether to continue, and the traversal reports whether it +// ran to completion without the visitor stopping it +template +auto filter_query_visit(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + const JSON &root, const Visitor &visitor) -> bool { + if (cursor == segments.size()) { + return visitor(current); + } + + const auto &segment{segments[cursor]}; + for (const auto &selector : segment.selectors) { + switch (static_cast(selector.index())) { + case JSONPath::SelectorKind::Name: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr && !filter_query_visit(segments, cursor + 1, + *child, root, visitor)) { + return false; + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (!filter_query_visit(segments, cursor + 1, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (!filter_query_visit(segments, cursor + 1, element, root, + visitor)) { + return false; + } + } + } + + break; + case JSONPath::SelectorKind::Index: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size && + !filter_query_visit(segments, cursor + 1, + current.at(static_cast(index)), + root, visitor)) { + return false; + } + } + + break; + } + case JSONPath::SelectorKind::Slice: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + if (!filter_query_visit( + segments, cursor + 1, + current.at(static_cast(index)), root, + visitor)) { + return false; + } + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + if (!filter_query_visit( + segments, cursor + 1, + current.at(static_cast(index)), root, + visitor)) { + return false; + } + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (filter_matches(entry->expression, member.second, root) && + !filter_query_visit(segments, cursor + 1, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (filter_matches(entry->expression, element, root) && + !filter_query_visit(segments, cursor + 1, element, root, + visitor)) { + return false; + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (!filter_query_visit(segments, cursor, member.second, root, + visitor)) { + return false; + } + } + } else if (current.is_array()) { + for (const auto &element : current.as_array()) { + if (!filter_query_visit(segments, cursor, element, root, visitor)) { + return false; + } + } + } + } + + return true; +} + +// RFC 9535 Section 2.3.5.1: a singular query "produces a nodelist containing +// at most one node", so it resolves directly without traversal +inline auto resolve_singular(const JSONPath::FilterQuery &query, + const JSON &candidate, const JSON &root) + -> const JSON * { + const auto *current{query.relative ? &candidate : &root}; + for (const auto &segment : query.segments) { + assert(!segment.descendant); + assert(segment.selectors.size() == 1); + const auto &selector{segment.selectors.front()}; + if (std::holds_alternative(selector)) { + const auto *entry{std::get_if(&selector)}; + current = current->is_object() ? current->try_at(entry->name, entry->hash) + : nullptr; + } else { + const auto *entry{std::get_if(&selector)}; + if (current->is_array()) { + const auto size{static_cast(current->as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + current = index >= 0 && index < size + ? ¤t->at(static_cast(index)) + : nullptr; + } else { + current = nullptr; + } + } + + if (current == nullptr) { + return nullptr; + } + } + + return current; +} + +auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> FilterValue; + +inline auto evaluate_operand(const JSONPath::FilterOperand &operand, + const JSON &candidate, const JSON &root) + -> FilterValue { + if (std::holds_alternative(operand.value)) { + return FilterValue{&std::get(operand.value)}; + } + + if (std::holds_alternative(operand.value)) { + const auto *result{resolve_singular( + std::get(operand.value), candidate, root)}; + return result == nullptr ? FilterValue{} : FilterValue{result}; + } + + return evaluate_value_function( + std::get(operand.value), candidate, root); +} + +inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> FilterValue { + switch (call.function) { + // RFC 9535 Section 2.4.4: the length function counts string characters, + // array elements, or object members, and is nothing for anything else + case JSONPath::FilterFunctionName::Length: { + const auto value{ + evaluate_operand(call.arguments.front(), candidate, root)}; + const auto *target{filter_value_pointer(value)}; + if (target == nullptr) { + return FilterValue{}; + } + + if (target->is_string()) { + return FilterValue{JSON{static_cast( + utf8_codepoint_count(target->to_string()))}}; + } + + if (target->is_array()) { + return FilterValue{ + JSON{static_cast(target->as_array().size())}}; + } + + if (target->is_object()) { + return FilterValue{ + JSON{static_cast(target->as_object().size())}}; + } + + return FilterValue{}; + } + // RFC 9535 Section 2.4.5: the count function yields the number of nodes + case JSONPath::FilterFunctionName::Count: { + const auto &query{ + std::get(call.arguments.front().value)}; + std::int64_t count{0}; + filter_query_visit(query.segments, 0, query.relative ? candidate : root, + root, [&count](const JSON &) -> bool { + count += 1; + return true; + }); + return FilterValue{JSON{count}}; + } + // RFC 9535 Section 2.4.8: the value function yields the value of a + // single node and nothing otherwise + case JSONPath::FilterFunctionName::Value: { + const auto &query{ + std::get(call.arguments.front().value)}; + const JSON *single{nullptr}; + std::size_t count{0}; + filter_query_visit(query.segments, 0, query.relative ? candidate : root, + root, [&single, &count](const JSON &node) -> bool { + single = &node; + count += 1; + return count < 2; + }); + return count == 1 ? FilterValue{single} : FilterValue{}; + } + default: + assert(false); + return FilterValue{}; + } +} + +// RFC 9535 Sections 2.4.6 and 2.4.7: the match function considers the whole +// input while the search function considers any substring, and any argument +// mismatch yields a false outcome rather than an error +inline auto evaluate_logical_function(const JSONPath::FilterFunctionCall &call, + const JSON &candidate, const JSON &root) + -> bool { + const auto input{evaluate_operand(call.arguments.front(), candidate, root)}; + const auto *subject{filter_value_pointer(input)}; + if (subject == nullptr || !subject->is_string()) { + return false; + } + + if (std::holds_alternative(call.arguments.back().value) && + std::get(call.arguments.back().value).is_string()) { + return call.compiled.has_value() && + matches(call.compiled.value(), subject->to_string()); + } + + const auto pattern{evaluate_operand(call.arguments.back(), candidate, root)}; + const auto *expression{filter_value_pointer(pattern)}; + if (expression == nullptr || !expression->is_string()) { + return false; + } + + const auto compiled{ + to_regex(expression->to_string(), + call.function == JSONPath::FilterFunctionName::Match + ? RegexDialect::IRegexp + : RegexDialect::IRegexpSearch)}; + return compiled.has_value() && + matches(compiled.value(), subject->to_string()); +} + +// RFC 9535 Section 2.3.5.2.2: equality is deep JSON equality where numbers +// compare mathematically, and an absent value equals only an absent value +inline auto filter_equals(const JSON *left, const JSON *right) -> bool { + if (left == nullptr || right == nullptr) { + return left == nullptr && right == nullptr; + } + + return *left == *right; +} + +// RFC 9535 Section 2.3.5.2.2: ordering applies only when both sides are +// numbers or both sides are strings +inline auto filter_less(const JSON *left, const JSON *right) -> bool { + if (left == nullptr || right == nullptr) { + return false; + } + + const bool left_number{left->is_integer() || left->is_real()}; + const bool right_number{right->is_integer() || right->is_real()}; + if (left_number && right_number) { + if (left->is_integer() && right->is_integer()) { + return left->to_integer() < right->to_integer(); + } + + const auto left_value{left->is_integer() + ? static_cast(left->to_integer()) + : left->to_real()}; + const auto right_value{right->is_integer() + ? static_cast(right->to_integer()) + : right->to_real()}; + return left_value < right_value; + } + + if (left->is_string() && right->is_string()) { + return left->to_string() < right->to_string(); + } + + return false; +} + +inline auto filter_compare(const JSONPath::FilterComparison &comparison, + const JSON &candidate, const JSON &root) -> bool { + const auto left{evaluate_operand(comparison.left, candidate, root)}; + const auto right{evaluate_operand(comparison.right, candidate, root)}; + const auto *left_value{filter_value_pointer(left)}; + const auto *right_value{filter_value_pointer(right)}; + switch (comparison.operation) { + case JSONPath::FilterComparisonOperator::Equal: + return filter_equals(left_value, right_value); + case JSONPath::FilterComparisonOperator::NotEqual: + return !filter_equals(left_value, right_value); + case JSONPath::FilterComparisonOperator::Less: + return filter_less(left_value, right_value); + case JSONPath::FilterComparisonOperator::LessEqual: + return filter_less(left_value, right_value) || + filter_equals(left_value, right_value); + case JSONPath::FilterComparisonOperator::Greater: + return filter_less(right_value, left_value); + case JSONPath::FilterComparisonOperator::GreaterEqual: + return filter_less(right_value, left_value) || + filter_equals(left_value, right_value); + } + + assert(false); + return false; +} + +inline auto filter_matches(const JSONPath::FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool { + if (std::holds_alternative(expression.value)) { + return filter_compare( + std::get(expression.value), candidate, + root); + } + + if (std::holds_alternative(expression.value)) { + const auto &test{std::get(expression.value)}; + bool result{false}; + if (std::holds_alternative(test.subject)) { + const auto &query{std::get(test.subject)}; + result = !filter_query_visit(query.segments, 0, + query.relative ? candidate : root, root, + [](const JSON &) -> bool { return false; }); + } else { + result = evaluate_logical_function( + std::get(test.subject), candidate, + root); + } + + return test.negated ? !result : result; + } + + if (std::holds_alternative(expression.value)) { + const auto &conjunction{ + std::get(expression.value)}; + for (const auto &child : conjunction.children) { + if (!filter_matches(child, candidate, root)) { + return false; + } + } + + return true; + } + + if (std::holds_alternative(expression.value)) { + const auto &disjunction{ + std::get(expression.value)}; + for (const auto &child : disjunction.children) { + if (filter_matches(child, candidate, root)) { + return true; + } + } + + return false; + } + + return !filter_matches( + std::get(expression.value).children.front(), + candidate, root); +} + +auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void; + +inline auto evaluate_selectors(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + for (const auto &selector : segments[cursor].selectors) { + switch (static_cast(selector.index())) { + case JSONPath::SelectorKind::Name: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + state.location.emplace_back(std::cref(entry->name), entry->hash); + evaluate_segments(segments, cursor + 1, *child, state); + state.location.pop_back(); + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (current.is_object()) { + for (const auto &member : current.as_object()) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_segments(segments, cursor + 1, member.second, state); + state.location.pop_back(); + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + state.location.emplace_back(index); + evaluate_segments(segments, cursor + 1, current.at(index), state); + state.location.pop_back(); + } + } + + break; + case JSONPath::SelectorKind::Index: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } + + break; + } + case JSONPath::SelectorKind::Slice: { + const auto *entry{std::get_if(&selector)}; + if (current.is_array()) { + const auto size{static_cast(current.as_array().size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + state.location.emplace_back(static_cast(index)); + evaluate_segments(segments, cursor + 1, + current.at(static_cast(index)), + state); + state.location.pop_back(); + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(&selector)}; + if (current.is_object()) { + for (const auto &member : current.as_object()) { + if (filter_matches(entry->expression, member.second, *state.root)) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_segments(segments, cursor + 1, member.second, state); + state.location.pop_back(); + } + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + if (filter_matches(entry->expression, current.at(index), + *state.root)) { + state.location.emplace_back(index); + evaluate_segments(segments, cursor + 1, current.at(index), state); + state.location.pop_back(); + } + } + } + + break; + } + } + } +} + +// RFC 9535 Section 2.5.2.2: a descendant segment visits the input node and +// every descendant in depth-first document order, applying its selectors at +// each visited node +inline auto evaluate_descendant(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + evaluate_selectors(segments, cursor, current, state); + if (current.is_object()) { + for (const auto &member : current.as_object()) { + state.location.emplace_back(std::cref(member.first), member.hash); + evaluate_descendant(segments, cursor, member.second, state); + state.location.pop_back(); + } + } else if (current.is_array()) { + const auto size{current.as_array().size()}; + for (std::size_t index = 0; index < size; ++index) { + state.location.emplace_back(index); + evaluate_descendant(segments, cursor, current.at(index), state); + state.location.pop_back(); + } + } +} + +inline auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + EvaluationState &state) -> void { + if (cursor == segments.size()) { + (*state.callback)(current, state.location); + return; + } + + if (segments[cursor].descendant) { + evaluate_descendant(segments, cursor, current, state); + } else { + evaluate_selectors(segments, cursor, current, state); + } +} + +} // namespace + JSONPath::JSONPath(const JSON::StringView expression) : query_{parse_jsonpath(expression)} {} diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index 2757345a1a..d4ef2cf8fb 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -2,13 +2,12 @@ #define SOURCEMETA_CORE_JSONPATH_PARSER_H_ #include +#include #include #include #include #include -#include "grammar.h" - #include // assert #include // std::from_chars #include // std::size_t @@ -33,14 +32,14 @@ class JSONPathParser { public: JSONPathParser(const std::string_view input) : input_{input} {} - auto parse() -> JSONPathQuery { + auto parse() -> JSONPath::Query { // jsonpath-query = root-identifier segments if (this->at_end() || this->peek() != '$') { this->fail(); } this->position_ += 1; - JSONPathQuery result; + JSONPath::Query result; bool singular{true}; result.segments = this->parse_segments(singular); if (!this->at_end()) { @@ -120,8 +119,8 @@ class JSONPathParser { } // segments = *(S segment) - auto parse_segments(bool &singular) -> std::vector { - std::vector segments; + auto parse_segments(bool &singular) -> std::vector { + std::vector segments; while (true) { const auto saved{this->position_}; this->skip_whitespace(); @@ -137,8 +136,8 @@ class JSONPathParser { } // segment = child-segment / descendant-segment - auto parse_segment(bool &singular) -> JSONPathSegment { - JSONPathSegment segment; + auto parse_segment(bool &singular) -> JSONPath::Segment { + JSONPath::Segment segment; segment.descendant = false; if (this->peek() == '[') { this->parse_bracketed_selection(segment, singular); @@ -160,7 +159,7 @@ class JSONPathParser { this->parse_bracketed_selection(segment, singular); } else if (this->peek() == '*') { this->position_ += 1; - segment.selectors.emplace_back(SelectorWildcard{}); + segment.selectors.emplace_back(JSONPath::SelectorWildcard{}); } else { segment.selectors.emplace_back(this->parse_shorthand_name()); } @@ -177,7 +176,7 @@ class JSONPathParser { if (this->peek() == '*') { this->position_ += 1; singular = false; - segment.selectors.emplace_back(SelectorWildcard{}); + segment.selectors.emplace_back(JSONPath::SelectorWildcard{}); } else { segment.selectors.emplace_back(this->parse_shorthand_name()); } @@ -188,7 +187,7 @@ class JSONPathParser { // bracketed-selection = "[" S selector *(S "," S selector) S "]" // Whitespace and multiple selectors disqualify the enclosing query from // the stricter singular-query grammar of RFC 9535 Section 2.3.5.1 - auto parse_bracketed_selection(JSONPathSegment &segment, bool &singular) + auto parse_bracketed_selection(JSONPath::Segment &segment, bool &singular) -> void { this->position_ += 1; bool internal_whitespace{this->skip_whitespace()}; @@ -218,7 +217,7 @@ class JSONPathParser { // selector = name-selector / wildcard-selector / slice-selector / // index-selector / filter-selector - auto parse_selector(bool &singular) -> Selector { + auto parse_selector(bool &singular) -> JSONPath::Selector { if (this->at_end()) { this->fail(); } @@ -227,13 +226,13 @@ class JSONPathParser { if (character == '"' || character == '\'') { auto name{this->parse_string_literal()}; const auto hash{JSON::Object::hash(name)}; - return SelectorName{.name = std::move(name), .hash = hash}; + return JSONPath::SelectorName{.name = std::move(name), .hash = hash}; } if (character == '*') { this->position_ += 1; singular = false; - return SelectorWildcard{}; + return JSONPath::SelectorWildcard{}; } if (character == '?') { @@ -241,7 +240,7 @@ class JSONPathParser { this->position_ += 1; singular = false; this->skip_whitespace(); - return SelectorFilter{this->parse_logical_or()}; + return JSONPath::SelectorFilter{this->parse_logical_or()}; } if (character == ':') { @@ -260,14 +259,15 @@ class JSONPathParser { } this->position_ = saved; - return SelectorIndex{value}; + return JSONPath::SelectorIndex{value}; } this->fail(); } - auto parse_slice(const std::optional start) -> SelectorSlice { - SelectorSlice slice; + auto parse_slice(const std::optional start) + -> JSONPath::SelectorSlice { + JSONPath::SelectorSlice slice; slice.start = start; slice.step = 1; assert(this->peek() == ':'); @@ -464,7 +464,7 @@ class JSONPathParser { } // member-name-shorthand = name-first *name-char - auto parse_shorthand_name() -> SelectorName { + auto parse_shorthand_name() -> JSONPath::SelectorName { JSON::String name; if (this->at_end()) { this->fail(); @@ -494,12 +494,12 @@ class JSONPathParser { } const auto hash{JSON::Object::hash(name)}; - return SelectorName{.name = std::move(name), .hash = hash}; + return JSONPath::SelectorName{.name = std::move(name), .hash = hash}; } // logical-or-expr = logical-and-expr *(S "||" S logical-and-expr) - auto parse_logical_or() -> FilterExpression { - std::vector children; + auto parse_logical_or() -> JSONPath::FilterExpression { + std::vector children; children.push_back(this->parse_logical_and()); while (true) { const auto saved{this->position_}; @@ -520,12 +520,13 @@ class JSONPathParser { return std::move(children.front()); } - return FilterExpression{FilterDisjunction{std::move(children)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterDisjunction{std::move(children)}}; } // logical-and-expr = basic-expr *(S "&&" S basic-expr) - auto parse_logical_and() -> FilterExpression { - std::vector children; + auto parse_logical_and() -> JSONPath::FilterExpression { + std::vector children; children.push_back(this->parse_basic_expression()); while (true) { const auto saved{this->position_}; @@ -546,11 +547,12 @@ class JSONPathParser { return std::move(children.front()); } - return FilterExpression{FilterConjunction{std::move(children)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterConjunction{std::move(children)}}; } // basic-expr = paren-expr / comparison-expr / test-expr - auto parse_basic_expression() -> FilterExpression { + auto parse_basic_expression() -> JSONPath::FilterExpression { if (this->at_end()) { this->fail(); } @@ -564,13 +566,13 @@ class JSONPathParser { } if (this->peek() == '(') { - std::vector children; + std::vector children; children.push_back(this->parse_parenthesized()); - return FilterExpression{ - FilterNegation{.children = std::move(children)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterNegation{.children = std::move(children)}}; } - return FilterExpression{this->parse_test(true)}; + return JSONPath::FilterExpression{this->parse_test(true)}; } if (character == '(') { @@ -581,7 +583,7 @@ class JSONPathParser { } // paren-expr = [logical-not-op S] "(" S logical-expr S ")" - auto parse_parenthesized() -> FilterExpression { + auto parse_parenthesized() -> JSONPath::FilterExpression { assert(this->peek() == '('); this->position_ += 1; this->skip_whitespace(); @@ -596,23 +598,25 @@ class JSONPathParser { } // test-expr = [logical-not-op S] (filter-query / function-expr) - auto parse_test(const bool negated) -> FilterTest { + auto parse_test(const bool negated) -> JSONPath::FilterTest { if (this->peek() == '@' || this->peek() == '$') { bool singular{true}; auto query{this->parse_filter_query(singular)}; - return FilterTest{.negated = negated, .subject = std::move(query)}; + return JSONPath::FilterTest{.negated = negated, + .subject = std::move(query)}; } if (is_lower_alpha(this->peek())) { auto call{this->parse_function()}; // RFC 9535 Section 2.4.3: a test expression admits functions whose // declared result is of logical or nodes type only - if (call.function != FilterFunctionName::Match && - call.function != FilterFunctionName::Search) { + if (call.function != JSONPath::FilterFunctionName::Match && + call.function != JSONPath::FilterFunctionName::Search) { this->fail(); } - return FilterTest{.negated = negated, .subject = std::move(call)}; + return JSONPath::FilterTest{.negated = negated, + .subject = std::move(call)}; } this->fail(); @@ -634,7 +638,7 @@ class JSONPathParser { } // comparison-op = "==" / "!=" / "<=" / ">=" / "<" / ">" - auto parse_comparison_operator() -> FilterComparisonOperator { + auto parse_comparison_operator() -> JSONPath::FilterComparisonOperator { const char character{this->peek()}; if (character == '=' || character == '!') { this->position_ += 1; @@ -643,8 +647,8 @@ class JSONPathParser { } this->position_ += 1; - return character == '=' ? FilterComparisonOperator::Equal - : FilterComparisonOperator::NotEqual; + return character == '=' ? JSONPath::FilterComparisonOperator::Equal + : JSONPath::FilterComparisonOperator::NotEqual; } if (character == '<' || character == '>') { @@ -655,18 +659,18 @@ class JSONPathParser { } if (character == '<') { - return inclusive ? FilterComparisonOperator::LessEqual - : FilterComparisonOperator::Less; + return inclusive ? JSONPath::FilterComparisonOperator::LessEqual + : JSONPath::FilterComparisonOperator::Less; } - return inclusive ? FilterComparisonOperator::GreaterEqual - : FilterComparisonOperator::Greater; + return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual + : JSONPath::FilterComparisonOperator::Greater; } this->fail(); } - auto parse_comparison_or_test() -> FilterExpression { + auto parse_comparison_or_test() -> JSONPath::FilterExpression { const char character{this->peek()}; if (character == '@' || character == '$') { bool singular{true}; @@ -683,15 +687,15 @@ class JSONPathParser { const auto operation{this->parse_comparison_operator()}; this->skip_whitespace(); auto right{this->parse_comparable()}; - return FilterExpression{ - FilterComparison{.left = FilterOperand{.value = std::move(query)}, - .operation = operation, - .right = std::move(right)}}; + return JSONPath::FilterExpression{JSONPath::FilterComparison{ + .left = JSONPath::FilterOperand{.value = std::move(query)}, + .operation = operation, + .right = std::move(right)}}; } this->position_ = saved; - return FilterExpression{ - FilterTest{.negated = false, .subject = std::move(query)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterTest{.negated = false, .subject = std::move(query)}}; } if (character == '"' || character == '\'' || character == '-' || @@ -705,10 +709,10 @@ class JSONPathParser { const auto operation{this->parse_comparison_operator()}; this->skip_whitespace(); auto right{this->parse_comparable()}; - return FilterExpression{ - FilterComparison{.left = FilterOperand{.value = std::move(literal)}, - .operation = operation, - .right = std::move(right)}}; + return JSONPath::FilterExpression{JSONPath::FilterComparison{ + .left = JSONPath::FilterOperand{.value = std::move(literal)}, + .operation = operation, + .right = std::move(right)}}; } if (is_lower_alpha(character)) { @@ -716,47 +720,52 @@ class JSONPathParser { const auto saved{this->position_}; this->skip_whitespace(); if (this->comparison_operator_ahead()) { - if (std::holds_alternative(operand.value) && + if (std::holds_alternative( + operand.value) && !is_value_type_function( - std::get(operand.value))) { + std::get(operand.value))) { this->fail(); } const auto operation{this->parse_comparison_operator()}; this->skip_whitespace(); auto right{this->parse_comparable()}; - return FilterExpression{FilterComparison{.left = std::move(operand), - .operation = operation, - .right = std::move(right)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterComparison{.left = std::move(operand), + .operation = operation, + .right = std::move(right)}}; } this->position_ = saved; - if (!std::holds_alternative(operand.value)) { + if (!std::holds_alternative( + operand.value)) { // A literal is not a valid test expression this->fail(); } - auto call{std::move(std::get(operand.value))}; - if (call.function != FilterFunctionName::Match && - call.function != FilterFunctionName::Search) { + auto call{ + std::move(std::get(operand.value))}; + if (call.function != JSONPath::FilterFunctionName::Match && + call.function != JSONPath::FilterFunctionName::Search) { this->fail(); } - return FilterExpression{ - FilterTest{.negated = false, .subject = std::move(call)}}; + return JSONPath::FilterExpression{ + JSONPath::FilterTest{.negated = false, .subject = std::move(call)}}; } this->fail(); } - static auto is_value_type_function(const FilterFunctionCall &call) -> bool { - return call.function == FilterFunctionName::Length || - call.function == FilterFunctionName::Count || - call.function == FilterFunctionName::Value; + static auto is_value_type_function(const JSONPath::FilterFunctionCall &call) + -> bool { + return call.function == JSONPath::FilterFunctionName::Length || + call.function == JSONPath::FilterFunctionName::Count || + call.function == JSONPath::FilterFunctionName::Value; } // comparable = literal / singular-query / function-expr - auto parse_comparable() -> FilterOperand { + auto parse_comparable() -> JSONPath::FilterOperand { if (this->at_end()) { this->fail(); } @@ -769,19 +778,19 @@ class JSONPathParser { this->fail(); } - return FilterOperand{.value = std::move(query)}; + return JSONPath::FilterOperand{.value = std::move(query)}; } if (character == '"' || character == '\'' || character == '-' || is_digit(character)) { - return FilterOperand{.value = this->parse_filter_literal()}; + return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; } if (is_lower_alpha(character)) { auto operand{this->parse_keyword_or_function()}; - if (std::holds_alternative(operand.value) && + if (std::holds_alternative(operand.value) && !is_value_type_function( - std::get(operand.value))) { + std::get(operand.value))) { this->fail(); } @@ -872,8 +881,8 @@ class JSONPathParser { } // filter-query = rel-query / jsonpath-query - auto parse_filter_query(bool &singular) -> FilterQuery { - FilterQuery query; + auto parse_filter_query(bool &singular) -> JSONPath::FilterQuery { + JSONPath::FilterQuery query; query.relative = this->peek() == '@'; this->position_ += 1; query.segments = this->parse_segments(singular); @@ -891,24 +900,24 @@ class JSONPathParser { return this->input_.substr(begin, this->position_ - begin); } - auto parse_keyword_or_function() -> FilterOperand { + auto parse_keyword_or_function() -> JSONPath::FilterOperand { const auto saved{this->position_}; const auto identifier{this->parse_identifier()}; if (!this->at_end() && this->peek() == '(') { this->position_ = saved; - return FilterOperand{.value = this->parse_function()}; + return JSONPath::FilterOperand{.value = this->parse_function()}; } if (identifier == "true") { - return FilterOperand{.value = JSON{true}}; + return JSONPath::FilterOperand{.value = JSON{true}}; } if (identifier == "false") { - return FilterOperand{.value = JSON{false}}; + return JSONPath::FilterOperand{.value = JSON{false}}; } if (identifier == "null") { - return FilterOperand{.value = JSON{nullptr}}; + return JSONPath::FilterOperand{.value = JSON{nullptr}}; } this->fail(); @@ -916,19 +925,19 @@ class JSONPathParser { // function-expr = function-name "(" S [function-argument // *(S "," S function-argument)] S ")" - auto parse_function() -> FilterFunctionCall { + auto parse_function() -> JSONPath::FilterFunctionCall { const auto identifier{this->parse_identifier()}; - FilterFunctionCall call; + JSONPath::FilterFunctionCall call; if (identifier == "length") { - call.function = FilterFunctionName::Length; + call.function = JSONPath::FilterFunctionName::Length; } else if (identifier == "count") { - call.function = FilterFunctionName::Count; + call.function = JSONPath::FilterFunctionName::Count; } else if (identifier == "match") { - call.function = FilterFunctionName::Match; + call.function = JSONPath::FilterFunctionName::Match; } else if (identifier == "search") { - call.function = FilterFunctionName::Search; + call.function = JSONPath::FilterFunctionName::Search; } else if (identifier == "value") { - call.function = FilterFunctionName::Value; + call.function = JSONPath::FilterFunctionName::Value; } else { this->fail(); } @@ -965,7 +974,7 @@ class JSONPathParser { return call; } - auto parse_function_argument() -> FilterOperand { + auto parse_function_argument() -> JSONPath::FilterOperand { if (this->at_end()) { this->fail(); } @@ -973,12 +982,13 @@ class JSONPathParser { const char character{this->peek()}; if (character == '@' || character == '$') { bool singular{true}; - return FilterOperand{.value = this->parse_filter_query(singular)}; + return JSONPath::FilterOperand{.value = + this->parse_filter_query(singular)}; } if (character == '"' || character == '\'' || character == '-' || is_digit(character)) { - return FilterOperand{.value = this->parse_filter_literal()}; + return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; } if (is_lower_alpha(character)) { @@ -991,40 +1001,42 @@ class JSONPathParser { this->fail(); } - [[nodiscard]] static auto is_value_type_operand(const FilterOperand &operand) - -> bool { + [[nodiscard]] static auto + is_value_type_operand(const JSONPath::FilterOperand &operand) -> bool { if (std::holds_alternative(operand.value)) { return true; } - if (std::holds_alternative(operand.value)) { - return std::get(operand.value).singular; + if (std::holds_alternative(operand.value)) { + return std::get(operand.value).singular; } - return is_value_type_function(std::get(operand.value)); + return is_value_type_function( + std::get(operand.value)); } // RFC 9535 Section 2.4.3: "the function's arguments need to be well-typed // against the declared parameter types" - auto check_function(FilterFunctionCall &call) -> void { + auto check_function(JSONPath::FilterFunctionCall &call) -> void { switch (call.function) { - case FilterFunctionName::Length: + case JSONPath::FilterFunctionName::Length: if (call.arguments.size() != 1 || !is_value_type_operand(call.arguments.front())) { this->fail(); } return; - case FilterFunctionName::Count: - case FilterFunctionName::Value: - if (call.arguments.size() != 1 || !std::holds_alternative( - call.arguments.front().value)) { + case JSONPath::FilterFunctionName::Count: + case JSONPath::FilterFunctionName::Value: + if (call.arguments.size() != 1 || + !std::holds_alternative( + call.arguments.front().value)) { this->fail(); } return; - case FilterFunctionName::Match: - case FilterFunctionName::Search: + case JSONPath::FilterFunctionName::Match: + case JSONPath::FilterFunctionName::Search: if (call.arguments.size() != 2 || !is_value_type_operand(call.arguments.front()) || !is_value_type_operand(call.arguments.back())) { @@ -1035,7 +1047,7 @@ class JSONPathParser { std::get(call.arguments.back().value).is_string()) { call.compiled = to_regex(std::get(call.arguments.back().value).to_string(), - call.function == FilterFunctionName::Match + call.function == JSONPath::FilterFunctionName::Match ? RegexDialect::IRegexp : RegexDialect::IRegexpSearch); } @@ -1050,7 +1062,7 @@ class JSONPathParser { } // namespace -inline auto parse_jsonpath(const std::string_view input) -> JSONPathQuery { +inline auto parse_jsonpath(const std::string_view input) -> JSONPath::Query { JSONPathParser parser{input}; return parser.parse(); } From 53174e61844466e0aedeb8b038b5b7c5b66e8e10 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:04:40 -0300 Subject: [PATCH 04/13] More Signed-off-by: Juan Cruz Viotti --- test/jsonpath/jsonpath_normalize_test.cc | 113 +++++++++-------------- 1 file changed, 45 insertions(+), 68 deletions(-) diff --git a/test/jsonpath/jsonpath_normalize_test.cc b/test/jsonpath/jsonpath_normalize_test.cc index f5288ce01a..45665bb605 100644 --- a/test/jsonpath/jsonpath_normalize_test.cc +++ b/test/jsonpath/jsonpath_normalize_test.cc @@ -4,20 +4,15 @@ namespace { -struct ResultNode { - const sourcemeta::core::JSON *value; - sourcemeta::core::WeakPointer location; -}; - -auto evaluate_nodes(const sourcemeta::core::JSONPath &path, - const sourcemeta::core::JSON &document) - -> std::vector { - std::vector result; +auto normalized_paths(const sourcemeta::core::JSONPath &path, + const sourcemeta::core::JSON &document) + -> std::vector { + std::vector result; path.evaluate( document, - [&result](const sourcemeta::core::JSON &value, + [&result](const sourcemeta::core::JSON &, const sourcemeta::core::WeakPointer &location) -> void { - result.push_back({&value, location}); + result.push_back(sourcemeta::core::JSONPath::normalize(location)); }); return result; } @@ -27,133 +22,115 @@ auto evaluate_nodes(const sourcemeta::core::JSONPath &path, TEST(jsonpath_normalize_root) { const auto document{sourcemeta::core::parse_json(R"JSON(1)JSON")}; const sourcemeta::core::JSONPath path{"$"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), "$"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$"); } TEST(jsonpath_normalize_property_and_index) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "a": [ { "b": 1 } ] })JSON")}; const sourcemeta::core::JSONPath path{"$..b"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 1); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['a'][0]['b']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(paths.at(0), "$['a'][0]['b']"); } TEST(jsonpath_normalize_single_quote_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a'b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['a\\'b']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['a\\'b']"); } TEST(jsonpath_normalize_backslash_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a\\b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['a\\\\b']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['a\\\\b']"); } TEST(jsonpath_normalize_double_quote_not_escaped) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a\"b": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['a\"b']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['a\"b']"); } TEST(jsonpath_normalize_two_character_escapes) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "\b\f\n\r\t": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['\\b\\f\\n\\r\\t']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['\\b\\f\\n\\r\\t']"); } TEST(jsonpath_normalize_control_hexadecimal) { const auto document{ sourcemeta::core::parse_json(R"JSON({ "\u000b\u0000\u001f": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['\\u000b\\u0000\\u001f']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['\\u000b\\u0000\\u001f']"); } TEST(jsonpath_normalize_non_ascii_verbatim) { const auto document{sourcemeta::core::parse_json(R"JSON({ "á🤔": 1 })JSON")}; const sourcemeta::core::JSONPath path{"$.*"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['á🤔']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$['á🤔']"); } TEST(jsonpath_normalize_deep_indexes) { const auto document{sourcemeta::core::parse_json(R"JSON([ [ [ 1 ] ] ])JSON")}; const sourcemeta::core::JSONPath path{"$[0][0][0]"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$[0][0][0]"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.at(0), "$[0][0][0]"); } TEST(jsonpath_normalize_negative_index_resolved) { const auto document{ sourcemeta::core::parse_json(R"JSON([ 10, 20, 30 ])JSON")}; const sourcemeta::core::JSONPath path{"$[-1]"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 1); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$[2]"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(paths.at(0), "$[2]"); } TEST(jsonpath_normalize_slice_negative_step) { const auto document{sourcemeta::core::parse_json(R"JSON([ 0, 1, 2 ])JSON")}; const sourcemeta::core::JSONPath path{"$[::-1]"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 3); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$[2]"); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), - "$[1]"); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(2).location), - "$[0]"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 3); + EXPECT_EQ(paths.at(0), "$[2]"); + EXPECT_EQ(paths.at(1), "$[1]"); + EXPECT_EQ(paths.at(2), "$[0]"); } TEST(jsonpath_normalize_descendant_document_order) { const auto document{sourcemeta::core::parse_json( R"JSON({ "a": { "a": 1 }, "b": [ { "a": 2 } ] })JSON")}; const sourcemeta::core::JSONPath path{"$..a"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 3); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['a']"); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), - "$['a']['a']"); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(2).location), - "$['b'][0]['a']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 3); + EXPECT_EQ(paths.at(0), "$['a']"); + EXPECT_EQ(paths.at(1), "$['a']['a']"); + EXPECT_EQ(paths.at(2), "$['b'][0]['a']"); } TEST(jsonpath_normalize_filter_locations) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1 }, { "b": 2 }, { "a": null } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 2); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$[0]"); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(1).location), - "$[2]"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 2); + EXPECT_EQ(paths.at(0), "$[0]"); + EXPECT_EQ(paths.at(1), "$[2]"); } TEST(jsonpath_normalize_filter_object_member_location) { const auto document{sourcemeta::core::parse_json( R"JSON({ "x": { "a": 1 }, "y": { "b": 2 } })JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a]"}; - const auto nodes{evaluate_nodes(path, document)}; - EXPECT_EQ(nodes.size(), 1); - EXPECT_EQ(sourcemeta::core::JSONPath::normalize(nodes.at(0).location), - "$['x']"); + const auto paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(paths.at(0), "$['x']"); } From 7e8e7bf8146a299ff1709d504919a45944eada90 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:08:40 -0300 Subject: [PATCH 05/13] Benchmark Signed-off-by: Juan Cruz Viotti --- benchmark/CMakeLists.txt | 10 ++++++ benchmark/jsonpath.cc | 69 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 benchmark/jsonpath.cc diff --git a/benchmark/CMakeLists.txt b/benchmark/CMakeLists.txt index da215b633f..02bbd7e599 100644 --- a/benchmark/CMakeLists.txt +++ b/benchmark/CMakeLists.txt @@ -12,6 +12,10 @@ if(SOURCEMETA_CORE_JSONPOINTER) list(APPEND BENCHMARK_SOURCES jsonpointer.cc) endif() +if(SOURCEMETA_CORE_JSONPATH) + list(APPEND BENCHMARK_SOURCES jsonpath.cc) +endif() + if(SOURCEMETA_CORE_URITEMPLATE) list(APPEND BENCHMARK_SOURCES uritemplate.cc) endif() @@ -57,6 +61,12 @@ if(BENCHMARK_SOURCES) PRIVATE sourcemeta::core::jsonpointer) endif() + if(SOURCEMETA_CORE_JSONPATH) + target_link_libraries(sourcemeta_core_benchmark + PRIVATE sourcemeta::core::jsonpath sourcemeta::core::json + sourcemeta::core::jsonpointer) + endif() + if(SOURCEMETA_CORE_URITEMPLATE) target_link_libraries(sourcemeta_core_benchmark PRIVATE sourcemeta::core::uritemplate) diff --git a/benchmark/jsonpath.cc b/benchmark/jsonpath.cc new file mode 100644 index 0000000000..15ce6cf6f1 --- /dev/null +++ b/benchmark/jsonpath.cc @@ -0,0 +1,69 @@ +#include + +#include // assert +#include // std::size_t + +#include +#include + +static void JSONPath_Descendant_Filter_Nested(benchmark::State &state) { + const auto document{sourcemeta::core::parse_json(R"JSON({ + "store": { + "departments": [ + { + "name": "literature", + "shelves": [ + { + "books": [ + { "category": "fiction", "title": "Sayings of the Century", "price": 8.95 }, + { "category": "fiction", "title": "Sword of Honour", "price": 12.99 }, + { "category": "reference", "title": "Moby Dick", "price": 8.99 }, + { "category": "fiction", "title": "The Lord of the Rings", "price": 22.99 } + ] + }, + { + "books": [ + { "category": "reference", "title": "Atlas of the World", "price": 45.5 }, + { "category": "fictional biography", "title": "The Quixote", "price": 6.15 }, + { "category": "fiction", "title": "The Aleph", "price": 9.99 }, + { "category": "poetry", "title": "Leaves of Grass", "price": 5.75 } + ] + } + ] + }, + { + "name": "science", + "shelves": [ + { + "books": [ + { "category": "fiction", "title": "Solaris", "price": 7.25 }, + { "category": "reference", "title": "Relativity", "price": 11.5 }, + { "category": "fiction", "title": "Foundation", "price": 3.99 }, + { "category": "fiction", "title": "Dune", "price": 10.0 } + ] + } + ], + "archive": { + "books": [ + { "category": "fiction", "title": "The Time Machine", "price": 2.5 }, + { "category": "essay", "title": "On the Motion of Bodies", "price": 4.05 } + ] + } + } + ] + } + })JSON")}; + + const sourcemeta::core::JSONPath path{ + "$..books[?@.price < 10 && match(@.category, 'fic.*')]"}; + + for (auto _ : state) { + std::size_t count{0}; + path.evaluate(document, + [&count](const auto &, const auto &) { count += 1; }); + assert(count == 6); + benchmark::DoNotOptimize(count); + } +} + +BENCHMARK(JSONPath_Descendant_Filter_Nested); From 8b812af517d500a2f4431a9ca95a08a965968692 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:13:57 -0300 Subject: [PATCH 06/13] More Signed-off-by: Juan Cruz Viotti --- src/core/jsonpath/CMakeLists.txt | 2 + src/core/jsonpath/jsonpath.cc | 8 +--- src/core/jsonpath/parser.h | 76 +++++++++++++------------------- 3 files changed, 35 insertions(+), 51 deletions(-) diff --git a/src/core/jsonpath/CMakeLists.txt b/src/core/jsonpath/CMakeLists.txt index 86a23de371..c85e3e435b 100644 --- a/src/core/jsonpath/CMakeLists.txt +++ b/src/core/jsonpath/CMakeLists.txt @@ -12,6 +12,8 @@ target_link_libraries(sourcemeta_core_jsonpath PUBLIC sourcemeta::core::jsonpointer) target_link_libraries(sourcemeta_core_jsonpath PUBLIC sourcemeta::core::regex) +target_link_libraries(sourcemeta_core_jsonpath PRIVATE + sourcemeta::core::numeric) target_link_libraries(sourcemeta_core_jsonpath PRIVATE sourcemeta::core::text) target_link_libraries(sourcemeta_core_jsonpath PRIVATE diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index a6d2536985..4e2b61bf13 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -1,4 +1,5 @@ #include +#include #include #include "parser.h" @@ -679,13 +680,8 @@ auto JSONPath::normalize(const WeakPointer &location) -> JSON::String { break; default: if (static_cast(character) < 0x20) { - constexpr JSON::StringView hexadecimal{"0123456789abcdef"}; result += "\\u00"; - result += - hexadecimal[(static_cast(character) >> 4) & - 0xF]; - result += - hexadecimal[static_cast(character) & 0xF]; + result += bytes_to_hex(JSON::StringView{&character, 1}); } else { result += character; } diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index d4ef2cf8fb..d0d3adc7cf 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -4,21 +4,19 @@ #include #include #include +#include #include #include #include -#include // assert -#include // std::from_chars -#include // std::size_t -#include // std::int64_t, std::uint32_t -#include // std::strtod -#include // std::optional, std::nullopt -#include // std::string -#include // std::string_view -#include // std::errc -#include // std::move -#include // std::get, std::holds_alternative +#include // assert +#include // std::size_t +#include // std::int64_t, std::uint32_t +#include // std::optional, std::nullopt +#include // std::string +#include // std::string_view +#include // std::move +#include // std::get, std::holds_alternative namespace sourcemeta::core { @@ -63,19 +61,6 @@ class JSONPathParser { return this->input_[this->position_]; } - static auto is_digit(const char character) -> bool { - return character >= '0' && character <= '9'; - } - - static auto is_lower_alpha(const char character) -> bool { - return character >= 'a' && character <= 'z'; - } - - static auto is_alpha(const char character) -> bool { - return (character >= 'a' && character <= 'z') || - (character >= 'A' && character <= 'Z'); - } - // S = *( %x20 / %x09 / %x0A / %x0D ) auto skip_whitespace() -> bool { bool consumed{false}; @@ -320,16 +305,14 @@ class JSONPathParser { this->position_ += 1; } - std::int64_t value{0}; - const auto outcome{std::from_chars(this->input_.data() + begin, - this->input_.data() + this->position_, - value)}; - if (outcome.ec != std::errc{} || value > JSONPATH_MAXIMUM_INTEGER || - value < -JSONPATH_MAXIMUM_INTEGER) { + const auto value{ + to_int64_t(this->input_.substr(begin, this->position_ - begin))}; + if (!value.has_value() || value.value() > JSONPATH_MAXIMUM_INTEGER || + value.value() < -JSONPATH_MAXIMUM_INTEGER) { this->fail(); } - return value; + return value.value(); } // string-literal = %x22 *double-quoted %x22 / %x27 *single-quoted %x27 @@ -606,7 +589,7 @@ class JSONPathParser { .subject = std::move(query)}; } - if (is_lower_alpha(this->peek())) { + if (is_alpha(this->peek()) && is_lowercase(this->peek())) { auto call{this->parse_function()}; // RFC 9535 Section 2.4.3: a test expression admits functions whose // declared result is of logical or nodes type only @@ -715,7 +698,7 @@ class JSONPathParser { .right = std::move(right)}}; } - if (is_lower_alpha(character)) { + if (is_alpha(character) && is_lowercase(character)) { auto operand{this->parse_keyword_or_function()}; const auto saved{this->position_}; this->skip_whitespace(); @@ -786,7 +769,7 @@ class JSONPathParser { return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; } - if (is_lower_alpha(character)) { + if (is_alpha(character) && is_lowercase(character)) { auto operand{this->parse_keyword_or_function()}; if (std::holds_alternative(operand.value) && !is_value_type_function( @@ -865,19 +848,21 @@ class JSONPathParser { const auto text{this->input_.substr(begin, this->position_ - begin)}; if (!real) { - std::int64_t value{0}; - const auto outcome{ - std::from_chars(text.data(), text.data() + text.size(), value)}; - if (outcome.ec != std::errc{} || value > JSONPATH_MAXIMUM_INTEGER || - value < -JSONPATH_MAXIMUM_INTEGER) { + const auto value{to_int64_t(text)}; + if (!value.has_value() || value.value() > JSONPATH_MAXIMUM_INTEGER || + value.value() < -JSONPATH_MAXIMUM_INTEGER) { this->fail(); } - return JSON{value}; + return JSON{value.value()}; + } + + const auto value{to_double(text)}; + if (!value.has_value()) { + this->fail(); } - const std::string buffer{text}; - return JSON{std::strtod(buffer.c_str(), nullptr)}; + return JSON{value.value()}; } // filter-query = rel-query / jsonpath-query @@ -892,8 +877,9 @@ class JSONPathParser { auto parse_identifier() -> std::string_view { const auto begin{this->position_}; - while (!this->at_end() && (is_lower_alpha(this->peek()) || - is_digit(this->peek()) || this->peek() == '_')) { + while (!this->at_end() && + ((is_alpha(this->peek()) && is_lowercase(this->peek())) || + is_digit(this->peek()) || this->peek() == '_')) { this->position_ += 1; } @@ -991,7 +977,7 @@ class JSONPathParser { return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; } - if (is_lower_alpha(character)) { + if (is_alpha(character) && is_lowercase(character)) { return this->parse_keyword_or_function(); } From 14a521cf4f3d67904522ab6fb50b33298b41f98e Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:46:47 -0300 Subject: [PATCH 07/13] More Signed-off-by: Juan Cruz Viotti --- src/core/jsonpath/parser.h | 21 +++++++++++ test/jsonpath/jsonpath_parse_test.cc | 55 ++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index d0d3adc7cf..34669620b9 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -26,6 +26,11 @@ namespace { // values" bounds every int production constexpr std::int64_t JSONPATH_MAXIMUM_INTEGER{9007199254740991}; +// RFC 9535 Section 4.1: crafted queries must not "trigger surprisingly +// high, possibly exponential, CPU usage or ... stack overflow", so the +// recursive filter expression productions are capped in nesting depth +constexpr std::size_t JSONPATH_MAXIMUM_NESTING_DEPTH{64}; + class JSONPathParser { public: JSONPathParser(const std::string_view input) : input_{input} {} @@ -482,6 +487,14 @@ class JSONPathParser { // logical-or-expr = logical-and-expr *(S "||" S logical-and-expr) auto parse_logical_or() -> JSONPath::FilterExpression { + // Every cycle in the filter grammar, through parentheses, nested filter + // selectors, or function arguments, passes through this production or + // the function one, so guarding both bounds the parser recursion + this->depth_ += 1; + if (this->depth_ > JSONPATH_MAXIMUM_NESTING_DEPTH) { + this->fail(); + } + std::vector children; children.push_back(this->parse_logical_and()); while (true) { @@ -499,6 +512,7 @@ class JSONPathParser { } } + this->depth_ -= 1; if (children.size() == 1) { return std::move(children.front()); } @@ -912,6 +926,11 @@ class JSONPathParser { // function-expr = function-name "(" S [function-argument // *(S "," S function-argument)] S ")" auto parse_function() -> JSONPath::FilterFunctionCall { + this->depth_ += 1; + if (this->depth_ > JSONPATH_MAXIMUM_NESTING_DEPTH) { + this->fail(); + } + const auto identifier{this->parse_identifier()}; JSONPath::FilterFunctionCall call; if (identifier == "length") { @@ -957,6 +976,7 @@ class JSONPathParser { this->position_ += 1; this->check_function(call); + this->depth_ -= 1; return call; } @@ -1044,6 +1064,7 @@ class JSONPathParser { std::string_view input_; std::size_t position_{0}; + std::size_t depth_{0}; }; } // namespace diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc index 81576298af..600c42c55d 100644 --- a/test/jsonpath/jsonpath_parse_test.cc +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -1,6 +1,9 @@ #include #include +#include +#include + #define EXPECT_JSONPATH_PARSE_ERROR(input, expected_column) \ try { \ const sourcemeta::core::JSONPath path{input}; \ @@ -265,6 +268,58 @@ TEST(jsonpath_parse_filter_number_real_allowed) { EXPECT_JSONPATH_VALID("$[?@.a == 1.5e-2]"); } +TEST(jsonpath_parse_deep_parenthesis_nesting_rejected) { + std::string expression{"$[?"}; + expression += std::string(100, '('); + expression += "@.a"; + expression += std::string(100, ')'); + expression += "]"; + try { + const sourcemeta::core::JSONPath path{expression}; + FAIL(); + } catch (const sourcemeta::core::JSONPathParseError &error) { + EXPECT_TRUE(error.column() > 0); + } catch (...) { + FAIL(); + } +} + +TEST(jsonpath_parse_deep_filter_nesting_rejected) { + std::string expression{"$"}; + for (std::size_t index = 0; index < 100; ++index) { + expression += "[?@"; + } + + expression += std::string(100, ']'); + try { + const sourcemeta::core::JSONPath path{expression}; + FAIL(); + } catch (const sourcemeta::core::JSONPathParseError &error) { + EXPECT_TRUE(error.column() > 0); + } catch (...) { + FAIL(); + } +} + +TEST(jsonpath_parse_deep_function_nesting_rejected) { + std::string expression{"$[?"}; + for (std::size_t index = 0; index < 100; ++index) { + expression += "length("; + } + + expression += "@.a"; + expression += std::string(100, ')'); + expression += " == 1]"; + try { + const sourcemeta::core::JSONPath path{expression}; + FAIL(); + } catch (const sourcemeta::core::JSONPathParseError &error) { + EXPECT_TRUE(error.column() > 0); + } catch (...) { + FAIL(); + } +} + TEST(jsonpath_parse_invalid_selector_character) { EXPECT_JSONPATH_PARSE_ERROR("$[!]", 3); } From 8ace9357c10b0d498c80128d56ca1d625187d0dc Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 11:56:15 -0300 Subject: [PATCH 08/13] More Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/core/jsonpath_error.h | 4 ++++ src/core/jsonpath/jsonpath.cc | 24 +++++-------------- src/core/jsonpath/parser.h | 21 ++++------------ test/jsonpath/jsonpath_filter_test.cc | 16 +++++++++++++ test/jsonpath/jsonpath_parse_test.cc | 12 ++++++++++ 5 files changed, 42 insertions(+), 35 deletions(-) diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h index 057fbd5a88..1773df9618 100644 --- a/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h @@ -38,6 +38,10 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPathParseError std::uint64_t column_; }; +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + } // namespace sourcemeta::core #endif diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index 4e2b61bf13..8aae39a351 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -377,24 +377,12 @@ inline auto filter_less(const JSON *left, const JSON *right) -> bool { return false; } - const bool left_number{left->is_integer() || left->is_real()}; - const bool right_number{right->is_integer() || right->is_real()}; - if (left_number && right_number) { - if (left->is_integer() && right->is_integer()) { - return left->to_integer() < right->to_integer(); - } - - const auto left_value{left->is_integer() - ? static_cast(left->to_integer()) - : left->to_real()}; - const auto right_value{right->is_integer() - ? static_cast(right->to_integer()) - : right->to_real()}; - return left_value < right_value; - } - - if (left->is_string() && right->is_string()) { - return left->to_string() < right->to_string(); + // RFC 9535 Section 2.3.5.2.2: ordering applies only when both sides are + // numbers or both sides are strings. The value comparison itself orders + // numbers of different representations by exact value + if ((left->is_number() && right->is_number()) || + (left->is_string() && right->is_string())) { + return *left < *right; } return false; diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index 34669620b9..c16c8b2024 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -83,27 +83,14 @@ class JSONPathParser { return consumed; } - // Copy one full UTF-8 encoded code point, rejecting malformed sequences + // Copy one full UTF-8 encoded code point, rejecting malformed sequences, + // overlong encodings, surrogates, and code points beyond U+10FFFF auto copy_character(JSON::String &output) -> void { - const auto lead{static_cast(this->peek())}; - if (lead < 0x80) { - output += static_cast(lead); - this->position_ += 1; - return; - } - - const auto size{utf8_lead_byte_size(lead)}; - if (size == 0 || this->position_ + size > this->input_.size()) { + const auto size{utf8_codepoint_length(this->input_, this->position_)}; + if (size == 0) { this->fail(); } - for (std::size_t offset{1}; offset < size; ++offset) { - if (!is_utf8_continuation(static_cast( - this->input_[this->position_ + offset]))) { - this->fail(); - } - } - output.append(this->input_.substr(this->position_, size)); this->position_ += size; } diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index bd24810105..887e815832 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -296,6 +296,22 @@ TEST(jsonpath_filter_match_dot_accepts_line_separator) { EXPECT_EQ(nodes.size(), 1); } +TEST(jsonpath_filter_less_than_decimal) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 1e400 }, { "a": 5 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a > 100]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); +} + +TEST(jsonpath_filter_less_than_exact_beyond_double_precision) { + const auto document{sourcemeta::core::parse_json( + R"JSON([ { "a": 9007199254740993 }, { "a": 9007199254740991 } ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?@.a > 9007199254740992.0]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); +} + TEST(jsonpath_filter_current_node_comparison) { const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 5, 10 ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@ > 4]"}; diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc index 600c42c55d..c81c21d2a3 100644 --- a/test/jsonpath/jsonpath_parse_test.cc +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -268,6 +268,18 @@ TEST(jsonpath_parse_filter_number_real_allowed) { EXPECT_JSONPATH_VALID("$[?@.a == 1.5e-2]"); } +TEST(jsonpath_parse_overlong_utf8_in_name){ + EXPECT_JSONPATH_PARSE_ERROR("$['\xE0\x80\xAF']", 4)} + +TEST(jsonpath_parse_surrogate_utf8_in_name){ + EXPECT_JSONPATH_PARSE_ERROR("$['\xED\xA0\x80']", 4)} + +TEST(jsonpath_parse_utf8_beyond_unicode_in_name){ + EXPECT_JSONPATH_PARSE_ERROR("$['\xF4\x90\x80\x80']", 4)} + +TEST(jsonpath_parse_overlong_utf8_in_shorthand){ + EXPECT_JSONPATH_PARSE_ERROR("$.\xE0\x80\xAF", 3)} + TEST(jsonpath_parse_deep_parenthesis_nesting_rejected) { std::string expression{"$[?"}; expression += std::string(100, '('); From 2463f71ceba6757c7e47d8718bb35876b2fe1f06 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 12:24:49 -0300 Subject: [PATCH 09/13] More Signed-off-by: Juan Cruz Viotti --- test/jsonpath/jsonpath_filter_test.cc | 50 +++++++++++++++++++++++++++ test/jsonpath/jsonpath_suite.cc | 14 +++++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index 887e815832..5bb2ea4370 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -312,6 +312,56 @@ TEST(jsonpath_filter_less_than_exact_beyond_double_precision) { EXPECT_EQ(nodes.size(), 1); } +TEST(jsonpath_filter_match_literal_caret) { + // NOTE: RFC 9485 Section 4 normatively makes an unescaped caret a literal + // character, unlike the not normative Section 5 engine mappings that the + // compliance suite anchor cases codify + const auto document{sourcemeta::core::parse_json( + R"JSON([ "^ab", "^abc", "ab", "abc" ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@, '^ab.*')]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_string(), "^ab"); + EXPECT_EQ(nodes.at(1).value->to_string(), "^abc"); +} + +TEST(jsonpath_filter_match_literal_middle_dollar) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ "a$b", "ab" ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@, 'a$b')]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_string(), "a$b"); +} + +TEST(jsonpath_filter_match_dollar_in_class) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ "a$b", "ab" ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@, 'a[$]b')]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_string(), "a$b"); +} + +TEST(jsonpath_filter_match_quantified_caret) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ "^^", "", "a" ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?match(@, '^*')]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 2); + EXPECT_EQ(nodes.at(0).value->to_string(), "^^"); + EXPECT_EQ(nodes.at(1).value->to_string(), ""); +} + +TEST(jsonpath_filter_search_literal_dollar) { + const auto document{ + sourcemeta::core::parse_json(R"JSON([ "xab$cx", "xabcx" ])JSON")}; + const sourcemeta::core::JSONPath path{"$[?search(@, 'ab$c')]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_string(), "xab$cx"); +} + TEST(jsonpath_filter_current_node_comparison) { const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 5, 10 ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@ > 4]"}; diff --git a/test/jsonpath/jsonpath_suite.cc b/test/jsonpath/jsonpath_suite.cc index 10496164f9..227c1a7c73 100644 --- a/test/jsonpath/jsonpath_suite.cc +++ b/test/jsonpath/jsonpath_suite.cc @@ -125,8 +125,20 @@ auto main(int argc, char **argv) -> int { auto prefix{std::filesystem::relative(file, base)}; prefix.replace_extension(); for (const auto &test_case : document.at("tests").as_array()) { + const auto &name{test_case.at("name").to_string()}; + // NOTE: These two cases codify the RFC 9485 Section 5 engine mappings, + // which declare themselves not normative and under which an unescaped + // caret or dollar sign acts as an assertion. RFC 9485 Section 4 + // normatively makes them literal characters and RFC 9535 defers to + // RFC 9485 by reference, so this implementation deliberately + // disagrees with these cases + if (prefix.generic_string() == "functions/match" && + (name == "explicit caret" || name == "explicit dollar")) { + continue; + } + sourcemeta::core::test_register( - prefix.generic_string() + ": " + test_case.at("name").to_string(), + prefix.generic_string() + ": " + name, [test_case]() -> void { run_jsonpath_test_case(test_case); }); } } From 8507ebe6c575db542bd9e9cd0f25eca13e9c1035 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 12:27:24 -0300 Subject: [PATCH 10/13] More Signed-off-by: Juan Cruz Viotti --- test/jsonpath/jsonpath_filter_test.cc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index 5bb2ea4370..d4ef7af277 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -296,7 +296,7 @@ TEST(jsonpath_filter_match_dot_accepts_line_separator) { EXPECT_EQ(nodes.size(), 1); } -TEST(jsonpath_filter_less_than_decimal) { +TEST(jsonpath_filter_greater_than_decimal) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 1e400 }, { "a": 5 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a > 100]"}; @@ -304,7 +304,7 @@ TEST(jsonpath_filter_less_than_decimal) { EXPECT_EQ(nodes.size(), 1); } -TEST(jsonpath_filter_less_than_exact_beyond_double_precision) { +TEST(jsonpath_filter_greater_than_exact_beyond_double_precision) { const auto document{sourcemeta::core::parse_json( R"JSON([ { "a": 9007199254740993 }, { "a": 9007199254740991 } ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@.a > 9007199254740992.0]"}; From 3f1f3b5085d6d0f4c224652aab4176af3e13c336 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 12:54:14 -0300 Subject: [PATCH 11/13] Performance Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/core/jsonpath.h | 12 + src/core/jsonpath/jsonpath.cc | 465 +++++++++++------- 2 files changed, 300 insertions(+), 177 deletions(-) diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h index 36d3dc5ee0..ad22933a9c 100644 --- a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -181,6 +181,18 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { std::vector children; }; +#if !defined(DOXYGEN) + // For fast internal dispatching. It must stay in sync with the expression + // variant below + enum class FilterExpressionKind : std::uint8_t { + Comparison = 0, + Test, + Conjunction, + Disjunction, + Negation + }; +#endif + /// A logical expression within a filter selector struct FilterExpression { /// The comparison, test, or combination this expression stands for diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index 8aae39a351..c98afe9ff7 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -18,29 +18,89 @@ namespace sourcemeta::core { namespace { +// One step of the current traversal location. The full pointer is only +// materialized when a result node fires, so the walk itself performs raw +// stores instead of token constructions +struct LocationFrame { + const JSON::String *property; + JSON::Object::hash_type hash; + std::size_t index; +}; + +class LocationStack { +public: + LocationStack() { this->storage_.resize(this->capacity_); } + + auto push_property(const JSON::String &property, + const JSON::Object::hash_type hash) -> void { + if (this->depth_ == this->capacity_) { + this->grow(); + } + + auto &frame{*(this->storage_.data() + this->depth_)}; + frame.property = &property; + frame.hash = hash; + this->depth_ += 1; + } + + auto push_index(const std::size_t index) -> void { + if (this->depth_ == this->capacity_) { + this->grow(); + } + + auto &frame{*(this->storage_.data() + this->depth_)}; + frame.property = nullptr; + frame.index = index; + this->depth_ += 1; + } + + auto pop() -> void { this->depth_ -= 1; } + + auto materialize(WeakPointer &location) const -> void { + location.pop_back(location.size()); + const auto *frame{this->storage_.data()}; + const auto *const frames_end{frame + this->depth_}; + for (; frame != frames_end; ++frame) { + if (frame->property == nullptr) { + location.emplace_back(frame->index); + } else { + location.emplace_back(std::cref(*frame->property), frame->hash); + } + } + } + +private: + auto grow() -> void { + this->capacity_ *= 2; + this->storage_.resize(this->capacity_); + } + + std::size_t depth_{0}; + std::size_t capacity_{64}; + std::vector storage_; +}; + struct EvaluationState { const JSON *root; const JSONPath::Callback *callback; + LocationStack frames; WeakPointer location; }; -// The value of a comparable during evaluation: absent, borrowed from the -// document or the compiled query, or computed by a function -using FilterValue = std::variant; +auto filter_matches(const JSONPath::FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool; -auto filter_value_pointer(const FilterValue &value) -> const JSON * { - switch (value.index()) { - case 0: - return nullptr; - case 1: - return *std::get_if(&value); - default: - return std::get_if(&value); - } +// The underlying containers are contiguous, so iterating through raw +// pointers instead of iterators keeps the hot traversal loops free of the +// unoptimized iterator call chains of debug builds +inline auto first_member(const JSON::Object &object) + -> const JSON::Object::Entry * { + return object.empty() ? nullptr : &*object.cbegin(); } -auto filter_matches(const JSONPath::FilterExpression &expression, - const JSON &candidate, const JSON &root) -> bool; +inline auto first_element(const JSON::Array &array) -> const JSON * { + return array.size() == 0 ? nullptr : &*array.cbegin(); +} // RFC 9535 Section 2.3.4.2.2: slice expression bounds against a concrete // array length, where an omitted start or end defaults according to the @@ -80,8 +140,12 @@ auto filter_query_visit(const std::vector &segments, return visitor(current); } - const auto &segment{segments[cursor]}; - for (const auto &selector : segment.selectors) { + const auto &segment{*(segments.data() + cursor)}; + const auto type{current.type()}; + const auto *selector_cursor{segment.selectors.data()}; + const auto *const selectors_end{selector_cursor + segment.selectors.size()}; + for (; selector_cursor != selectors_end; ++selector_cursor) { + const auto &selector{*selector_cursor}; switch (static_cast(selector.index())) { case JSONPath::SelectorKind::Name: { const auto *entry{std::get_if(&selector)}; @@ -96,7 +160,7 @@ auto filter_query_visit(const std::vector &segments, break; } case JSONPath::SelectorKind::Wildcard: - if (current.is_object()) { + if (type == JSON::Type::Object) { for (const auto &member : current.as_object()) { if (!filter_query_visit(segments, cursor + 1, member.second, root, visitor)) { @@ -213,21 +277,24 @@ inline auto resolve_singular(const JSONPath::FilterQuery &query, const JSON &candidate, const JSON &root) -> const JSON * { const auto *current{query.relative ? &candidate : &root}; - for (const auto &segment : query.segments) { - assert(!segment.descendant); - assert(segment.selectors.size() == 1); - const auto &selector{segment.selectors.front()}; - if (std::holds_alternative(selector)) { - const auto *entry{std::get_if(&selector)}; + const auto *segment{query.segments.data()}; + const auto *const segments_end{segment + query.segments.size()}; + for (; segment != segments_end; ++segment) { + assert(!segment->descendant); + assert(segment->selectors.size() == 1); + const auto *selector{segment->selectors.data()}; + if (selector->index() == 0) { + const auto *entry{std::get_if(selector)}; current = current->is_object() ? current->try_at(entry->name, entry->hash) : nullptr; } else { - const auto *entry{std::get_if(&selector)}; + const auto *entry{std::get_if(selector)}; if (current->is_array()) { - const auto size{static_cast(current->as_array().size())}; + const auto &array{current->as_array()}; + const auto size{static_cast(array.size())}; const auto index{entry->index < 0 ? entry->index + size : entry->index}; current = index >= 0 && index < size - ? ¤t->at(static_cast(index)) + ? first_element(array) + static_cast(index) : nullptr; } else { current = nullptr; @@ -243,56 +310,60 @@ inline auto resolve_singular(const JSONPath::FilterQuery &query, } auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, - const JSON &candidate, const JSON &root) - -> FilterValue; + const JSON &candidate, const JSON &root, + std::optional &storage) -> const JSON *; +// The value of a comparable during evaluation is a borrowed pointer, or no +// pointer for the special result Nothing. Computed function results live in +// the caller provided storage inline auto evaluate_operand(const JSONPath::FilterOperand &operand, - const JSON &candidate, const JSON &root) - -> FilterValue { - if (std::holds_alternative(operand.value)) { - return FilterValue{&std::get(operand.value)}; - } - - if (std::holds_alternative(operand.value)) { - const auto *result{resolve_singular( - std::get(operand.value), candidate, root)}; - return result == nullptr ? FilterValue{} : FilterValue{result}; + const JSON &candidate, const JSON &root, + std::optional &storage) -> const JSON * { + switch (operand.value.index()) { + case 0: + return std::get_if(&operand.value); + case 1: + return resolve_singular( + *std::get_if(&operand.value), candidate, root); + default: + return evaluate_value_function( + *std::get_if(&operand.value), candidate, + root, storage); } - - return evaluate_value_function( - std::get(operand.value), candidate, root); } inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, - const JSON &candidate, const JSON &root) - -> FilterValue { + const JSON &candidate, const JSON &root, + std::optional &storage) + -> const JSON * { switch (call.function) { // RFC 9535 Section 2.4.4: the length function counts string characters, // array elements, or object members, and is nothing for anything else case JSONPath::FilterFunctionName::Length: { - const auto value{ - evaluate_operand(call.arguments.front(), candidate, root)}; - const auto *target{filter_value_pointer(value)}; + std::optional argument_storage; + const auto *target{evaluate_operand(call.arguments.front(), candidate, + root, argument_storage)}; if (target == nullptr) { - return FilterValue{}; + return nullptr; } if (target->is_string()) { - return FilterValue{JSON{static_cast( - utf8_codepoint_count(target->to_string()))}}; + storage.emplace(static_cast( + utf8_codepoint_count(target->to_string()))); + return &storage.value(); } if (target->is_array()) { - return FilterValue{ - JSON{static_cast(target->as_array().size())}}; + storage.emplace(static_cast(target->as_array().size())); + return &storage.value(); } if (target->is_object()) { - return FilterValue{ - JSON{static_cast(target->as_object().size())}}; + storage.emplace(static_cast(target->as_object().size())); + return &storage.value(); } - return FilterValue{}; + return nullptr; } // RFC 9535 Section 2.4.5: the count function yields the number of nodes case JSONPath::FilterFunctionName::Count: { @@ -304,7 +375,8 @@ inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, count += 1; return true; }); - return FilterValue{JSON{count}}; + storage.emplace(count); + return &storage.value(); } // RFC 9535 Section 2.4.8: the value function yields the value of a // single node and nothing otherwise @@ -319,11 +391,11 @@ inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, count += 1; return count < 2; }); - return count == 1 ? FilterValue{single} : FilterValue{}; + return count == 1 ? single : nullptr; } default: assert(false); - return FilterValue{}; + return nullptr; } } @@ -333,8 +405,9 @@ inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, inline auto evaluate_logical_function(const JSONPath::FilterFunctionCall &call, const JSON &candidate, const JSON &root) -> bool { - const auto input{evaluate_operand(call.arguments.front(), candidate, root)}; - const auto *subject{filter_value_pointer(input)}; + std::optional input_storage; + const auto *subject{ + evaluate_operand(call.arguments.front(), candidate, root, input_storage)}; if (subject == nullptr || !subject->is_string()) { return false; } @@ -345,8 +418,9 @@ inline auto evaluate_logical_function(const JSONPath::FilterFunctionCall &call, matches(call.compiled.value(), subject->to_string()); } - const auto pattern{evaluate_operand(call.arguments.back(), candidate, root)}; - const auto *expression{filter_value_pointer(pattern)}; + std::optional pattern_storage; + const auto *expression{evaluate_operand(call.arguments.back(), candidate, + root, pattern_storage)}; if (expression == nullptr || !expression->is_string()) { return false; } @@ -390,25 +464,25 @@ inline auto filter_less(const JSON *left, const JSON *right) -> bool { inline auto filter_compare(const JSONPath::FilterComparison &comparison, const JSON &candidate, const JSON &root) -> bool { - const auto left{evaluate_operand(comparison.left, candidate, root)}; - const auto right{evaluate_operand(comparison.right, candidate, root)}; - const auto *left_value{filter_value_pointer(left)}; - const auto *right_value{filter_value_pointer(right)}; + std::optional left_storage; + std::optional right_storage; + const auto *left{ + evaluate_operand(comparison.left, candidate, root, left_storage)}; + const auto *right{ + evaluate_operand(comparison.right, candidate, root, right_storage)}; switch (comparison.operation) { case JSONPath::FilterComparisonOperator::Equal: - return filter_equals(left_value, right_value); + return filter_equals(left, right); case JSONPath::FilterComparisonOperator::NotEqual: - return !filter_equals(left_value, right_value); + return !filter_equals(left, right); case JSONPath::FilterComparisonOperator::Less: - return filter_less(left_value, right_value); + return filter_less(left, right); case JSONPath::FilterComparisonOperator::LessEqual: - return filter_less(left_value, right_value) || - filter_equals(left_value, right_value); + return filter_less(left, right) || filter_equals(left, right); case JSONPath::FilterComparisonOperator::Greater: - return filter_less(right_value, left_value); + return filter_less(right, left); case JSONPath::FilterComparisonOperator::GreaterEqual: - return filter_less(right_value, left_value) || - filter_equals(left_value, right_value); + return filter_less(right, left) || filter_equals(left, right); } assert(false); @@ -417,56 +491,63 @@ inline auto filter_compare(const JSONPath::FilterComparison &comparison, inline auto filter_matches(const JSONPath::FilterExpression &expression, const JSON &candidate, const JSON &root) -> bool { - if (std::holds_alternative(expression.value)) { - return filter_compare( - std::get(expression.value), candidate, - root); - } + switch ( + static_cast(expression.value.index())) { + case JSONPath::FilterExpressionKind::Comparison: + return filter_compare( + *std::get_if(&expression.value), + candidate, root); + case JSONPath::FilterExpressionKind::Test: { + const auto &test{*std::get_if(&expression.value)}; + bool result{false}; + if (test.subject.index() == 0) { + const auto &query{*std::get_if(&test.subject)}; + result = !filter_query_visit( + query.segments, 0, query.relative ? candidate : root, root, + [](const JSON &) -> bool { return false; }); + } else { + result = evaluate_logical_function( + *std::get_if(&test.subject), + candidate, root); + } - if (std::holds_alternative(expression.value)) { - const auto &test{std::get(expression.value)}; - bool result{false}; - if (std::holds_alternative(test.subject)) { - const auto &query{std::get(test.subject)}; - result = !filter_query_visit(query.segments, 0, - query.relative ? candidate : root, root, - [](const JSON &) -> bool { return false; }); - } else { - result = evaluate_logical_function( - std::get(test.subject), candidate, - root); + return test.negated ? !result : result; } - - return test.negated ? !result : result; - } - - if (std::holds_alternative(expression.value)) { - const auto &conjunction{ - std::get(expression.value)}; - for (const auto &child : conjunction.children) { - if (!filter_matches(child, candidate, root)) { - return false; + case JSONPath::FilterExpressionKind::Conjunction: { + const auto &conjunction{ + *std::get_if(&expression.value)}; + const auto *child{conjunction.children.data()}; + const auto *const children_end{child + conjunction.children.size()}; + for (; child != children_end; ++child) { + if (!filter_matches(*child, candidate, root)) { + return false; + } } - } - - return true; - } - if (std::holds_alternative(expression.value)) { - const auto &disjunction{ - std::get(expression.value)}; - for (const auto &child : disjunction.children) { - if (filter_matches(child, candidate, root)) { - return true; - } + return true; } + case JSONPath::FilterExpressionKind::Disjunction: { + const auto &disjunction{ + *std::get_if(&expression.value)}; + const auto *child{disjunction.children.data()}; + const auto *const children_end{child + disjunction.children.size()}; + for (; child != children_end; ++child) { + if (filter_matches(*child, candidate, root)) { + return true; + } + } - return false; + return false; + } + case JSONPath::FilterExpressionKind::Negation: + return !filter_matches( + std::get_if(&expression.value) + ->children.front(), + candidate, root); } - return !filter_matches( - std::get(expression.value).children.front(), - candidate, root); + assert(false); + return false; } auto evaluate_segments(const std::vector &segments, @@ -476,80 +557,94 @@ auto evaluate_segments(const std::vector &segments, inline auto evaluate_selectors(const std::vector &segments, const std::size_t cursor, const JSON ¤t, EvaluationState &state) -> void { - for (const auto &selector : segments[cursor].selectors) { - switch (static_cast(selector.index())) { + const auto &segment{*(segments.data() + cursor)}; + const auto type{current.type()}; + const auto *selector{segment.selectors.data()}; + const auto *const selectors_end{selector + segment.selectors.size()}; + for (; selector != selectors_end; ++selector) { + switch (static_cast(selector->index())) { case JSONPath::SelectorKind::Name: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { const auto *child{current.try_at(entry->name, entry->hash)}; if (child != nullptr) { - state.location.emplace_back(std::cref(entry->name), entry->hash); + state.frames.push_property(entry->name, entry->hash); evaluate_segments(segments, cursor + 1, *child, state); - state.location.pop_back(); + state.frames.pop(); } } break; } case JSONPath::SelectorKind::Wildcard: - if (current.is_object()) { - for (const auto &member : current.as_object()) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_segments(segments, cursor + 1, member.second, state); - state.location.pop_back(); + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + state.frames.push_property(member->first, member->hash); + evaluate_segments(segments, cursor + 1, member->second, state); + state.frames.pop(); } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - state.location.emplace_back(index); - evaluate_segments(segments, cursor + 1, current.at(index), state); - state.location.pop_back(); + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + state.frames.push_index(index); + evaluate_segments(segments, cursor + 1, *element, state); + state.frames.pop(); } } break; case JSONPath::SelectorKind::Index: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; const auto index{entry->index < 0 ? entry->index + size : entry->index}; if (index >= 0 && index < size) { - state.location.emplace_back(static_cast(index)); - evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), - state); - state.location.pop_back(); + state.frames.push_index(static_cast(index)); + evaluate_segments( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), + state); + state.frames.pop(); } } break; } case JSONPath::SelectorKind::Slice: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; std::int64_t lower{0}; std::int64_t upper{0}; if (!slice_bounds(*entry, size, lower, upper)) { break; } + const auto *const base{first_element(array)}; if (entry->step > 0) { for (auto index{lower}; index < upper; index += entry->step) { - state.location.emplace_back(static_cast(index)); + state.frames.push_index(static_cast(index)); evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), + *(base + static_cast(index)), state); - state.location.pop_back(); + state.frames.pop(); } } else { for (auto index{upper}; index > lower; index += entry->step) { - state.location.emplace_back(static_cast(index)); + state.frames.push_index(static_cast(index)); evaluate_segments(segments, cursor + 1, - current.at(static_cast(index)), + *(base + static_cast(index)), state); - state.location.pop_back(); + state.frames.pop(); } } } @@ -557,23 +652,29 @@ inline auto evaluate_selectors(const std::vector &segments, break; } case JSONPath::SelectorKind::Filter: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (filter_matches(entry->expression, member.second, *state.root)) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_segments(segments, cursor + 1, member.second, state); - state.location.pop_back(); + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (filter_matches(entry->expression, member->second, + *state.root)) { + state.frames.push_property(member->first, member->hash); + evaluate_segments(segments, cursor + 1, member->second, state); + state.frames.pop(); } } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - if (filter_matches(entry->expression, current.at(index), - *state.root)) { - state.location.emplace_back(index); - evaluate_segments(segments, cursor + 1, current.at(index), state); - state.location.pop_back(); + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + if (filter_matches(entry->expression, *element, *state.root)) { + state.frames.push_index(index); + evaluate_segments(segments, cursor + 1, *element, state); + state.frames.pop(); } } } @@ -591,18 +692,24 @@ inline auto evaluate_descendant(const std::vector &segments, const std::size_t cursor, const JSON ¤t, EvaluationState &state) -> void { evaluate_selectors(segments, cursor, current, state); - if (current.is_object()) { - for (const auto &member : current.as_object()) { - state.location.emplace_back(std::cref(member.first), member.hash); - evaluate_descendant(segments, cursor, member.second, state); - state.location.pop_back(); + const auto type{current.type()}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + state.frames.push_property(member->first, member->hash); + evaluate_descendant(segments, cursor, member->second, state); + state.frames.pop(); } - } else if (current.is_array()) { - const auto size{current.as_array().size()}; - for (std::size_t index = 0; index < size; ++index) { - state.location.emplace_back(index); - evaluate_descendant(segments, cursor, current.at(index), state); - state.location.pop_back(); + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; ++element, ++index) { + state.frames.push_index(index); + evaluate_descendant(segments, cursor, *element, state); + state.frames.pop(); } } } @@ -611,6 +718,7 @@ inline auto evaluate_segments(const std::vector &segments, const std::size_t cursor, const JSON ¤t, EvaluationState &state) -> void { if (cursor == segments.size()) { + state.frames.materialize(state.location); (*state.callback)(current, state.location); return; } @@ -629,8 +737,11 @@ JSONPath::JSONPath(const JSON::StringView expression) auto JSONPath::evaluate(const JSON &document, const Callback &callback) const -> void { - EvaluationState state{ - .root = &document, .callback = &callback, .location = WeakPointer{}}; + EvaluationState state{.root = &document, + .callback = &callback, + .frames = LocationStack{}, + .location = WeakPointer{}}; + state.location.reserve(32); evaluate_segments(this->query_.segments, 0, document, state); } From dd7fc734b4b267b840b1fe69e4112c9bf27cb4a1 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 13:14:09 -0300 Subject: [PATCH 12/13] More Signed-off-by: Juan Cruz Viotti --- .../include/sourcemeta/core/jsonpath.h | 12 + src/core/jsonpath/jsonpath.cc | 692 ++++++++++++++++-- src/core/jsonpath/parser.h | 21 +- test/jsonpath/jsonpath_evaluate_test.cc | 33 + test/jsonpath/jsonpath_filter_test.cc | 15 + 5 files changed, 704 insertions(+), 69 deletions(-) diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h index ad22933a9c..a2a7ad6f10 100644 --- a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -223,10 +223,22 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { }; #endif + /// The precomputed shape of a query segment + enum class SegmentKind : std::uint8_t { + /// A single name selector applied to children + SingleName, + /// A single index selector applied to children + SingleIndex, + /// Any other combination of selectors + General + }; + /// A step in a query struct Segment { /// Whether the segment also applies to every descendant of its input bool descendant; + /// The precomputed shape of the segment for fast dispatching + SegmentKind kind; /// The selectors the segment applies std::vector selectors; }; diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index c98afe9ff7..e2dae4c828 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -56,6 +56,10 @@ class LocationStack { auto pop() -> void { this->depth_ -= 1; } + auto truncate(const std::size_t depth) -> void { this->depth_ = depth; } + + [[nodiscard]] auto depth() const -> std::size_t { return this->depth_; } + auto materialize(WeakPointer &location) const -> void { location.pop_back(location.size()); const auto *frame{this->storage_.data()}; @@ -80,10 +84,94 @@ class LocationStack { std::vector storage_; }; +// A pending traversal step. A frame carrying step pushes its location frame +// when it executes and leaves a paired marker that restores the location +// once its whole subtree has been processed +enum class WorkKind : std::uint8_t { Visit, VisitProperty, VisitIndex }; + +struct WorkItem { + WorkKind kind; + const JSON *node; + std::size_t cursor; + std::size_t frame_depth; + const JSON::String *property; + JSON::Object::hash_type hash; + std::size_t index; +}; + +class WorkStack { +public: + WorkStack() = default; + + [[nodiscard]] auto empty() const -> bool { return this->depth_ == 0; } + + [[nodiscard]] auto mark() const -> std::size_t { return this->depth_; } + + auto reverse_from(const std::size_t from) -> void { + std::reverse(this->storage_.data() + from, + this->storage_.data() + this->depth_); + } + + auto pop() -> WorkItem { + this->depth_ -= 1; + return *(this->storage_.data() + this->depth_); + } + + auto push_visit(const JSON *node, const std::size_t cursor, + const std::size_t frame_depth) -> void { + auto &item{this->next()}; + item.kind = WorkKind::Visit; + item.node = node; + item.cursor = cursor; + item.frame_depth = frame_depth; + } + + auto push_visit_property(const JSON *node, const std::size_t cursor, + const std::size_t frame_depth, + const JSON::String &property, + const JSON::Object::hash_type hash) -> void { + auto &item{this->next()}; + item.kind = WorkKind::VisitProperty; + item.node = node; + item.cursor = cursor; + item.frame_depth = frame_depth; + item.property = &property; + item.hash = hash; + } + + auto push_visit_index(const JSON *node, const std::size_t cursor, + const std::size_t frame_depth, const std::size_t index) + -> void { + auto &item{this->next()}; + item.kind = WorkKind::VisitIndex; + item.node = node; + item.cursor = cursor; + item.frame_depth = frame_depth; + item.index = index; + } + +private: + auto next() -> WorkItem & { + if (this->depth_ == this->capacity_) { + this->capacity_ = this->capacity_ == 0 ? 64 : this->capacity_ * 2; + this->storage_.resize(this->capacity_); + } + + auto &item{*(this->storage_.data() + this->depth_)}; + this->depth_ += 1; + return item; + } + + std::size_t depth_{0}; + std::size_t capacity_{0}; + std::vector storage_; +}; + struct EvaluationState { const JSON *root; const JSONPath::Callback *callback; LocationStack frames; + WorkStack work; WeakPointer location; }; @@ -131,28 +219,235 @@ inline auto slice_bounds(const JSONPath::SelectorSlice &slice, // Walk the nodes selected by a filter query without tracking locations. The // visitor returns whether to continue, and the traversal reports whether it -// ran to completion without the visitor stopping it +// ran to completion without the visitor stopping it. The walk is iterative +// over an explicit stack, so document depth cannot exhaust the machine +// stack, as RFC 9535 Section 4.1 warns about naive recursive descendants +struct FilterWorkItem { + const JSON *node; + std::size_t cursor; +}; + +template +auto filter_query_visit_iterative( + const std::vector &segments, const std::size_t cursor, + const JSON &origin, const JSON &root, const Visitor &visitor) -> bool { + // Filters nest, so invocations share the pool as a segmented stack, each + // one operating strictly above its entry depth + thread_local std::vector pool; + const auto base{pool.size()}; + pool.push_back({.node = &origin, .cursor = cursor}); + const auto total{segments.size()}; + bool completed{true}; + while (pool.size() > base) { + const auto item{pool.back()}; + pool.pop_back(); + if (item.cursor == total) { + if (!visitor(*item.node)) { + completed = false; + break; + } + + continue; + } + + const auto &segment{*(segments.data() + item.cursor)}; + const auto ¤t{*item.node}; + const auto type{current.type()}; + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleName) { + if (type == JSON::Type::Object) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + pool.push_back({.node = child, .cursor = item.cursor + 1}); + } + } + + continue; + } + + const auto mark{pool.size()}; + const auto *selector{segment.selectors.data()}; + const auto *const selectors_end{selector + segment.selectors.size()}; + for (; selector != selectors_end; ++selector) { + switch (static_cast(selector->index())) { + case JSONPath::SelectorKind::Name: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + pool.push_back({.node = child, .cursor = item.cursor + 1}); + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + pool.push_back( + {.node = &member->second, .cursor = item.cursor + 1}); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + pool.push_back({.node = element, .cursor = item.cursor + 1}); + } + } + + break; + case JSONPath::SelectorKind::Index: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size) { + pool.push_back({.node = first_element(array) + + static_cast(index), + .cursor = item.cursor + 1}); + } + } + + break; + } + case JSONPath::SelectorKind::Slice: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + const auto *const array_base{first_element(array)}; + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + pool.push_back( + {.node = array_base + static_cast(index), + .cursor = item.cursor + 1}); + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + pool.push_back( + {.node = array_base + static_cast(index), + .cursor = item.cursor + 1}); + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (filter_matches(entry->expression, member->second, root)) { + pool.push_back( + {.node = &member->second, .cursor = item.cursor + 1}); + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + if (filter_matches(entry->expression, *element, root)) { + pool.push_back({.node = element, .cursor = item.cursor + 1}); + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + pool.push_back({.node = &member->second, .cursor = item.cursor}); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + pool.push_back({.node = element, .cursor = item.cursor}); + } + } + } + + std::reverse(pool.begin() + static_cast(mark), pool.end()); + } + + pool.resize(base); + return completed; +} + +// The recursion depth after which the walks defer to their iterative +// twins, so that absurdly deep documents cannot exhaust the machine stack, +// as RFC 9535 Section 4.1 warns, while typical documents keep the cheaper +// native call frames +constexpr std::size_t JSONPATH_EVALUATION_RECURSION_LIMIT{256}; + template auto filter_query_visit(const std::vector &segments, const std::size_t cursor, const JSON ¤t, - const JSON &root, const Visitor &visitor) -> bool { + const JSON &root, const Visitor &visitor, + const std::size_t depth = 0) -> bool { + if (depth >= JSONPATH_EVALUATION_RECURSION_LIMIT) { + return filter_query_visit_iterative(segments, cursor, current, root, + visitor); + } + if (cursor == segments.size()) { return visitor(current); } const auto &segment{*(segments.data() + cursor)}; const auto type{current.type()}; - const auto *selector_cursor{segment.selectors.data()}; - const auto *const selectors_end{selector_cursor + segment.selectors.size()}; - for (; selector_cursor != selectors_end; ++selector_cursor) { - const auto &selector{*selector_cursor}; - switch (static_cast(selector.index())) { + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleName) { + if (type == JSON::Type::Object) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + return filter_query_visit(segments, cursor + 1, *child, root, visitor, + depth + 1); + } + } + + return true; + } + + const auto *selector{segment.selectors.data()}; + const auto *const selectors_end{selector + segment.selectors.size()}; + for (; selector != selectors_end; ++selector) { + switch (static_cast(selector->index())) { case JSONPath::SelectorKind::Name: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { const auto *child{current.try_at(entry->name, entry->hash)}; - if (child != nullptr && !filter_query_visit(segments, cursor + 1, - *child, root, visitor)) { + if (child != nullptr && + !filter_query_visit(segments, cursor + 1, *child, root, visitor, + depth + 1)) { return false; } } @@ -161,16 +456,22 @@ auto filter_query_visit(const std::vector &segments, } case JSONPath::SelectorKind::Wildcard: if (type == JSON::Type::Object) { - for (const auto &member : current.as_object()) { - if (!filter_query_visit(segments, cursor + 1, member.second, root, - visitor)) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (!filter_query_visit(segments, cursor + 1, member->second, root, + visitor, depth + 1)) { return false; } } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (!filter_query_visit(segments, cursor + 1, element, root, - visitor)) { + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + if (!filter_query_visit(segments, cursor + 1, *element, root, + visitor, depth + 1)) { return false; } } @@ -178,15 +479,17 @@ auto filter_query_visit(const std::vector &segments, break; case JSONPath::SelectorKind::Index: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; const auto index{entry->index < 0 ? entry->index + size : entry->index}; if (index >= 0 && index < size && - !filter_query_visit(segments, cursor + 1, - current.at(static_cast(index)), - root, visitor)) { + !filter_query_visit( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), + root, visitor, depth + 1)) { return false; } } @@ -194,21 +497,23 @@ auto filter_query_visit(const std::vector &segments, break; } case JSONPath::SelectorKind::Slice: { - const auto *entry{std::get_if(&selector)}; - if (current.is_array()) { - const auto size{static_cast(current.as_array().size())}; + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; std::int64_t lower{0}; std::int64_t upper{0}; if (!slice_bounds(*entry, size, lower, upper)) { break; } + const auto *const array_base{first_element(array)}; if (entry->step > 0) { for (auto index{lower}; index < upper; index += entry->step) { if (!filter_query_visit( segments, cursor + 1, - current.at(static_cast(index)), root, - visitor)) { + *(array_base + static_cast(index)), root, + visitor, depth + 1)) { return false; } } @@ -216,8 +521,8 @@ auto filter_query_visit(const std::vector &segments, for (auto index{upper}; index > lower; index += entry->step) { if (!filter_query_visit( segments, cursor + 1, - current.at(static_cast(index)), root, - visitor)) { + *(array_base + static_cast(index)), root, + visitor, depth + 1)) { return false; } } @@ -227,20 +532,26 @@ auto filter_query_visit(const std::vector &segments, break; } case JSONPath::SelectorKind::Filter: { - const auto *entry{std::get_if(&selector)}; - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (filter_matches(entry->expression, member.second, root) && - !filter_query_visit(segments, cursor + 1, member.second, root, - visitor)) { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (filter_matches(entry->expression, member->second, root) && + !filter_query_visit(segments, cursor + 1, member->second, root, + visitor, depth + 1)) { return false; } } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (filter_matches(entry->expression, element, root) && - !filter_query_visit(segments, cursor + 1, element, root, - visitor)) { + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + if (filter_matches(entry->expression, *element, root) && + !filter_query_visit(segments, cursor + 1, *element, root, + visitor, depth + 1)) { return false; } } @@ -252,16 +563,23 @@ auto filter_query_visit(const std::vector &segments, } if (segment.descendant) { - if (current.is_object()) { - for (const auto &member : current.as_object()) { - if (!filter_query_visit(segments, cursor, member.second, root, - visitor)) { + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (!filter_query_visit(segments, cursor, member->second, root, visitor, + depth + 1)) { return false; } } - } else if (current.is_array()) { - for (const auto &element : current.as_array()) { - if (!filter_query_visit(segments, cursor, element, root, visitor)) { + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (; element != elements_end; ++element) { + if (!filter_query_visit(segments, cursor, *element, root, visitor, + depth + 1)) { return false; } } @@ -451,6 +769,16 @@ inline auto filter_less(const JSON *left, const JSON *right) -> bool { return false; } + // Same representation comparisons are trivially exact without the + // generic value ordering dispatch + if (left->is_integer() && right->is_integer()) { + return left->to_integer() < right->to_integer(); + } + + if (left->is_real() && right->is_real()) { + return left->to_real() < right->to_real(); + } + // RFC 9535 Section 2.3.5.2.2: ordering applies only when both sides are // numbers or both sides are strings. The value comparison itself orders // numbers of different representations by exact value @@ -550,15 +878,233 @@ inline auto filter_matches(const JSONPath::FilterExpression &expression, return false; } +// The main walk is iterative over an explicit stack, so document depth +// cannot exhaust the machine stack, as RFC 9535 Section 4.1 warns about +// naive recursive implementations of the descendant segment +inline auto evaluate_query(const std::vector &segments, + const std::size_t cursor, const JSON &origin, + EvaluationState &state) -> void { + auto &stack{state.work}; + stack.push_visit(&origin, cursor, state.frames.depth()); + const auto total{segments.size()}; + while (!stack.empty()) { + const auto item{stack.pop()}; + switch (item.kind) { + case WorkKind::VisitProperty: + state.frames.truncate(item.frame_depth); + state.frames.push_property(*item.property, item.hash); + break; + case WorkKind::VisitIndex: + state.frames.truncate(item.frame_depth); + state.frames.push_index(item.index); + break; + case WorkKind::Visit: + state.frames.truncate(item.frame_depth); + break; + } + + if (item.cursor == total) { + state.frames.materialize(state.location); + (*state.callback)(*item.node, state.location); + continue; + } + + const auto &segment{*(segments.data() + item.cursor)}; + const auto ¤t{*item.node}; + const auto type{current.type()}; + const auto child_depth{state.frames.depth()}; + // The overwhelmingly common segment shape skips the general selector + // loop, the variant dispatch, and the ordering reversal entirely + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleName) { + if (type == JSON::Type::Object) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + stack.push_visit_property(child, item.cursor + 1, child_depth, + entry->name, entry->hash); + } + } + + continue; + } + + // Steps are appended in document order and then reversed, so the stack + // pops them back in document order with every subtree fully processed + // before its next sibling + const auto mark{stack.mark()}; + const auto *selector{segment.selectors.data()}; + const auto *const selectors_end{selector + segment.selectors.size()}; + for (; selector != selectors_end; ++selector) { + switch (static_cast(selector->index())) { + case JSONPath::SelectorKind::Name: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + stack.push_visit_property(child, item.cursor + 1, child_depth, + entry->name, entry->hash); + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + stack.push_visit_property(&member->second, item.cursor + 1, + child_depth, member->first, + member->hash); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + stack.push_visit_index(element, item.cursor + 1, child_depth, + index); + } + } + + break; + case JSONPath::SelectorKind::Index: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size + : entry->index}; + if (index >= 0 && index < size) { + stack.push_visit_index(first_element(array) + + static_cast(index), + item.cursor + 1, child_depth, + static_cast(index)); + } + } + + break; + } + case JSONPath::SelectorKind::Slice: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + std::int64_t lower{0}; + std::int64_t upper{0}; + if (!slice_bounds(*entry, size, lower, upper)) { + break; + } + + const auto *const array_base{first_element(array)}; + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + stack.push_visit_index(array_base + + static_cast(index), + item.cursor + 1, child_depth, + static_cast(index)); + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + stack.push_visit_index(array_base + + static_cast(index), + item.cursor + 1, child_depth, + static_cast(index)); + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + if (filter_matches(entry->expression, member->second, + *state.root)) { + stack.push_visit_property(&member->second, item.cursor + 1, + child_depth, member->first, + member->hash); + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + if (filter_matches(entry->expression, *element, *state.root)) { + stack.push_visit_index(element, item.cursor + 1, child_depth, + index); + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + const auto *member{first_member(object)}; + const auto *const members_end{member + object.size()}; + for (; member != members_end; ++member) { + stack.push_visit_property(&member->second, item.cursor, child_depth, + member->first, member->hash); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + const auto *element{first_element(array)}; + const auto *const elements_end{element + array.size()}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + stack.push_visit_index(element, item.cursor, child_depth, index); + } + } + } + + stack.reverse_from(mark); + } +} + auto evaluate_segments(const std::vector &segments, const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void; + EvaluationState &state, const std::size_t depth) -> void; inline auto evaluate_selectors(const std::vector &segments, const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { + EvaluationState &state, const std::size_t depth) + -> void { const auto &segment{*(segments.data() + cursor)}; const auto type{current.type()}; + // The overwhelmingly common segment shape skips the general selector + // loop and variant dispatch entirely. Unlike the iterative walk, this + // function only applies selectors, so the shape check needs no descendant + // distinction + if (segment.kind == JSONPath::SegmentKind::SingleName) { + if (type == JSON::Type::Object) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto *child{current.try_at(entry->name, entry->hash)}; + if (child != nullptr) { + state.frames.push_property(entry->name, entry->hash); + evaluate_segments(segments, cursor + 1, *child, state, depth); + state.frames.pop(); + } + } + + return; + } + const auto *selector{segment.selectors.data()}; const auto *const selectors_end{selector + segment.selectors.size()}; for (; selector != selectors_end; ++selector) { @@ -569,7 +1115,7 @@ inline auto evaluate_selectors(const std::vector &segments, const auto *child{current.try_at(entry->name, entry->hash)}; if (child != nullptr) { state.frames.push_property(entry->name, entry->hash); - evaluate_segments(segments, cursor + 1, *child, state); + evaluate_segments(segments, cursor + 1, *child, state, depth); state.frames.pop(); } } @@ -583,7 +1129,8 @@ inline auto evaluate_selectors(const std::vector &segments, const auto *const members_end{member + object.size()}; for (; member != members_end; ++member) { state.frames.push_property(member->first, member->hash); - evaluate_segments(segments, cursor + 1, member->second, state); + evaluate_segments(segments, cursor + 1, member->second, state, + depth); state.frames.pop(); } } else if (type == JSON::Type::Array) { @@ -593,7 +1140,7 @@ inline auto evaluate_selectors(const std::vector &segments, for (std::size_t index{0}; element != elements_end; ++element, ++index) { state.frames.push_index(index); - evaluate_segments(segments, cursor + 1, *element, state); + evaluate_segments(segments, cursor + 1, *element, state, depth); state.frames.pop(); } } @@ -611,7 +1158,7 @@ inline auto evaluate_selectors(const std::vector &segments, evaluate_segments( segments, cursor + 1, *(first_element(array) + static_cast(index)), - state); + state, depth); state.frames.pop(); } } @@ -635,7 +1182,7 @@ inline auto evaluate_selectors(const std::vector &segments, state.frames.push_index(static_cast(index)); evaluate_segments(segments, cursor + 1, *(base + static_cast(index)), - state); + state, depth); state.frames.pop(); } } else { @@ -643,7 +1190,7 @@ inline auto evaluate_selectors(const std::vector &segments, state.frames.push_index(static_cast(index)); evaluate_segments(segments, cursor + 1, *(base + static_cast(index)), - state); + state, depth); state.frames.pop(); } } @@ -661,7 +1208,8 @@ inline auto evaluate_selectors(const std::vector &segments, if (filter_matches(entry->expression, member->second, *state.root)) { state.frames.push_property(member->first, member->hash); - evaluate_segments(segments, cursor + 1, member->second, state); + evaluate_segments(segments, cursor + 1, member->second, state, + depth); state.frames.pop(); } } @@ -673,7 +1221,7 @@ inline auto evaluate_selectors(const std::vector &segments, ++element, ++index) { if (filter_matches(entry->expression, *element, *state.root)) { state.frames.push_index(index); - evaluate_segments(segments, cursor + 1, *element, state); + evaluate_segments(segments, cursor + 1, *element, state, depth); state.frames.pop(); } } @@ -690,8 +1238,9 @@ inline auto evaluate_selectors(const std::vector &segments, // each visited node inline auto evaluate_descendant(const std::vector &segments, const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { - evaluate_selectors(segments, cursor, current, state); + EvaluationState &state, const std::size_t depth) + -> void { + evaluate_selectors(segments, cursor, current, state, depth); const auto type{current.type()}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; @@ -699,7 +1248,7 @@ inline auto evaluate_descendant(const std::vector &segments, const auto *const members_end{member + object.size()}; for (; member != members_end; ++member) { state.frames.push_property(member->first, member->hash); - evaluate_descendant(segments, cursor, member->second, state); + evaluate_descendant(segments, cursor, member->second, state, depth + 1); state.frames.pop(); } } else if (type == JSON::Type::Array) { @@ -708,7 +1257,7 @@ inline auto evaluate_descendant(const std::vector &segments, const auto *const elements_end{element + array.size()}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { state.frames.push_index(index); - evaluate_descendant(segments, cursor, *element, state); + evaluate_descendant(segments, cursor, *element, state, depth + 1); state.frames.pop(); } } @@ -716,17 +1265,23 @@ inline auto evaluate_descendant(const std::vector &segments, inline auto evaluate_segments(const std::vector &segments, const std::size_t cursor, const JSON ¤t, - EvaluationState &state) -> void { + EvaluationState &state, const std::size_t depth) + -> void { + if (depth >= JSONPATH_EVALUATION_RECURSION_LIMIT) { + evaluate_query(segments, cursor, current, state); + return; + } + if (cursor == segments.size()) { state.frames.materialize(state.location); (*state.callback)(current, state.location); return; } - if (segments[cursor].descendant) { - evaluate_descendant(segments, cursor, current, state); + if ((segments.data() + cursor)->descendant) { + evaluate_descendant(segments, cursor, current, state, depth + 1); } else { - evaluate_selectors(segments, cursor, current, state); + evaluate_selectors(segments, cursor, current, state, depth + 1); } } @@ -740,9 +1295,10 @@ auto JSONPath::evaluate(const JSON &document, const Callback &callback) const EvaluationState state{.root = &document, .callback = &callback, .frames = LocationStack{}, + .work = WorkStack{}, .location = WeakPointer{}}; state.location.reserve(32); - evaluate_segments(this->query_.segments, 0, document, state); + evaluate_segments(this->query_.segments, 0, document, state, 0); } auto JSONPath::normalize(const WeakPointer &location) -> JSON::String { diff --git a/src/core/jsonpath/parser.h b/src/core/jsonpath/parser.h index c16c8b2024..114d6aac11 100644 --- a/src/core/jsonpath/parser.h +++ b/src/core/jsonpath/parser.h @@ -106,12 +106,31 @@ class JSONPathParser { break; } - segments.push_back(this->parse_segment(singular)); + auto segment{this->parse_segment(singular)}; + segment.kind = classify_segment(segment); + segments.push_back(std::move(segment)); } return segments; } + [[nodiscard]] static auto classify_segment(const JSONPath::Segment &segment) + -> JSONPath::SegmentKind { + if (segment.selectors.size() != 1) { + return JSONPath::SegmentKind::General; + } + + switch (static_cast( + segment.selectors.front().index())) { + case JSONPath::SelectorKind::Name: + return JSONPath::SegmentKind::SingleName; + case JSONPath::SelectorKind::Index: + return JSONPath::SegmentKind::SingleIndex; + default: + return JSONPath::SegmentKind::General; + } + } + // segment = child-segment / descendant-segment auto parse_segment(bool &singular) -> JSONPath::Segment { JSONPath::Segment segment; diff --git a/test/jsonpath/jsonpath_evaluate_test.cc b/test/jsonpath/jsonpath_evaluate_test.cc index 08bb16f0a1..5776c6bd0d 100644 --- a/test/jsonpath/jsonpath_evaluate_test.cc +++ b/test/jsonpath/jsonpath_evaluate_test.cc @@ -229,6 +229,39 @@ TEST(jsonpath_evaluate_escaped_name_decodes) { EXPECT_EQ(nodes.at(0).value->to_integer(), 1); } +TEST(jsonpath_evaluate_descendant_deep_document) { + std::string text; + for (std::size_t index = 0; index < 10000; ++index) { + text += "{\"a\":"; + } + + text += "{\"leaf\":1}"; + text += std::string(10000, '}'); + const auto document{sourcemeta::core::parse_json(text)}; + const sourcemeta::core::JSONPath path{"$..leaf"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + EXPECT_EQ(nodes.at(0).value->to_integer(), 1); +} + +TEST(jsonpath_evaluate_deep_name_chain_document) { + std::string text; + for (std::size_t index = 0; index < 10000; ++index) { + text += "["; + } + + text += "1"; + text += std::string(10000, ']'); + const auto document{sourcemeta::core::parse_json(text)}; + const sourcemeta::core::JSONPath path{"$..*"}; + std::size_t count{0}; + path.evaluate(document, [&count](const sourcemeta::core::JSON &, + const sourcemeta::core::WeakPointer &) { + count += 1; + }); + EXPECT_EQ(count, 10000); +} + TEST(jsonpath_evaluate_copy_construction) { const auto document{sourcemeta::core::parse_json(R"JSON({ "a": 1 })JSON")}; const sourcemeta::core::JSONPath original{"$.a"}; diff --git a/test/jsonpath/jsonpath_filter_test.cc b/test/jsonpath/jsonpath_filter_test.cc index d4ef7af277..06ce8a8766 100644 --- a/test/jsonpath/jsonpath_filter_test.cc +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -362,6 +362,21 @@ TEST(jsonpath_filter_search_literal_dollar) { EXPECT_EQ(nodes.at(0).value->to_string(), "xab$cx"); } +TEST(jsonpath_filter_existence_deep_document) { + std::string text{"[ "}; + for (std::size_t index = 0; index < 10000; ++index) { + text += "{\"a\":"; + } + + text += "{\"leaf\":1}"; + text += std::string(10000, '}'); + text += ", { \"b\": 2 } ]"; + const auto document{sourcemeta::core::parse_json(text)}; + const sourcemeta::core::JSONPath path{"$[?@..leaf]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); +} + TEST(jsonpath_filter_current_node_comparison) { const auto document{sourcemeta::core::parse_json(R"JSON([ 1, 5, 10 ])JSON")}; const sourcemeta::core::JSONPath path{"$[?@ > 4]"}; From dfb000d24d73bd701c701c26562314d3a7ee7bc8 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Fri, 10 Jul 2026 13:26:37 -0300 Subject: [PATCH 13/13] More Signed-off-by: Juan Cruz Viotti --- src/core/jsonpath/jsonpath.cc | 179 ++++++++++++++++++++++++---------- 1 file changed, 127 insertions(+), 52 deletions(-) diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index e2dae4c828..43f674ed6c 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -180,10 +180,26 @@ auto filter_matches(const JSONPath::FilterExpression &expression, // The underlying containers are contiguous, so iterating through raw // pointers instead of iterators keeps the hot traversal loops free of the -// unoptimized iterator call chains of debug builds -inline auto first_member(const JSON::Object &object) - -> const JSON::Object::Entry * { - return object.empty() ? nullptr : &*object.cbegin(); +// unoptimized iterator call chains of debug builds. Empty containers yield +// an empty range of null pointers without any pointer arithmetic +inline auto member_range(const JSON::Object &object) + -> std::pair { + if (object.empty()) { + return {nullptr, nullptr}; + } + + const auto *begin{&*object.cbegin()}; + return {begin, begin + object.size()}; +} + +inline auto element_range(const JSON::Array &array) + -> std::pair { + if (array.size() == 0) { + return {nullptr, nullptr}; + } + + const auto *begin{&*array.cbegin()}; + return {begin, begin + array.size()}; } inline auto first_element(const JSON::Array &array) -> const JSON * { @@ -267,6 +283,24 @@ auto filter_query_visit_iterative( continue; } + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleIndex) { + if (type == JSON::Type::Array) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + if (index >= 0 && index < size) { + pool.push_back( + {.node = first_element(array) + static_cast(index), + .cursor = item.cursor + 1}); + } + } + + continue; + } + const auto mark{pool.size()}; const auto *selector{segment.selectors.data()}; const auto *const selectors_end{selector + segment.selectors.size()}; @@ -286,16 +320,14 @@ auto filter_query_visit_iterative( case JSONPath::SelectorKind::Wildcard: if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { pool.push_back( {.node = &member->second, .cursor = item.cursor + 1}); } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { pool.push_back({.node = element, .cursor = item.cursor + 1}); } @@ -351,8 +383,7 @@ auto filter_query_visit_iterative( const auto *entry{std::get_if(selector)}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (filter_matches(entry->expression, member->second, root)) { pool.push_back( @@ -361,8 +392,7 @@ auto filter_query_visit_iterative( } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { if (filter_matches(entry->expression, *element, root)) { pool.push_back({.node = element, .cursor = item.cursor + 1}); @@ -378,15 +408,13 @@ auto filter_query_visit_iterative( if (segment.descendant) { if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { pool.push_back({.node = &member->second, .cursor = item.cursor}); } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { pool.push_back({.node = element, .cursor = item.cursor}); } @@ -437,6 +465,25 @@ auto filter_query_visit(const std::vector &segments, return true; } + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleIndex) { + if (type == JSON::Type::Array) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + if (index >= 0 && index < size) { + return filter_query_visit( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), root, + visitor, depth + 1); + } + } + + return true; + } + const auto *selector{segment.selectors.data()}; const auto *const selectors_end{selector + segment.selectors.size()}; for (; selector != selectors_end; ++selector) { @@ -457,8 +504,7 @@ auto filter_query_visit(const std::vector &segments, case JSONPath::SelectorKind::Wildcard: if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (!filter_query_visit(segments, cursor + 1, member->second, root, visitor, depth + 1)) { @@ -467,8 +513,7 @@ auto filter_query_visit(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { if (!filter_query_visit(segments, cursor + 1, *element, root, visitor, depth + 1)) { @@ -535,8 +580,7 @@ auto filter_query_visit(const std::vector &segments, const auto *entry{std::get_if(selector)}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (filter_matches(entry->expression, member->second, root) && !filter_query_visit(segments, cursor + 1, member->second, root, @@ -546,8 +590,7 @@ auto filter_query_visit(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { if (filter_matches(entry->expression, *element, root) && !filter_query_visit(segments, cursor + 1, *element, root, @@ -565,8 +608,7 @@ auto filter_query_visit(const std::vector &segments, if (segment.descendant) { if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (!filter_query_visit(segments, cursor, member->second, root, visitor, depth + 1)) { @@ -575,8 +617,7 @@ auto filter_query_visit(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (; element != elements_end; ++element) { if (!filter_query_visit(segments, cursor, *element, root, visitor, depth + 1)) { @@ -930,6 +971,24 @@ inline auto evaluate_query(const std::vector &segments, continue; } + if (!segment.descendant && + segment.kind == JSONPath::SegmentKind::SingleIndex) { + if (type == JSON::Type::Array) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + if (index >= 0 && index < size) { + stack.push_visit_index( + first_element(array) + static_cast(index), + item.cursor + 1, child_depth, static_cast(index)); + } + } + + continue; + } + // Steps are appended in document order and then reversed, so the stack // pops them back in document order with every subtree fully processed // before its next sibling @@ -953,8 +1012,7 @@ inline auto evaluate_query(const std::vector &segments, case JSONPath::SelectorKind::Wildcard: if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { stack.push_visit_property(&member->second, item.cursor + 1, child_depth, member->first, @@ -962,8 +1020,7 @@ inline auto evaluate_query(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { stack.push_visit_index(element, item.cursor + 1, child_depth, @@ -1024,8 +1081,7 @@ inline auto evaluate_query(const std::vector &segments, const auto *entry{std::get_if(selector)}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (filter_matches(entry->expression, member->second, *state.root)) { @@ -1036,8 +1092,7 @@ inline auto evaluate_query(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { if (filter_matches(entry->expression, *element, *state.root)) { @@ -1055,16 +1110,14 @@ inline auto evaluate_query(const std::vector &segments, if (segment.descendant) { if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { stack.push_visit_property(&member->second, item.cursor, child_depth, member->first, member->hash); } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { stack.push_visit_index(element, item.cursor, child_depth, index); @@ -1105,6 +1158,26 @@ inline auto evaluate_selectors(const std::vector &segments, return; } + if (segment.kind == JSONPath::SegmentKind::SingleIndex) { + if (type == JSON::Type::Array) { + const auto *entry{ + std::get_if(segment.selectors.data())}; + const auto &array{current.as_array()}; + const auto size{static_cast(array.size())}; + const auto index{entry->index < 0 ? entry->index + size : entry->index}; + if (index >= 0 && index < size) { + state.frames.push_index(static_cast(index)); + evaluate_segments( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), state, + depth); + state.frames.pop(); + } + } + + return; + } + const auto *selector{segment.selectors.data()}; const auto *const selectors_end{selector + segment.selectors.size()}; for (; selector != selectors_end; ++selector) { @@ -1125,8 +1198,7 @@ inline auto evaluate_selectors(const std::vector &segments, case JSONPath::SelectorKind::Wildcard: if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { state.frames.push_property(member->first, member->hash); evaluate_segments(segments, cursor + 1, member->second, state, @@ -1135,8 +1207,7 @@ inline auto evaluate_selectors(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { state.frames.push_index(index); @@ -1202,8 +1273,7 @@ inline auto evaluate_selectors(const std::vector &segments, const auto *entry{std::get_if(selector)}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { if (filter_matches(entry->expression, member->second, *state.root)) { @@ -1215,8 +1285,7 @@ inline auto evaluate_selectors(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { if (filter_matches(entry->expression, *element, *state.root)) { @@ -1240,12 +1309,19 @@ inline auto evaluate_descendant(const std::vector &segments, const std::size_t cursor, const JSON ¤t, EvaluationState &state, const std::size_t depth) -> void { + // The descendant recursion advances on document depth without passing + // through the segment dispatcher, so it needs its own handoff to the + // iterative tier + if (depth >= JSONPATH_EVALUATION_RECURSION_LIMIT) { + evaluate_query(segments, cursor, current, state); + return; + } + evaluate_selectors(segments, cursor, current, state, depth); const auto type{current.type()}; if (type == JSON::Type::Object) { const auto &object{current.as_object()}; - const auto *member{first_member(object)}; - const auto *const members_end{member + object.size()}; + auto [member, members_end]{member_range(object)}; for (; member != members_end; ++member) { state.frames.push_property(member->first, member->hash); evaluate_descendant(segments, cursor, member->second, state, depth + 1); @@ -1253,8 +1329,7 @@ inline auto evaluate_descendant(const std::vector &segments, } } else if (type == JSON::Type::Array) { const auto &array{current.as_array()}; - const auto *element{first_element(array)}; - const auto *const elements_end{element + array.size()}; + auto [element, elements_end]{element_range(array)}; for (std::size_t index{0}; element != elements_end; ++element, ++index) { state.frames.push_index(index); evaluate_descendant(segments, cursor, *element, state, depth + 1);