diff --git a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h index a2a7ad6f1..65119b9e5 100644 --- a/src/core/jsonpath/include/sourcemeta/core/jsonpath.h +++ b/src/core/jsonpath/include/sourcemeta/core/jsonpath.h @@ -293,7 +293,35 @@ class SOURCEMETA_CORE_JSONPATH_EXPORT JSONPath { [[nodiscard]] static auto normalize(const WeakPointer &location) -> JSON::String; + /// Serialize the query into its expression string as JSON. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSONPath path{"$.foo[0]"}; + /// const sourcemeta::core::JSON result{path.to_json()}; + /// assert(result.is_string()); + /// assert(result.to_string() == "$.foo[0]"); + /// ``` + [[nodiscard]] auto to_json() const -> JSON; + + /// Deserialize a query from its expression string as JSON, returning no + /// result on invalid input. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON input{"$.foo[0]"}; + /// const auto path{sourcemeta::core::JSONPath::from_json(input)}; + /// assert(path.has_value()); + /// ``` + [[nodiscard]] static auto from_json(const JSON &value) + -> std::optional; + private: + JSON::String expression_; Query query_; }; diff --git a/src/core/jsonpath/jsonpath.cc b/src/core/jsonpath/jsonpath.cc index 43f674ed6..b821d0edc 100644 --- a/src/core/jsonpath/jsonpath.cc +++ b/src/core/jsonpath/jsonpath.cc @@ -1363,7 +1363,7 @@ inline auto evaluate_segments(const std::vector &segments, } // namespace JSONPath::JSONPath(const JSON::StringView expression) - : query_{parse_jsonpath(expression)} {} + : expression_{expression}, query_{parse_jsonpath(expression)} {} auto JSONPath::evaluate(const JSON &document, const Callback &callback) const -> void { @@ -1429,4 +1429,18 @@ auto JSONPath::normalize(const WeakPointer &location) -> JSON::String { return result; } +auto JSONPath::to_json() const -> JSON { return JSON{this->expression_}; } + +auto JSONPath::from_json(const JSON &value) -> std::optional { + if (!value.is_string()) { + return std::nullopt; + } + + try { + return JSONPath{value.to_string()}; + } catch (const JSONPathParseError &) { + return std::nullopt; + } +} + } // namespace sourcemeta::core diff --git a/test/jsonpath/CMakeLists.txt b/test/jsonpath/CMakeLists.txt index 6b7e868e3..94dc4b308 100644 --- a/test/jsonpath/CMakeLists.txt +++ b/test/jsonpath/CMakeLists.txt @@ -3,7 +3,8 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME jsonpath jsonpath_parse_test.cc jsonpath_evaluate_test.cc jsonpath_filter_test.cc - jsonpath_normalize_test.cc) + jsonpath_normalize_test.cc + jsonpath_json_auto_test.cc) target_link_libraries(sourcemeta_core_jsonpath_unit PRIVATE sourcemeta::core::json) diff --git a/test/jsonpath/jsonpath_json_auto_test.cc b/test/jsonpath/jsonpath_json_auto_test.cc new file mode 100644 index 000000000..7b0b1ca72 --- /dev/null +++ b/test/jsonpath/jsonpath_json_auto_test.cc @@ -0,0 +1,77 @@ +#include +#include +#include + +TEST(jsonpath_to_json_single_name) { + const sourcemeta::core::JSONPath path{"$.foo"}; + const auto result{sourcemeta::core::to_json(path)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "$.foo"); +} + +TEST(jsonpath_to_json_filter_expression) { + const sourcemeta::core::JSONPath path{ + "$..books[?@.price < 10 && match(@.category, 'fic.*')]"}; + const auto result{sourcemeta::core::to_json(path)}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), + "$..books[?@.price < 10 && match(@.category, 'fic.*')]"); +} + +TEST(jsonpath_from_json_valid) { + const sourcemeta::core::JSON input{"$.foo[0]"}; + const auto path{ + sourcemeta::core::from_json(input)}; + EXPECT_TRUE(path.has_value()); + const auto result{sourcemeta::core::to_json(path.value())}; + EXPECT_TRUE(result.is_string()); + EXPECT_EQ(result.to_string(), "$.foo[0]"); +} + +TEST(jsonpath_from_json_round_trip_evaluate) { + const sourcemeta::core::JSONPath path{"$.foo[0]"}; + const auto back{sourcemeta::core::from_json( + sourcemeta::core::to_json(path))}; + EXPECT_TRUE(back.has_value()); + const auto document{ + sourcemeta::core::parse_json(R"JSON({ "foo": [ 1, 2 ] })JSON")}; + std::size_t count{0}; + back.value().evaluate( + document, + [&count](const sourcemeta::core::JSON &value, + const sourcemeta::core::WeakPointer &location) -> void { + EXPECT_TRUE(value.is_integer()); + EXPECT_EQ(value.to_integer(), 1); + EXPECT_EQ(location.size(), 2); + count += 1; + }); + EXPECT_EQ(count, 1); +} + +TEST(jsonpath_from_json_invalid_type_integer) { + const sourcemeta::core::JSON input{1}; + const auto path{ + sourcemeta::core::from_json(input)}; + EXPECT_FALSE(path.has_value()); +} + +TEST(jsonpath_from_json_invalid_type_array) { + const auto input{sourcemeta::core::parse_json(R"JSON([ "$.foo" ])JSON")}; + const auto path{ + sourcemeta::core::from_json(input)}; + EXPECT_FALSE(path.has_value()); +} + +TEST(jsonpath_from_json_invalid_expression) { + const sourcemeta::core::JSON input{"foo"}; + const auto path{ + sourcemeta::core::from_json(input)}; + EXPECT_FALSE(path.has_value()); +} + +TEST(jsonpath_from_json_empty_string) { + const sourcemeta::core::JSON input{""}; + const auto path{ + sourcemeta::core::from_json(input)}; + EXPECT_FALSE(path.has_value()); +}