From b0dbe56de7334d9b0d137966950c18e315b6e700 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Thu, 19 Dec 2024 13:54:41 -0400 Subject: [PATCH] Upgrade JSON Toolkit to improve dynamic references detection (#314) Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- test/evaluator/evaluator_2020_12_test.cc | 21 +----- .../jsontoolkit/src/jsonschema/reference.cc | 72 ++++++++++++++++++- 3 files changed, 75 insertions(+), 20 deletions(-) diff --git a/DEPENDENCIES b/DEPENDENCIES index ab15d666..51fa3631 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,6 +1,6 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 99f8b42d5f1a8b0f9b3c024f5957dc399bc0262f -jsontoolkit https://github.com/sourcemeta/jsontoolkit 18207a35d087c85f5acfb5176f2ec37283e91420 +jsontoolkit https://github.com/sourcemeta/jsontoolkit 47856cbaeb80d72705c655dd0085d4bc4f579b67 googletest https://github.com/google/googletest a7f443b80b105f940225332ed3c31f2790092f47 googlebenchmark https://github.com/google/benchmark 378fe693a1ef51500db21b11ff05a8018c5f0e55 jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite c2badb1298a8698f86dadf1aea7b44b3a894e5ac diff --git a/test/evaluator/evaluator_2020_12_test.cc b/test/evaluator/evaluator_2020_12_test.cc index 5af365dd..1afe02f4 100644 --- a/test/evaluator/evaluator_2020_12_test.cc +++ b/test/evaluator/evaluator_2020_12_test.cc @@ -1244,29 +1244,14 @@ TEST(Evaluator_2020_12, dynamicRef_1) { })JSON")}; const sourcemeta::jsontoolkit::JSON instance{"foo"}; - EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 3); + EVALUATE_WITH_TRACE_FAST_SUCCESS(schema, instance, 1); - EVALUATE_TRACE_PRE(0, ControlMark, "", "#/$defs/string", ""); - EVALUATE_TRACE_PRE(1, ControlDynamicAnchorJump, "/$dynamicRef", - "#/$dynamicRef", ""); - EVALUATE_TRACE_PRE(2, AssertionTypeStrict, "/$dynamicRef/type", + EVALUATE_TRACE_PRE(0, AssertionTypeStrict, "/$dynamicRef/type", "#/$defs/string/type", ""); - - EVALUATE_TRACE_POST_SUCCESS(0, ControlMark, "", "#/$defs/string", ""); - EVALUATE_TRACE_POST_SUCCESS(1, AssertionTypeStrict, "/$dynamicRef/type", + EVALUATE_TRACE_POST_SUCCESS(0, AssertionTypeStrict, "/$dynamicRef/type", "#/$defs/string/type", ""); - EVALUATE_TRACE_POST_SUCCESS(2, ControlDynamicAnchorJump, "/$dynamicRef", - "#/$dynamicRef", ""); - EVALUATE_TRACE_POST_DESCRIBE(instance, 0, - "The schema location was marked for " - "future use"); - EVALUATE_TRACE_POST_DESCRIBE(instance, 1, "The value was expected to be of type string"); - EVALUATE_TRACE_POST_DESCRIBE( - instance, 2, - "The string value was expected to validate against the first subschema " - "in scope that declared the dynamic anchor \"foo\""); } TEST(Evaluator_2020_12, definitions_1) { diff --git a/vendor/jsontoolkit/src/jsonschema/reference.cc b/vendor/jsontoolkit/src/jsonschema/reference.cc index d492cdc8..51cafcaf 100644 --- a/vendor/jsontoolkit/src/jsonschema/reference.cc +++ b/vendor/jsontoolkit/src/jsonschema/reference.cc @@ -3,7 +3,7 @@ #include #include -#include // std::sort +#include // std::sort, std::all_of #include // assert #include // std::less #include // std::map @@ -464,4 +464,74 @@ auto sourcemeta::jsontoolkit::frame( } } } + + // A schema is standalone if all references can be resolved within itself + const bool standalone{std::all_of( + references.cbegin(), references.cend(), [&frame](const auto &reference) { + assert(!reference.first.second.empty()); + assert(reference.first.second.back().is_property()); + return reference.first.second.back().to_property() == "$schema" || + frame.contains( + {ReferenceType::Static, reference.second.destination}) || + frame.contains( + {ReferenceType::Dynamic, reference.second.destination}); + })}; + + if (standalone) { + // Find all dynamic anchors + std::map> dynamic_anchors; + for (const auto &entry : frame) { + if (entry.first.first != ReferenceType::Dynamic || + entry.second.type != ReferenceEntryType::Anchor) { + continue; + } + + const URI anchor_uri{entry.first.second}; + const std::string fragment{anchor_uri.fragment().value_or("")}; + if (!dynamic_anchors.contains(fragment)) { + dynamic_anchors.emplace(fragment, std::vector{}); + } + + dynamic_anchors[fragment].push_back(entry.first.second); + } + + // If there is a dynamic reference that only has one possible + // dynamic anchor destination, then that dynamic reference + // is a static reference in disguise + std::vector to_delete; + std::vector to_insert; + for (const auto &reference : references) { + if (reference.first.first != ReferenceType::Dynamic || + !reference.second.fragment.has_value()) { + continue; + } + + const auto match{dynamic_anchors.find(reference.second.fragment.value())}; + assert(match != dynamic_anchors.cend()); + // Otherwise we can assume there is only one possible target for the + // dynamic reference + if (match->second.size() != 1) { + continue; + } + + to_delete.push_back(reference.first); + const URI new_destination{match->second.front()}; + to_insert.emplace_back( + ReferenceMap::key_type{ReferenceType::Static, reference.first.second}, + ReferenceMap::mapped_type{ + match->second.front(), + new_destination.recompose_without_fragment(), + fragment_string(new_destination)}); + } + + // Because we can't mutate a map as we are traversing it + + for (const auto &key : to_delete) { + references.erase(key); + } + + for (auto &&entry : to_insert) { + references.emplace(std::move(entry)); + } + } }