diff --git a/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_template.h b/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_template.h index a0106fa35..9beab2fac 100644 --- a/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_template.h +++ b/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_template.h @@ -115,7 +115,7 @@ template class GenericPointerTemplate { /// ``` template auto emplace_back(Args &&...args) -> reference { // It is a logical error to push a token after a key wildcard - assert(this->data.empty() || + assert(this->empty() || !std::holds_alternative(this->data.back()) || std::get(this->data.back()) != Wildcard::Key); return this->data.emplace_back(args...); @@ -133,7 +133,7 @@ template class GenericPointerTemplate { /// ``` auto push_back(const PointerT &other) -> void { // It is a logical error to push a token after a key wildcard - assert(this->data.empty() || + assert(this->empty() || !std::holds_alternative(this->data.back()) || std::get(this->data.back()) != Wildcard::Key); this->data.reserve(this->data.size() + other.size()); @@ -150,7 +150,7 @@ template class GenericPointerTemplate { /// pointer.pop_back(); /// ``` auto pop_back() -> void { - assert(!this->data.empty()); + assert(!this->empty()); this->data.pop_back(); } @@ -182,6 +182,20 @@ template class GenericPointerTemplate { return result; } + /// Check if a JSON Pointer template is empty. + /// For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::PointerTemplate empty_pointer; + /// assert(empty_pointer.empty()); + /// ``` + [[nodiscard]] auto empty() const noexcept -> bool { + return this->data.empty(); + } + /// Compare JSON Pointer template instances auto operator==(const GenericPointerTemplate &other) const noexcept -> bool { diff --git a/src/core/jsonschema/frame.cc b/src/core/jsonschema/frame.cc index 735d0185d..cd307ec6d 100644 --- a/src/core/jsonschema/frame.cc +++ b/src/core/jsonschema/frame.cc @@ -254,11 +254,13 @@ static auto repopulate_instance_locations( sourcemeta::core::SchemaFrame::Instances::mapped_type &destination, const std::optional &accumulator) -> void { - if (cache_entry.orphan - // TODO: Implement an .empty() method - && cache_entry.instance_location == sourcemeta::core::PointerTemplate{}) { + if (cache_entry.orphan && cache_entry.instance_location.empty()) { return; - } else if (cache_entry.parent.has_value()) { + } else if (cache_entry.parent.has_value() && + // Don't consider bases from the root subschema, as if that + // subschema has any instance location other than "", then it + // indicates a recursive reference + !cache_entry.parent.value().empty()) { const auto match{instances.find(cache_entry.parent.value())}; if (match == instances.cend()) { return; diff --git a/test/jsonpointer/jsonpointer_template_test.cc b/test/jsonpointer/jsonpointer_template_test.cc index fe6714e13..662cbe8d5 100644 --- a/test/jsonpointer/jsonpointer_template_test.cc +++ b/test/jsonpointer/jsonpointer_template_test.cc @@ -215,3 +215,14 @@ TEST(JSONPointer_template, concat_move) { EXPECT_EQ(result, expected); } + +TEST(JSONPointer_template, empty_true) { + const sourcemeta::core::PointerTemplate pointer; + EXPECT_TRUE(pointer.empty()); +} + +TEST(JSONPointer_template, empty_false) { + const sourcemeta::core::Pointer base{"foo"}; + const sourcemeta::core::PointerTemplate pointer{base}; + EXPECT_FALSE(pointer.empty()); +} diff --git a/test/jsonschema/jsonschema_frame_2020_12_test.cc b/test/jsonschema/jsonschema_frame_2020_12_test.cc index d08342f18..cd7b67ded 100644 --- a/test/jsonschema/jsonschema_frame_2020_12_test.cc +++ b/test/jsonschema/jsonschema_frame_2020_12_test.cc @@ -2129,3 +2129,117 @@ TEST(JSONSchema_frame_2020_12, property_cross_ref) { "https://www.sourcemeta.com/schema", "/properties/bar"); } + +TEST(JSONSchema_frame_2020_12, dynamic_ref_multiple_targets) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$id": "https://www.example.com", + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "test", + "properties": { + "foo": { + "$id": "foo", + "$dynamicAnchor": "test" + }, + "bar": { + "$dynamicRef": "#test" + } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::Instances}; + frame.analyse(document, sourcemeta::core::schema_official_walker, + sourcemeta::core::schema_official_resolver); + + EXPECT_EQ(frame.locations().size(), 17); + + EXPECT_FRAME_STATIC_2020_12_RESOURCE( + frame, "https://www.example.com", "https://www.example.com", "", + "https://www.example.com", "", POINTER_TEMPLATES("", "/bar"), + std::nullopt); + EXPECT_FRAME_STATIC_2020_12_RESOURCE( + frame, "https://www.example.com/foo", "https://www.example.com", + "/properties/foo", "https://www.example.com/foo", "", + POINTER_TEMPLATES("/foo", "/bar"), ""); + + // Subschemas + + EXPECT_FRAME_STATIC_2020_12_SUBSCHEMA( + frame, "https://www.example.com#/properties/foo", + "https://www.example.com", "/properties/foo", + "https://www.example.com/foo", "", POINTER_TEMPLATES("/foo", "/bar"), ""); + EXPECT_FRAME_STATIC_2020_12_SUBSCHEMA( + frame, "https://www.example.com#/properties/bar", + "https://www.example.com", "/properties/bar", "https://www.example.com", + "/properties/bar", {"/bar"}, ""); + + // JSON Pointers + + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/$id", "https://www.example.com", "/$id", + "https://www.example.com", "/$id", {}, ""); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/$schema", "https://www.example.com", + "/$schema", "https://www.example.com", "/$schema", {}, ""); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/$dynamicAnchor", + "https://www.example.com", "/$dynamicAnchor", "https://www.example.com", + "/$dynamicAnchor", {}, ""); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/properties", "https://www.example.com", + "/properties", "https://www.example.com", "/properties", {}, ""); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/properties/foo/$id", + "https://www.example.com", "/properties/foo/$id", + "https://www.example.com/foo", "/$id", {}, "/properties/foo"); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/properties/foo/$dynamicAnchor", + "https://www.example.com", "/properties/foo/$dynamicAnchor", + "https://www.example.com/foo", "/$dynamicAnchor", {}, "/properties/foo"); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com#/properties/bar/$dynamicRef", + "https://www.example.com", "/properties/bar/$dynamicRef", + "https://www.example.com", "/properties/bar/$dynamicRef", {}, + "/properties/bar"); + + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com/foo#/$id", "https://www.example.com", + "/properties/foo/$id", "https://www.example.com/foo", "/$id", {}, + "/properties/foo"); + EXPECT_FRAME_STATIC_2020_12_POINTER( + frame, "https://www.example.com/foo#/$dynamicAnchor", + "https://www.example.com", "/properties/foo/$dynamicAnchor", + "https://www.example.com/foo", "/$dynamicAnchor", {}, "/properties/foo"); + + // Anchors + + // TODO: Static variants should not cover dynamic cases + EXPECT_FRAME_STATIC_2020_12_ANCHOR( + frame, "https://www.example.com#test", "https://www.example.com", "", + "https://www.example.com", "", POINTER_TEMPLATES("", "/bar"), + std::nullopt); + EXPECT_FRAME_STATIC_2020_12_ANCHOR( + frame, "https://www.example.com/foo#test", "https://www.example.com", + "/properties/foo", "https://www.example.com/foo", "", + POINTER_TEMPLATES("/foo", "/bar"), ""); + + EXPECT_FRAME_DYNAMIC_2020_12_ANCHOR( + frame, "https://www.example.com#test", "https://www.example.com", "", + "https://www.example.com", "", POINTER_TEMPLATES("", "/bar"), + std::nullopt); + EXPECT_FRAME_DYNAMIC_2020_12_ANCHOR( + frame, "https://www.example.com/foo#test", "https://www.example.com", + "/properties/foo", "https://www.example.com/foo", "", + POINTER_TEMPLATES("/foo", "/bar"), ""); + + // References + + EXPECT_EQ(frame.references().size(), 2); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "https://json-schema.org/draft/2020-12/schema", + "https://json-schema.org/draft/2020-12/schema", std::nullopt); + EXPECT_DYNAMIC_REFERENCE(frame, "/properties/bar/$dynamicRef", + "https://www.example.com#test", + "https://www.example.com", "test"); +}