From 9f16bf5a12da236df69b7d9d96fc2526a5fc67dc Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Wed, 14 Aug 2024 18:35:35 -0400 Subject: [PATCH] Fully support JSON Schema 2020-12 (#149) Signed-off-by: Juan Cruz Viotti --- DEPENDENCIES | 2 +- README.markdown | 2 +- docs/compile.markdown | 4 +- docs/metaschema.markdown | 4 +- docs/test.markdown | 4 +- docs/validate.markdown | 4 +- test/CMakeLists.txt | 3 + test/metaschema_pass_2020_12.sh | 17 ++ test/validate/fail_2020_12.sh | 41 +++ .../fail_relative_external_ref_missing.sh | 4 +- test/validate/pass_2020_12.sh | 25 ++ .../jsontoolkit/src/jsonschema/CMakeLists.txt | 4 +- vendor/jsontoolkit/src/jsonschema/compile.cc | 7 +- .../src/jsonschema/compile_evaluate.cc | 48 +--- .../src/jsonschema/default_compiler_2020_12.h | 16 +- vendor/jsontoolkit/src/jsonschema/explain.cc | 233 ------------------ .../sourcemeta/jsontoolkit/jsonschema.h | 1 - .../jsontoolkit/jsonschema_explain.h | 81 ------ .../jsontoolkit/src/jsonschema/jsonschema.cc | 17 ++ .../jsontoolkit/src/jsonschema/reference.cc | 5 +- 20 files changed, 145 insertions(+), 377 deletions(-) create mode 100755 test/metaschema_pass_2020_12.sh create mode 100755 test/validate/fail_2020_12.sh create mode 100755 test/validate/pass_2020_12.sh delete mode 100644 vendor/jsontoolkit/src/jsonschema/explain.cc delete mode 100644 vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_explain.h diff --git a/DEPENDENCIES b/DEPENDENCIES index 3639ee0..0777ef0 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull dea311b5bfb53b6926a4140267959ae334d3ecf4 noa https://github.com/sourcemeta/noa 7e26abce7a4e31e86a16ef2851702a56773ca527 -jsontoolkit https://github.com/sourcemeta/jsontoolkit e7a8f35b0b85cd87b5fa587b2ae767ed939a0b4d +jsontoolkit https://github.com/sourcemeta/jsontoolkit 00251a4161434463c24bc18418e3ffd37f998f29 hydra https://github.com/sourcemeta/hydra 3c53d3fdef79e9ba603d48470a508cc45472a0dc diff --git a/README.markdown b/README.markdown index a7e1d5e..3646d4b 100644 --- a/README.markdown +++ b/README.markdown @@ -29,7 +29,7 @@ We aim to fully support _every_ version of JSON Schema and combinations between | Dialect | Support | |---------------------|------------------------------------------------------------------| -| JSON Schema 2020-12 | Partial (except `validate`, `test`, `metaschema`, and `compile`) | +| JSON Schema 2020-12 | **Full** | | JSON Schema 2019-09 | **Full** | | JSON Schema Draft 7 | **Full** | | JSON Schema Draft 6 | **Full** | diff --git a/docs/compile.markdown b/docs/compile.markdown index edcede5..daec635 100644 --- a/docs/compile.markdown +++ b/docs/compile.markdown @@ -2,9 +2,7 @@ Compile ======= > [!WARNING] -> Only JSON Schema Draft 4, Draft 6, Draft 7, and 2019-09 are supported at this -> point in time. We have plans to support *every* dialect of JSON Schema from -> Draft 0 to Draft 2020-12 soon. +> JSON Schema Draft 3 and older are not supported at this point in time. ```sh jsonschema compile diff --git a/docs/metaschema.markdown b/docs/metaschema.markdown index 7e9bbd2..f068536 100644 --- a/docs/metaschema.markdown +++ b/docs/metaschema.markdown @@ -2,9 +2,7 @@ Metaschema ========== > [!WARNING] -> Only JSON Schema Draft 4, Draft 6, Draft 7, and 2019-09 are supported at this -> point in time. We have plans to support *every* dialect of JSON Schema from -> Draft 0 to Draft 2020-12 soon. +> JSON Schema Draft 3 and older are not supported at this point in time. ```sh jsonschema metaschema [schemas-or-directories...] diff --git a/docs/test.markdown b/docs/test.markdown index 475ef80..3f2be54 100644 --- a/docs/test.markdown +++ b/docs/test.markdown @@ -2,9 +2,7 @@ Testing ======= > [!WARNING] -> Only JSON Schema Draft 4, Draft 6, Draft 7, and 2019-09 are supported at this -> point in time. We have plans to support *every* dialect of JSON Schema from -> Draft 0 to Draft 2020-12 soon. +> JSON Schema Draft 3 and older are not supported at this point in time. ```sh jsonschema test [schemas-or-directories...] diff --git a/docs/validate.markdown b/docs/validate.markdown index d606b7d..693a42d 100644 --- a/docs/validate.markdown +++ b/docs/validate.markdown @@ -2,9 +2,7 @@ Validating ========== > [!WARNING] -> Only JSON Schema Draft 4, Draft 6, Draft 7, and 2019-09 are supported at this -> point in time. We have plans to support *every* dialect of JSON Schema from -> Draft 0 to Draft 2020-12 soon. +> JSON Schema Draft 3 and older are not supported at this point in time. ```sh jsonschema validate [--http/-h] diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a640263..829295e 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -33,6 +33,7 @@ add_jsonschema_test_unix(metaschema_fail_single) add_jsonschema_test_unix(metaschema_fail_non_schema) add_jsonschema_test_unix(metaschema_pass_cwd) add_jsonschema_test_unix(metaschema_pass_single) +add_jsonschema_test_unix(metaschema_pass_2020_12) # Validate add_jsonschema_test_unix(validate/fail_instance_directory) @@ -58,10 +59,12 @@ add_jsonschema_test_unix(validate/pass_draft4) add_jsonschema_test_unix(validate/pass_draft6) add_jsonschema_test_unix(validate/pass_draft7) add_jsonschema_test_unix(validate/pass_2019_09) +add_jsonschema_test_unix(validate/pass_2020_12) add_jsonschema_test_unix(validate/fail_draft4) add_jsonschema_test_unix(validate/fail_draft6) add_jsonschema_test_unix(validate/fail_draft7) add_jsonschema_test_unix(validate/fail_2019_09) +add_jsonschema_test_unix(validate/fail_2020_12) add_jsonschema_test_unix(validate/pass_jsonl) add_jsonschema_test_unix(validate/pass_jsonl_verbose) add_jsonschema_test_unix(validate/fail_jsonl_invalid_entry) diff --git a/test/metaschema_pass_2020_12.sh b/test/metaschema_pass_2020_12.sh new file mode 100755 index 0000000..b2a93e9 --- /dev/null +++ b/test/metaschema_pass_2020_12.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "string" +} +EOF + +"$1" metaschema "$TMP/schema.json" diff --git a/test/validate/fail_2020_12.sh b/test/validate/fail_2020_12.sh new file mode 100755 index 0000000..67b2298 --- /dev/null +++ b/test/validate/fail_2020_12.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": 1 } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" 2> "$TMP/stderr.txt" \ + && CODE="$?" || CODE="$?" +test "$CODE" = "1" || exit 1 + +cat << EOF > "$TMP/expected.txt" +fail: $(realpath "$TMP")/instance.json +error: Schema validation failure + The target document is expected to be of the given type + at instance location "/foo" + at evaluate path "/properties/foo/type" + The target is expected to match all of the given assertions + at instance location "" + at evaluate path "/properties" +EOF + +diff "$TMP/stderr.txt" "$TMP/expected.txt" diff --git a/test/validate/fail_relative_external_ref_missing.sh b/test/validate/fail_relative_external_ref_missing.sh index d7ee9cf..d08516d 100755 --- a/test/validate/fail_relative_external_ref_missing.sh +++ b/test/validate/fail_relative_external_ref_missing.sh @@ -11,7 +11,9 @@ cat << 'EOF' > "$TMP/schema.json" { "$schema": "http://json-schema.org/draft-04/schema#", "id": "https://example.com", - "$ref": "nested" + "allOf": [ + { "$ref": "nested" } + ] } EOF diff --git a/test/validate/pass_2020_12.sh b/test/validate/pass_2020_12.sh new file mode 100755 index 0000000..7596a0f --- /dev/null +++ b/test/validate/pass_2020_12.sh @@ -0,0 +1,25 @@ +#!/bin/sh + +set -o errexit +set -o nounset + +TMP="$(mktemp -d)" +clean() { rm -rf "$TMP"; } +trap clean EXIT + +cat << 'EOF' > "$TMP/schema.json" +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "properties": { + "foo": { + "type": "string" + } + } +} +EOF + +cat << 'EOF' > "$TMP/instance.json" +{ "foo": "bar" } +EOF + +"$1" validate "$TMP/schema.json" "$TMP/instance.json" diff --git a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt index 684ed2c..2aaed1d 100644 --- a/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt +++ b/vendor/jsontoolkit/src/jsonschema/CMakeLists.txt @@ -5,11 +5,11 @@ include(./official_resolver.cmake) noa_library(NAMESPACE sourcemeta PROJECT jsontoolkit NAME jsonschema FOLDER "JSON Toolkit/JSON Schema" PRIVATE_HEADERS anchor.h bundle.h resolver.h walker.h reference.h - error.h transformer.h transform_rule.h transform_bundle.h compile.h explain.h + error.h transformer.h transform_rule.h transform_bundle.h compile.h SOURCES jsonschema.cc default_walker.cc reference.cc anchor.cc resolver.cc walker.cc bundle.cc transformer.cc transform_rule.cc transform_bundle.cc compile.cc compile_evaluate.cc compile_json.cc compile_describe.cc - compile_helpers.h default_compiler.cc explain.cc + compile_helpers.h default_compiler.cc default_compiler_2020_12.h default_compiler_2019_09.h diff --git a/vendor/jsontoolkit/src/jsonschema/compile.cc b/vendor/jsontoolkit/src/jsonschema/compile.cc index 836f4b0..40bfe8d 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile.cc @@ -117,10 +117,11 @@ auto compile(const JSON &schema, const SchemaWalker &walker, relative_dynamic_context}; sourcemeta::jsontoolkit::SchemaCompilerTemplate compiler_template; - // TODO: Support dynamic anchors on 2020-12 if (uses_dynamic_scopes && - schema_context.vocabularies.contains( - "https://json-schema.org/draft/2019-09/vocab/core")) { + (schema_context.vocabularies.contains( + "https://json-schema.org/draft/2019-09/vocab/core") || + schema_context.vocabularies.contains( + "https://json-schema.org/draft/2020-12/vocab/core"))) { for (const auto &entry : frame) { // We are only trying to find dynamic anchors if (entry.second.type != ReferenceEntryType::Anchor || diff --git a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc index fc0ebc4..19fa27d 100644 --- a/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc +++ b/vendor/jsontoolkit/src/jsonschema/compile_evaluate.cc @@ -2,7 +2,7 @@ #include #include -#include // std::min, std::adjacent_find +#include // std::min #include // assert #include // std::reference_wrapper #include // std::distance, std::advance @@ -82,24 +82,12 @@ class EvaluationContext { // TODO: Do schema resource management using hashes to avoid // expensive string comparisons - if (step.dynamic && (this->resources_.empty() || - this->resources_.back() != step.schema_resource)) { - // Some keywords may operate under the previous schema resource. For - // example, `additionalProperties` will emit annotations at that level - // after evaluating the `additionalProperties` subschema. - if (this->resources_.size() > 1 && - this->resources_[this->resources_.size() - 2] == - step.schema_resource) { - return; - } - + if (step.dynamic) { + // Note that we are potentially repeatedly pushing back the + // same schema resource over and over again. However, the + // logic for making sure this list is "pure" takes a lot of + // computation power. Being silly seems faster. this->resources_.push_back(step.schema_resource); - // If we are doing things right, there should never be adjacent - // equal schema resources on the stack, as we cannot jump from - // a schema resource to the same schema resource - assert(std::adjacent_find(this->resources_.cbegin(), - this->resources_.cend()) == - this->resources_.cend()); } } @@ -117,8 +105,7 @@ class EvaluationContext { // TODO: Do schema resource management using hashes to avoid // expensive string comparisons - if (step.dynamic && !this->resources_.empty() && - this->resources_.back() != step.schema_resource) { + if (step.dynamic) { assert(!this->resources_.empty()); this->resources_.pop_back(); } @@ -241,26 +228,18 @@ class EvaluationContext { auto find_dynamic_anchor(const std::string &anchor) const -> std::optional { - std::optional result; - - // We want to check every schema resource from the current one - // backwards until the dynamic anchor chain is lost. The one - // before its lost is the one we want to jump to - for (auto iterator = this->resources().crbegin(); - iterator < this->resources().crend(); ++iterator) { + for (const auto &resource : this->resources()) { std::ostringstream name; - name << *iterator; + name << resource; name << '#'; name << anchor; const auto label{std::hash{}(name.str())}; if (this->labels.contains(label)) { - result = label; - } else { - break; + return label; } } - return result; + return std::nullopt; } auto jump(const std::size_t id) const -> const Template & { @@ -1102,10 +1081,7 @@ auto evaluate(const SchemaCompilerTemplate &steps, const JSON &instance, // we are done, otherwise there was a frame push/pop mismatch assert(context.evaluate_path().empty()); assert(context.instance_location().empty()); - // The stack of schema resources will either be empty if no dynamic - // scoping was necessary, or it will contain exactly one schema resource, - // the top-level one. - assert(context.resources().empty() || context.resources().size() == 1); + assert(context.resources().empty()); return overall; } diff --git a/vendor/jsontoolkit/src/jsonschema/default_compiler_2020_12.h b/vendor/jsontoolkit/src/jsonschema/default_compiler_2020_12.h index 221cabe..46d4929 100644 --- a/vendor/jsontoolkit/src/jsonschema/default_compiler_2020_12.h +++ b/vendor/jsontoolkit/src/jsonschema/default_compiler_2020_12.h @@ -3,6 +3,7 @@ #include #include +#include #include "compile_helpers.h" #include "default_compiler_draft4.h" @@ -55,8 +56,19 @@ auto compiler_2020_12_core_dynamicref( return compiler_draft4_core_ref(context, schema_context, dynamic_context); } - // TODO: Implement - return {}; + assert(schema_context.schema.at(dynamic_context.keyword).is_string()); + URI reference{schema_context.schema.at(dynamic_context.keyword).to_string()}; + reference.resolve_from_if_absolute(schema_context.base); + reference.canonicalize(); + // We handle the non-anchor variant by not treating it as a dynamic reference + assert(reference.fragment().has_value()); + + // Note we don't need to even care about the static part of the dynamic + // reference (if any), as even if we jump first there, we will still + // look for the oldest dynamic anchor in the schema resource chain. + return {make( + context, schema_context, dynamic_context, + std::string{reference.fragment().value()}, {})}; } } // namespace internal diff --git a/vendor/jsontoolkit/src/jsonschema/explain.cc b/vendor/jsontoolkit/src/jsonschema/explain.cc deleted file mode 100644 index 008256a..0000000 --- a/vendor/jsontoolkit/src/jsonschema/explain.cc +++ /dev/null @@ -1,233 +0,0 @@ -#include - -#include // assert - -static auto quantify(const std::int64_t value, const std::string &singular, - const std::string &plural) -> std::string { - std::ostringstream result; - result << value; - result << ' '; - result << (value == 1 ? singular : plural); - return result.str(); -} - -static auto -make_range_constraint(std::map &constraints, - const std::optional &minimum, - const std::optional &maximum, - const std::optional &lower_bound, - const std::string &singular, - const std::string &plural) -> void { - if (constraints.contains("range")) { - return; - } else if ((!minimum.has_value() || - (lower_bound.has_value() && - minimum.value() <= lower_bound.value())) && - maximum.has_value()) { - constraints.emplace("range", - "<= " + quantify(maximum.value(), singular, plural)); - } else if (minimum.has_value() && maximum.has_value()) { - if (minimum.value() == maximum.value()) { - constraints.emplace( - "range", "exactly " + quantify(minimum.value(), singular, plural)); - } else { - constraints.emplace("range", - std::to_string(minimum.value()) + " to " + - quantify(maximum.value(), singular, plural)); - } - } else if (minimum.has_value()) { - if (lower_bound.has_value() && minimum.value() <= lower_bound.value()) { - return; - } - - constraints.emplace("range", - ">= " + quantify(minimum.value(), singular, plural)); - } -} - -static auto -explain_constant_from_value(const sourcemeta::jsontoolkit::JSON &schema, - const sourcemeta::jsontoolkit::JSON &value) - -> std::optional { - std::optional title; - std::optional description; - - if (schema.defines("title")) { - assert(schema.at("title").is_string()); - title = schema.at("title").to_string(); - } else if (schema.defines("description")) { - assert(schema.at("description").is_string()); - description = schema.at("description").to_string(); - } - - return sourcemeta::jsontoolkit::SchemaExplanationConstant{value, title, - description}; -} - -static auto translate_format(const std::string &type) -> std::string { - if (type == "date-time") { - return "Timestamp"; - } else if (type == "date") { - return "Date"; - } else if (type == "time") { - return "Time"; - } else if (type == "email") { - return "Email Address"; - } else if (type == "idn-email") { - return "Email Address"; - } else if (type == "hostname") { - return "Hostname"; - } else if (type == "idn-hostname") { - return "Hostname"; - } else if (type == "ipv4") { - return "IP Address v4"; - } else if (type == "ipv6") { - return "IP Address v6"; - } else if (type == "uri") { - return "Absolute URI"; - } else if (type == "uri-reference") { - return "URI"; - } else if (type == "iri") { - return "Absolute URI"; - } else if (type == "iri-reference") { - return "URI"; - } else if (type == "uri-template") { - return "URI Template"; - } else if (type == "json-pointer") { - return "JSON Pointer"; - } else if (type == "relative-json-pointer") { - return "Relative JSON Pointer"; - } else if (type == "regex") { - return "Regular Expression"; - } else { - return type; - } -} - -static auto explain_string(const sourcemeta::jsontoolkit::JSON &schema, - const std::map &vocabularies) - -> std::optional { - sourcemeta::jsontoolkit::SchemaExplanationScalar explanation; - explanation.type = "String"; - - if (vocabularies.contains("http://json-schema.org/draft-07/schema#")) { - if (schema.defines("minLength") && schema.defines("maxLength")) { - make_range_constraint( - explanation.constraints, schema.at("minLength").to_integer(), - schema.at("maxLength").to_integer(), 0, "character", "characters"); - } - - static const std::set IGNORE{"$id", "$schema", "$comment", - "type"}; - for (const auto &[keyword, value] : schema.as_object()) { - if (IGNORE.contains(keyword)) { - continue; - } else if (keyword == "title") { - assert(value.is_string()); - explanation.title = value.to_string(); - } else if (keyword == "description") { - assert(value.is_string()); - explanation.description = value.to_string(); - } else if (keyword == "minLength") { - make_range_constraint(explanation.constraints, - schema.at("minLength").to_integer(), std::nullopt, - 0, "character", "characters"); - } else if (keyword == "maxLength") { - make_range_constraint(explanation.constraints, std::nullopt, - schema.at("maxLength").to_integer(), 0, - "character", "characters"); - } else if (keyword == "pattern") { - assert(value.is_string()); - explanation.constraints.emplace("matches", value.to_string()); - } else if (keyword == "format") { - assert(value.is_string()); - explanation.type = - "String (" + translate_format(value.to_string()) + ")"; - } else if (keyword == "examples") { - assert(value.is_array()); - for (const auto &item : value.as_array()) { - assert(item.is_string()); - explanation.examples.insert(item); - } - } else { - return std::nullopt; - } - } - } - - return explanation; -} - -static auto explain_enumeration(const sourcemeta::jsontoolkit::JSON &schema, - const std::map &vocabularies) - -> std::optional { - sourcemeta::jsontoolkit::SchemaExplanationEnumeration explanation; - if (vocabularies.contains("http://json-schema.org/draft-07/schema#")) { - static const std::set IGNORE{ - "$id", "$schema", "$comment", - // We don't collect examples, as by definition - // the enumeration is a set of examples - "examples"}; - for (const auto &[keyword, value] : schema.as_object()) { - if (IGNORE.contains(keyword)) { - continue; - } else if (keyword == "enum") { - assert(value.is_array()); - for (const auto &item : value.as_array()) { - explanation.values.insert(item); - } - } else if (keyword == "title") { - assert(value.is_string()); - explanation.title = value.to_string(); - } else if (keyword == "description") { - assert(value.is_string()); - explanation.description = value.to_string(); - } else { - return std::nullopt; - } - } - } - - return explanation; -} - -namespace sourcemeta::jsontoolkit { - -auto explain(const JSON &schema, const SchemaResolver &resolver, - const std::optional &default_dialect) - -> std::optional { - assert(is_schema(schema)); - - if (schema.is_boolean()) { - return std::nullopt; - } - - const auto vocabularies{ - sourcemeta::jsontoolkit::vocabularies(schema, resolver, default_dialect) - .get()}; - - // TODO: Support all dialects - - if (vocabularies.contains("http://json-schema.org/draft-07/schema#") && - schema.defines("type") && schema.at("type").is_string() && - schema.at("type").to_string() == "string") { - - if (schema.defines("maxLength") && schema.at("maxLength").is_integer() && - schema.at("maxLength").to_integer() == 0) { - return explain_constant_from_value(schema, - sourcemeta::jsontoolkit::JSON{""}); - } - - return explain_string(schema, vocabularies); - } - - if (vocabularies.contains("http://json-schema.org/draft-07/schema#") && - schema.defines("enum") && schema.at("enum").is_array() && - !schema.at("enum").empty()) { - return explain_enumeration(schema, vocabularies); - } - - return std::nullopt; -} - -} // namespace sourcemeta::jsontoolkit diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h index c8cc2d6..10f88fb 100644 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h +++ b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema.h @@ -8,7 +8,6 @@ #include #include #include -#include #include #include #include diff --git a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_explain.h b/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_explain.h deleted file mode 100644 index 59abbbd..0000000 --- a/vendor/jsontoolkit/src/jsonschema/include/sourcemeta/jsontoolkit/jsonschema_explain.h +++ /dev/null @@ -1,81 +0,0 @@ -#ifndef SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPLAIN_H_ -#define SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPLAIN_H_ - -#include "jsonschema_export.h" - -#include -#include - -#include // std::map -#include // std::optional, std::nullopt -#include // std::set -#include // std::string -#include // std::variant - -namespace sourcemeta::jsontoolkit { - -/// @ingroup jsonschema -/// The explanation result of a constant value -struct SchemaExplanationConstant { - JSON value; - std::optional title; - std::optional description; -}; - -/// @ingroup jsonschema -/// The explanation result of a scalar schema (non container and non -/// enumeration) -struct SchemaExplanationScalar { - std::string type; - std::map constraints; - std::optional title; - std::optional description; - std::set examples; -}; - -/// @ingroup jsonschema -/// The explanation result of an enumeration schema -struct SchemaExplanationEnumeration { - std::optional title; - std::optional description; - std::set values; -}; - -/// @ingroup jsonschema -/// The explanation result of a schema -using SchemaExplanation = - std::variant; - -/// @ingroup jsonschema -/// -/// Analyze a schema and return a structured high level explanation of it. If -/// the schema cannot be explained (yet), an empty optional is returned. -/// -/// For example: -/// -/// ```cpp -/// #include -/// #include -/// #include -/// -/// const auto schema = sourcemeta::jsontoolkit::parse(R"JSON({ -/// "$schema": "https://json-schema.org/draft/2020-12/schema", -/// "type": "string" -/// })JSON"); -/// -/// const auto result{sourcemeta::jsontoolkit::explain( -/// schema, sourcemeta::jsontoolkit::official_resolver)}; -/// -/// assert(result.has_value()); -/// assert(std::holds_alternative< -/// sourcemeta::jsontoolkit::SchemaExplanationScalar>(result.value())); -/// ``` -SOURCEMETA_JSONTOOLKIT_JSONSCHEMA_EXPORT -auto explain(const JSON &schema, const SchemaResolver &resolver, - const std::optional &default_dialect = std::nullopt) - -> std::optional; - -} // namespace sourcemeta::jsontoolkit - -#endif diff --git a/vendor/jsontoolkit/src/jsonschema/jsonschema.cc b/vendor/jsontoolkit/src/jsonschema/jsonschema.cc index ac80581..ff71b8a 100644 --- a/vendor/jsontoolkit/src/jsonschema/jsonschema.cc +++ b/vendor/jsontoolkit/src/jsonschema/jsonschema.cc @@ -135,6 +135,23 @@ auto sourcemeta::jsontoolkit::identify( throw sourcemeta::jsontoolkit::SchemaError(error.str()); } + // In older drafts, the presence of `$ref` would override any sibling + // keywords. Note that `$ref` was first introduced in Draft 3, so we + // don't check for base dialects lower than that. + // See + // https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3 + if (schema.defines("$ref") && + (base_dialect == "http://json-schema.org/draft-07/schema#" || + base_dialect == "http://json-schema.org/draft-07/hyper-schema#" || + base_dialect == "http://json-schema.org/draft-06/schema#" || + base_dialect == "http://json-schema.org/draft-06/hyper-schema#" || + base_dialect == "http://json-schema.org/draft-04/schema#" || + base_dialect == "http://json-schema.org/draft-04/hyper-schema#" || + base_dialect == "http://json-schema.org/draft-03/schema#" || + base_dialect == "http://json-schema.org/draft-03/hyper-schema#")) { + return std::nullopt; + } + return identifier.to_string(); } diff --git a/vendor/jsontoolkit/src/jsonschema/reference.cc b/vendor/jsontoolkit/src/jsonschema/reference.cc index 72c3998..a1862d5 100644 --- a/vendor/jsontoolkit/src/jsonschema/reference.cc +++ b/vendor/jsontoolkit/src/jsonschema/reference.cc @@ -66,10 +66,7 @@ ref_overrides_adjacent_keywords(const std::string &base_dialect) -> bool { base_dialect == "http://json-schema.org/draft-04/schema#" || base_dialect == "http://json-schema.org/draft-04/hyper-schema#" || base_dialect == "http://json-schema.org/draft-03/schema#" || - base_dialect == "http://json-schema.org/draft-03/hyper-schema#" || - base_dialect == "http://json-schema.org/draft-02/hyper-schema#" || - base_dialect == "http://json-schema.org/draft-01/hyper-schema#" || - base_dialect == "http://json-schema.org/draft-00/hyper-schema#"; + base_dialect == "http://json-schema.org/draft-03/hyper-schema#"; } static auto supports_id_anchors(const std::string &base_dialect) -> bool {