diff --git a/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h b/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h index 2a77f7192..c616e2e64 100644 --- a/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h +++ b/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h @@ -8,7 +8,7 @@ #include // std::size_t #include // std::reference_wrapper #include // std::initializer_list -#include // std::advance, std::back_inserter +#include // std::advance, std::back_inserter, std::next #include // std::optional #include // std::ranges::subrange #include // std::is_same_v, std::decay_t @@ -663,6 +663,47 @@ template class GenericPointer { this->data[prefix_size + 1].to_property() == tail_right; } + /// Check whether two JSON Pointers are equal up to a given number of + /// leading tokens. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + /// const sourcemeta::core::Pointer other{"foo", "bar", "qux"}; + /// assert(pointer.shares_prefix(other, 2)); + /// assert(!pointer.shares_prefix(other, 3)); + /// ``` + [[nodiscard]] auto shares_prefix(const GenericPointer &other, + const size_type prefix_size) const -> bool { + return this->data.size() >= prefix_size && + other.data.size() >= prefix_size && + std::equal(this->data.cbegin(), + std::next(this->data.cbegin(), + static_cast(prefix_size)), + other.data.cbegin()); + } + + /// Check whether a JSON Pointer starts with another JSON Pointer without + /// the two being equal. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + /// const sourcemeta::core::Pointer prefix{"foo", "bar"}; + /// assert(pointer.starts_with_strict(prefix)); + /// assert(!pointer.starts_with_strict(pointer)); + /// ``` + [[nodiscard]] auto + starts_with_strict(const GenericPointer &other) const + -> bool { + return this->data.size() > other.data.size() && + this->shares_prefix(other, other.data.size()); + } + /// Check whether a JSON Pointer starts with the initial part of another JSON /// Pointer. For example: /// diff --git a/test/jsonpointer/CMakeLists.txt b/test/jsonpointer/CMakeLists.txt index b4d1ecff0..643ef6270 100644 --- a/test/jsonpointer/CMakeLists.txt +++ b/test/jsonpointer/CMakeLists.txt @@ -20,7 +20,9 @@ sourcemeta_test(NAMESPACE sourcemeta PROJECT core NAME jsonpointer jsonpointer_fragment_to_pointer_test.cc jsonpointer_concat_test.cc jsonpointer_starts_with_test.cc + jsonpointer_starts_with_strict_test.cc jsonpointer_starts_with_initial_test.cc + jsonpointer_shares_prefix_test.cc jsonpointer_json_auto_test.cc jsonpointer_rebase_test.cc jsonpointer_remove_test.cc diff --git a/test/jsonpointer/jsonpointer_shares_prefix_test.cc b/test/jsonpointer/jsonpointer_shares_prefix_test.cc new file mode 100644 index 000000000..36878be13 --- /dev/null +++ b/test/jsonpointer/jsonpointer_shares_prefix_test.cc @@ -0,0 +1,93 @@ +#include +#include + +TEST(property_common_prefix_true) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer other{"foo", "bar", "qux"}; + EXPECT_TRUE(pointer.shares_prefix(other, 2)); +} + +TEST(property_common_prefix_false) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer other{"foo", "bar", "qux"}; + EXPECT_FALSE(pointer.shares_prefix(other, 3)); +} + +TEST(index_common_prefix_true) { + const sourcemeta::core::Pointer pointer{0, 1, 2}; + const sourcemeta::core::Pointer other{0, 1, 3}; + EXPECT_TRUE(pointer.shares_prefix(other, 2)); +} + +TEST(index_common_prefix_false) { + const sourcemeta::core::Pointer pointer{0, 1, 2}; + const sourcemeta::core::Pointer other{0, 1, 3}; + EXPECT_FALSE(pointer.shares_prefix(other, 3)); +} + +TEST(equal_pointers_full_size) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer other{"foo", "bar"}; + EXPECT_TRUE(pointer.shares_prefix(other, 2)); +} + +TEST(different_first_token) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer other{"baz", "bar"}; + EXPECT_FALSE(pointer.shares_prefix(other, 1)); +} + +TEST(zero_size_non_empty) { + const sourcemeta::core::Pointer pointer{"foo"}; + const sourcemeta::core::Pointer other{"bar"}; + EXPECT_TRUE(pointer.shares_prefix(other, 0)); +} + +TEST(zero_size_empty) { + const sourcemeta::core::Pointer pointer; + const sourcemeta::core::Pointer other; + EXPECT_TRUE(pointer.shares_prefix(other, 0)); +} + +TEST(size_beyond_left) { + const sourcemeta::core::Pointer pointer{"foo"}; + const sourcemeta::core::Pointer other{"foo", "bar"}; + EXPECT_FALSE(pointer.shares_prefix(other, 2)); +} + +TEST(size_beyond_right) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer other{"foo"}; + EXPECT_FALSE(pointer.shares_prefix(other, 2)); +} + +TEST(size_beyond_both) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer other{"foo", "bar"}; + EXPECT_FALSE(pointer.shares_prefix(other, 3)); +} + +TEST(commutative_true) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer other{"foo", "bar"}; + EXPECT_TRUE(pointer.shares_prefix(other, 2)); + EXPECT_TRUE(other.shares_prefix(pointer, 2)); +} + +TEST(property_and_index_mixed_true) { + const sourcemeta::core::Pointer pointer{"foo", 0, "bar"}; + const sourcemeta::core::Pointer other{"foo", 0, "baz"}; + EXPECT_TRUE(pointer.shares_prefix(other, 2)); +} + +TEST(property_and_index_mixed_false) { + const sourcemeta::core::Pointer pointer{"foo", 0, "bar"}; + const sourcemeta::core::Pointer other{"foo", 1, "bar"}; + EXPECT_FALSE(pointer.shares_prefix(other, 2)); +} + +TEST(property_token_against_index_token) { + const sourcemeta::core::Pointer pointer{"foo", "0"}; + const sourcemeta::core::Pointer other{"foo", 0}; + EXPECT_FALSE(pointer.shares_prefix(other, 2)); +} diff --git a/test/jsonpointer/jsonpointer_starts_with_strict_test.cc b/test/jsonpointer/jsonpointer_starts_with_strict_test.cc new file mode 100644 index 000000000..d3a532926 --- /dev/null +++ b/test/jsonpointer/jsonpointer_starts_with_strict_test.cc @@ -0,0 +1,91 @@ +#include +#include + +TEST(property_prefix_true) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer prefix{"foo", "bar"}; + EXPECT_TRUE(pointer.starts_with_strict(prefix)); +} + +TEST(property_prefix_false) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer prefix{"bar", "baz"}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(index_prefix_true) { + const sourcemeta::core::Pointer pointer{0, 1, 2}; + const sourcemeta::core::Pointer prefix{0, 1}; + EXPECT_TRUE(pointer.starts_with_strict(prefix)); +} + +TEST(index_prefix_false) { + const sourcemeta::core::Pointer pointer{0, 1, 2}; + const sourcemeta::core::Pointer prefix{1, 2}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(property_equal) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer prefix{"foo", "bar"}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(index_equal) { + const sourcemeta::core::Pointer pointer{0, 1}; + const sourcemeta::core::Pointer prefix{0, 1}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(same_pointer) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + EXPECT_FALSE(pointer.starts_with_strict(pointer)); +} + +TEST(longer_prefix) { + const sourcemeta::core::Pointer pointer{"foo", "bar"}; + const sourcemeta::core::Pointer prefix{"foo", "bar", "baz"}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(non_empty_empty) { + const sourcemeta::core::Pointer pointer{"foo"}; + const sourcemeta::core::Pointer prefix; + EXPECT_TRUE(pointer.starts_with_strict(prefix)); +} + +TEST(empty_non_empty) { + const sourcemeta::core::Pointer pointer; + const sourcemeta::core::Pointer prefix{"foo"}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(empty_empty) { + const sourcemeta::core::Pointer pointer; + const sourcemeta::core::Pointer prefix; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(divergent_last_prefix_token) { + const sourcemeta::core::Pointer pointer{"foo", "bar", "baz"}; + const sourcemeta::core::Pointer prefix{"foo", "qux"}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(property_and_index_mixed_true) { + const sourcemeta::core::Pointer pointer{"foo", 0, "bar"}; + const sourcemeta::core::Pointer prefix{"foo", 0}; + EXPECT_TRUE(pointer.starts_with_strict(prefix)); +} + +TEST(property_and_index_mixed_false) { + const sourcemeta::core::Pointer pointer{"foo", 0, "bar"}; + const sourcemeta::core::Pointer prefix{"foo", 1}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +} + +TEST(property_token_against_index_token) { + const sourcemeta::core::Pointer pointer{"foo", "0", "bar"}; + const sourcemeta::core::Pointer prefix{"foo", 0}; + EXPECT_FALSE(pointer.starts_with_strict(prefix)); +}