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/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); 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..c85e3e435b --- /dev/null +++ b/src/core/jsonpath/CMakeLists.txt @@ -0,0 +1,20 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonpath + PRIVATE_HEADERS error.h + SOURCES jsonpath.cc parser.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 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 + sourcemeta::core::unicode) 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..a2a7ad6f10 --- /dev/null +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -0,0 +1,306 @@ +#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 + +#include // std::int64_t, std::uint8_t +#include // std::function +#include // std::optional +#include // std::variant +#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 + +/// @ingroup jsonpath +/// A parsed RFC 9535 JSONPath query that can be evaluated many times. +class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { +public: + /// 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; + }; + +#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 + 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 + + /// 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; + }; + + /// 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 + /// #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; + + /// 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]"}; + /// 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; + +private: + Query query_; +}; + +#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..1773df9618 --- /dev/null +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath_error.h @@ -0,0 +1,47 @@ +#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_; +}; + +#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 new file mode 100644 index 0000000000..43f674ed6c --- /dev/null +++ b/src/core/jsonpath/jsonpath.cc @@ -0,0 +1,1432 @@ +#include +#include +#include + +#include "parser.h" + +#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 { + +// 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 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()}; + 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_; +}; + +// 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; +}; + +auto filter_matches(const JSONPath::FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool; + +// 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. 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 * { + 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 +// 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. 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; + } + + 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()}; + 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()}; + 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()}; + auto [element, elements_end]{element_range(array)}; + 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()}; + auto [member, members_end]{member_range(object)}; + 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()}; + 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}); + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + 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()}; + auto [element, elements_end]{element_range(array)}; + 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, + 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()}; + 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; + } + + 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) { + 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 && + !filter_query_visit(segments, cursor + 1, *child, root, visitor, + depth + 1)) { + return false; + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + 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)) { + return false; + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + auto [element, elements_end]{element_range(array)}; + for (; element != elements_end; ++element) { + if (!filter_query_visit(segments, cursor + 1, *element, root, + visitor, depth + 1)) { + return false; + } + } + } + + 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 && + !filter_query_visit( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), + root, visitor, depth + 1)) { + return false; + } + } + + 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) { + if (!filter_query_visit( + segments, cursor + 1, + *(array_base + static_cast(index)), root, + visitor, depth + 1)) { + return false; + } + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + if (!filter_query_visit( + segments, cursor + 1, + *(array_base + static_cast(index)), root, + visitor, depth + 1)) { + return false; + } + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + 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, + visitor, depth + 1)) { + return false; + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + 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, + visitor, depth + 1)) { + return false; + } + } + } + + break; + } + } + } + + if (segment.descendant) { + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + auto [member, members_end]{member_range(object)}; + for (; member != members_end; ++member) { + if (!filter_query_visit(segments, cursor, member->second, root, visitor, + depth + 1)) { + return false; + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + auto [element, elements_end]{element_range(array)}; + for (; element != elements_end; ++element) { + if (!filter_query_visit(segments, cursor, *element, root, visitor, + depth + 1)) { + 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}; + 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)}; + if (current->is_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}; + current = index >= 0 && index < size + ? first_element(array) + 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, + 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, + 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); + } +} + +inline auto evaluate_value_function(const JSONPath::FilterFunctionCall &call, + 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: { + std::optional argument_storage; + const auto *target{evaluate_operand(call.arguments.front(), candidate, + root, argument_storage)}; + if (target == nullptr) { + return nullptr; + } + + if (target->is_string()) { + storage.emplace(static_cast( + utf8_codepoint_count(target->to_string()))); + return &storage.value(); + } + + if (target->is_array()) { + storage.emplace(static_cast(target->as_array().size())); + return &storage.value(); + } + + if (target->is_object()) { + storage.emplace(static_cast(target->as_object().size())); + return &storage.value(); + } + + return nullptr; + } + // 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; + }); + 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 + 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 ? single : nullptr; + } + default: + assert(false); + return nullptr; + } +} + +// 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 { + std::optional input_storage; + const auto *subject{ + evaluate_operand(call.arguments.front(), candidate, root, input_storage)}; + 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()); + } + + std::optional pattern_storage; + const auto *expression{evaluate_operand(call.arguments.back(), candidate, + root, pattern_storage)}; + 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; + } + + // 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 + if ((left->is_number() && right->is_number()) || + (left->is_string() && right->is_string())) { + return *left < *right; + } + + return false; +} + +inline auto filter_compare(const JSONPath::FilterComparison &comparison, + const JSON &candidate, const JSON &root) -> bool { + 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, right); + case JSONPath::FilterComparisonOperator::NotEqual: + return !filter_equals(left, right); + case JSONPath::FilterComparisonOperator::Less: + return filter_less(left, right); + case JSONPath::FilterComparisonOperator::LessEqual: + return filter_less(left, right) || filter_equals(left, right); + case JSONPath::FilterComparisonOperator::Greater: + return filter_less(right, left); + case JSONPath::FilterComparisonOperator::GreaterEqual: + return filter_less(right, left) || filter_equals(left, right); + } + + assert(false); + return false; +} + +inline auto filter_matches(const JSONPath::FilterExpression &expression, + const JSON &candidate, const JSON &root) -> bool { + 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); + } + + return test.negated ? !result : result; + } + 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; + } + 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; + } + case JSONPath::FilterExpressionKind::Negation: + return !filter_matches( + std::get_if(&expression.value) + ->children.front(), + candidate, root); + } + + assert(false); + 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; + } + + 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 + 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()}; + 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, + member->hash); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + 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, + 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()}; + auto [member, members_end]{member_range(object)}; + 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()}; + 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)) { + 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()}; + 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()}; + 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); + } + } + } + + stack.reverse_from(mark); + } +} + +auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + 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, 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; + } + + 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) { + 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) { + state.frames.push_property(entry->name, entry->hash); + evaluate_segments(segments, cursor + 1, *child, state, depth); + state.frames.pop(); + } + } + + break; + } + case JSONPath::SelectorKind::Wildcard: + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + 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, + depth); + state.frames.pop(); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + auto [element, elements_end]{element_range(array)}; + for (std::size_t index{0}; element != elements_end; + ++element, ++index) { + state.frames.push_index(index); + evaluate_segments(segments, cursor + 1, *element, state, depth); + state.frames.pop(); + } + } + + 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) { + state.frames.push_index(static_cast(index)); + evaluate_segments( + segments, cursor + 1, + *(first_element(array) + static_cast(index)), + state, depth); + state.frames.pop(); + } + } + + 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 base{first_element(array)}; + if (entry->step > 0) { + for (auto index{lower}; index < upper; index += entry->step) { + state.frames.push_index(static_cast(index)); + evaluate_segments(segments, cursor + 1, + *(base + static_cast(index)), + state, depth); + state.frames.pop(); + } + } else { + for (auto index{upper}; index > lower; index += entry->step) { + state.frames.push_index(static_cast(index)); + evaluate_segments(segments, cursor + 1, + *(base + static_cast(index)), + state, depth); + state.frames.pop(); + } + } + } + + break; + } + case JSONPath::SelectorKind::Filter: { + const auto *entry{std::get_if(selector)}; + if (type == JSON::Type::Object) { + const auto &object{current.as_object()}; + auto [member, members_end]{member_range(object)}; + 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, + depth); + state.frames.pop(); + } + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + 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)) { + state.frames.push_index(index); + evaluate_segments(segments, cursor + 1, *element, state, depth); + state.frames.pop(); + } + } + } + + 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, 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()}; + 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); + state.frames.pop(); + } + } else if (type == JSON::Type::Array) { + const auto &array{current.as_array()}; + 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); + state.frames.pop(); + } + } +} + +inline auto evaluate_segments(const std::vector &segments, + const std::size_t cursor, const JSON ¤t, + 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.data() + cursor)->descendant) { + evaluate_descendant(segments, cursor, current, state, depth + 1); + } else { + evaluate_selectors(segments, cursor, current, state, depth + 1); + } +} + +} // namespace + +JSONPath::JSONPath(const JSON::StringView expression) + : query_{parse_jsonpath(expression)} {} + +auto JSONPath::evaluate(const JSON &document, const Callback &callback) const + -> void { + EvaluationState state{.root = &document, + .callback = &callback, + .frames = LocationStack{}, + .work = WorkStack{}, + .location = WeakPointer{}}; + state.location.reserve(32); + evaluate_segments(this->query_.segments, 0, document, state, 0); +} + +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) { + result += "\\u00"; + result += bytes_to_hex(JSON::StringView{&character, 1}); + } 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..114d6aac11 --- /dev/null +++ b/src/core/jsonpath/parser.h @@ -0,0 +1,1085 @@ +#ifndef SOURCEMETA_CORE_JSONPATH_PARSER_H_ +#define SOURCEMETA_CORE_JSONPATH_PARSER_H_ + +#include +#include +#include +#include +#include +#include +#include + +#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 { + +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}; + +// 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} {} + + auto parse() -> JSONPath::Query { + // jsonpath-query = root-identifier segments + if (this->at_end() || this->peek() != '$') { + this->fail(); + } + + this->position_ += 1; + JSONPath::Query 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_]; + } + + // 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, + // overlong encodings, surrogates, and code points beyond U+10FFFF + auto copy_character(JSON::String &output) -> void { + const auto size{utf8_codepoint_length(this->input_, this->position_)}; + if (size == 0) { + 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; + } + + 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; + 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(JSONPath::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(JSONPath::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(JSONPath::Segment &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) -> JSONPath::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 JSONPath::SelectorName{.name = std::move(name), .hash = hash}; + } + + if (character == '*') { + this->position_ += 1; + singular = false; + return JSONPath::SelectorWildcard{}; + } + + if (character == '?') { + // filter-selector = "?" S logical-expr + this->position_ += 1; + singular = false; + this->skip_whitespace(); + return JSONPath::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 JSONPath::SelectorIndex{value}; + } + + this->fail(); + } + + auto parse_slice(const std::optional start) + -> JSONPath::SelectorSlice { + JSONPath::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; + } + + 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.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() -> JSONPath::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 JSONPath::SelectorName{.name = std::move(name), .hash = hash}; + } + + // 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) { + 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; + } + } + + this->depth_ -= 1; + if (children.size() == 1) { + return std::move(children.front()); + } + + return JSONPath::FilterExpression{ + JSONPath::FilterDisjunction{std::move(children)}}; + } + + // logical-and-expr = basic-expr *(S "&&" S basic-expr) + auto parse_logical_and() -> JSONPath::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 JSONPath::FilterExpression{ + JSONPath::FilterConjunction{std::move(children)}}; + } + + // basic-expr = paren-expr / comparison-expr / test-expr + auto parse_basic_expression() -> JSONPath::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() == '(') { + std::vector children; + children.push_back(this->parse_parenthesized()); + return JSONPath::FilterExpression{ + JSONPath::FilterNegation{.children = std::move(children)}}; + } + + return JSONPath::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() -> JSONPath::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) -> JSONPath::FilterTest { + if (this->peek() == '@' || this->peek() == '$') { + bool singular{true}; + auto query{this->parse_filter_query(singular)}; + return JSONPath::FilterTest{.negated = negated, + .subject = std::move(query)}; + } + + 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 + if (call.function != JSONPath::FilterFunctionName::Match && + call.function != JSONPath::FilterFunctionName::Search) { + this->fail(); + } + + return JSONPath::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() -> JSONPath::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 == '=' ? JSONPath::FilterComparisonOperator::Equal + : JSONPath::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 ? JSONPath::FilterComparisonOperator::LessEqual + : JSONPath::FilterComparisonOperator::Less; + } + + return inclusive ? JSONPath::FilterComparisonOperator::GreaterEqual + : JSONPath::FilterComparisonOperator::Greater; + } + + this->fail(); + } + + auto parse_comparison_or_test() -> JSONPath::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 JSONPath::FilterExpression{JSONPath::FilterComparison{ + .left = JSONPath::FilterOperand{.value = std::move(query)}, + .operation = operation, + .right = std::move(right)}}; + } + + this->position_ = saved; + return JSONPath::FilterExpression{ + JSONPath::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 JSONPath::FilterExpression{JSONPath::FilterComparison{ + .left = JSONPath::FilterOperand{.value = std::move(literal)}, + .operation = operation, + .right = std::move(right)}}; + } + + if (is_alpha(character) && is_lowercase(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.value) && + !is_value_type_function( + std::get(operand.value))) { + this->fail(); + } + + const auto operation{this->parse_comparison_operator()}; + this->skip_whitespace(); + auto right{this->parse_comparable()}; + return JSONPath::FilterExpression{ + JSONPath::FilterComparison{.left = std::move(operand), + .operation = operation, + .right = std::move(right)}}; + } + + this->position_ = saved; + 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 != JSONPath::FilterFunctionName::Match && + call.function != JSONPath::FilterFunctionName::Search) { + this->fail(); + } + + return JSONPath::FilterExpression{ + JSONPath::FilterTest{.negated = false, .subject = std::move(call)}}; + } + + this->fail(); + } + + 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() -> JSONPath::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 JSONPath::FilterOperand{.value = std::move(query)}; + } + + if (character == '"' || character == '\'' || character == '-' || + is_digit(character)) { + return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; + } + + if (is_alpha(character) && is_lowercase(character)) { + auto operand{this->parse_keyword_or_function()}; + if (std::holds_alternative(operand.value) && + !is_value_type_function( + std::get(operand.value))) { + 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) { + 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.value()}; + } + + const auto value{to_double(text)}; + if (!value.has_value()) { + this->fail(); + } + + return JSON{value.value()}; + } + + // filter-query = rel-query / jsonpath-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); + query.singular = singular; + return query; + } + + auto parse_identifier() -> std::string_view { + const auto begin{this->position_}; + while (!this->at_end() && + ((is_alpha(this->peek()) && is_lowercase(this->peek())) || + is_digit(this->peek()) || this->peek() == '_')) { + this->position_ += 1; + } + + return this->input_.substr(begin, this->position_ - begin); + } + + 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 JSONPath::FilterOperand{.value = this->parse_function()}; + } + + if (identifier == "true") { + return JSONPath::FilterOperand{.value = JSON{true}}; + } + + if (identifier == "false") { + return JSONPath::FilterOperand{.value = JSON{false}}; + } + + if (identifier == "null") { + return JSONPath::FilterOperand{.value = JSON{nullptr}}; + } + + this->fail(); + } + + // 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") { + call.function = JSONPath::FilterFunctionName::Length; + } else if (identifier == "count") { + call.function = JSONPath::FilterFunctionName::Count; + } else if (identifier == "match") { + call.function = JSONPath::FilterFunctionName::Match; + } else if (identifier == "search") { + call.function = JSONPath::FilterFunctionName::Search; + } else if (identifier == "value") { + call.function = JSONPath::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); + this->depth_ -= 1; + return call; + } + + auto parse_function_argument() -> JSONPath::FilterOperand { + if (this->at_end()) { + this->fail(); + } + + const char character{this->peek()}; + if (character == '@' || character == '$') { + bool singular{true}; + return JSONPath::FilterOperand{.value = + this->parse_filter_query(singular)}; + } + + if (character == '"' || character == '\'' || character == '-' || + is_digit(character)) { + return JSONPath::FilterOperand{.value = this->parse_filter_literal()}; + } + + if (is_alpha(character) && is_lowercase(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 JSONPath::FilterOperand &operand) -> bool { + if (std::holds_alternative(operand.value)) { + return true; + } + + if (std::holds_alternative(operand.value)) { + return std::get(operand.value).singular; + } + + 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(JSONPath::FilterFunctionCall &call) -> void { + switch (call.function) { + case JSONPath::FilterFunctionName::Length: + if (call.arguments.size() != 1 || + !is_value_type_operand(call.arguments.front())) { + this->fail(); + } + + return; + case JSONPath::FilterFunctionName::Count: + case JSONPath::FilterFunctionName::Value: + if (call.arguments.size() != 1 || + !std::holds_alternative( + call.arguments.front().value)) { + this->fail(); + } + + return; + 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())) { + this->fail(); + } + + 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().value).to_string(), + call.function == JSONPath::FilterFunctionName::Match + ? RegexDialect::IRegexp + : RegexDialect::IRegexpSearch); + } + + return; + } + } + + std::string_view input_; + std::size_t position_{0}; + std::size_t depth_{0}; +}; + +} // namespace + +inline auto parse_jsonpath(const std::string_view input) -> JSONPath::Query { + 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..5776c6bd0d --- /dev/null +++ b/test/jsonpath/jsonpath_evaluate_test.cc @@ -0,0 +1,279 @@ +#include +#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{evaluate_nodes(path, 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{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); +} + +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{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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{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{evaluate_nodes(path, 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{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); +} + +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{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); +} + +TEST(jsonpath_evaluate_wildcard_scalar) { + const auto document{sourcemeta::core::parse_json("42")}; + const sourcemeta::core::JSONPath path{"$.*"}; + const auto nodes{evaluate_nodes(path, 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{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); +} + +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{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); + 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{evaluate_nodes(path, 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{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); +} + +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{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); + 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{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); + 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{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); +} + +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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); + 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"}; + 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{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 new file mode 100644 index 0000000000..06ce8a8766 --- /dev/null +++ b/test/jsonpath/jsonpath_filter_test.cc @@ -0,0 +1,385 @@ +#include +#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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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(evaluate_nodes(first, document).size(), 1); + const sourcemeta::core::JSONPath second{"$[?length(@.a) == 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, 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{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); +} + +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]"}; + const auto nodes{evaluate_nodes(path, document)}; + EXPECT_EQ(nodes.size(), 1); +} + +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]"}; + const auto nodes{evaluate_nodes(path, document)}; + 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_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]"}; + 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 new file mode 100644 index 0000000000..45665bb605 --- /dev/null +++ b/test/jsonpath/jsonpath_normalize_test.cc @@ -0,0 +1,136 @@ +#include +#include +#include + +namespace { + +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 &, + const sourcemeta::core::WeakPointer &location) -> void { + result.push_back(sourcemeta::core::JSONPath::normalize(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 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 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 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 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 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 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 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 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 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 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 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 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 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 paths{normalized_paths(path, document)}; + EXPECT_EQ(paths.size(), 1); + EXPECT_EQ(paths.at(0), "$['x']"); +} diff --git a/test/jsonpath/jsonpath_parse_test.cc b/test/jsonpath/jsonpath_parse_test.cc new file mode 100644 index 0000000000..c81c21d2a3 --- /dev/null +++ b/test/jsonpath/jsonpath_parse_test.cc @@ -0,0 +1,337 @@ +#include +#include + +#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_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, '('); + 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); +} diff --git a/test/jsonpath/jsonpath_suite.cc b/test/jsonpath/jsonpath_suite.cc new file mode 100644 index 0000000000..227c1a7c73 --- /dev/null +++ b/test/jsonpath/jsonpath_suite.cc @@ -0,0 +1,147 @@ +#include +#include +#include + +#include // std::sort +#include // std::size_t +#include // std::filesystem +#include // std::string +#include // std::vector + +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; +} + +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{evaluate_nodes(path, 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()) { + 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() + ": " + name, + [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