diff --git a/DEPENDENCIES b/DEPENDENCIES index 4ddefe511..ddf3c9bfc 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,4 +1,4 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 3ea3a54756071232ca017b96bc15f17d8b5be370 -blaze https://github.com/sourcemeta/blaze 0ff98cb5e537f571bdf04f0d59a031a0d8634e07 +core https://github.com/sourcemeta/core aa314809fdb59ed6fcb1b10b3087f32eebf708c5 +blaze https://github.com/sourcemeta/blaze 5554d3106d7920b28b852e929f0a31d7805effe1 bootstrap https://github.com/twbs/bootstrap 1a6fdfae6be09b09eaced8f0e442ca6f7680a61e diff --git a/config.cmake.in b/config.cmake.in index 591a57879..537141d0c 100644 --- a/config.cmake.in +++ b/config.cmake.in @@ -9,7 +9,7 @@ if(NOT JSONBINPACK_COMPONENTS) endif() include(CMakeFindDependencyMacro) -find_dependency(Core COMPONENTS numeric regex uri json jsonpointer jsonschema) +find_dependency(Core COMPONENTS numeric regex uri json jsonpointer jsonschema io) find_dependency(Blaze COMPONENTS alterschema) foreach(component ${JSONBINPACK_COMPONENTS}) diff --git a/src/runtime/CMakeLists.txt b/src/runtime/CMakeLists.txt index dd53bf5e9..94dd8e1ca 100644 --- a/src/runtime/CMakeLists.txt +++ b/src/runtime/CMakeLists.txt @@ -10,7 +10,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT jsonbinpack NAME runtime SOURCES input_stream.cc output_stream.cc - varint.h unreachable.h cache.cc @@ -44,3 +43,5 @@ target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC sourcemeta::core::json) target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC sourcemeta::core::numeric) +target_link_libraries(sourcemeta_jsonbinpack_runtime PUBLIC + sourcemeta::core::io) diff --git a/src/runtime/encoder_string.cc b/src/runtime/encoder_string.cc index ffcf81c42..f74516697 100644 --- a/src/runtime/encoder_string.cc +++ b/src/runtime/encoder_string.cc @@ -115,7 +115,7 @@ auto Encoder::RFC3339_DATE_INTEGER_TRIPLET( assert(month >= 1 && month <= 12); assert(day >= 1 && day <= 31); - this->put_bytes(year); + this->put_word(year); this->put_byte(month); this->put_byte(day); } diff --git a/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h b/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h index e1981e027..ee9d4bac9 100644 --- a/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h +++ b/src/runtime/include/sourcemeta/jsonbinpack/runtime_input_stream.h @@ -5,40 +5,29 @@ #include #endif +#include #include -#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::uint64_t, std::int64_t #include // std::basic_istream namespace sourcemeta::jsonbinpack { /// @ingroup runtime -class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT InputStream { +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT InputStream + : public sourcemeta::core::BinaryReader { public: using Stream = std::basic_istream; InputStream(Stream &input); - // Prevent copying, as this class is tied to a stream resource - InputStream(const InputStream &) = delete; - auto operator=(const InputStream &) -> InputStream & = delete; - [[nodiscard]] auto position() const noexcept -> std::uint64_t; - auto seek(const std::uint64_t offset) -> void; // Seek backwards given a relative offset auto rewind(const std::uint64_t relative_offset, const std::uint64_t position) -> std::uint64_t; - auto get_byte() -> std::uint8_t; - // A "word" corresponds to two bytes - // See https://stackoverflow.com/questions/28066462/how-many-bits-is-a-word - auto get_word() -> std::uint16_t; auto get_varint() -> std::uint64_t; auto get_varint_zigzag() -> std::int64_t; - [[nodiscard]] auto has_more_data() const noexcept -> bool; auto get_string_utf8(const std::uint64_t length) -> sourcemeta::core::JSON::String; - -private: - Stream &stream; }; } // namespace sourcemeta::jsonbinpack diff --git a/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h b/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h index a7c4c4995..3207d66b6 100644 --- a/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h +++ b/src/runtime/include/sourcemeta/jsonbinpack/runtime_output_stream.h @@ -5,34 +5,26 @@ #include #endif +#include #include -#include // std::uint8_t, std::uint16_t, std::uint64_t +#include // std::uint64_t, std::int64_t #include // std::basic_ostream namespace sourcemeta::jsonbinpack { /// @ingroup runtime -class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT OutputStream { +class SOURCEMETA_JSONBINPACK_RUNTIME_EXPORT OutputStream + : public sourcemeta::core::BinaryWriter { public: using Stream = std::basic_ostream; OutputStream(Stream &output); - // Prevent copying, as this class is tied to a stream resource - OutputStream(const OutputStream &) = delete; - auto operator=(const OutputStream &) -> OutputStream & = delete; - - [[nodiscard]] auto position() const noexcept -> std::uint64_t; - auto put_byte(const std::uint8_t byte) -> void; - auto put_bytes(const std::uint16_t bytes) -> void; auto put_varint(const std::uint64_t value) -> void; auto put_varint_zigzag(const std::int64_t value) -> void; auto put_string_utf8(const sourcemeta::core::JSON::String &string, const std::uint64_t length) -> void; - -private: - Stream &stream; }; } // namespace sourcemeta::jsonbinpack diff --git a/src/runtime/input_stream.cc b/src/runtime/input_stream.cc index 76efff45f..f98ed5af7 100644 --- a/src/runtime/input_stream.cc +++ b/src/runtime/input_stream.cc @@ -2,25 +2,14 @@ #include -#include "varint.h" - #include // assert -#include // std::ios_base +#include // std::size_t +#include // std::uint8_t, std::uint64_t, std::int64_t namespace sourcemeta::jsonbinpack { -InputStream::InputStream(Stream &input) : stream{input} { - this->stream.exceptions(std::ios_base::badbit | std::ios_base::failbit | - std::ios_base::eofbit); -} - -auto InputStream::position() const noexcept -> std::uint64_t { - return static_cast(this->stream.tellg()); -} - -auto InputStream::seek(const std::uint64_t offset) -> void { - this->stream.seekg(static_cast(offset)); -} +InputStream::InputStream(Stream &input) + : sourcemeta::core::BinaryReader{input} {} auto InputStream::rewind(const std::uint64_t relative_offset, const std::uint64_t position) -> std::uint64_t { @@ -32,33 +21,33 @@ auto InputStream::rewind(const std::uint64_t relative_offset, return current; } -auto InputStream::get_byte() -> std::uint8_t { - return static_cast(this->stream.get()); -} - -auto InputStream::get_word() -> std::uint16_t { - std::uint16_t word; - this->stream.read(reinterpret_cast(&word), sizeof word); - return word; -} - auto InputStream::get_varint() -> std::uint64_t { - return varint_decode(this->stream); -} + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t result{0}; + std::size_t cursor{0}; + while (true) { + const std::uint8_t byte{this->get_byte()}; + const std::uint64_t value{ + static_cast(byte & LEAST_SIGNIFICANT_BITS)}; +#ifndef NDEBUG + const std::uint64_t current = result; +#endif + result += static_cast(value << SHIFT * cursor); + // Try to catch potential overflows from the above addition + assert(result >= current); + cursor += 1; + if ((byte & MOST_SIGNIFICANT_BIT) == 0) { + break; + } + } -auto InputStream::get_varint_zigzag() -> std::int64_t { - const std::uint64_t value = varint_decode(this->stream); - return sourcemeta::core::zigzag_decode(value); + return result; } -auto InputStream::has_more_data() const noexcept -> bool { - // A way to check if the stream is empty without using `.peek()`, - // which throws given we set exceptions on the EOF bit. - // However, `in_avail()` works on characters and will return zero - // if all that's remaining is 0x00 (null), so we need to handle - // that case separately. - return this->stream.rdbuf()->in_avail() > 0 || - this->stream.rdbuf()->sgetc() == '\0'; +auto InputStream::get_varint_zigzag() -> std::int64_t { + return sourcemeta::core::zigzag_decode(this->get_varint()); } auto InputStream::get_string_utf8(const std::uint64_t length) diff --git a/src/runtime/output_stream.cc b/src/runtime/output_stream.cc index 8ab03a4a7..b95789b9f 100644 --- a/src/runtime/output_stream.cc +++ b/src/runtime/output_stream.cc @@ -2,38 +2,31 @@ #include -#include "varint.h" - #include // assert -#include // std::ios_base +#include // std::uint8_t, std::uint64_t, std::int64_t namespace sourcemeta::jsonbinpack { -OutputStream::OutputStream(Stream &output) : stream{output} { - this->stream.exceptions(std::ios_base::badbit | std::ios_base::failbit | - std::ios_base::eofbit); -} - -auto OutputStream::position() const noexcept -> std::uint64_t { - return static_cast(this->stream.tellp()); -} - -auto OutputStream::put_byte(const std::uint8_t byte) -> void { - this->stream.put(static_cast(byte)); -} - -auto OutputStream::put_bytes(const std::uint16_t bytes) -> void { - this->stream.write( - reinterpret_cast(&bytes), - sizeof bytes); -} +OutputStream::OutputStream(Stream &output) + : sourcemeta::core::BinaryWriter{output} {} auto OutputStream::put_varint(const std::uint64_t value) -> void { - varint_encode(this->stream, value); + constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; + constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; + constexpr std::uint8_t SHIFT{7}; + std::uint64_t accumulator = value; + + while (accumulator > LEAST_SIGNIFICANT_BITS) { + this->put_byte(static_cast( + (accumulator & LEAST_SIGNIFICANT_BITS) | MOST_SIGNIFICANT_BIT)); + accumulator >>= SHIFT; + } + + this->put_byte(static_cast(accumulator)); } auto OutputStream::put_varint_zigzag(const std::int64_t value) -> void { - varint_encode(this->stream, sourcemeta::core::zigzag_encode(value)); + this->put_varint(sourcemeta::core::zigzag_encode(value)); } auto OutputStream::put_string_utf8(const sourcemeta::core::JSON::String &string, diff --git a/src/runtime/varint.h b/src/runtime/varint.h deleted file mode 100644 index 9017a7a00..000000000 --- a/src/runtime/varint.h +++ /dev/null @@ -1,60 +0,0 @@ -#ifndef SOURCEMETA_JSONBINPACK_RUNTIME_VARINT_H_ -#define SOURCEMETA_JSONBINPACK_RUNTIME_VARINT_H_ - -#include // assert -#include // std::uint8_t, std::uint64_t -#include // std::basic_istream -#include // std::basic_ostream - -namespace sourcemeta::jsonbinpack { - -// This encoder does not handle negative integers. Use ZigZag first instead. -template -auto varint_encode(std::basic_ostream &stream, - const std::uint64_t value) - -> std::basic_ostream & { - constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; - constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; - constexpr std::uint8_t SHIFT{7}; - std::uint64_t accumulator = value; - - while (accumulator > LEAST_SIGNIFICANT_BITS) { - stream.put(static_cast((accumulator & LEAST_SIGNIFICANT_BITS) | - MOST_SIGNIFICANT_BIT)); - accumulator >>= SHIFT; - } - - stream.put(static_cast(accumulator)); - return stream; -} - -template -auto varint_decode(std::basic_istream &stream) -> std::uint64_t { - constexpr std::uint8_t LEAST_SIGNIFICANT_BITS{0b01111111}; - constexpr std::uint8_t MOST_SIGNIFICANT_BIT{0b10000000}; - constexpr std::uint8_t SHIFT{7}; - std::uint64_t result{0}; - std::size_t cursor{0}; - while (true) { - const std::uint8_t byte{static_cast(stream.get())}; - assert(!stream.eof()); - const std::uint64_t value{ - static_cast(byte & LEAST_SIGNIFICANT_BITS)}; -#ifndef NDEBUG - const std::uint64_t current = result; -#endif - result += static_cast(value << SHIFT * cursor); - // Try to catch potential overflows from the above addition - assert(result >= current); - cursor += 1; - if ((byte & MOST_SIGNIFICANT_BIT) == 0) { - break; - } - } - - return result; -} - -} // namespace sourcemeta::jsonbinpack - -#endif diff --git a/vendor/blaze/CMakeLists.txt b/vendor/blaze/CMakeLists.txt index 8a4459fdf..64f5f6c17 100644 --- a/vendor/blaze/CMakeLists.txt +++ b/vendor/blaze/CMakeLists.txt @@ -11,6 +11,8 @@ option(BLAZE_OUTPUT "Build the Blaze output formats library" ON) option(BLAZE_TEST "Build the Blaze test runner library" ON) option(BLAZE_CONFIGURATION "Build the Blaze configuration file library" ON) option(BLAZE_ALTERSCHEMA "Build the Blaze alterschema rule library" ON) +option(BLAZE_CODEGEN "Build the Blaze codegen library" ON) +option(BLAZE_DOCUMENTATION "Build the Blaze documentation generator library" ON) option(BLAZE_TESTS "Build the Blaze tests" OFF) option(BLAZE_BENCHMARK "Build the Blaze benchmarks" OFF) option(BLAZE_CONTRIB "Build the Blaze contrib programs" OFF) @@ -67,6 +69,14 @@ if(BLAZE_ALTERSCHEMA) add_subdirectory(src/alterschema) endif() +if(BLAZE_CODEGEN) + add_subdirectory(src/codegen) +endif() + +if(BLAZE_DOCUMENTATION) + add_subdirectory(src/documentation) +endif() + if(BLAZE_CONTRIB) add_subdirectory(contrib) endif() @@ -94,6 +104,7 @@ if(PROJECT_IS_TOP_LEVEL) add_custom_target(blaze_format_trace_json COMMAND "${Python3_EXECUTABLE}" "${PROJECT_SOURCE_DIR}/contrib/format_trace_json.py" + "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_draft3.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_draft4.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_draft6.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_draft7.json" @@ -101,6 +112,8 @@ if(PROJECT_IS_TOP_LEVEL) "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_2020_12.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_1.json" "${PROJECT_SOURCE_DIR}/test/evaluator/evaluator_openapi_3_2.json" + "${PROJECT_SOURCE_DIR}/test/output/output_standard_basic.json" + "${PROJECT_SOURCE_DIR}/test/output/output_standard_flag.json" COMMENT "Formatting trace JSON test files" VERBATIM) endif() @@ -134,6 +147,14 @@ if(BLAZE_TESTS) add_subdirectory(test/alterschema) endif() + if(BLAZE_CODEGEN) + add_subdirectory(test/codegen) + endif() + + if(BLAZE_DOCUMENTATION) + add_subdirectory(test/documentation) + endif() + if(PROJECT_IS_TOP_LEVEL) # Otherwise we need the child project to link # against the sanitizers too. diff --git a/vendor/blaze/DEPENDENCIES b/vendor/blaze/DEPENDENCIES index 18cb9dcaa..bb37e6984 100644 --- a/vendor/blaze/DEPENDENCIES +++ b/vendor/blaze/DEPENDENCIES @@ -1,3 +1,3 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core c43332629d71475f44d212f140effbf0a46c1492 -jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite 06481b143722c8c06671bd40dcde99b422ffd531 +core https://github.com/sourcemeta/core aa314809fdb59ed6fcb1b10b3087f32eebf708c5 +jsonschema-test-suite https://github.com/json-schema-org/JSON-Schema-Test-Suite c7257e92580678a086f0b9243a1903ed88bd27f7 diff --git a/vendor/blaze/config.cmake.in b/vendor/blaze/config.cmake.in index 8bcccb78b..363cf57b5 100644 --- a/vendor/blaze/config.cmake.in +++ b/vendor/blaze/config.cmake.in @@ -10,10 +10,12 @@ if(NOT BLAZE_COMPONENTS) list(APPEND BLAZE_COMPONENTS test) list(APPEND BLAZE_COMPONENTS configuration) list(APPEND BLAZE_COMPONENTS alterschema) + list(APPEND BLAZE_COMPONENTS codegen) + list(APPEND BLAZE_COMPONENTS documentation) endif() include(CMakeFindDependencyMacro) -find_dependency(Core COMPONENTS regex uri json jsonpointer jsonschema io yaml crypto) +find_dependency(Core COMPONENTS regex uri json jsonpointer jsonschema io yaml crypto html) foreach(component ${BLAZE_COMPONENTS}) if(component STREQUAL "compiler") @@ -35,6 +37,14 @@ foreach(component ${BLAZE_COMPONENTS}) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + elseif(component STREQUAL "codegen") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_evaluator.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_compiler.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_output.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_alterschema.cmake") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_codegen.cmake") + elseif(component STREQUAL "documentation") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_blaze_documentation.cmake") else() message(FATAL_ERROR "Unknown Blaze component: ${component}") endif() diff --git a/vendor/blaze/package-lock.json b/vendor/blaze/package-lock.json new file mode 100644 index 000000000..b2c332930 --- /dev/null +++ b/vendor/blaze/package-lock.json @@ -0,0 +1,29 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "devDependencies": { + "typescript": "^5.9.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + } + } +} diff --git a/vendor/blaze/package.json b/vendor/blaze/package.json new file mode 100644 index 000000000..a2dea343e --- /dev/null +++ b/vendor/blaze/package.json @@ -0,0 +1,8 @@ +{ + "name": "@sourcemeta/blaze", + "version": "0.0.1", + "private": true, + "devDependencies": { + "typescript": "^5.9.3" + } +} diff --git a/vendor/blaze/patches/jsonschema-test-suite/0001-draft3-ref-sibling.patch b/vendor/blaze/patches/jsonschema-test-suite/0001-draft3-ref-sibling.patch new file mode 100644 index 000000000..8c41d471b --- /dev/null +++ b/vendor/blaze/patches/jsonschema-test-suite/0001-draft3-ref-sibling.patch @@ -0,0 +1,26 @@ +From 95fec2442e66b3de6651972e5f7d4635ca6f7ff9 Mon Sep 17 00:00:00 2001 +From: Juan Cruz Viotti +Date: Tue, 5 May 2026 09:15:10 -0400 +Subject: [PATCH] Do not use `definitions` sibling to `$ref` in `ref.json` for + Draft 3 + +I'm extending Blaze to support Draft 3. As per the spec (applies to all drafts up to 7), a `$ref` overrides any sibling keyword. This seems to be the only case in the test suite where we do this. +--- + tests/draft3/ref.json | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/tests/draft3/ref.json b/tests/draft3/ref.json +index 609eaa46..6686c78e 100644 +--- a/tests/draft3/ref.json ++++ b/tests/draft3/ref.json +@@ -127,7 +127,9 @@ + "b": {"$ref": "#/definitions/a"}, + "c": {"$ref": "#/definitions/b"} + }, +- "$ref": "#/definitions/c" ++ "extends": { ++ "$ref": "#/definitions/c" ++ } + }, + "tests": [ + { diff --git a/vendor/blaze/ports/javascript/README.md b/vendor/blaze/ports/javascript/README.md index 4f6d20550..1d1b8789f 100644 --- a/vendor/blaze/ports/javascript/README.md +++ b/vendor/blaze/ports/javascript/README.md @@ -18,30 +18,24 @@ npm install --save @sourcemeta/blaze ## Usage -Blaze evaluates pre-compiled schema templates. Compile your JSON Schema using +Blaze evaluates pre-compiled schema templates. Compile your JSON Schema with the [JSON Schema CLI](https://github.com/sourcemeta/jsonschema) (see the [`compile`](https://github.com/sourcemeta/jsonschema/blob/main/docs/compile.markdown) -command): +command), then load the resulting template and validate instances against it. +The mode (fast or exhaustive) is fixed at compile time, not evaluation time. ```sh npm install --global @sourcemeta/jsonschema - -cat > schema.json <<'EOF' -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "type": "object", - "properties": { - "name": { "type": "string" }, - "age": { "type": "integer" } - }, - "required": [ "name" ] -} -EOF - jsonschema compile schema.json --fast > template.json ``` -Then validate instances: +Pass a string as the second argument to `validate` to receive a JSON object +that follows the JSON Schema [Standard Output Format](https://json-schema.org/draft/2020-12/json-schema-core#name-output-formatting) +instead of a boolean. Two formats are supported: +[`'flag'`](https://json-schema.org/draft/2020-12/json-schema-core#name-flag) +(validity only) and +[`'basic'`](https://json-schema.org/draft/2020-12/json-schema-core#name-basic) +(errors and annotations): ```javascript import { readFileSync } from "node:fs"; @@ -50,24 +44,25 @@ import { Blaze } from "@sourcemeta/blaze"; const template = JSON.parse(readFileSync("template.json", "utf-8")); const evaluator = new Blaze(template); +const instance = { name: "John", age: 30 }; -// true or false -console.log(evaluator.validate({ name: "John", age: 30 })); -``` +// Plain boolean +evaluator.validate(instance); -With an evaluation callback for tracing: +// { valid: true } or { valid: false } +evaluator.validate(instance, "flag"); -```javascript -const instance = { name: "John", age: 30 }; -const result = evaluator.validate(instance, - (type, valid, instruction, - evaluatePath, instanceLocation, annotation) => { - console.log(type, evaluatePath, - instanceLocation, valid); - }); -console.log(result); // true or false +// { valid: true, annotations?: [ ... ] } +// { valid: false, errors: [ ... ] } +evaluator.validate(instance, "basic"); ``` +For lower-level integration, `validate` also accepts a callback that fires for +every instruction with `(type, valid, instruction, evaluatePath, +instanceLocation, annotation)`. The exported `describe(valid, instruction, +evaluatePath, instanceLocation, instance, annotation)` helper turns any such +event into a human-readable message. + ### Parsing large integers JavaScript's diff --git a/vendor/blaze/ports/javascript/describe.mjs b/vendor/blaze/ports/javascript/describe.mjs new file mode 100644 index 000000000..4f3b1050a --- /dev/null +++ b/vendor/blaze/ports/javascript/describe.mjs @@ -0,0 +1,1624 @@ +import { + ASSERTION_FAIL, ASSERTION_DEFINES, ASSERTION_DEFINES_STRICT, + ASSERTION_DEFINES_ALL, ASSERTION_DEFINES_ALL_STRICT, + ASSERTION_DEFINES_EXACTLY, ASSERTION_DEFINES_EXACTLY_STRICT, + ASSERTION_DEFINES_EXACTLY_STRICT_HASH3, ASSERTION_PROPERTY_DEPENDENCIES, + ASSERTION_TYPE, ASSERTION_TYPE_ANY, ASSERTION_TYPE_STRICT, + ASSERTION_TYPE_STRICT_ANY, ASSERTION_NOT_TYPE_STRICT_ANY, + ASSERTION_TYPE_STRING_BOUNDED, + ASSERTION_TYPE_STRING_UPPER, ASSERTION_TYPE_ARRAY_BOUNDED, + ASSERTION_TYPE_ARRAY_UPPER, ASSERTION_TYPE_OBJECT_BOUNDED, + ASSERTION_TYPE_OBJECT_UPPER, ASSERTION_REGEX, + ASSERTION_STRING_SIZE_LESS, ASSERTION_STRING_SIZE_GREATER, + ASSERTION_ARRAY_SIZE_LESS, ASSERTION_ARRAY_SIZE_GREATER, + ASSERTION_OBJECT_SIZE_LESS, ASSERTION_OBJECT_SIZE_GREATER, + ASSERTION_EQUAL, ASSERTION_EQUALS_ANY, ASSERTION_EQUALS_ANY_STRING_HASH, + ASSERTION_GREATER_EQUAL, ASSERTION_LESS_EQUAL, + ASSERTION_GREATER, ASSERTION_LESS, + ASSERTION_UNIQUE, ASSERTION_DIVISIBLE, + ASSERTION_TYPE_INTEGER_BOUNDED, ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, + ASSERTION_TYPE_INTEGER_LOWER_BOUND, ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + ASSERTION_STRING_TYPE, + ASSERTION_PROPERTY_TYPE, ASSERTION_PROPERTY_TYPE_EVALUATE, + ASSERTION_PROPERTY_TYPE_STRICT, ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE, + ASSERTION_PROPERTY_TYPE_STRICT_ANY, ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE, + ASSERTION_ARRAY_PREFIX, ASSERTION_ARRAY_PREFIX_EVALUATE, + ASSERTION_OBJECT_PROPERTIES_SIMPLE, + ANNOTATION_EMIT, ANNOTATION_TO_PARENT, ANNOTATION_BASENAME_TO_PARENT, + EVALUATE, + LOGICAL_NOT, LOGICAL_NOT_EVALUATE, + LOGICAL_OR, LOGICAL_AND, LOGICAL_XOR, LOGICAL_CONDITION, + LOGICAL_WHEN_TYPE, LOGICAL_WHEN_DEFINES, LOGICAL_WHEN_ARRAY_SIZE_GREATER, + LOOP_PROPERTIES_UNEVALUATED, LOOP_PROPERTIES_UNEVALUATED_EXCEPT, + LOOP_PROPERTIES_MATCH, LOOP_PROPERTIES_MATCH_CLOSED, + LOOP_PROPERTIES, LOOP_PROPERTIES_EVALUATE, + LOOP_PROPERTIES_REGEX, LOOP_PROPERTIES_REGEX_CLOSED, + LOOP_PROPERTIES_STARTS_WITH, LOOP_PROPERTIES_EXCEPT, + LOOP_PROPERTIES_TYPE, LOOP_PROPERTIES_TYPE_EVALUATE, + LOOP_PROPERTIES_EXACTLY_TYPE_STRICT, LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH, + LOOP_PROPERTIES_TYPE_STRICT, LOOP_PROPERTIES_TYPE_STRICT_EVALUATE, + LOOP_PROPERTIES_TYPE_STRICT_ANY, LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE, + LOOP_KEYS, LOOP_ITEMS, LOOP_ITEMS_FROM, LOOP_ITEMS_UNEVALUATED, + LOOP_ITEMS_TYPE, LOOP_ITEMS_TYPE_STRICT, LOOP_ITEMS_TYPE_STRICT_ANY, + LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH, + LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3, + LOOP_ITEMS_INTEGER_BOUNDED, LOOP_ITEMS_INTEGER_BOUNDED_SIZED, + LOOP_CONTAINS, + CONTROL_DYNAMIC_ANCHOR_JUMP, CONTROL_JUMP +} from './opcodes.mjs'; + +const TYPE_INTEGER = 2; +const TYPE_REAL = 3; +const TYPE_OBJECT = 6; +const TYPE_DECIMAL = 7; + +const TYPE_NAMES = [ 'null', 'boolean', 'integer', 'number', + 'string', 'array', 'object', 'number' ]; + +function typeName(typeIndex) { + return TYPE_NAMES[typeIndex]; +} + +function jsonTypeOf(value) { + if (value === null) return 0; + switch (typeof value) { + case 'boolean': return 1; + case 'number': return Number.isInteger(value) ? 2 : 3; + case 'bigint': return 2; + case 'string': return 4; + case 'object': return Array.isArray(value) ? 5 : 6; + default: return 0; + } +} + +function valueTypeName(value) { + if (typeof value === 'bigint') return 'integer'; + if (typeof value === 'number') { + return Number.isInteger(value) ? 'integer' : 'number'; + } + return typeName(jsonTypeOf(value)); +} + +function escapeString(input) { + return '"' + String(input).replaceAll('"', '\\"') + '"'; +} + +function stringifyValue(value) { + if (typeof value === 'bigint') return String(value); + return JSON.stringify(value, (key, current) => + typeof current === 'bigint' ? JSON.rawJSON(String(current)) : current); +} + +function resolveTarget(instance, instanceLocation) { + if (instanceLocation === '') return instance; + const tokens = instanceLocation.slice(1).split('/'); + let current = instance; + for (const raw of tokens) { + const token = raw.replaceAll('~1', '/').replaceAll('~0', '~'); + if (Array.isArray(current)) { + current = current[Number(token)]; + } else { + current = current[token]; + } + } + return current; +} + +function extractKeyword(evaluatePath) { + if (evaluatePath === '') return ''; + const lastSlash = evaluatePath.lastIndexOf('/'); + if (lastSlash === -1) return ''; + const token = evaluatePath.slice(lastSlash + 1) + .replaceAll('~1', '/').replaceAll('~0', '~'); + if (/^[0-9]+$/.test(token)) return ''; + return token; +} + +function isWithinKeyword(evaluatePath, keyword) { + const segments = evaluatePath.split('/'); + for (let index = 1; index < segments.length; index++) { + if (segments[index].replaceAll('~1', '/').replaceAll('~0', '~') === keyword) { + return true; + } + } + return false; +} + +function lastInstanceToken(instanceLocation) { + if (instanceLocation === '') return null; + const lastSlash = instanceLocation.lastIndexOf('/'); + return instanceLocation.slice(lastSlash + 1) + .replaceAll('~1', '/').replaceAll('~0', '~'); +} + +function objectSize(value) { + let count = 0; + for (const _key in value) count++; + return count; +} + +function objectKeys(value) { + const keys = []; + for (const key in value) keys.push(key); + return keys; +} + +function unicodeLength(string) { + let count = 0; + for (let index = 0; index < string.length; index++) { + count++; + const code = string.charCodeAt(index); + if (code >= 0xD800 && code <= 0xDBFF) index++; + } + return count; +} + +function isObject(value) { + return value !== null && typeof value === 'object' && !Array.isArray(value); +} + +function jsonEqual(left, right) { + if (left === right) return true; + if (left === null || right === null) return left === right; + if (typeof left !== typeof right) return false; + if (typeof left !== 'object') return left === right; + if (Array.isArray(left) !== Array.isArray(right)) return false; + if (Array.isArray(left)) { + if (left.length !== right.length) return false; + for (let index = 0; index < left.length; index++) { + if (!jsonEqual(left[index], right[index])) return false; + } + return true; + } + const leftKeys = Object.keys(left).sort(); + const rightKeys = Object.keys(right).sort(); + if (leftKeys.length !== rightKeys.length) return false; + for (let index = 0; index < leftKeys.length; index++) { + if (leftKeys[index] !== rightKeys[index]) return false; + if (!jsonEqual(left[leftKeys[index]], right[rightKeys[index]])) return false; + } + return true; +} + +function jsonCompare(left, right) { + const leftType = jsonTypeOf(left); + const rightType = jsonTypeOf(right); + if (leftType !== rightType) return leftType - rightType; + if (left === null) return 0; + if (typeof left === 'boolean') return (left ? 1 : 0) - (right ? 1 : 0); + if (typeof left === 'number' || typeof left === 'bigint') { + if (left < right) return -1; + if (left > right) return 1; + return 0; + } + if (typeof left === 'string') return left < right ? -1 : left > right ? 1 : 0; + const leftStr = JSON.stringify(left); + const rightStr = JSON.stringify(right); + return leftStr < rightStr ? -1 : leftStr > rightStr ? 1 : 0; +} + +function normalizeTypes(bitmask) { + let types = bitmask; + const hasReal = (types & (1 << TYPE_REAL)) !== 0; + const hasInteger = (types & (1 << TYPE_INTEGER)) !== 0; + const hasDecimal = (types & (1 << 7)) !== 0; + if (hasReal && hasInteger) types &= ~(1 << TYPE_INTEGER); + if (hasReal && hasDecimal) types &= ~(1 << 7); + if (hasInteger && hasDecimal) types &= ~(1 << 7); + return types; +} + +function describeTypeCheck(valid, currentType, expectedType) { + let message = 'The value was expected to be of type ' + typeName(expectedType); + if (!valid) { + message += ' but it was of type ' + typeName(currentType); + } + return message; +} + +function describeNotTypeCheck(valid, currentType, expectedType) { + let message = 'The value was expected to NOT be of type ' + typeName(expectedType); + if (!valid) { + message += ' but it was of type '; + if (currentType === TYPE_DECIMAL && expectedType === TYPE_INTEGER) { + message += 'integer'; + } else if ((currentType === TYPE_INTEGER && expectedType === TYPE_REAL) || + currentType === TYPE_DECIMAL) { + message += 'number'; + } else { + message += typeName(currentType); + } + } + return message; +} + +function describeTypesCheck(valid, currentType, bitmask) { + let types = normalizeTypes(bitmask); + const hasReal = (bitmask & (1 << TYPE_REAL)) !== 0; + const hasInteger = (bitmask & (1 << TYPE_INTEGER)) !== 0; + + let popcount = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) popcount++; + } + + if (popcount === 1) { + let typeIndex = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) { typeIndex = bit; break; } + } + return describeTypeCheck(valid, currentType, typeIndex); + } + + let message = 'The value was expected to be of type '; + let first = true; + let lastBit = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) lastBit = bit; + } + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) { + if (!first) message += ', '; + if (bit === lastBit) message += 'or '; + message += typeName(bit); + first = false; + } + } + + if (valid) { + message += ' and it was of type '; + } else { + message += ' but it was of type '; + } + + if (valid && currentType === TYPE_INTEGER && hasReal) { + message += 'number'; + } else if ((valid && currentType === TYPE_INTEGER && hasReal) || + currentType === TYPE_REAL) { + message += 'number'; + } else { + message += typeName(currentType); + } + + return message; +} + +function describeNotTypesCheck(valid, currentType, bitmask) { + let types = normalizeTypes(bitmask); + const hasReal = (bitmask & (1 << TYPE_REAL)) !== 0; + const hasInteger = (bitmask & (1 << TYPE_INTEGER)) !== 0; + + let popcount = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) popcount++; + } + + if (popcount === 1) { + let typeIndex = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) { typeIndex = bit; break; } + } + return describeNotTypeCheck(valid, currentType, typeIndex); + } + + let message = 'The value was expected to NOT be of type '; + let first = true; + let lastBit = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) lastBit = bit; + } + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) { + if (!first) message += ', '; + if (bit === lastBit) message += 'or '; + message += typeName(bit); + first = false; + } + } + + if (valid) { + message += ' and it was of type '; + } else { + message += ' but it was of type '; + } + + if (!valid && currentType === TYPE_INTEGER && hasReal) { + message += 'number'; + } else if ((!valid && currentType === TYPE_INTEGER && hasReal) || + currentType === TYPE_REAL) { + message += 'number'; + } else { + message += typeName(currentType); + } + + return message; +} + +function describeReference(target) { + return 'The ' + typeName(jsonTypeOf(target)) + + ' value was expected to validate against the referenced schema'; +} + +function describeTypeList(bitmask) { + let types = normalizeTypes(bitmask); + + let popcount = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) popcount++; + } + + if (popcount === 1) { + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) return typeName(bit); + } + } + + let result = ''; + let first = true; + let lastBit = 0; + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) lastBit = bit; + } + for (let bit = 0; bit < 8; bit++) { + if ((types & (1 << bit)) !== 0) { + if (!first) result += ', '; + if (bit === lastBit) result += 'or '; + result += typeName(bit); + first = false; + } + } + return result; +} + +function formatList(items, conjunction) { + let result = ''; + for (let index = 0; index < items.length; index++) { + if (index === items.length - 1) { + result += conjunction + ' ' + items[index]; + } else { + result += items[index] + ', '; + } + } + return result; +} + +export function describe(valid, instruction, evaluatePath, + instanceLocation, instance, annotation) { + const opcode = instruction[0]; + const value = instruction[5]; + const children = instruction[6]; + const keyword = extractKeyword(evaluatePath); + const target = resolveTarget(instance, instanceLocation); + const targetType = jsonTypeOf(target); + + if (opcode === ASSERTION_FAIL) { + if (keyword === 'enum') { + return 'The ' + typeName(targetType) + + ' value was not expected to validate against the empty enumeration'; + } + if (keyword === 'contains') { + return 'The constraints declared for this keyword were not satisfiable'; + } + if (keyword === 'additionalProperties' || + keyword === 'unevaluatedProperties') { + const property = lastInstanceToken(instanceLocation); + return 'The object value was not expected to define the property ' + + escapeString(property); + } + if (keyword === 'unevaluatedItems') { + const tokenValue = lastInstanceToken(instanceLocation); + return 'The array value was not expected to define the item at index ' + + tokenValue; + } + return 'No instance is expected to succeed against the false schema'; + } + + if (opcode === LOGICAL_OR) { + const childCount = children ? children.length : 0; + let message = 'The ' + typeName(targetType) + + ' value was expected to validate against '; + if (childCount > 1) { + message += 'at least one of the ' + childCount + ' given subschemas'; + } else { + message += 'the given subschema'; + } + return message; + } + + if (opcode === LOGICAL_AND) { + if (keyword === 'allOf' || keyword === 'extends') { + const childCount = children ? children.length : 0; + let message = 'The ' + typeName(targetType) + + ' value was expected to validate against the '; + if (childCount > 1) { + message += childCount + ' given subschemas'; + } else { + message += 'given subschema'; + } + return message; + } + if (keyword === '$ref') { + return describeReference(target); + } + return ''; + } + + if (opcode === LOGICAL_XOR) { + const childCount = children ? children.length : 0; + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '' && lastInstanceToken(instanceLocation) !== null) { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The property name ' + escapeString(propertyName); + } else { + message += 'The ' + typeName(targetType) + ' value'; + } + message += ' was expected to validate against '; + if (childCount > 1) { + message += 'one and only one of the ' + childCount + ' given subschemas'; + } else { + message += 'the given subschema'; + } + return message; + } + + if (opcode === LOGICAL_CONDITION) { + return 'The ' + typeName(targetType) + + ' value was expected to validate against the given conditional'; + } + + if (opcode === LOGICAL_NOT) { + let message = 'The ' + typeName(targetType) + + ' value was expected to not validate against the given subschema'; + if (!valid) message += ', but it did'; + return message; + } + + if (opcode === LOGICAL_NOT_EVALUATE) { + let message = 'The ' + typeName(targetType) + + ' value was expected to not validate against the given subschema'; + if (!valid) message += ', but it did'; + return message; + } + + if (opcode === EVALUATE) { + return 'The instance location was marked as evaluated'; + } + + if (opcode === CONTROL_DYNAMIC_ANCHOR_JUMP) { + if (keyword === '$dynamicRef') { + return 'The ' + typeName(targetType) + + ' value was expected to validate against the first subschema ' + + 'in scope that declared the dynamic anchor ' + escapeString(value); + } + return 'The ' + typeName(targetType) + + ' value was expected to validate against the first subschema ' + + 'in scope that declared a recursive anchor'; + } + + if (opcode === ANNOTATION_EMIT) { + if (keyword === 'properties') { + return 'The object property ' + escapeString(annotation) + + ' successfully validated against its property subschema'; + } + + if ((keyword === 'items' || keyword === 'additionalItems') && + annotation === true) { + return 'Every item in the array value was successfully validated'; + } + + if ((keyword === 'prefixItems' || keyword === 'items') && + typeof annotation === 'number') { + if (annotation === 0) { + return 'The first item of the array value successfully validated ' + + 'against the first positional subschema'; + } + return 'The first ' + (annotation + 1) + + ' items of the array value successfully validated against the given ' + + 'positional subschemas'; + } + + if (keyword === 'prefixItems' && annotation === true) { + return 'Every item of the array value validated against the given ' + + 'positional subschemas'; + } + + if (keyword === 'title' || keyword === 'description') { + let message = 'The ' + keyword + ' of the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was ' + escapeString(annotation); + return message; + } + + if (keyword === 'default') { + let message = 'The default value of the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was ' + stringifyValue(annotation); + return message; + } + + if (keyword === 'deprecated' && typeof annotation === 'boolean') { + let message = ''; + if (instanceLocation === '') { + message += 'The instance'; + } else { + message += 'The instance location "' + instanceLocation + '"'; + } + message += annotation + ? ' was considered deprecated' + : ' was not considered deprecated'; + return message; + } + + if (keyword === 'readOnly' && typeof annotation === 'boolean') { + let message = ''; + if (instanceLocation === '') { + message += 'The instance'; + } else { + message += 'The instance location "' + instanceLocation + '"'; + } + message += annotation + ? ' was considered read-only' + : ' was not considered read-only'; + return message; + } + + if (keyword === 'writeOnly' && typeof annotation === 'boolean') { + let message = ''; + if (instanceLocation === '') { + message += 'The instance'; + } else { + message += 'The instance location "' + instanceLocation + '"'; + } + message += annotation + ? ' was considered write-only' + : ' was not considered write-only'; + return message; + } + + if (keyword === 'examples') { + let message = ''; + if (instanceLocation === '') { + message += 'Examples of the instance'; + } else { + message += 'Examples of the instance location "' + + instanceLocation + '"'; + } + message += ' were '; + for (let index = 0; index < annotation.length; index++) { + if (index === annotation.length - 1) { + message += 'and ' + stringifyValue(annotation[index]); + } else { + message += stringifyValue(annotation[index]) + ', '; + } + } + return message; + } + + if (keyword === 'contentEncoding') { + let message = 'The content encoding of the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was ' + escapeString(annotation); + return message; + } + + if (keyword === 'contentMediaType') { + let message = 'The content media type of the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was ' + escapeString(annotation); + return message; + } + + if (keyword === 'contentSchema') { + let message = 'When decoded, the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was expected to validate against the schema ' + + stringifyValue(annotation); + return message; + } + + if (keyword === 'format') { + let message = 'The logical type of the'; + if (instanceLocation === '') { + message += ' instance'; + } else { + message += ' instance location "' + instanceLocation + '"'; + } + message += ' was expected to be ' + stringifyValue(annotation); + return message; + } + + return 'The unrecognized keyword ' + escapeString(keyword) + + ' was collected as the annotation ' + stringifyValue(annotation); + } + + if (opcode === ANNOTATION_TO_PARENT) { + if (keyword === 'unevaluatedItems' && annotation === true) { + return 'At least one item of the array value successfully validated ' + + 'against the subschema for unevaluated items'; + } + return ''; + } + + if (opcode === ANNOTATION_BASENAME_TO_PARENT) { + if (keyword === 'patternProperties') { + return 'The object property ' + escapeString(String(annotation)) + + ' successfully validated against its pattern property subschema'; + } + if (keyword === 'additionalProperties') { + return 'The object property ' + escapeString(String(annotation)) + + ' successfully validated against the additional properties subschema'; + } + if (keyword === 'unevaluatedProperties') { + return 'The object property ' + escapeString(String(annotation)) + + ' successfully validated against the subschema for ' + + 'unevaluated properties'; + } + if (keyword === 'contains' && typeof annotation === 'number') { + return 'The item at index ' + annotation + + ' of the array value successfully validated against the ' + + 'containment check subschema'; + } + return ''; + } + + if (opcode === LOOP_PROPERTIES) { + const childCount = children ? children.length : 0; + if (childCount > 0 && children[0][0] === ASSERTION_FAIL) { + if (keyword === 'unevaluatedProperties') { + return 'The object value was not expected to define unevaluated properties'; + } + return 'The object value was not expected to define additional properties'; + } + if (keyword === 'unevaluatedProperties') { + return 'The object properties not covered by other object ' + + 'keywords were expected to validate against this subschema'; + } + return 'The object properties not covered by other adjacent object ' + + 'keywords were expected to validate against this subschema'; + } + + if (opcode === LOOP_PROPERTIES_EVALUATE) { + const childCount = children ? children.length : 0; + if (childCount === 1 && children[0][0] === ASSERTION_FAIL) { + return 'The object value was not expected to define additional properties'; + } + return 'The object properties not covered by other adjacent object ' + + 'keywords were expected to validate against this subschema'; + } + + if (opcode === LOOP_PROPERTIES_UNEVALUATED) { + if (keyword === 'unevaluatedProperties') { + const childCount = children ? children.length : 0; + if (childCount > 0 && children[0][0] === ASSERTION_FAIL) { + return 'The object value was not expected to define unevaluated properties'; + } + return 'The object properties not covered by other object ' + + 'keywords were expected to validate against this subschema'; + } + return ''; + } + + if (opcode === LOOP_PROPERTIES_UNEVALUATED_EXCEPT) { + if (keyword === 'unevaluatedProperties') { + const childCount = children ? children.length : 0; + if (childCount > 0 && children[0][0] === ASSERTION_FAIL) { + return 'The object value was not expected to define unevaluated properties'; + } + return 'The object properties not covered by other object ' + + 'keywords were expected to validate against this subschema'; + } + return ''; + } + + if (opcode === LOOP_PROPERTIES_EXCEPT) { + const childCount = children ? children.length : 0; + if (childCount > 0 && children[0][0] === ASSERTION_FAIL) { + if (keyword === 'unevaluatedProperties') { + return 'The object value was not expected to define unevaluated properties'; + } + return 'The object value was not expected to define additional properties'; + } + if (keyword === 'unevaluatedProperties') { + return 'The object properties not covered by other object ' + + 'keywords were expected to validate against this subschema'; + } + return 'The object properties not covered by other adjacent object ' + + 'keywords were expected to validate against this subschema'; + } + + if (opcode === LOOP_PROPERTIES_EXACTLY_TYPE_STRICT) { + return 'The required object properties were expected to be of type ' + + typeName(value[0]); + } + + if (opcode === LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH) { + return 'The required object properties were expected to be of type ' + + typeName(value[0]); + } + + if (opcode === LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH || + opcode === LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3) { + return 'Every item in the array was expected to be an object whose ' + + 'required properties were of type ' + typeName(value[0]); + } + + if (opcode === LOOP_ITEMS_INTEGER_BOUNDED || + opcode === LOOP_ITEMS_INTEGER_BOUNDED_SIZED) { + return 'Every item in the array was expected to be a number within the given range'; + } + + if (opcode === ASSERTION_TYPE_INTEGER_BOUNDED || + opcode === ASSERTION_TYPE_INTEGER_BOUNDED_STRICT) { + return 'The value was expected to be an integer within the given range'; + } + + if (opcode === ASSERTION_TYPE_INTEGER_LOWER_BOUND || + opcode === ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT) { + return 'The value was expected to be an integer above the given minimum'; + } + + if (opcode === ASSERTION_OBJECT_PROPERTIES_SIMPLE) { + return 'The object value was expected to validate against the defined property subschemas'; + } + + if (opcode === LOOP_PROPERTIES_TYPE || + opcode === LOOP_PROPERTIES_TYPE_EVALUATE || + opcode === LOOP_PROPERTIES_TYPE_STRICT || + opcode === LOOP_PROPERTIES_TYPE_STRICT_EVALUATE) { + return 'The object properties were expected to be of type ' + typeName(value); + } + + if (opcode === LOOP_PROPERTIES_TYPE_STRICT_ANY) { + return 'The object properties were expected to be of type ' + + describeTypeList(value); + } + + if (opcode === LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE) { + return 'The object properties were expected to be of type ' + + describeTypeList(value); + } + + if (opcode === LOOP_KEYS) { + const size = objectSize(target); + const keys = objectKeys(target); + if (size === 0) { + return 'The object is empty and no properties were expected to ' + + 'validate against the given subschema'; + } + if (size === 1) { + return 'The object property ' + escapeString(keys[0]) + + ' was expected to validate against the given subschema'; + } + let message = 'The object properties '; + for (let index = 0; index < keys.length; index++) { + if (index === keys.length - 1) { + message += 'and ' + escapeString(keys[index]); + } else { + message += escapeString(keys[index]) + ', '; + } + } + message += ' were expected to validate against the given subschema'; + return message; + } + + if (opcode === LOOP_ITEMS) { + return 'Every item in the array value was expected to validate against the given subschema'; + } + + if (opcode === LOOP_ITEMS_FROM) { + let message = 'Every item in the array value'; + if (value === 1) { + message += ' except for the first one'; + } else if (value > 0) { + message += ' except for the first ' + value; + } + message += ' was expected to validate against the given subschema'; + return message; + } + + if (opcode === LOOP_ITEMS_UNEVALUATED) { + return 'The array items not covered by other array keywords, if any, ' + + 'were expected to validate against this subschema'; + } + + if (opcode === LOOP_ITEMS_TYPE || opcode === LOOP_ITEMS_TYPE_STRICT) { + return 'The array items were expected to be of type ' + typeName(value); + } + + if (opcode === LOOP_ITEMS_TYPE_STRICT_ANY) { + return 'The array items were expected to be of type ' + describeTypeList(value); + } + + if (opcode === LOOP_CONTAINS) { + const minimum = value[0]; + const maximum = value[1]; + let plural = true; + let message = 'The array value was expected to contain '; + if (maximum !== null) { + if (minimum === maximum && minimum === 0) { + message += 'any number of'; + } else if (minimum === maximum) { + message += 'exactly ' + minimum; + if (minimum === 1) plural = false; + } else if (minimum === 0) { + message += 'up to ' + maximum; + if (maximum === 1) plural = false; + } else { + message += minimum + ' to ' + maximum; + if (maximum === 1) plural = false; + } + } else { + message += 'at least ' + minimum; + if (minimum === 1) plural = false; + } + message += plural + ? ' items that validate against the given subschema' + : ' item that validates against the given subschema'; + return message; + } + + if (opcode === ASSERTION_DEFINES) { + return 'The object value was expected to define the property ' + + escapeString(value); + } + + if (opcode === ASSERTION_DEFINES_STRICT) { + return 'The value was expected to be an object that defines the property ' + + escapeString(value); + } + + if (opcode === ASSERTION_DEFINES_ALL) { + let message = 'The object value was expected to define properties '; + for (let index = 0; index < value.length; index++) { + if (index === value.length - 1) { + message += 'and ' + escapeString(value[index]); + } else { + message += escapeString(value[index]) + ', '; + } + } + if (valid) return message; + + const missing = []; + for (let index = 0; index < value.length; index++) { + if (!Object.hasOwn(target, value[index])) { + missing.push(value[index]); + } + } + missing.sort(); + + if (missing.length === 1) { + message += ' but did not define the property ' + escapeString(missing[0]); + } else { + message += ' but did not define properties '; + for (let index = 0; index < missing.length; index++) { + if (index === missing.length - 1) { + message += 'and ' + escapeString(missing[index]); + } else { + message += escapeString(missing[index]) + ', '; + } + } + } + return message; + } + + if (opcode === ASSERTION_DEFINES_ALL_STRICT) { + let message = 'The value was expected to be an object that defines properties '; + for (let index = 0; index < value.length; index++) { + if (index === value.length - 1) { + message += 'and ' + escapeString(value[index]); + } else { + message += escapeString(value[index]) + ', '; + } + } + return message; + } + + if (opcode === ASSERTION_DEFINES_EXACTLY) { + const sorted = [...value].sort(); + let message = 'The object value was expected to only define properties '; + for (let index = 0; index < sorted.length; index++) { + if (index === sorted.length - 1) { + message += 'and ' + escapeString(sorted[index]); + } else { + message += escapeString(sorted[index]) + ', '; + } + } + return message; + } + + if (opcode === ASSERTION_DEFINES_EXACTLY_STRICT) { + const sorted = [...value].sort(); + let message = + 'The value was expected to be an object that only defines properties '; + for (let index = 0; index < sorted.length; index++) { + if (index === sorted.length - 1) { + message += 'and ' + escapeString(sorted[index]); + } else { + message += escapeString(sorted[index]) + ', '; + } + } + return message; + } + + if (opcode === ASSERTION_DEFINES_EXACTLY_STRICT_HASH3) { + const entries = value[0]; + return 'The value was expected to be an object that only defines the ' + + entries.length + ' given properties'; + } + + if (opcode === ASSERTION_TYPE || opcode === ASSERTION_TYPE_STRICT) { + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + const propertyName = lastInstanceToken(instanceLocation); + return 'The property name ' + escapeString(propertyName) + + ' was expected to be of type ' + typeName(value); + } + return describeTypeCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_TYPE_ANY) { + return describeTypesCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_TYPE_STRICT_ANY) { + return describeTypesCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_NOT_TYPE_STRICT_ANY) { + return describeNotTypesCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_TYPE_STRING_BOUNDED) { + const minimum = value[0]; + const maximum = value[1]; + if (minimum === 0 && maximum !== null) { + return 'The value was expected to consist of a string of at most ' + + maximum + (maximum === 1 ? ' character' : ' characters'); + } + if (maximum !== null) { + return 'The value was expected to consist of a string of ' + minimum + + ' to ' + maximum + (maximum === 1 ? ' character' : ' characters'); + } + return 'The value was expected to consist of a string of at least ' + + minimum + (minimum === 1 ? ' character' : ' characters'); + } + + if (opcode === ASSERTION_TYPE_STRING_UPPER) { + return 'The value was expected to consist of a string of at most ' + + value + (value === 1 ? ' character' : ' characters'); + } + + if (opcode === ASSERTION_TYPE_ARRAY_BOUNDED) { + const minimum = value[0]; + const maximum = value[1]; + if (minimum === 0 && maximum !== null) { + return 'The value was expected to consist of an array of at most ' + + maximum + (maximum === 1 ? ' item' : ' items'); + } + if (maximum !== null) { + return 'The value was expected to consist of an array of ' + minimum + + ' to ' + maximum + (maximum === 1 ? ' item' : ' items'); + } + return 'The value was expected to consist of an array of at least ' + + minimum + (minimum === 1 ? ' item' : ' items'); + } + + if (opcode === ASSERTION_TYPE_ARRAY_UPPER) { + return 'The value was expected to consist of an array of at most ' + + value + (value === 1 ? ' item' : ' items'); + } + + if (opcode === ASSERTION_TYPE_OBJECT_BOUNDED) { + const minimum = value[0]; + const maximum = value[1]; + if (minimum === 0 && maximum !== null) { + return 'The value was expected to consist of an object of at most ' + + maximum + (maximum === 1 ? ' property' : ' properties'); + } + if (maximum !== null) { + return 'The value was expected to consist of an object of ' + minimum + + ' to ' + maximum + (maximum === 1 ? ' property' : ' properties'); + } + return 'The value was expected to consist of an object of at least ' + + minimum + (minimum === 1 ? ' property' : ' properties'); + } + + if (opcode === ASSERTION_TYPE_OBJECT_UPPER) { + return 'The value was expected to consist of an object of at most ' + + value + (value === 1 ? ' property' : ' properties'); + } + + if (opcode === ASSERTION_REGEX) { + const pattern = value.source; + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + const propertyName = lastInstanceToken(instanceLocation); + return 'The property name ' + escapeString(propertyName) + + ' was expected to match the regular expression ' + + escapeString(pattern); + } + return 'The string value ' + escapeString(target) + + ' was expected to match the regular expression ' + + escapeString(pattern); + } + + if (opcode === ASSERTION_STRING_SIZE_LESS) { + if (keyword === 'maxLength') { + const maximum = value - 1; + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames')) { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The object property name ' + escapeString(propertyName); + message += ' was expected to consist of at most ' + maximum + + (maximum === 1 ? ' character' : ' characters'); + message += valid ? ' and' : ' but'; + message += ' it consisted of '; + const length = unicodeLength(propertyName); + message += length + (length === 1 ? ' character' : ' characters'); + } else { + message += 'The string value ' + stringifyValue(target); + message += ' was expected to consist of at most ' + maximum + + (maximum === 1 ? ' character' : ' characters'); + message += valid ? ' and' : ' but'; + message += ' it consisted of '; + const length = unicodeLength(target); + message += length + (length === 1 ? ' character' : ' characters'); + } + return message; + } + return ''; + } + + if (opcode === ASSERTION_STRING_SIZE_GREATER) { + if (keyword === 'minLength') { + const minimum = value + 1; + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames')) { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The object property name ' + escapeString(propertyName); + message += ' was expected to consist of at least ' + minimum + + (minimum === 1 ? ' character' : ' characters'); + message += valid ? ' and' : ' but'; + message += ' it consisted of '; + const length = unicodeLength(propertyName); + message += length + (length === 1 ? ' character' : ' characters'); + } else { + message += 'The string value ' + stringifyValue(target); + message += ' was expected to consist of at least ' + minimum + + (minimum === 1 ? ' character' : ' characters'); + message += valid ? ' and' : ' but'; + message += ' it consisted of '; + const length = unicodeLength(target); + message += length + (length === 1 ? ' character' : ' characters'); + } + return message; + } + return ''; + } + + if (opcode === ASSERTION_ARRAY_SIZE_LESS) { + if (keyword === 'maxItems') { + const maximum = value - 1; + let message = 'The array value was expected to contain at most ' + + maximum + (maximum === 1 ? ' item' : ' items'); + message += valid ? ' and' : ' but'; + message += ' it contained ' + target.length + + (target.length === 1 ? ' item' : ' items'); + return message; + } + return ''; + } + + if (opcode === ASSERTION_ARRAY_SIZE_GREATER) { + const minimum = value + 1; + let message = 'The array value was expected to contain at least ' + + minimum + (minimum === 1 ? ' item' : ' items'); + message += valid ? ' and' : ' but'; + message += ' it contained ' + target.length + + (target.length === 1 ? ' item' : ' items'); + return message; + } + + if (opcode === ASSERTION_OBJECT_SIZE_LESS) { + if (keyword === 'additionalProperties') { + return 'The object value was not expected to define additional properties'; + } + if (keyword === 'maxProperties') { + const maximum = value - 1; + const size = objectSize(target); + let message = 'The object value was expected to contain at most ' + + maximum + (maximum === 1 ? ' property' : ' properties'); + message += valid ? ' and' : ' but'; + message += ' it contained ' + size; + if (size === 0) { + message += ' properties'; + } else if (size === 1) { + message += ' property: ' + escapeString(objectKeys(target)[0]); + } else { + message += ' properties: '; + const properties = objectKeys(target).sort(); + message += formatList(properties.map(escapeString), 'and'); + } + return message; + } + return ''; + } + + if (opcode === ASSERTION_OBJECT_SIZE_GREATER) { + if (keyword === 'minProperties') { + const minimum = value + 1; + const size = objectSize(target); + let message = 'The object value was expected to contain at least ' + + minimum + (minimum === 1 ? ' property' : ' properties'); + message += valid ? ' and' : ' but'; + message += ' it contained ' + size; + if (size === 1) { + message += ' property: ' + escapeString(objectKeys(target)[0]); + } else { + message += ' properties: '; + const properties = objectKeys(target).sort(); + message += formatList(properties.map(escapeString), 'and'); + } + return message; + } + return ''; + } + + if (opcode === ASSERTION_EQUAL) { + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The property name ' + escapeString(propertyName); + } else { + message += 'The ' + typeName(targetType) + ' value '; + message += stringifyValue(target); + } + message += ' was expected to equal the ' + typeName(jsonTypeOf(value)) + + ' constant ' + stringifyValue(value); + return message; + } + + if (opcode === ASSERTION_EQUALS_ANY) { + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The property name ' + escapeString(propertyName); + } else { + message += 'The ' + typeName(targetType) + ' value '; + message += stringifyValue(target); + } + + const values = Array.isArray(value) ? value : (value.values || []); + if (values.length === 1) { + message += ' was expected to equal the ' + + typeName(jsonTypeOf(values[0])) + ' constant ' + + stringifyValue(values[0]); + } else { + if (valid) { + message += ' was expected to equal one of the ' + + values.length + ' declared values'; + } else { + message += ' was expected to equal one of the following values: '; + const sorted = [...values].sort(jsonCompare); + for (let index = 0; index < sorted.length; index++) { + if (index === sorted.length - 1) { + message += 'and ' + stringifyValue(sorted[index]); + } else { + message += stringifyValue(sorted[index]) + ', '; + } + } + } + } + return message; + } + + if (opcode === ASSERTION_EQUALS_ANY_STRING_HASH) { + let message = ''; + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + const propertyName = lastInstanceToken(instanceLocation); + message += 'The property name ' + escapeString(propertyName); + } else { + message += 'The ' + typeName(targetType) + ' value '; + message += stringifyValue(target); + } + const entries = value[0]; + if (entries.length === 1) { + message += ' was expected to equal the given constant'; + } else { + message += ' was expected to equal one of the given declared values'; + } + return message; + } + + if (opcode === ASSERTION_GREATER_EQUAL) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be greater than or equal to the ' + + valueTypeName(value) + ' ' + stringifyValue(value); + } + + if (opcode === ASSERTION_LESS_EQUAL) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be less than or equal to the ' + + valueTypeName(value) + ' ' + stringifyValue(value); + } + + if (opcode === ASSERTION_GREATER) { + let message = 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be greater than the ' + + valueTypeName(value) + ' ' + stringifyValue(value); + if (!valid && jsonEqual(value, target)) { + message += ', but they were equal'; + } + return message; + } + + if (opcode === ASSERTION_LESS) { + let message = 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be less than the ' + + valueTypeName(value) + ' ' + stringifyValue(value); + if (!valid && jsonEqual(value, target)) { + message += ', but they were equal'; + } + return message; + } + + if (opcode === ASSERTION_UNIQUE) { + if (valid) { + return 'The array value was expected to not contain duplicate items'; + } + const duplicateStrs = new Set(); + for (let index = 0; index < target.length; index++) { + for (let other = index + 1; other < target.length; other++) { + if (jsonEqual(target[index], target[other])) { + duplicateStrs.add(stringifyValue(target[index])); + } + } + } + const sorted = [...duplicateStrs].sort(); + if (sorted.length === 1) { + return 'The array value contained the following duplicate item: ' + sorted[0]; + } + let message = 'The array value contained the following duplicate items: '; + for (let index = 0; index < sorted.length; index++) { + if (index === sorted.length - 1) { + message += 'and ' + sorted[index]; + } else { + message += sorted[index] + ', '; + } + } + return message; + } + + if (opcode === ASSERTION_DIVISIBLE) { + return 'The ' + valueTypeName(target) + ' value ' + + stringifyValue(target) + + ' was expected to be divisible by the ' + + valueTypeName(value) + ' ' + stringifyValue(value); + } + + if (opcode === ASSERTION_STRING_TYPE) { + return 'The string value ' + escapeString(target) + + ' was expected to represent a valid URI'; + } + + if (opcode === ASSERTION_PROPERTY_TYPE || + opcode === ASSERTION_PROPERTY_TYPE_EVALUATE || + opcode === ASSERTION_PROPERTY_TYPE_STRICT || + opcode === ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE) { + return describeTypeCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_PROPERTY_TYPE_STRICT_ANY || + opcode === ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE) { + return describeTypesCheck(valid, targetType, value); + } + + if (opcode === ASSERTION_ARRAY_PREFIX || + opcode === ASSERTION_ARRAY_PREFIX_EVALUATE) { + const childCount = children ? children.length : 0; + let message = 'The first '; + if (childCount <= 2) { + message += 'item of the array value was'; + } else { + message += (childCount - 1) + ' items of the array value were'; + } + message += ' expected to validate against the corresponding subschemas'; + return message; + } + + if (opcode === LOOP_PROPERTIES_MATCH) { + const childCount = children ? children.length : 0; + let message = 'The object value was expected to validate against the '; + if (childCount === 1) { + message += 'single defined property subschema'; + } else { + message += childCount + ' defined properties subschemas'; + } + return message; + } + + if (opcode === LOOP_PROPERTIES_MATCH_CLOSED) { + const childCount = children ? children.length : 0; + if (childCount === 1) { + return 'The object value was expected to validate against the ' + + 'single defined property subschema'; + } + return 'Every object value was expected to validate against one of the ' + + childCount + ' defined properties subschemas'; + } + + if (opcode === LOGICAL_WHEN_DEFINES) { + return 'The object value defined the property "' + value + '"'; + } + + if (opcode === LOOP_PROPERTIES_REGEX) { + return 'The object properties that match the regular expression "' + + value.source + '" were expected to validate against the defined ' + + 'pattern property subschema'; + } + + if (opcode === LOOP_PROPERTIES_REGEX_CLOSED) { + return 'The object properties were expected to match the regular ' + + 'expression "' + value.source + '" and validate against the ' + + 'defined pattern property subschema'; + } + + if (opcode === LOOP_PROPERTIES_STARTS_WITH) { + return 'The object properties that start with the string "' + value + + '" were expected to validate against the defined pattern property subschema'; + } + + if (opcode === LOGICAL_WHEN_TYPE) { + if (keyword === 'items') { + return describeTypeCheck(valid, targetType, value); + } + + if (keyword === 'properties') { + const childCount = children ? children.length : 0; + if (targetType !== TYPE_OBJECT) { + return describeTypeCheck(valid, targetType, TYPE_OBJECT); + } + let message = 'The object value was expected to validate against the '; + if (childCount === 1) { + message += 'single defined property subschema'; + } else { + message += 'defined properties subschemas'; + } + return message; + } + + if (keyword === 'dependencies') { + const childCount = children ? children.length : 0; + const present = new Set(); + const presentWithSchemas = new Set(); + const presentWithProperties = new Set(); + const allDependencies = new Set(); + const requiredProperties = new Set(); + + for (let index = 0; index < childCount; index++) { + const child = children[index]; + if (child[0] === LOGICAL_WHEN_DEFINES) { + const property = child[5]; + allDependencies.add(property); + if (!Object.hasOwn(target, property)) continue; + present.add(property); + presentWithSchemas.add(property); + } else if (child[0] === ASSERTION_PROPERTY_DEPENDENCIES) { + const entries = child[5]; + for (const property in entries) { + allDependencies.add(property); + if (Object.hasOwn(target, property)) { + present.add(property); + presentWithProperties.add(property); + const dependencies = entries[property]; + for (let depIndex = 0; depIndex < dependencies.length; depIndex++) { + if (valid || !Object.hasOwn(target, dependencies[depIndex])) { + requiredProperties.add(dependencies[depIndex]); + } + } + } + } + } + } + + const allDepsArr = [...allDependencies].sort(); + const presentArr = [...present].sort(); + const presentSchemasArr = [...presentWithSchemas].sort(); + const requiredArr = [...requiredProperties].sort(); + + if (presentWithSchemas.size === 0 && presentWithProperties.size === 0) { + let message = 'The object value did not define the'; + if (allDepsArr.length === 1) { + message += ' property ' + escapeString(allDepsArr[0]); + } else { + message += ' properties '; + message += formatList(allDepsArr.map(escapeString), 'or'); + } + return message; + } + + let message = ''; + if (presentArr.length === 1) { + message += 'Because the object value defined the'; + message += ' property ' + escapeString(presentArr[0]); + } else { + message += 'Because the object value defined the'; + message += ' properties '; + message += formatList(presentArr.map(escapeString), 'and'); + } + + if (requiredArr.length > 0) { + message += ', it was also expected to define the'; + if (requiredArr.length === 1) { + message += ' property ' + escapeString(requiredArr[0]); + } else { + message += ' properties '; + message += formatList(requiredArr.map(escapeString), 'and'); + } + } + + if (presentSchemasArr.length > 0) { + message += ', '; + if (requiredArr.length > 0) message += 'and '; + message += 'it was also expected to successfully validate against ' + + 'the corresponding '; + if (presentSchemasArr.length === 1) { + message += escapeString(presentSchemasArr[0]) + ' subschema'; + } else { + message += formatList(presentSchemasArr.map(escapeString), 'and'); + message += ' subschemas'; + } + } + + return message; + } + + if (keyword === 'dependentSchemas') { + const childCount = children ? children.length : 0; + const present = new Set(); + const allDependencies = new Set(); + + for (let index = 0; index < childCount; index++) { + const child = children[index]; + const property = child[5]; + allDependencies.add(property); + if (Object.hasOwn(target, property)) { + present.add(property); + } + } + + const allDepsArr = [...allDependencies].sort(); + const presentArr = [...present].sort(); + + if (present.size === 0) { + let message = 'The object value did not define the'; + if (allDepsArr.length === 1) { + message += ' property ' + escapeString(allDepsArr[0]); + } else { + message += ' properties '; + message += formatList(allDepsArr.map(escapeString), 'or'); + } + return message; + } + + if (present.size === 1) { + return 'Because the object value defined the property ' + + escapeString(presentArr[0]) + + ', it was also expected to validate against the corresponding subschema'; + } + + let message = 'Because the object value defined the properties '; + message += formatList(presentArr.map(escapeString), 'and'); + message += ', it was also expected to validate against the ' + + 'corresponding subschemas'; + return message; + } + + return ''; + } + + if (opcode === ASSERTION_PROPERTY_DEPENDENCIES) { + const present = []; + const allDependencies = []; + const required = new Set(); + + for (const property in value) { + allDependencies.push(property); + if (Object.hasOwn(target, property)) { + present.push(property); + const dependencies = value[property]; + for (let index = 0; index < dependencies.length; index++) { + if (valid || !Object.hasOwn(target, dependencies[index])) { + required.add(dependencies[index]); + } + } + } + } + + allDependencies.sort(); + present.sort(); + const requiredArr = [...required].sort(); + + if (present.length === 0) { + let message = 'The object value did not define the'; + if (allDependencies.length === 1) { + message += ' property ' + escapeString(allDependencies[0]); + } else { + message += ' properties '; + message += formatList(allDependencies.map(escapeString), 'or'); + } + return message; + } + + let message = ''; + if (present.length === 1) { + message += 'Because the object value defined the'; + message += ' property ' + escapeString(present[0]); + } else { + message += 'Because the object value defined the'; + message += ' properties '; + message += formatList(present.map(escapeString), 'and'); + } + + message += ', it was also expected to define the'; + if (requiredArr.length === 1) { + message += ' property ' + escapeString(requiredArr[0]); + } else { + message += ' properties '; + message += formatList(requiredArr.map(escapeString), 'and'); + } + return message; + } + + if (opcode === LOGICAL_WHEN_ARRAY_SIZE_GREATER) { + if (keyword === 'additionalItems' || keyword === 'items') { + if (target.length > value) { + const rest = target.length - value; + return 'The array value contains ' + rest + ' additional' + + (rest === 1 ? ' item' : ' items') + + ' not described by related keywords'; + } + return 'The array value does not contain additional items not ' + + 'described by related keywords'; + } + return ''; + } + + if (opcode === CONTROL_JUMP) { + if (isWithinKeyword(evaluatePath, 'propertyNames') && + instanceLocation !== '') { + return 'The string value was expected to validate against the referenced schema'; + } + return describeReference(target); + } + + return ''; +} diff --git a/vendor/blaze/ports/javascript/index.d.mts b/vendor/blaze/ports/javascript/index.d.mts index f1bc89204..bcb7f112d 100644 --- a/vendor/blaze/ports/javascript/index.d.mts +++ b/vendor/blaze/ports/javascript/index.d.mts @@ -9,6 +9,32 @@ export type EvaluationCallback = ( annotation: unknown ) => void; +export type StandardOutputFormat = 'flag' | 'basic'; + +export interface StandardOutputErrorEntry { + keywordLocation: string; + absoluteKeywordLocation: string; + instanceLocation: string; + error: string; +} + +export interface StandardOutputAnnotationEntry { + keywordLocation: string; + absoluteKeywordLocation: string; + instanceLocation: string; + annotation: unknown[]; +} + +export type StandardOutputFlagResult = { valid: boolean }; + +export type StandardOutputBasicResult = + | { valid: true; annotations?: StandardOutputAnnotationEntry[] } + | { valid: false; errors: StandardOutputErrorEntry[] }; + +export type StandardOutputResult = + | StandardOutputFlagResult + | StandardOutputBasicResult; + export declare class Blaze { static reviver( key: string, @@ -16,5 +42,16 @@ export declare class Blaze { context: { source: string } ): unknown; constructor(template: Template); + validate(instance: unknown, format: 'flag'): StandardOutputFlagResult; + validate(instance: unknown, format: 'basic'): StandardOutputBasicResult; validate(instance: unknown, callback?: EvaluationCallback): boolean; } + +export declare function describe( + valid: boolean, + instruction: unknown[], + evaluatePath: string, + instanceLocation: string, + instance: unknown, + annotation: unknown +): string; diff --git a/vendor/blaze/ports/javascript/index.mjs b/vendor/blaze/ports/javascript/index.mjs index a070777d3..f58f86bf5 100644 --- a/vendor/blaze/ports/javascript/index.mjs +++ b/vendor/blaze/ports/javascript/index.mjs @@ -1,10 +1,13 @@ -const JSON_VERSION = 4; +import { + ANNOTATION_EMIT, ANNOTATION_TO_PARENT, ANNOTATION_BASENAME_TO_PARENT, + ASSERTION_EQUALS_ANY, + CONTROL_GROUP as CONTROL_GROUP_START, + CONTROL_EVALUATE as CONTROL_EVALUATE_END, + CONTROL_JUMP, CONTROL_DYNAMIC_ANCHOR_JUMP +} from './opcodes.mjs'; + +const JSON_VERSION = 5; const DEPTH_LIMIT = 300; -const ANNOTATION_EMIT = 49; -const ANNOTATION_TO_PARENT = 50; -const ANNOTATION_BASENAME_TO_PARENT = 51; -const CONTROL_GROUP_START = 92; -const CONTROL_EVALUATE_END = 96; const URI_REGEX = /^[a-zA-Z][a-zA-Z0-9+\-.]*:[^\s]*$/; function buildJsonPointer(tokens, length) { @@ -120,7 +123,7 @@ function prepareInstruction(instruction) { } const opcode = instruction[0]; - if (opcode === 27 && Array.isArray(instruction[5])) { + if (opcode === ASSERTION_EQUALS_ANY && Array.isArray(instruction[5])) { const values = instruction[5]; let allPrimitive = true; for (let index = 0; index < values.length; index++) { @@ -152,7 +155,7 @@ function prepareInstruction(instruction) { function resolveJumpTargets(instructions, targets) { for (let index = 0; index < instructions.length; index++) { const instruction = instructions[index]; - if (instruction[0] === 98) { + if (instruction[0] === CONTROL_JUMP) { const targetIndex = instruction[5]; if (targetIndex < targets.length) { instruction[5] = targets[targetIndex]; @@ -187,7 +190,7 @@ function collectAnchorNames(targets, result) { function collectAnchorNamesFromInstructions(instructions, result) { for (let index = 0; index < instructions.length; index++) { const instruction = instructions[index]; - if (instruction[0] === 97 && typeof instruction[5] === 'string') { + if (instruction[0] === CONTROL_DYNAMIC_ANCHOR_JUMP && typeof instruction[5] === 'string') { result.add(instruction[5]); } if (instruction[6]) { @@ -252,7 +255,7 @@ function compileInstructionToCode(instruction, captures, visited, budget) { if (cr.length !== 0) return null; switch (op) { case 11: return '{if(_es(' + tv + ')!==' + cv + ')return false;}'; - case 42: return 'if(' + tv + '!=null&&typeof ' + tv + "==='object'&&!Array.isArray(" + tv + ')){' + emitResolve('t', tv, cr) + 'if(t!==void 0&&_es(t)!==' + cv + ')return false;}'; + case 43: return 'if(' + tv + '!=null&&typeof ' + tv + "==='object'&&!Array.isArray(" + tv + ')){' + emitResolve('t', tv, cr) + 'if(t!==void 0&&_es(t)!==' + cv + ')return false;}'; default: return null; } } @@ -275,65 +278,66 @@ function compileInstructionToCode(instruction, captures, visited, budget) { case 10: { var r=R('t'); return r?r+'var a=_jt(t);if(('+value+'&(1<'+value[1]+')return false;':'')+'return true;':null; } - case 14: { var r=R('t'); return r?r+"return typeof t==='string'&&_ul(t)<="+value+';':null; } - case 15: { var r=R('t'); return r?r+'if(!Array.isArray(t))return false;if(t.length<'+value[0]+')return false;'+(value[1]!==null?'if(t.length>'+value[1]+')return false;':'')+'return true;':null; } - case 16: { var r=R('t'); return r?r+'return Array.isArray(t)&&t.length<='+value+';':null; } - case 17: { var r=R('t'); return r?r+TO+'return false;var s=0;for(var k in t)s++;if(s<'+value[0]+')return false;'+(value[1]!==null?'if(s>'+value[1]+')return false;':'')+'return true;':null; } - case 18: { var r=R('t'); return r?r+TO+'return false;var s=0;for(var k in t)s++;return s<='+value+';':null; } - case 19: return fb(19); case 20: return fb(20); case 21: return fb(21); - case 22: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;return t.length<'+value+';':null; } - case 23: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;return t.length>'+value+';':null; } - case 24: { var r=R('t'); return r?r+TO+'return true;var s=0;for(var k in t)s++;return s<'+value+';':null; } - case 25: { var r=R('t'); return r?r+TO+'return true;var s=0;for(var k in t)s++;return s>'+value+';':null; } - case 26: { if(typeof value==='string'||typeof value==='number'||typeof value==='boolean'||value===null){var r=R('t');return r?r+'return t==='+JSON.stringify(value)+';':null;}return fb(26); } - case 27: return fb(27); case 28: return fb(28); - case 29: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t>="+v+';':null; } - case 30: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t<="+v+';':null; } - case 31: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t>"+v+';':null; } - case 32: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t<"+v+';':null; } - case 33: return fb(33); case 34: return fb(34); - case 35: return fb(35); case 36: return fb(36); case 37: return fb(37); case 38: return fb(38); - case 39: return fb(39); - case 40: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } - case 41: return fb(41); - case 42: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return _es(t)==='+value+';':null; } - case 43: return fb(43); - case 44: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return('+value+'&(1<<_es(t)))!==0;':null; } - case 45: return fb(45); case 46: return fb(46); case 47: return fb(47); - case 48: return fb(48); - case 49: return 'return true;'; case 50: return 'return true;'; case 51: return 'return true;'; case 52: return 'return true;'; - case 53: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return 'return false;'; var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } - case 60: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 61: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } - case 62: return fb(62); case 63: return fb(63); - case 64: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } - case 65: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } - case 66: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } - case 67: return fb(67); case 68: return fb(68); case 69: return fb(69); case 70: return fb(70); case 71: return fb(71); - case 72: return fb(72); case 73: return fb(73); case 74: return fb(74); case 75: return fb(75); - case 76: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } - case 77: return fb(77); case 78: return fb(78); case 79: return fb(79); case 80: return fb(80); - case 81: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } - case 94: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 95: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } - case 96: return 'return true;'; - case 97: return fb(97); - case 98: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(98); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(98); var c=r; for(var j=0;j'+value[1]+')return false;':'')+'return true;':null; } + case 15: { var r=R('t'); return r?r+"return typeof t==='string'&&_ul(t)<="+value+';':null; } + case 16: { var r=R('t'); return r?r+'if(!Array.isArray(t))return false;if(t.length<'+value[0]+')return false;'+(value[1]!==null?'if(t.length>'+value[1]+')return false;':'')+'return true;':null; } + case 17: { var r=R('t'); return r?r+'return Array.isArray(t)&&t.length<='+value+';':null; } + case 18: { var r=R('t'); return r?r+TO+'return false;var s=0;for(var k in t)s++;if(s<'+value[0]+')return false;'+(value[1]!==null?'if(s>'+value[1]+')return false;':'')+'return true;':null; } + case 19: { var r=R('t'); return r?r+TO+'return false;var s=0;for(var k in t)s++;return s<='+value+';':null; } + case 20: return fb(20); case 21: return fb(21); case 22: return fb(22); + case 23: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;return t.length<'+value+';':null; } + case 24: { var r=R('t'); return r?r+'if(!Array.isArray(t))return true;return t.length>'+value+';':null; } + case 25: { var r=R('t'); return r?r+TO+'return true;var s=0;for(var k in t)s++;return s<'+value+';':null; } + case 26: { var r=R('t'); return r?r+TO+'return true;var s=0;for(var k in t)s++;return s>'+value+';':null; } + case 27: { if(typeof value==='string'||typeof value==='number'||typeof value==='boolean'||value===null){var r=R('t');return r?r+'return t==='+JSON.stringify(value)+';':null;}return fb(27); } + case 28: return fb(28); case 29: return fb(29); + case 30: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t>="+v+';':null; } + case 31: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t<="+v+';':null; } + case 32: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t>"+v+';':null; } + case 33: { var r=R('t'),v=typeof value==='bigint'?value+'n':value; return r?r+"var tt=typeof t;if(tt!=='number'&&tt!=='bigint')return true;return t<"+v+';':null; } + case 34: return fb(34); case 35: return fb(35); + case 36: return fb(36); case 37: return fb(37); case 38: return fb(38); case 39: return fb(39); + case 40: return fb(40); + case 41: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;var a=_jt(t);return a==='+value+'||('+value+'===2&&_ii(t));':null; } + case 42: return fb(42); + case 43: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return _es(t)==='+value+';':null; } + case 44: return fb(44); + case 45: { var r=R('t'); return r?IO+r+'if(t===void 0)return true;return('+value+'&(1<<_es(t)))!==0;':null; } + case 46: return fb(46); case 47: return fb(47); case 48: return fb(48); + case 49: return fb(49); + case 50: return 'return true;'; case 51: return 'return true;'; case 52: return 'return true;'; case 53: return 'return true;'; + case 54: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return 'return false;'; var c=r; for(var j=0;j0)c+=seq(children,'t'); return c+'return true;'; } + case 61: { var r=R('t'); if(!r)return null; var c=r+TO+'return true;if(!Object.hasOwn(t,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 62: { var r=R('t'); if(!r)return null; var c=r+'if(!Array.isArray(t)||t.length<='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'t'); return c+'return true;'; } + case 63: return fb(63); case 64: return fb(64); + case 65: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi!==void 0){var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}}return true;'; } + case 66: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; var mi=captures.length; captures.push(value); var gf=''; for(var gi=0;gi0){gf+='function(i,d,_t,_v){'+lb(gc,'i')+'return true;},';}else{gf+='null,';}} return r+TO+'return true;var __cg=['+gf+'];for(var k in t){var __mi=_c['+mi+'][k];if(__mi===void 0)return false;var __cf=__cg[__mi];if(__cf&&!__cf(t,d,_t,_v))return false;}return true;'; } + case 67: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+TO+'return true;for(var k in t){'+lb(children,'t[k]')+'}return true;'; } + case 68: return fb(68); case 69: return fb(69); case 70: return fb(70); case 71: return fb(71); case 72: return fb(72); + case 73: return fb(73); case 74: return fb(74); case 75: return fb(75); case 76: return fb(76); + case 77: { var r=R('t'); return r?r+TO+'return true;for(var k in t){if(_es(t[k])!=='+value+')return false;}return true;':null; } + case 78: return fb(78); case 79: return fb(79); case 80: return fb(80); case 81: return fb(81); + case 82: { var r=R('t'); if(!r)return null; if(!children||children.length===0)return r+'return true;'; return r+'if(!Array.isArray(t))return true;for(var j=0;j=t.length)return true;for(var j='+value+';j0)c+=seq(children,'i'); return c+'return true;'; } + case 95: { var c=IO+'if(!Object.hasOwn(i,'+JSON.stringify(value)+'))return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 96: { var c='if(_jt(i)!=='+value+')return true;'; if(children&&children.length>0)c+=seq(children,'i'); return c+'return true;'; } + case 97: return 'return true;'; + case 98: return fb(98); + case 99: { if(!value)return 'return true;'; if(visited&&visited.has(instruction))return fb(99); if(!visited)visited=new Set(); visited.add(instruction); var r=R('t'); if(!r)return fb(99); var c=r; for(var j=0;j 96) { + if (type < CONTROL_GROUP_START || type > CONTROL_EVALUATE_END) { if (evaluator.trackMode) { evaluator.pushPath(instruction[1]); } @@ -1047,6 +1057,14 @@ function AssertionTypeStrictAny(instruction, instance, depth, template, evaluato return __result; }; +function AssertionNotTypeStrictAny(instruction, instance, depth, template, evaluator) { + if (evaluator.callbackMode) evaluator.callbackPush(instruction); + const target = resolveInstance(instance, instruction[2]); + const __result = !typeSetTest(instruction[5], effectiveTypeStrictReal(target)); + if (evaluator.callbackMode) evaluator.callbackPop(instruction, __result); + return __result; +}; + function AssertionTypeStringBounded(instruction, instance, depth, template, evaluator) { if (evaluator.callbackMode) evaluator.callbackPush(instruction); const target = resolveInstance(instance, instruction[2]); @@ -2610,92 +2628,93 @@ const handlers = [ AssertionTypeAny, // 10 AssertionTypeStrict, // 11 AssertionTypeStrictAny, // 12 - AssertionTypeStringBounded, // 13 - AssertionTypeStringUpper, // 14 - AssertionTypeArrayBounded, // 15 - AssertionTypeArrayUpper, // 16 - AssertionTypeObjectBounded, // 17 - AssertionTypeObjectUpper, // 18 - AssertionRegex, // 19 - AssertionStringSizeLess, // 20 - AssertionStringSizeGreater, // 21 - AssertionArraySizeLess, // 22 - AssertionArraySizeGreater, // 23 - AssertionObjectSizeLess, // 24 - AssertionObjectSizeGreater, // 25 - AssertionEqual, // 26 - AssertionEqualsAny, // 27 - AssertionEqualsAnyStringHash, // 28 - AssertionGreaterEqual, // 29 - AssertionLessEqual, // 30 - AssertionGreater, // 31 - AssertionLess, // 32 - AssertionUnique, // 33 - AssertionDivisible, // 34 - AssertionTypeIntegerBounded, // 35 - AssertionTypeIntegerBoundedStrict, // 36 - AssertionTypeIntegerLowerBound, // 37 - AssertionTypeIntegerLowerBoundStrict, // 38 - AssertionStringType, // 39 - AssertionPropertyType, // 40 - AssertionPropertyTypeEvaluate, // 41 - AssertionPropertyTypeStrict, // 42 - AssertionPropertyTypeStrictEvaluate, // 43 - AssertionPropertyTypeStrictAny, // 44 - AssertionPropertyTypeStrictAnyEvaluate, // 45 - AssertionArrayPrefix, // 46 - AssertionArrayPrefixEvaluate, // 47 - AssertionObjectPropertiesSimple, // 48 - AnnotationEmit, // 49 - AnnotationToParent, // 50 - AnnotationBasenameToParent, // 51 - Evaluate, // 52 - LogicalNot, // 53 - LogicalNotEvaluate, // 54 - LogicalOr, // 55 - LogicalAnd, // 56 - LogicalXor, // 57 - LogicalCondition, // 58 - LogicalWhenType, // 59 - LogicalWhenDefines, // 60 - LogicalWhenArraySizeGreater, // 61 - LoopPropertiesUnevaluated, // 62 - LoopPropertiesUnevaluatedExcept, // 63 - LoopPropertiesMatch, // 64 - LoopPropertiesMatchClosed, // 65 - LoopProperties, // 66 - LoopPropertiesEvaluate, // 67 - LoopPropertiesRegex, // 68 - LoopPropertiesRegexClosed, // 69 - LoopPropertiesStartsWith, // 70 - LoopPropertiesExcept, // 71 - LoopPropertiesType, // 72 - LoopPropertiesTypeEvaluate, // 73 - LoopPropertiesExactlyTypeStrict, // 74 - LoopPropertiesExactlyTypeStrictHash, // 75 - LoopPropertiesTypeStrict, // 76 - LoopPropertiesTypeStrictEvaluate, // 77 - LoopPropertiesTypeStrictAny, // 78 - LoopPropertiesTypeStrictAnyEvaluate, // 79 - LoopKeys, // 80 - LoopItems, // 81 - LoopItemsFrom, // 82 - LoopItemsUnevaluated, // 83 - LoopItemsType, // 84 - LoopItemsTypeStrict, // 85 - LoopItemsTypeStrictAny, // 86 - LoopItemsPropertiesExactlyTypeStrictHash, // 87 + AssertionNotTypeStrictAny, // 13 + AssertionTypeStringBounded, // 14 + AssertionTypeStringUpper, // 15 + AssertionTypeArrayBounded, // 16 + AssertionTypeArrayUpper, // 17 + AssertionTypeObjectBounded, // 18 + AssertionTypeObjectUpper, // 19 + AssertionRegex, // 20 + AssertionStringSizeLess, // 21 + AssertionStringSizeGreater, // 22 + AssertionArraySizeLess, // 23 + AssertionArraySizeGreater, // 24 + AssertionObjectSizeLess, // 25 + AssertionObjectSizeGreater, // 26 + AssertionEqual, // 27 + AssertionEqualsAny, // 28 + AssertionEqualsAnyStringHash, // 29 + AssertionGreaterEqual, // 30 + AssertionLessEqual, // 31 + AssertionGreater, // 32 + AssertionLess, // 33 + AssertionUnique, // 34 + AssertionDivisible, // 35 + AssertionTypeIntegerBounded, // 36 + AssertionTypeIntegerBoundedStrict, // 37 + AssertionTypeIntegerLowerBound, // 38 + AssertionTypeIntegerLowerBoundStrict, // 39 + AssertionStringType, // 40 + AssertionPropertyType, // 41 + AssertionPropertyTypeEvaluate, // 42 + AssertionPropertyTypeStrict, // 43 + AssertionPropertyTypeStrictEvaluate, // 44 + AssertionPropertyTypeStrictAny, // 45 + AssertionPropertyTypeStrictAnyEvaluate, // 46 + AssertionArrayPrefix, // 47 + AssertionArrayPrefixEvaluate, // 48 + AssertionObjectPropertiesSimple, // 49 + AnnotationEmit, // 50 + AnnotationToParent, // 51 + AnnotationBasenameToParent, // 52 + Evaluate, // 53 + LogicalNot, // 54 + LogicalNotEvaluate, // 55 + LogicalOr, // 56 + LogicalAnd, // 57 + LogicalXor, // 58 + LogicalCondition, // 59 + LogicalWhenType, // 60 + LogicalWhenDefines, // 61 + LogicalWhenArraySizeGreater, // 62 + LoopPropertiesUnevaluated, // 63 + LoopPropertiesUnevaluatedExcept, // 64 + LoopPropertiesMatch, // 65 + LoopPropertiesMatchClosed, // 66 + LoopProperties, // 67 + LoopPropertiesEvaluate, // 68 + LoopPropertiesRegex, // 69 + LoopPropertiesRegexClosed, // 70 + LoopPropertiesStartsWith, // 71 + LoopPropertiesExcept, // 72 + LoopPropertiesType, // 73 + LoopPropertiesTypeEvaluate, // 74 + LoopPropertiesExactlyTypeStrict, // 75 + LoopPropertiesExactlyTypeStrictHash, // 76 + LoopPropertiesTypeStrict, // 77 + LoopPropertiesTypeStrictEvaluate, // 78 + LoopPropertiesTypeStrictAny, // 79 + LoopPropertiesTypeStrictAnyEvaluate, // 80 + LoopKeys, // 81 + LoopItems, // 82 + LoopItemsFrom, // 83 + LoopItemsUnevaluated, // 84 + LoopItemsType, // 85 + LoopItemsTypeStrict, // 86 + LoopItemsTypeStrictAny, // 87 LoopItemsPropertiesExactlyTypeStrictHash, // 88 - LoopItemsIntegerBounded, // 89 - LoopItemsIntegerBoundedSized, // 90 - LoopContains, // 91 - ControlGroup, // 92 - ControlGroupWhenDefines, // 93 - ControlGroupWhenDefinesDirect, // 94 - ControlGroupWhenType, // 95 - ControlEvaluate, // 96 - ControlDynamicAnchorJump, // 97 - ControlJump // 98 + LoopItemsPropertiesExactlyTypeStrictHash, // 89 + LoopItemsIntegerBounded, // 90 + LoopItemsIntegerBoundedSized, // 91 + LoopContains, // 92 + ControlGroup, // 93 + ControlGroupWhenDefines, // 94 + ControlGroupWhenDefinesDirect, // 95 + ControlGroupWhenType, // 96 + ControlEvaluate, // 97 + ControlDynamicAnchorJump, // 98 + ControlJump // 99 ]; function AssertionTypeArrayBounded_fast(instruction, instance, depth, template, evaluator) { @@ -3204,6 +3223,12 @@ function AssertionTypeStrictAny_fast(instruction, instance, depth, template, eva return typeSetTest(instruction[5], effectiveTypeStrictReal(target)); } +function AssertionNotTypeStrictAny_fast(instruction, instance, depth, template, evaluator) { + const relInstance = instruction[2]; + const target = relInstance.length === 0 ? instance : resolveInstance(instance, relInstance); + return !typeSetTest(instruction[5], effectiveTypeStrictReal(target)); +} + function AssertionTypeStringUpper_fast(instruction, instance, depth, template, evaluator) { const relInstance = instruction[2]; const target = relInstance.length === 0 ? instance : resolveInstance(instance, relInstance); @@ -3872,99 +3897,274 @@ function AssertionTypeIntegerLowerBoundStrict_fast(instruction, instance, depth, } const fastHandlers = handlers.slice(); -fastHandlers[15] = AssertionTypeArrayBounded_fast; -fastHandlers[86] = LoopItemsTypeStrictAny_fast; -fastHandlers[42] = AssertionPropertyTypeStrict_fast; +fastHandlers[16] = AssertionTypeArrayBounded_fast; +fastHandlers[87] = LoopItemsTypeStrictAny_fast; +fastHandlers[43] = AssertionPropertyTypeStrict_fast; fastHandlers[11] = AssertionTypeStrict_fast; fastHandlers[4] = AssertionDefinesAllStrict_fast; -fastHandlers[26] = AssertionEqual_fast; -fastHandlers[64] = LoopPropertiesMatch_fast; -fastHandlers[55] = LogicalOr_fast; -fastHandlers[98] = ControlJump_fast; -fastHandlers[28] = AssertionEqualsAnyStringHash_fast; -fastHandlers[57] = LogicalXor_fast; +fastHandlers[27] = AssertionEqual_fast; +fastHandlers[65] = LoopPropertiesMatch_fast; +fastHandlers[56] = LogicalOr_fast; +fastHandlers[99] = ControlJump_fast; +fastHandlers[29] = AssertionEqualsAnyStringHash_fast; +fastHandlers[58] = LogicalXor_fast; fastHandlers[2] = AssertionDefinesStrict_fast; -fastHandlers[81] = LoopItems_fast; -fastHandlers[65] = LoopPropertiesMatchClosed_fast; -fastHandlers[13] = AssertionTypeStringBounded_fast; -fastHandlers[56] = LogicalAnd_fast; +fastHandlers[82] = LoopItems_fast; +fastHandlers[66] = LoopPropertiesMatchClosed_fast; +fastHandlers[14] = AssertionTypeStringBounded_fast; +fastHandlers[57] = LogicalAnd_fast; fastHandlers[8] = AssertionPropertyDependencies_fast; fastHandlers[10] = AssertionTypeAny_fast; -fastHandlers[58] = LogicalCondition_fast; -fastHandlers[71] = LoopPropertiesExcept_fast; -fastHandlers[19] = AssertionRegex_fast; -fastHandlers[66] = LoopProperties_fast; +fastHandlers[59] = LogicalCondition_fast; +fastHandlers[72] = LoopPropertiesExcept_fast; +fastHandlers[20] = AssertionRegex_fast; +fastHandlers[67] = LoopProperties_fast; fastHandlers[1] = AssertionDefines_fast; -fastHandlers[59] = LogicalWhenType_fast; -fastHandlers[60] = LogicalWhenDefines_fast; +fastHandlers[60] = LogicalWhenType_fast; +fastHandlers[61] = LogicalWhenDefines_fast; fastHandlers[0] = AssertionFail_fast; -fastHandlers[91] = LoopContains_fast; -fastHandlers[53] = LogicalNot_fast; -fastHandlers[84] = LoopItemsType_fast; -fastHandlers[85] = LoopItemsTypeStrict_fast; -fastHandlers[27] = AssertionEqualsAny_fast; +fastHandlers[92] = LoopContains_fast; +fastHandlers[54] = LogicalNot_fast; +fastHandlers[85] = LoopItemsType_fast; +fastHandlers[86] = LoopItemsTypeStrict_fast; +fastHandlers[28] = AssertionEqualsAny_fast; fastHandlers[3] = AssertionDefinesAll_fast; fastHandlers[5] = AssertionDefinesExactly_fast; fastHandlers[6] = AssertionDefinesExactlyStrict_fast; fastHandlers[7] = AssertionDefinesExactlyStrictHash3_fast; fastHandlers[9] = AssertionType_fast; fastHandlers[12] = AssertionTypeStrictAny_fast; -fastHandlers[14] = AssertionTypeStringUpper_fast; -fastHandlers[16] = AssertionTypeArrayUpper_fast; -fastHandlers[17] = AssertionTypeObjectBounded_fast; -fastHandlers[18] = AssertionTypeObjectUpper_fast; -fastHandlers[20] = AssertionStringSizeLess_fast; -fastHandlers[21] = AssertionStringSizeGreater_fast; -fastHandlers[22] = AssertionArraySizeLess_fast; -fastHandlers[23] = AssertionArraySizeGreater_fast; -fastHandlers[24] = AssertionObjectSizeLess_fast; -fastHandlers[25] = AssertionObjectSizeGreater_fast; -fastHandlers[29] = AssertionGreaterEqual_fast; -fastHandlers[30] = AssertionLessEqual_fast; -fastHandlers[31] = AssertionGreater_fast; -fastHandlers[32] = AssertionLess_fast; -fastHandlers[33] = AssertionUnique_fast; -fastHandlers[34] = AssertionDivisible_fast; -fastHandlers[35] = AssertionTypeIntegerBounded_fast; -fastHandlers[36] = AssertionTypeIntegerBoundedStrict_fast; -fastHandlers[37] = AssertionTypeIntegerLowerBound_fast; -fastHandlers[38] = AssertionTypeIntegerLowerBoundStrict_fast; -fastHandlers[39] = AssertionStringType_fast; -fastHandlers[40] = AssertionPropertyType_fast; -fastHandlers[41] = AssertionPropertyTypeEvaluate_fast; -fastHandlers[43] = AssertionPropertyTypeStrictEvaluate_fast; -fastHandlers[44] = AssertionPropertyTypeStrictAny_fast; -fastHandlers[45] = AssertionPropertyTypeStrictAnyEvaluate_fast; -fastHandlers[46] = AssertionArrayPrefix_fast; -fastHandlers[47] = AssertionArrayPrefixEvaluate_fast; -fastHandlers[48] = AssertionObjectPropertiesSimple_fast; -fastHandlers[49] = AnnotationEmit_fast; -fastHandlers[50] = AnnotationToParent_fast; -fastHandlers[51] = AnnotationBasenameToParent_fast; -fastHandlers[52] = Evaluate_fast; -fastHandlers[54] = LogicalNotEvaluate_fast; -fastHandlers[61] = LogicalWhenArraySizeGreater_fast; -fastHandlers[62] = LoopPropertiesUnevaluated_fast; -fastHandlers[63] = LoopPropertiesUnevaluatedExcept_fast; -fastHandlers[67] = LoopPropertiesEvaluate_fast; -fastHandlers[68] = LoopPropertiesRegex_fast; -fastHandlers[69] = LoopPropertiesRegexClosed_fast; -fastHandlers[70] = LoopPropertiesStartsWith_fast; -fastHandlers[72] = LoopPropertiesType_fast; -fastHandlers[73] = LoopPropertiesTypeEvaluate_fast; -fastHandlers[74] = LoopPropertiesExactlyTypeStrict_fast; -fastHandlers[75] = LoopPropertiesExactlyTypeStrictHash_fast; -fastHandlers[76] = LoopPropertiesTypeStrict_fast; -fastHandlers[77] = LoopPropertiesTypeStrictEvaluate_fast; -fastHandlers[78] = LoopPropertiesTypeStrictAny_fast; -fastHandlers[79] = LoopPropertiesTypeStrictAnyEvaluate_fast; -fastHandlers[80] = LoopKeys_fast; -fastHandlers[82] = LoopItemsFrom_fast; -fastHandlers[83] = LoopItemsUnevaluated_fast; -fastHandlers[87] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[13] = AssertionNotTypeStrictAny_fast; +fastHandlers[15] = AssertionTypeStringUpper_fast; +fastHandlers[17] = AssertionTypeArrayUpper_fast; +fastHandlers[18] = AssertionTypeObjectBounded_fast; +fastHandlers[19] = AssertionTypeObjectUpper_fast; +fastHandlers[21] = AssertionStringSizeLess_fast; +fastHandlers[22] = AssertionStringSizeGreater_fast; +fastHandlers[23] = AssertionArraySizeLess_fast; +fastHandlers[24] = AssertionArraySizeGreater_fast; +fastHandlers[25] = AssertionObjectSizeLess_fast; +fastHandlers[26] = AssertionObjectSizeGreater_fast; +fastHandlers[30] = AssertionGreaterEqual_fast; +fastHandlers[31] = AssertionLessEqual_fast; +fastHandlers[32] = AssertionGreater_fast; +fastHandlers[33] = AssertionLess_fast; +fastHandlers[34] = AssertionUnique_fast; +fastHandlers[35] = AssertionDivisible_fast; +fastHandlers[36] = AssertionTypeIntegerBounded_fast; +fastHandlers[37] = AssertionTypeIntegerBoundedStrict_fast; +fastHandlers[38] = AssertionTypeIntegerLowerBound_fast; +fastHandlers[39] = AssertionTypeIntegerLowerBoundStrict_fast; +fastHandlers[40] = AssertionStringType_fast; +fastHandlers[41] = AssertionPropertyType_fast; +fastHandlers[42] = AssertionPropertyTypeEvaluate_fast; +fastHandlers[44] = AssertionPropertyTypeStrictEvaluate_fast; +fastHandlers[45] = AssertionPropertyTypeStrictAny_fast; +fastHandlers[46] = AssertionPropertyTypeStrictAnyEvaluate_fast; +fastHandlers[47] = AssertionArrayPrefix_fast; +fastHandlers[48] = AssertionArrayPrefixEvaluate_fast; +fastHandlers[49] = AssertionObjectPropertiesSimple_fast; +fastHandlers[50] = AnnotationEmit_fast; +fastHandlers[51] = AnnotationToParent_fast; +fastHandlers[52] = AnnotationBasenameToParent_fast; +fastHandlers[53] = Evaluate_fast; +fastHandlers[55] = LogicalNotEvaluate_fast; +fastHandlers[62] = LogicalWhenArraySizeGreater_fast; +fastHandlers[63] = LoopPropertiesUnevaluated_fast; +fastHandlers[64] = LoopPropertiesUnevaluatedExcept_fast; +fastHandlers[68] = LoopPropertiesEvaluate_fast; +fastHandlers[69] = LoopPropertiesRegex_fast; +fastHandlers[70] = LoopPropertiesRegexClosed_fast; +fastHandlers[71] = LoopPropertiesStartsWith_fast; +fastHandlers[73] = LoopPropertiesType_fast; +fastHandlers[74] = LoopPropertiesTypeEvaluate_fast; +fastHandlers[75] = LoopPropertiesExactlyTypeStrict_fast; +fastHandlers[76] = LoopPropertiesExactlyTypeStrictHash_fast; +fastHandlers[77] = LoopPropertiesTypeStrict_fast; +fastHandlers[78] = LoopPropertiesTypeStrictEvaluate_fast; +fastHandlers[79] = LoopPropertiesTypeStrictAny_fast; +fastHandlers[80] = LoopPropertiesTypeStrictAnyEvaluate_fast; +fastHandlers[81] = LoopKeys_fast; +fastHandlers[83] = LoopItemsFrom_fast; +fastHandlers[84] = LoopItemsUnevaluated_fast; fastHandlers[88] = LoopItemsPropertiesExactlyTypeStrictHash_fast; -fastHandlers[89] = LoopItemsIntegerBounded_fast; -fastHandlers[90] = LoopItemsIntegerBoundedSized_fast; -fastHandlers[97] = ControlDynamicAnchorJump_fast; +fastHandlers[89] = LoopItemsPropertiesExactlyTypeStrictHash_fast; +fastHandlers[90] = LoopItemsIntegerBounded_fast; +fastHandlers[91] = LoopItemsIntegerBoundedSized_fast; +fastHandlers[98] = ControlDynamicAnchorJump_fast; + +import { describe } from './describe.mjs'; + +const STANDARD_MASK_KEYWORDS = + new Set([ 'anyOf', 'oneOf', 'not', 'if', 'contains' ]); + +function isAnnotationOpcode(opcode) { + return opcode >= ANNOTATION_EMIT && opcode <= ANNOTATION_BASENAME_TO_PARENT; +} + +function lastEvaluatePathToken(evaluatePath) { + const lastSlash = evaluatePath.lastIndexOf('/'); + if (lastSlash < 0) return ''; + return evaluatePath.slice(lastSlash + 1); +} + +function isPathPrefix(path, prefix) { + if (path === prefix) return true; + return path.startsWith(prefix) && path[prefix.length] === '/'; +} + +function maskKey(evaluatePath, instanceLocation) { + return evaluatePath + '\u0000' + instanceLocation; +} + +class SimpleOutput { + constructor(instance) { + this.instance = instance; + this.errors = []; + this.mask = []; + this.maskedTraces = new Map(); + this.annotations = new Map(); + } + + callback(type, valid, instruction, evaluatePath, instanceLocation, annotation) { + if (evaluatePath === '') return; + + const opcode = instruction[0]; + const isAnnotation = isAnnotationOpcode(opcode); + const keyword = lastEvaluatePathToken(evaluatePath); + + if (valid && !isAnnotation) { + if (type === 'pre' && STANDARD_MASK_KEYWORDS.has(keyword)) { + this.mask.push({ + evaluatePath, instanceLocation, + key: maskKey(evaluatePath, instanceLocation) + }); + } else if (type === 'post' && this.mask.length > 0) { + const top = this.mask[this.mask.length - 1]; + if (top.evaluatePath === evaluatePath && + top.instanceLocation === instanceLocation) { + this.maskedTraces.delete(top.key); + this.mask.pop(); + } + } + return; + } + + if (isAnnotation) { + if (type !== 'post') return; + const annotationKey = + evaluatePath + '\u0000' + instanceLocation + '\u0000' + instruction[3]; + let bucket = this.annotations.get(annotationKey); + if (bucket === undefined) { + bucket = { + keywordLocation: evaluatePath, + absoluteKeywordLocation: instruction[3], + instanceLocation, + annotation: [ annotation ] + }; + this.annotations.set(annotationKey, bucket); + } else { + const last = bucket.annotation[bucket.annotation.length - 1]; + let isSame = last === annotation; + if (!isSame && Array.isArray(last) && Array.isArray(annotation) && + last.length === annotation.length) { + isSame = last.every((value, index) => value === annotation[index]); + } + if (!isSame) bucket.annotation.push(annotation); + } + return; + } + + if (type === 'pre') { + if (STANDARD_MASK_KEYWORDS.has(keyword)) { + this.mask.push({ + evaluatePath, instanceLocation, + key: maskKey(evaluatePath, instanceLocation) + }); + } + return; + } + + const currentKey = maskKey(evaluatePath, instanceLocation); + const matchIndex = this.mask.findIndex(entry => entry.key === currentKey); + if (matchIndex >= 0) { + if (!valid && keyword !== 'not' && keyword !== 'if') { + const buffered = this.maskedTraces.get(currentKey); + if (buffered !== undefined) { + for (const entry of buffered) this.errors.push(entry); + this.maskedTraces.delete(currentKey); + } + } else { + this.maskedTraces.delete(currentKey); + } + this.mask.splice(matchIndex, 1); + } + + if (valid) return; + + if (this.annotations.size > 0) { + const lastSlash = evaluatePath.lastIndexOf('/'); + const parentPath = lastSlash <= 0 ? '' : evaluatePath.slice(0, lastSlash); + for (const [ key, value ] of this.annotations) { + if (value.instanceLocation === instanceLocation && + (parentPath === '' || + isPathPrefix(value.keywordLocation, parentPath))) { + this.annotations.delete(key); + } + } + } + + if (keyword === 'if') return; + + const entry = { + keywordLocation: evaluatePath, + absoluteKeywordLocation: instruction[3], + instanceLocation, + error: describe(valid, instruction, evaluatePath, instanceLocation, + this.instance, annotation) + }; + + for (const maskEntry of this.mask) { + if (isPathPrefix(evaluatePath, maskEntry.evaluatePath)) { + let buffer = this.maskedTraces.get(maskEntry.key); + if (buffer === undefined) { + buffer = []; + this.maskedTraces.set(maskEntry.key, buffer); + } + buffer.push(entry); + return; + } + } + + this.errors.push(entry); + } + + toBasic(valid) { + if (valid) { + const result = { valid: true }; + if (this.annotations.size > 0) { + result.annotations = [ ...this.annotations.values() ]; + } + return result; + } + return { valid: false, errors: this.errors }; + } +} + +function runStandard(evaluator, instance, format) { + if (format === 'flag') { + return { valid: evaluator.validate(instance) }; + } + if (format !== 'basic') { + throw new Error(`Unknown standard output format: ${format}`); + } + const collector = new SimpleOutput(instance); + const valid = evaluator.validate(instance, + (type, ok, instruction, evaluatePath, instanceLocation, annotation) => + collector.callback(type, ok, instruction, evaluatePath, instanceLocation, + annotation)); + return collector.toBasic(valid); +} export { Blaze }; +export { describe } from './describe.mjs'; diff --git a/vendor/blaze/ports/javascript/opcodes.mjs b/vendor/blaze/ports/javascript/opcodes.mjs new file mode 100644 index 000000000..958382973 --- /dev/null +++ b/vendor/blaze/ports/javascript/opcodes.mjs @@ -0,0 +1,208 @@ +export const ASSERTION_FAIL = 0; +export const ASSERTION_DEFINES = 1; +export const ASSERTION_DEFINES_STRICT = 2; +export const ASSERTION_DEFINES_ALL = 3; +export const ASSERTION_DEFINES_ALL_STRICT = 4; +export const ASSERTION_DEFINES_EXACTLY = 5; +export const ASSERTION_DEFINES_EXACTLY_STRICT = 6; +export const ASSERTION_DEFINES_EXACTLY_STRICT_HASH3 = 7; +export const ASSERTION_PROPERTY_DEPENDENCIES = 8; +export const ASSERTION_TYPE = 9; +export const ASSERTION_TYPE_ANY = 10; +export const ASSERTION_TYPE_STRICT = 11; +export const ASSERTION_TYPE_STRICT_ANY = 12; +export const ASSERTION_NOT_TYPE_STRICT_ANY = 13; +export const ASSERTION_TYPE_STRING_BOUNDED = 14; +export const ASSERTION_TYPE_STRING_UPPER = 15; +export const ASSERTION_TYPE_ARRAY_BOUNDED = 16; +export const ASSERTION_TYPE_ARRAY_UPPER = 17; +export const ASSERTION_TYPE_OBJECT_BOUNDED = 18; +export const ASSERTION_TYPE_OBJECT_UPPER = 19; +export const ASSERTION_REGEX = 20; +export const ASSERTION_STRING_SIZE_LESS = 21; +export const ASSERTION_STRING_SIZE_GREATER = 22; +export const ASSERTION_ARRAY_SIZE_LESS = 23; +export const ASSERTION_ARRAY_SIZE_GREATER = 24; +export const ASSERTION_OBJECT_SIZE_LESS = 25; +export const ASSERTION_OBJECT_SIZE_GREATER = 26; +export const ASSERTION_EQUAL = 27; +export const ASSERTION_EQUALS_ANY = 28; +export const ASSERTION_EQUALS_ANY_STRING_HASH = 29; +export const ASSERTION_GREATER_EQUAL = 30; +export const ASSERTION_LESS_EQUAL = 31; +export const ASSERTION_GREATER = 32; +export const ASSERTION_LESS = 33; +export const ASSERTION_UNIQUE = 34; +export const ASSERTION_DIVISIBLE = 35; +export const ASSERTION_TYPE_INTEGER_BOUNDED = 36; +export const ASSERTION_TYPE_INTEGER_BOUNDED_STRICT = 37; +export const ASSERTION_TYPE_INTEGER_LOWER_BOUND = 38; +export const ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT = 39; +export const ASSERTION_STRING_TYPE = 40; +export const ASSERTION_PROPERTY_TYPE = 41; +export const ASSERTION_PROPERTY_TYPE_EVALUATE = 42; +export const ASSERTION_PROPERTY_TYPE_STRICT = 43; +export const ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE = 44; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY = 45; +export const ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE = 46; +export const ASSERTION_ARRAY_PREFIX = 47; +export const ASSERTION_ARRAY_PREFIX_EVALUATE = 48; +export const ASSERTION_OBJECT_PROPERTIES_SIMPLE = 49; +export const ANNOTATION_EMIT = 50; +export const ANNOTATION_TO_PARENT = 51; +export const ANNOTATION_BASENAME_TO_PARENT = 52; +export const EVALUATE = 53; +export const LOGICAL_NOT = 54; +export const LOGICAL_NOT_EVALUATE = 55; +export const LOGICAL_OR = 56; +export const LOGICAL_AND = 57; +export const LOGICAL_XOR = 58; +export const LOGICAL_CONDITION = 59; +export const LOGICAL_WHEN_TYPE = 60; +export const LOGICAL_WHEN_DEFINES = 61; +export const LOGICAL_WHEN_ARRAY_SIZE_GREATER = 62; +export const LOOP_PROPERTIES_UNEVALUATED = 63; +export const LOOP_PROPERTIES_UNEVALUATED_EXCEPT = 64; +export const LOOP_PROPERTIES_MATCH = 65; +export const LOOP_PROPERTIES_MATCH_CLOSED = 66; +export const LOOP_PROPERTIES = 67; +export const LOOP_PROPERTIES_EVALUATE = 68; +export const LOOP_PROPERTIES_REGEX = 69; +export const LOOP_PROPERTIES_REGEX_CLOSED = 70; +export const LOOP_PROPERTIES_STARTS_WITH = 71; +export const LOOP_PROPERTIES_EXCEPT = 72; +export const LOOP_PROPERTIES_TYPE = 73; +export const LOOP_PROPERTIES_TYPE_EVALUATE = 74; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT = 75; +export const LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 76; +export const LOOP_PROPERTIES_TYPE_STRICT = 77; +export const LOOP_PROPERTIES_TYPE_STRICT_EVALUATE = 78; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY = 79; +export const LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE = 80; +export const LOOP_KEYS = 81; +export const LOOP_ITEMS = 82; +export const LOOP_ITEMS_FROM = 83; +export const LOOP_ITEMS_UNEVALUATED = 84; +export const LOOP_ITEMS_TYPE = 85; +export const LOOP_ITEMS_TYPE_STRICT = 86; +export const LOOP_ITEMS_TYPE_STRICT_ANY = 87; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH = 88; +export const LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3 = 89; +export const LOOP_ITEMS_INTEGER_BOUNDED = 90; +export const LOOP_ITEMS_INTEGER_BOUNDED_SIZED = 91; +export const LOOP_CONTAINS = 92; +export const CONTROL_GROUP = 93; +export const CONTROL_GROUP_WHEN_DEFINES = 94; +export const CONTROL_GROUP_WHEN_DEFINES_DIRECT = 95; +export const CONTROL_GROUP_WHEN_TYPE = 96; +export const CONTROL_EVALUATE = 97; +export const CONTROL_DYNAMIC_ANCHOR_JUMP = 98; +export const CONTROL_JUMP = 99; + +export const INSTRUCTION_NAMES = { + "AssertionFail": ASSERTION_FAIL, + "AssertionDefines": ASSERTION_DEFINES, + "AssertionDefinesStrict": ASSERTION_DEFINES_STRICT, + "AssertionDefinesAll": ASSERTION_DEFINES_ALL, + "AssertionDefinesAllStrict": ASSERTION_DEFINES_ALL_STRICT, + "AssertionDefinesExactly": ASSERTION_DEFINES_EXACTLY, + "AssertionDefinesExactlyStrict": ASSERTION_DEFINES_EXACTLY_STRICT, + "AssertionDefinesExactlyStrictHash3": ASSERTION_DEFINES_EXACTLY_STRICT_HASH3, + "AssertionPropertyDependencies": ASSERTION_PROPERTY_DEPENDENCIES, + "AssertionType": ASSERTION_TYPE, + "AssertionTypeAny": ASSERTION_TYPE_ANY, + "AssertionTypeStrict": ASSERTION_TYPE_STRICT, + "AssertionTypeStrictAny": ASSERTION_TYPE_STRICT_ANY, + "AssertionNotTypeStrictAny": ASSERTION_NOT_TYPE_STRICT_ANY, + "AssertionTypeStringBounded": ASSERTION_TYPE_STRING_BOUNDED, + "AssertionTypeStringUpper": ASSERTION_TYPE_STRING_UPPER, + "AssertionTypeArrayBounded": ASSERTION_TYPE_ARRAY_BOUNDED, + "AssertionTypeArrayUpper": ASSERTION_TYPE_ARRAY_UPPER, + "AssertionTypeObjectBounded": ASSERTION_TYPE_OBJECT_BOUNDED, + "AssertionTypeObjectUpper": ASSERTION_TYPE_OBJECT_UPPER, + "AssertionRegex": ASSERTION_REGEX, + "AssertionStringSizeLess": ASSERTION_STRING_SIZE_LESS, + "AssertionStringSizeGreater": ASSERTION_STRING_SIZE_GREATER, + "AssertionArraySizeLess": ASSERTION_ARRAY_SIZE_LESS, + "AssertionArraySizeGreater": ASSERTION_ARRAY_SIZE_GREATER, + "AssertionObjectSizeLess": ASSERTION_OBJECT_SIZE_LESS, + "AssertionObjectSizeGreater": ASSERTION_OBJECT_SIZE_GREATER, + "AssertionEqual": ASSERTION_EQUAL, + "AssertionEqualsAny": ASSERTION_EQUALS_ANY, + "AssertionEqualsAnyStringHash": ASSERTION_EQUALS_ANY_STRING_HASH, + "AssertionGreaterEqual": ASSERTION_GREATER_EQUAL, + "AssertionLessEqual": ASSERTION_LESS_EQUAL, + "AssertionGreater": ASSERTION_GREATER, + "AssertionLess": ASSERTION_LESS, + "AssertionUnique": ASSERTION_UNIQUE, + "AssertionDivisible": ASSERTION_DIVISIBLE, + "AssertionTypeIntegerBounded": ASSERTION_TYPE_INTEGER_BOUNDED, + "AssertionTypeIntegerBoundedStrict": ASSERTION_TYPE_INTEGER_BOUNDED_STRICT, + "AssertionTypeIntegerLowerBound": ASSERTION_TYPE_INTEGER_LOWER_BOUND, + "AssertionTypeIntegerLowerBoundStrict": ASSERTION_TYPE_INTEGER_LOWER_BOUND_STRICT, + "AssertionStringType": ASSERTION_STRING_TYPE, + "AssertionPropertyType": ASSERTION_PROPERTY_TYPE, + "AssertionPropertyTypeEvaluate": ASSERTION_PROPERTY_TYPE_EVALUATE, + "AssertionPropertyTypeStrict": ASSERTION_PROPERTY_TYPE_STRICT, + "AssertionPropertyTypeStrictEvaluate": ASSERTION_PROPERTY_TYPE_STRICT_EVALUATE, + "AssertionPropertyTypeStrictAny": ASSERTION_PROPERTY_TYPE_STRICT_ANY, + "AssertionPropertyTypeStrictAnyEvaluate": ASSERTION_PROPERTY_TYPE_STRICT_ANY_EVALUATE, + "AssertionArrayPrefix": ASSERTION_ARRAY_PREFIX, + "AssertionArrayPrefixEvaluate": ASSERTION_ARRAY_PREFIX_EVALUATE, + "AssertionObjectPropertiesSimple": ASSERTION_OBJECT_PROPERTIES_SIMPLE, + "AnnotationEmit": ANNOTATION_EMIT, + "AnnotationToParent": ANNOTATION_TO_PARENT, + "AnnotationBasenameToParent": ANNOTATION_BASENAME_TO_PARENT, + "Evaluate": EVALUATE, + "LogicalNot": LOGICAL_NOT, + "LogicalNotEvaluate": LOGICAL_NOT_EVALUATE, + "LogicalOr": LOGICAL_OR, + "LogicalAnd": LOGICAL_AND, + "LogicalXor": LOGICAL_XOR, + "LogicalCondition": LOGICAL_CONDITION, + "LogicalWhenType": LOGICAL_WHEN_TYPE, + "LogicalWhenDefines": LOGICAL_WHEN_DEFINES, + "LogicalWhenArraySizeGreater": LOGICAL_WHEN_ARRAY_SIZE_GREATER, + "LoopPropertiesUnevaluated": LOOP_PROPERTIES_UNEVALUATED, + "LoopPropertiesUnevaluatedExcept": LOOP_PROPERTIES_UNEVALUATED_EXCEPT, + "LoopPropertiesMatch": LOOP_PROPERTIES_MATCH, + "LoopPropertiesMatchClosed": LOOP_PROPERTIES_MATCH_CLOSED, + "LoopProperties": LOOP_PROPERTIES, + "LoopPropertiesEvaluate": LOOP_PROPERTIES_EVALUATE, + "LoopPropertiesRegex": LOOP_PROPERTIES_REGEX, + "LoopPropertiesRegexClosed": LOOP_PROPERTIES_REGEX_CLOSED, + "LoopPropertiesStartsWith": LOOP_PROPERTIES_STARTS_WITH, + "LoopPropertiesExcept": LOOP_PROPERTIES_EXCEPT, + "LoopPropertiesType": LOOP_PROPERTIES_TYPE, + "LoopPropertiesTypeEvaluate": LOOP_PROPERTIES_TYPE_EVALUATE, + "LoopPropertiesExactlyTypeStrict": LOOP_PROPERTIES_EXACTLY_TYPE_STRICT, + "LoopPropertiesExactlyTypeStrictHash": LOOP_PROPERTIES_EXACTLY_TYPE_STRICT_HASH, + "LoopPropertiesTypeStrict": LOOP_PROPERTIES_TYPE_STRICT, + "LoopPropertiesTypeStrictEvaluate": LOOP_PROPERTIES_TYPE_STRICT_EVALUATE, + "LoopPropertiesTypeStrictAny": LOOP_PROPERTIES_TYPE_STRICT_ANY, + "LoopPropertiesTypeStrictAnyEvaluate": LOOP_PROPERTIES_TYPE_STRICT_ANY_EVALUATE, + "LoopKeys": LOOP_KEYS, + "LoopItems": LOOP_ITEMS, + "LoopItemsFrom": LOOP_ITEMS_FROM, + "LoopItemsUnevaluated": LOOP_ITEMS_UNEVALUATED, + "LoopItemsType": LOOP_ITEMS_TYPE, + "LoopItemsTypeStrict": LOOP_ITEMS_TYPE_STRICT, + "LoopItemsTypeStrictAny": LOOP_ITEMS_TYPE_STRICT_ANY, + "LoopItemsPropertiesExactlyTypeStrictHash": LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH, + "LoopItemsPropertiesExactlyTypeStrictHash3": LOOP_ITEMS_PROPERTIES_EXACTLY_TYPE_STRICT_HASH3, + "LoopItemsIntegerBounded": LOOP_ITEMS_INTEGER_BOUNDED, + "LoopItemsIntegerBoundedSized": LOOP_ITEMS_INTEGER_BOUNDED_SIZED, + "LoopContains": LOOP_CONTAINS, + "ControlGroup": CONTROL_GROUP, + "ControlGroupWhenDefines": CONTROL_GROUP_WHEN_DEFINES, + "ControlGroupWhenDefinesDirect": CONTROL_GROUP_WHEN_DEFINES_DIRECT, + "ControlGroupWhenType": CONTROL_GROUP_WHEN_TYPE, + "ControlEvaluate": CONTROL_EVALUATE, + "ControlDynamicAnchorJump": CONTROL_DYNAMIC_ANCHOR_JUMP, + "ControlJump": CONTROL_JUMP, + "Annotation": -1 +}; + +export const ANNOTATION_OPCODES = new Set([ + ANNOTATION_EMIT, ANNOTATION_TO_PARENT, ANNOTATION_BASENAME_TO_PARENT +]); diff --git a/vendor/blaze/ports/javascript/package.json b/vendor/blaze/ports/javascript/package.json index 56183a211..2fa58a6b7 100644 --- a/vendor/blaze/ports/javascript/package.json +++ b/vendor/blaze/ports/javascript/package.json @@ -20,7 +20,7 @@ "url": "https://www.sourcemeta.com" }, "engines": { - "node": ">=18" + "node": ">=21.7" }, "funding": "https://github.com/sponsors/sourcemeta", "keywords": [ @@ -66,6 +66,8 @@ "files": [ "index.mjs", "index.d.mts", + "opcodes.mjs", + "describe.mjs", "README.md", "LICENSE" ] diff --git a/vendor/blaze/schemas/documentation.json b/vendor/blaze/schemas/documentation.json new file mode 100644 index 000000000..f99b2aa7a --- /dev/null +++ b/vendor/blaze/schemas/documentation.json @@ -0,0 +1,368 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Blaze Documentation Format", + "description": "The JSON representation of a documentation table", + "examples": [ + { + "identifier": 0, + "rows": [ + { + "identifier": 1, + "path": [ + { + "type": "synthetic", + "value": "root" + } + ], + "type": { + "kind": "primitive", + "name": "string" + } + } + ] + } + ], + "$ref": "#/$defs/table", + "$defs": { + "table": { + "type": "object", + "required": [ "identifier", "rows" ], + "properties": { + "identifier": { + "description": "A unique monotonic identifier for this table", + "type": "integer", + "minimum": 0 + }, + "title": { + "description": "The title annotation from the source schema branch", + "type": "string", + "minLength": 1 + }, + "dynamicAnchor": { + "description": "The dynamic anchor value declared by this schema", + "type": "string", + "minLength": 1 + }, + "rows": { + "type": "array", + "uniqueItems": true, + "items": { + "$ref": "#/$defs/row" + } + }, + "children": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/section" + } + } + }, + "additionalProperties": false + }, + "row": { + "type": "object", + "required": [ "identifier", "path", "type" ], + "properties": { + "identifier": { + "description": "A unique monotonic identifier for this row", + "type": "integer", + "minimum": 0 + }, + "path": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pathSegment" + } + }, + "modifiers": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "enum": [ "readOnly", "writeOnly", "deprecated" ] + } + }, + "type": { + "$ref": "#/$defs/typeExpression" + }, + "badges": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/badge" + } + }, + "required": { + "type": "boolean" + }, + "constraints": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1 + } + }, + "title": { + "description": "The title annotation for this property", + "type": "string", + "minLength": 1 + }, + "description": { + "description": "The description annotation for this property", + "type": "string", + "minLength": 1 + }, + "default": { + "description": "The default value annotation for this property. Any JSON value is valid, including null" + }, + "examples": { + "description": "Example values for this property", + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "children": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/section" + } + } + }, + "additionalProperties": false + }, + "section": { + "type": "object", + "required": [ "label", "children" ], + "properties": { + "label": { + "anyOf": [ + { + "enum": [ + "Any of", + "All of", + "One of", + "Property names", + "Contains", + "Decoded content", + "Must NOT match", + "If", + "Then", + "Else" + ] + }, + { + "type": "string", + "pattern": "^Array item \\d+$" + } + ] + }, + "position": { + "description": "The JSON Pointer of the operator within the parent schema", + "type": "string", + "minLength": 1 + }, + "children": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/table" + } + } + }, + "additionalProperties": false + }, + "pathSegment": { + "type": "object", + "required": [ "type", "value" ], + "properties": { + "type": { + "enum": [ "literal", "pattern", "wildcard", "synthetic" ] + }, + "value": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "badge": { + "type": "object", + "required": [ "kind", "value" ], + "properties": { + "kind": { + "enum": [ "format", "encoding", "mime" ] + }, + "value": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + "typeExpression": { + "anyOf": [ + { + "type": "object", + "required": [ "kind" ], + "properties": { + "kind": { + "const": "object" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "name" ], + "properties": { + "kind": { + "const": "primitive" + }, + "name": { + "enum": [ "string", "integer", "number" ] + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind" ], + "properties": { + "kind": { + "const": "array" + }, + "items": { + "description": "The homogeneous item type. Absent when the array has no items constraint", + "$ref": "#/$defs/typeExpression" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "items" ], + "properties": { + "kind": { + "const": "tuple" + }, + "items": { + "type": "array", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/typeExpression" + } + }, + "additional": { + "description": "The open tail type. Absent when the tuple has no tail", + "$ref": "#/$defs/typeExpression" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "values" ], + "properties": { + "kind": { + "const": "enum" + }, + "values": { + "description": "The visible enum values (first 10 by default)", + "type": "array", + "minItems": 1, + "uniqueItems": true + }, + "overflow": { + "description": "The remaining enum values beyond the visible portion", + "type": "array", + "minItems": 1, + "uniqueItems": true + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "url" ], + "properties": { + "kind": { + "const": "externalRef" + }, + "url": { + "type": "string", + "format": "uri", + "minLength": 1 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "identifier", "path" ], + "properties": { + "kind": { + "const": "recursiveRef" + }, + "identifier": { + "description": "The identifier of the target table in the same documentation tree", + "type": "integer", + "minimum": 0 + }, + "path": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/$defs/pathSegment" + } + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind", "anchor" ], + "properties": { + "kind": { + "const": "dynamicRef" + }, + "anchor": { + "type": "string", + "minLength": 1 + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind" ], + "properties": { + "kind": { + "const": "any" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ "kind" ], + "properties": { + "kind": { + "const": "never" + } + }, + "additionalProperties": false + } + ] + } + } +} diff --git a/vendor/blaze/src/alterschema/CMakeLists.txt b/vendor/blaze/src/alterschema/CMakeLists.txt index db807d2b4..0a57b8495 100644 --- a/vendor/blaze/src/alterschema/CMakeLists.txt +++ b/vendor/blaze/src/alterschema/CMakeLists.txt @@ -11,6 +11,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/dependent_required_to_any_of.h canonicalizer/dependent_schemas_to_any_of.h canonicalizer/deprecated_false_drop.h + canonicalizer/draft3_type_any.h canonicalizer/disallow_to_array_of_schemas.h canonicalizer/divisible_by_implicit.h canonicalizer/empty_definitions_drop.h @@ -20,7 +21,6 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema canonicalizer/empty_dependent_schemas_drop.h canonicalizer/enum_drop_redundant_validation.h canonicalizer/enum_filter_by_type.h - canonicalizer/exclusive_bounds_false_drop.h canonicalizer/exclusive_maximum_boolean_integer_fold.h canonicalizer/exclusive_maximum_integer_to_maximum.h canonicalizer/exclusive_minimum_boolean_integer_fold.h @@ -72,14 +72,17 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema common/content_schema_without_media_type.h common/dependencies_property_tautology.h common/dependent_required_tautology.h + common/disallow_narrows_type.h common/double_negation_elimination.h common/draft_official_dialect_with_https.h common/draft_official_dialect_without_empty_fragment.h common/draft_ref_siblings.h common/drop_allof_empty_schemas.h + common/drop_extends_empty_schemas.h common/duplicate_allof_branches.h common/flatten_nested_allof.h common/flatten_nested_anyof.h + common/flatten_nested_extends.h common/duplicate_anyof_branches.h common/dynamic_ref_to_static_ref.h common/duplicate_enum_values.h @@ -88,6 +91,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema common/else_without_if.h common/enum_with_type.h common/equal_numeric_bounds_to_enum.h + common/exclusive_bounds_false_drop.h common/exclusive_maximum_number_and_maximum.h common/exclusive_minimum_number_and_minimum.h common/if_without_then_else.h @@ -99,6 +103,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema common/modern_official_dialect_with_empty_fragment.h common/modern_official_dialect_with_http.h common/non_applicable_additional_items.h + common/non_applicable_disallow_types.h common/non_applicable_enum_validation_keywords.h common/non_applicable_type_specific_keywords.h common/not_false.h @@ -111,6 +116,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema common/unknown_local_ref.h common/unsatisfiable_drop_validation.h common/unnecessary_allof_ref_wrapper_draft.h + common/unnecessary_extends_ref_wrapper.h common/unsatisfiable_in_place_applicator_type.h # Linter @@ -122,6 +128,8 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema linter/dependent_required_default.h linter/description_trailing_period.h linter/description_trim.h + linter/disallow_default.h + linter/divisible_by_default.h linter/duplicate_examples.h linter/else_empty.h linter/enum_to_const.h @@ -133,6 +141,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema linter/items_schema_default.h linter/multiple_of_default.h linter/pattern_properties_default.h + linter/portable_anchor_names.h linter/properties_default.h linter/property_names_default.h linter/property_names_type_default.h @@ -148,6 +157,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME alterschema linter/unevaluated_properties_default.h linter/unnecessary_allof_ref_wrapper_modern.h linter/unnecessary_allof_wrapper.h + linter/unnecessary_extends_wrapper.h linter/unsatisfiable_max_contains.h linter/unsatisfiable_min_properties.h linter/valid_default.h diff --git a/vendor/blaze/src/alterschema/alterschema.cc b/vendor/blaze/src/alterschema/alterschema.cc index 00615b538..6f5da5b0f 100644 --- a/vendor/blaze/src/alterschema/alterschema.cc +++ b/vendor/blaze/src/alterschema/alterschema.cc @@ -3,6 +3,7 @@ #include #include #include +#include // For built-in rules #include // std::sort, std::unique, std::ranges::none_of @@ -108,6 +109,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, } #include "canonicalizer/additional_items_implicit.h" +#include "canonicalizer/allof_merge_compatible_branches.h" #include "canonicalizer/comment_drop.h" #include "canonicalizer/const_as_enum.h" #include "canonicalizer/dependencies_to_any_of.h" @@ -117,6 +119,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/deprecated_false_drop.h" #include "canonicalizer/disallow_to_array_of_schemas.h" #include "canonicalizer/divisible_by_implicit.h" +#include "canonicalizer/draft3_type_any.h" #include "canonicalizer/empty_definitions_drop.h" #include "canonicalizer/empty_defs_drop.h" #include "canonicalizer/empty_dependencies_drop.h" @@ -124,7 +127,6 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/empty_dependent_schemas_drop.h" #include "canonicalizer/enum_drop_redundant_validation.h" #include "canonicalizer/enum_filter_by_type.h" -#include "canonicalizer/exclusive_bounds_false_drop.h" #include "canonicalizer/exclusive_maximum_boolean_integer_fold.h" #include "canonicalizer/exclusive_maximum_integer_to_maximum.h" #include "canonicalizer/exclusive_minimum_boolean_integer_fold.h" @@ -133,6 +135,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "canonicalizer/if_then_else_implicit.h" #include "canonicalizer/implicit_contains_keywords.h" #include "canonicalizer/implicit_object_keywords.h" +#include "canonicalizer/inline_single_use_ref.h" #include "canonicalizer/items_implicit.h" #include "canonicalizer/max_contains_covered_by_max_items.h" #include "canonicalizer/max_decimal_implicit.h" @@ -175,11 +178,13 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "common/content_schema_without_media_type.h" #include "common/dependencies_property_tautology.h" #include "common/dependent_required_tautology.h" +#include "common/disallow_narrows_type.h" #include "common/double_negation_elimination.h" #include "common/draft_official_dialect_with_https.h" #include "common/draft_official_dialect_without_empty_fragment.h" #include "common/draft_ref_siblings.h" #include "common/drop_allof_empty_schemas.h" +#include "common/drop_extends_empty_schemas.h" #include "common/duplicate_allof_branches.h" #include "common/duplicate_anyof_branches.h" #include "common/duplicate_enum_values.h" @@ -189,10 +194,12 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "common/empty_object_as_true.h" #include "common/enum_with_type.h" #include "common/equal_numeric_bounds_to_enum.h" +#include "common/exclusive_bounds_false_drop.h" #include "common/exclusive_maximum_number_and_maximum.h" #include "common/exclusive_minimum_number_and_minimum.h" #include "common/flatten_nested_allof.h" #include "common/flatten_nested_anyof.h" +#include "common/flatten_nested_extends.h" #include "common/if_without_then_else.h" #include "common/ignored_metaschema.h" #include "common/max_contains_without_contains.h" @@ -202,6 +209,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "common/modern_official_dialect_with_empty_fragment.h" #include "common/modern_official_dialect_with_http.h" #include "common/non_applicable_additional_items.h" +#include "common/non_applicable_disallow_types.h" #include "common/non_applicable_enum_validation_keywords.h" #include "common/non_applicable_type_specific_keywords.h" #include "common/not_false.h" @@ -214,12 +222,14 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "common/unknown_keywords_prefix.h" #include "common/unknown_local_ref.h" #include "common/unnecessary_allof_ref_wrapper_draft.h" +#include "common/unnecessary_extends_ref_wrapper.h" #include "common/unsatisfiable_drop_validation.h" #include "common/unsatisfiable_in_place_applicator_type.h" #include "linter/else_empty.h" #include "linter/then_empty.h" #include "linter/unnecessary_allof_ref_wrapper_modern.h" #include "linter/unnecessary_allof_wrapper.h" +#include "linter/unnecessary_extends_wrapper.h" // Linter #include "linter/comment_trim.h" @@ -230,6 +240,8 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "linter/dependent_required_default.h" #include "linter/description_trailing_period.h" #include "linter/description_trim.h" +#include "linter/disallow_default.h" +#include "linter/divisible_by_default.h" #include "linter/duplicate_examples.h" #include "linter/enum_to_const.h" #include "linter/equal_numeric_bounds_to_const.h" @@ -240,6 +252,7 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "linter/items_schema_default.h" #include "linter/multiple_of_default.h" #include "linter/pattern_properties_default.h" +#include "linter/portable_anchor_names.h" #include "linter/properties_default.h" #include "linter/property_names_default.h" #include "linter/property_names_type_default.h" @@ -252,21 +265,79 @@ auto WALK_UP_IN_PLACE_APPLICATORS(const JSON &root, const SchemaFrame &frame, #include "linter/top_level_title.h" #include "linter/unevaluated_items_default.h" #include "linter/unevaluated_properties_default.h" +#include "linter/unknown_format_prefix.h" #include "linter/unsatisfiable_max_contains.h" #include "linter/unsatisfiable_min_properties.h" #include "linter/valid_default.h" #include "linter/valid_examples.h" +// Upgrade +#include "upgrade/helpers.h" +#include "upgrade/prefix_promoted_2020_12_keywords.h" +#include "upgrade/prefix_promoted_draft_2019_09_keywords.h" +#include "upgrade/prefix_promoted_draft_4_keywords.h" +#include "upgrade/prefix_promoted_draft_6_keywords.h" +#include "upgrade/prefix_promoted_draft_7_keywords.h" +#include "upgrade/upgrade_2019_09_to_2020_12.h" +#include "upgrade/upgrade_dialect_override_cleanup.h" +#include "upgrade/upgrade_draft_3_to_draft_4.h" +#include "upgrade/upgrade_draft_4_to_draft_6.h" +#include "upgrade/upgrade_draft_6_to_draft_7.h" +#include "upgrade/upgrade_draft_7_to_draft_2019_09.h" + #undef ONLY_CONTINUE_IF } // namespace sourcemeta::blaze namespace sourcemeta::blaze { auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { + if (mode == AlterSchemaMode::UpgradeDraft4 || + mode == AlterSchemaMode::UpgradeDraft6 || + mode == AlterSchemaMode::UpgradeDraft7 || + mode == AlterSchemaMode::Upgrade201909 || + mode == AlterSchemaMode::Upgrade202012) { + bundle.add(); + bundle.add(); + bundle.add(); + bundle.add(); + + if (mode == AlterSchemaMode::UpgradeDraft6 || + mode == AlterSchemaMode::UpgradeDraft7 || + mode == AlterSchemaMode::Upgrade201909 || + mode == AlterSchemaMode::Upgrade202012) { + bundle.add(); + bundle.add(); + bundle.add(); + } + + if (mode == AlterSchemaMode::UpgradeDraft7 || + mode == AlterSchemaMode::Upgrade201909 || + mode == AlterSchemaMode::Upgrade202012) { + bundle.add(); + bundle.add(); + bundle.add(); + } + + if (mode == AlterSchemaMode::Upgrade201909 || + mode == AlterSchemaMode::Upgrade202012) { + bundle.add(); + bundle.add(); + bundle.add(); + } + + if (mode == AlterSchemaMode::Upgrade202012) { + bundle.add(); + bundle.add(); + } + + bundle.add(); + + return; + } + if (mode == AlterSchemaMode::Canonicalizer) { bundle.add(); bundle.add(); - bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -285,6 +356,8 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { } if (mode == AlterSchemaMode::Canonicalizer) { + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -300,12 +373,18 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); + if (mode == AlterSchemaMode::Canonicalizer) { + bundle.add(); + } bundle.add(); bundle.add(); bundle.add(); @@ -341,6 +420,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -373,6 +453,8 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -396,7 +478,9 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); bundle.add(); + bundle.add(); bundle.add(); bundle.add(); } @@ -405,12 +489,15 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); } bundle.add(); + bundle.add(); if (mode != AlterSchemaMode::Canonicalizer) { bundle.add(); + bundle.add(); } bundle.add(); + bundle.add(); bundle.add(); if (mode == AlterSchemaMode::Canonicalizer) { diff --git a/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h index 154a58743..320c99081 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/additional_items_implicit.h @@ -19,10 +19,13 @@ class AdditionalItemsImplicit final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "array" && schema.defines("items") && - schema.at("items").is_array() && !schema.defines("additionalItems")); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "array"); + const auto *items{schema.try_at("items")}; + ONLY_CONTINUE_IF(items && items->is_array() && + !schema.defines("additionalItems")); this->is_draft3_ = vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3); return true; diff --git a/vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h b/vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h new file mode 100644 index 000000000..ec26664c6 --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/allof_merge_compatible_branches.h @@ -0,0 +1,172 @@ +class AllOfMergeCompatibleBranches final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + AllOfMergeCompatibleBranches() + : SchemaTransformRule{"allof_merge_compatible_branches", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"allOf"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *all_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(all_of && all_of->is_array() && all_of->size() >= 2); + + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + + const auto &branches{*all_of}; + + for (std::size_t index_a = 0; index_a < branches.size(); ++index_a) { + const auto &branch_a{branches.at(index_a)}; + if (!is_mergeable_branch(branch_a)) { + continue; + } + + for (std::size_t index_b = index_a + 1; index_b < branches.size(); + ++index_b) { + const auto &branch_b{branches.at(index_b)}; + if (!is_mergeable_branch(branch_b)) { + continue; + } + + const auto a_is_type_only{is_type_only_branch(branch_a)}; + const auto b_is_type_only{is_type_only_branch(branch_b)}; + if (!a_is_type_only && !b_is_type_only) { + continue; + } + + const auto &non_type_branch{a_is_type_only ? branch_b : branch_a}; + if (has_in_place_applicators(non_type_branch)) { + continue; + } + + if (has_overlapping_keywords(branch_a, branch_b)) { + continue; + } + + if (has_cross_dependencies(branch_a, branch_b, walker, vocabularies)) { + continue; + } + + this->merge_into_ = index_a; + this->merge_from_ = index_b; + return true; + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + static const JSON::String KEYWORD{"allOf"}; + auto &target{schema.at(KEYWORD).at(this->merge_into_)}; + const auto &source{schema.at(KEYWORD).at(this->merge_from_)}; + target.merge(source.as_object()); + schema.at(KEYWORD).erase( + std::next(schema.at(KEYWORD).as_array().begin(), + static_cast(this->merge_from_))); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + static const JSON::String KEYWORD{"allOf"}; + const auto relative{target.resolve_from(current)}; + if (relative.size() < 2 || !relative.at(0).is_property() || + relative.at(0).to_property() != KEYWORD || !relative.at(1).is_index()) { + return target; + } + + const auto index{relative.at(1).to_index()}; + if (index == this->merge_from_) { + const Pointer old_prefix{current.concat({KEYWORD, this->merge_from_})}; + const Pointer new_prefix{current.concat({KEYWORD, this->merge_into_})}; + return target.rebase(old_prefix, new_prefix); + } + + if (index > this->merge_from_) { + const Pointer old_prefix{current.concat({KEYWORD, index})}; + const Pointer new_prefix{current.concat({KEYWORD, index - 1})}; + return target.rebase(old_prefix, new_prefix); + } + + return target; + } + +private: + static auto is_type_only_branch(const JSON &branch) -> bool { + return branch.size() == 1 && branch.defines("type"); + } + + static auto has_in_place_applicators(const JSON &branch) -> bool { + return branch.defines("anyOf") || branch.defines("oneOf") || + branch.defines("allOf") || branch.defines("not") || + branch.defines("if"); + } + + static auto is_mergeable_branch(const JSON &branch) -> bool { + if (!branch.is_object()) { + return false; + } + return !branch.defines("$ref") && !branch.defines("$dynamicRef") && + !branch.defines("$recursiveRef") && !branch.defines("$id") && + !branch.defines("$schema") && !branch.defines("id") && + !branch.defines("$anchor") && !branch.defines("$dynamicAnchor") && + !branch.defines("$recursiveAnchor"); + } + + static auto has_overlapping_keywords(const JSON &branch_a, + const JSON &branch_b) -> bool { + for (const auto &entry : branch_a.as_object()) { + if (branch_b.defines(entry.first)) { + return true; + } + } + return false; + } + + static auto + has_cross_dependencies(const JSON &branch_a, const JSON &branch_b, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::Vocabularies &vocabularies) + -> bool { + for (const auto &entry_a : branch_a.as_object()) { + const auto &metadata{walker(entry_a.first, vocabularies)}; + for (const auto &dependency : metadata.dependencies) { + if (branch_b.defines(JSON::String{dependency})) { + return true; + } + } + } + + for (const auto &entry_b : branch_b.as_object()) { + const auto &metadata{walker(entry_b.first, vocabularies)}; + for (const auto &dependency : metadata.dependencies) { + if (branch_a.defines(JSON::String{dependency})) { + return true; + } + } + } + + return false; + } + + mutable std::size_t merge_into_{0}; + mutable std::size_t merge_from_{0}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h index 655c738ce..ebd0a3154 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_any_of.h @@ -17,11 +17,13 @@ class DependenciesToAnyOf final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines("dependencies") && - schema.at("dependencies").is_object()); + schema.is_object()); - ONLY_CONTINUE_IF(std::ranges::any_of( - schema.at("dependencies").as_object(), [](const auto &entry) { + const auto *dependencies{schema.try_at("dependencies")}; + ONLY_CONTINUE_IF(dependencies && dependencies->is_object()); + + ONLY_CONTINUE_IF( + std::ranges::any_of(dependencies->as_object(), [](const auto &entry) { return is_schema(entry.second) || entry.second.is_array(); })); return true; diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h b/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h index 98e4f921b..2c36d9348 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependencies_to_extends_disallow.h @@ -16,11 +16,13 @@ class DependenciesToExtendsDisallow final : public SchemaTransformRule { -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && - schema.is_object() && schema.defines("dependencies") && - schema.at("dependencies").is_object()); + schema.is_object()); - ONLY_CONTINUE_IF(std::ranges::any_of( - schema.at("dependencies").as_object(), [](const auto &entry) { + const auto *dependencies{schema.try_at("dependencies")}; + ONLY_CONTINUE_IF(dependencies && dependencies->is_object()); + + ONLY_CONTINUE_IF( + std::ranges::any_of(dependencies->as_object(), [](const auto &entry) { return is_schema(entry.second) || entry.second.is_array() || entry.second.is_string(); })); diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h index 4a71d56aa..a98ffe5af 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_required_to_any_of.h @@ -18,12 +18,14 @@ class DependentRequiredToAnyOf final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - schema.is_object() && schema.defines("dependentRequired") && - schema.at("dependentRequired").is_object() && - !schema.at("dependentRequired").empty()); + schema.is_object()); + + const auto *dependent_required{schema.try_at("dependentRequired")}; + ONLY_CONTINUE_IF(dependent_required && dependent_required->is_object() && + !dependent_required->empty()); ONLY_CONTINUE_IF(std::ranges::any_of( - schema.at("dependentRequired").as_object(), + dependent_required->as_object(), [](const auto &entry) { return entry.second.is_array(); })); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h index 9dabcda96..61f7953b6 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/dependent_schemas_to_any_of.h @@ -18,9 +18,11 @@ class DependentSchemasToAnyOf final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_2020_12_Applicator}) && - schema.is_object() && schema.defines("dependentSchemas") && - schema.at("dependentSchemas").is_object() && - !schema.at("dependentSchemas").empty()); + schema.is_object()); + + const auto *dependent_schemas{schema.try_at("dependentSchemas")}; + ONLY_CONTINUE_IF(dependent_schemas && dependent_schemas->is_object() && + !dependent_schemas->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h b/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h index 1bee8c02a..d692d24af 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/deprecated_false_drop.h @@ -17,9 +17,11 @@ class DeprecatedFalseDrop final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Meta_Data, Vocabularies::Known::JSON_Schema_2020_12_Meta_Data}) && - schema.is_object() && schema.defines("deprecated") && - schema.at("deprecated").is_boolean() && - !schema.at("deprecated").to_boolean()); + schema.is_object()); + + const auto *deprecated{schema.try_at("deprecated")}; + ONLY_CONTINUE_IF(deprecated && deprecated->is_boolean() && + !deprecated->to_boolean()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h index 3d0fa521c..507b82c76 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/divisible_by_implicit.h @@ -16,10 +16,11 @@ class DivisibleByImplicit final : public SchemaTransformRule { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - !schema.defines("divisibleBy")); + schema.is_object() && !schema.defines("divisibleBy")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h b/vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h new file mode 100644 index 000000000..dea04abec --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/draft3_type_any.h @@ -0,0 +1,53 @@ +class Draft3TypeAny final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Draft3TypeAny() : SchemaTransformRule{"draft3_type_any", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type); + + if (type->is_string()) { + return type->to_string() == "any"; + } + + if (type->is_array()) { + for (const auto &element : type->as_array()) { + if (element.is_string() && element.to_string() == "any") { + return true; + } + if (element.is_object()) { + if (element.empty()) { + return true; + } + if (element.size() == 1) { + const auto *element_type{element.try_at("type")}; + if (element_type && element_type->is_string() && + element_type->to_string() == "any") { + return true; + } + } + } + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("type"); + } +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h index a06b42e58..cf79d81d5 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_definitions_drop.h @@ -17,9 +17,11 @@ class EmptyDefinitionsDrop final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines("definitions") && - schema.at("definitions").is_object() && - schema.at("definitions").empty()); + schema.is_object()); + + const auto *definitions{schema.try_at("definitions")}; + ONLY_CONTINUE_IF(definitions && definitions->is_object() && + definitions->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h index 1a3cbcd65..e82bf0970 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_defs_drop.h @@ -16,9 +16,10 @@ class EmptyDefsDrop final : public SchemaTransformRule { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Core, Vocabularies::Known::JSON_Schema_2020_12_Core}) && - schema.is_object() && schema.defines("$defs") && - schema.at("$defs").is_object() && - schema.at("$defs").empty()); + schema.is_object()); + + const auto *defs{schema.try_at("$defs")}; + ONLY_CONTINUE_IF(defs && defs->is_object() && defs->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h index e9b838e1d..cb3b7c4e3 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_dependencies_drop.h @@ -19,9 +19,11 @@ class EmptyDependenciesDrop final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines("dependencies") && - schema.at("dependencies").is_object() && - schema.at("dependencies").empty()); + schema.is_object()); + + const auto *dependencies{schema.try_at("dependencies")}; + ONLY_CONTINUE_IF(dependencies && dependencies->is_object() && + dependencies->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h index 3aa03ecd2..1753be269 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_required_drop.h @@ -18,9 +18,11 @@ class EmptyDependentRequiredDrop final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - schema.is_object() && schema.defines("dependentRequired") && - schema.at("dependentRequired").is_object() && - schema.at("dependentRequired").empty()); + schema.is_object()); + + const auto *dependent_required{schema.try_at("dependentRequired")}; + ONLY_CONTINUE_IF(dependent_required && dependent_required->is_object() && + dependent_required->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h b/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h index e6d0b4d6e..54e3c8aec 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/empty_dependent_schemas_drop.h @@ -18,9 +18,11 @@ class EmptyDependentSchemasDrop final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_2020_12_Applicator}) && - schema.is_object() && schema.defines("dependentSchemas") && - schema.at("dependentSchemas").is_object() && - schema.at("dependentSchemas").empty()); + schema.is_object()); + + const auto *dependent_schemas{schema.try_at("dependentSchemas")}; + ONLY_CONTINUE_IF(dependent_schemas && dependent_schemas->is_object() && + dependent_schemas->empty()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h index 42e16efa4..27e09a00f 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h +++ b/vendor/blaze/src/alterschema/canonicalizer/enum_drop_redundant_validation.h @@ -25,8 +25,10 @@ class EnumDropRedundantValidation final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - schema.is_object() && schema.defines("enum") && - schema.at("enum").is_array() && !schema.defines("type")); + schema.is_object() && !schema.defines("type")); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); this->keywords_.clear(); this->wrap_keywords_.clear(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h b/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h index 925bef83d..78f90299a 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h +++ b/vendor/blaze/src/alterschema/canonicalizer/enum_filter_by_type.h @@ -24,11 +24,16 @@ class EnumFilterByType final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && schema.defines("enum") && - schema.at("enum").is_array() && !schema.at("enum").empty()); + schema.is_object()); - const auto declared_types{parse_schema_type(schema.at("type"))}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string()); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + !enum_value->empty()); + + const auto declared_types{parse_schema_type(*type)}; + ONLY_CONTINUE_IF(declared_types.any()); const bool integer_matches_integral{ vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_6, @@ -40,7 +45,7 @@ class EnumFilterByType final : public SchemaTransformRule { this->matching_indices_.clear(); bool has_mismatch{false}; std::size_t index{0}; - for (const auto &value : schema.at("enum").as_array()) { + for (const auto &value : enum_value->as_array()) { const bool matches{ declared_types.test(std::to_underlying(value.type())) || (integer_matches_integral && value.is_integral())}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_bounds_false_drop.h b/vendor/blaze/src/alterschema/canonicalizer/exclusive_bounds_false_drop.h deleted file mode 100644 index 1d6c08056..000000000 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_bounds_false_drop.h +++ /dev/null @@ -1,49 +0,0 @@ -class ExclusiveBoundsFalseDrop final : public SchemaTransformRule { -public: - using mutates = std::true_type; - using reframe_after_transform = std::true_type; - ExclusiveBoundsFalseDrop() - : SchemaTransformRule{"exclusive_bounds_false_drop", ""} {}; - - [[nodiscard]] auto - condition(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &, - const sourcemeta::core::Vocabularies &vocabularies, - const sourcemeta::core::SchemaFrame &, - const sourcemeta::core::SchemaFrame::Location &, - const sourcemeta::core::SchemaWalker &, - const sourcemeta::core::SchemaResolver &) const - -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, - Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "integer" || - schema.at("type").to_string() == "number")); - - this->has_exclusive_min_ = schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_boolean() && - !schema.at("exclusiveMinimum").to_boolean(); - this->has_exclusive_max_ = schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_boolean() && - !schema.at("exclusiveMaximum").to_boolean(); - - ONLY_CONTINUE_IF(this->has_exclusive_min_ || this->has_exclusive_max_); - return true; - } - - auto transform(JSON &schema, const Result &) const -> void override { - if (this->has_exclusive_min_) { - schema.erase("exclusiveMinimum"); - } - - if (this->has_exclusive_max_) { - schema.erase("exclusiveMaximum"); - } - } - -private: - mutable bool has_exclusive_min_{false}; - mutable bool has_exclusive_max_{false}; -}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h b/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h index af14629bc..6ceaeb596 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h +++ b/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_boolean_integer_fold.h @@ -17,13 +17,16 @@ class ExclusiveMaximumBooleanIntegerFold final : public SchemaTransformRule { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_boolean() && - schema.at("exclusiveMaximum").to_boolean() && - schema.defines("maximum") && schema.at("maximum").is_number()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_boolean() && + exclusive_maximum->to_boolean()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h b/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h index 95d2c8ed1..9bb3d23b2 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h +++ b/vendor/blaze/src/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h @@ -22,12 +22,13 @@ class ExclusiveMaximumIntegerToMaximum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_number() && - !schema.defines("maximum")); + schema.is_object() && !schema.defines("maximum")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_number()); return APPLIES_TO_KEYWORDS("exclusiveMaximum", "type"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h b/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h index ceb0d6f74..ee6b7abb7 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h +++ b/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_boolean_integer_fold.h @@ -17,13 +17,16 @@ class ExclusiveMinimumBooleanIntegerFold final : public SchemaTransformRule { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_boolean() && - schema.at("exclusiveMinimum").to_boolean() && - schema.defines("minimum") && schema.at("minimum").is_number()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_boolean() && + exclusive_minimum->to_boolean()); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h b/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h index a86232c05..6406122df 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h +++ b/vendor/blaze/src/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h @@ -22,12 +22,13 @@ class ExclusiveMinimumIntegerToMinimum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_number() && - !schema.defines("minimum")); + schema.is_object() && !schema.defines("minimum")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_number()); return APPLIES_TO_KEYWORDS("exclusiveMinimum", "type"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h b/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h index 976d2216d..1269debbf 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h +++ b/vendor/blaze/src/alterschema/canonicalizer/extends_to_array.h @@ -18,8 +18,10 @@ class ExtendsToArray final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && schema.defines("extends") && - !schema.at("extends").is_array()); + schema.is_object()); + + const auto *extends{schema.try_at("extends")}; + ONLY_CONTINUE_IF(extends && !extends->is_array()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h b/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h index 39a9cb942..114676786 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h +++ b/vendor/blaze/src/alterschema/canonicalizer/implicit_contains_keywords.h @@ -18,9 +18,10 @@ class ImplicitContainsKeywords final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_2020_12_Applicator}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "array"); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "array"); if (schema.defines("contains")) { ONLY_CONTINUE_IF(!schema.defines("minContains")); diff --git a/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h b/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h index d002902cb..ca0a24cfd 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h +++ b/vendor/blaze/src/alterschema/canonicalizer/implicit_object_keywords.h @@ -14,10 +14,12 @@ class ImplicitObjectKeywords final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("type") && - schema.at("type").is_string()); + ONLY_CONTINUE_IF(schema.is_object()); - const auto &type_value{schema.at("type").to_string()}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string()); + + const auto &type_value{type->to_string()}; this->reset(); if (type_value == "object") { diff --git a/vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h b/vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h new file mode 100644 index 000000000..635b8160d --- /dev/null +++ b/vendor/blaze/src/alterschema/canonicalizer/inline_single_use_ref.h @@ -0,0 +1,115 @@ +class InlineSingleUseRef final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + InlineSingleUseRef() : SchemaTransformRule{"inline_single_use_ref", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); + + const auto *ref{schema.try_at("$ref")}; + ONLY_CONTINUE_IF(ref && ref->is_string()); + + if (!location.parent.has_value()) { + return false; + } + { + const auto &parent_pointer{location.parent.value()}; + const auto relative{location.pointer.resolve_from(parent_pointer)}; + ONLY_CONTINUE_IF(!relative.empty() && relative.at(0).is_property() && + relative.at(0).to_property() == "allOf" && + relative.size() >= 2 && relative.at(1).is_index()); + const auto &parent_schema{sourcemeta::core::get(root, parent_pointer)}; + ONLY_CONTINUE_IF(parent_schema.is_object()); + const auto *parent_all_of{parent_schema.try_at("allOf")}; + ONLY_CONTINUE_IF(parent_all_of && parent_all_of->is_array()); + const auto current_index{relative.at(1).to_index()}; + bool has_typed_sibling{false}; + for (std::size_t index = 0; index < parent_all_of->size(); ++index) { + if (index == current_index) { + continue; + } + const auto &sibling{parent_all_of->at(index)}; + if (sibling.is_object() && + (sibling.defines("type") || sibling.defines("enum"))) { + has_typed_sibling = true; + break; + } + } + ONLY_CONTINUE_IF(has_typed_sibling); + } + + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})); + + const auto target{frame.traverse(ref->to_string())}; + ONLY_CONTINUE_IF(target.has_value()); + const auto &target_pointer{target->get().pointer}; + + if (target_pointer.size() < 2 || !target_pointer.at(0).is_property()) { + return false; + } + const auto &container{target_pointer.at(0).to_property()}; + ONLY_CONTINUE_IF(container == "definitions" || container == "$defs"); + + std::size_t ref_count{0}; + for (const auto &reference : frame.references()) { + const auto dest{frame.traverse(reference.second.destination)}; + if (!dest.has_value()) { + continue; + } + if (dest->get().pointer.starts_with(target_pointer) || + target_pointer.starts_with(dest->get().pointer)) { + ++ref_count; + } + } + + ONLY_CONTINUE_IF(ref_count == 1); + + const auto &target_schema{sourcemeta::core::get(root, target_pointer)}; + ONLY_CONTINUE_IF(!target_schema.is_boolean()); + ONLY_CONTINUE_IF(target_schema.is_object() && + !target_schema.defines("type") && + !target_schema.defines("enum")); + ONLY_CONTINUE_IF((!target_schema.defines("$id") && + !target_schema.defines("id") && + !target_schema.defines("$anchor") && + !target_schema.defines("$dynamicAnchor") && + !target_schema.defines("$recursiveAnchor"))); + + this->target_pointer_ = sourcemeta::core::to_pointer(target_pointer); + this->target_copy_ = target_schema; + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.into(std::move(this->target_copy_)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + if (target.starts_with(this->target_pointer_)) { + const auto relative{target.resolve_from(this->target_pointer_)}; + return current.concat(relative); + } + return target; + } + +private: + mutable Pointer target_pointer_; + mutable sourcemeta::core::JSON target_copy_{sourcemeta::core::JSON{nullptr}}; +}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h index a3068a342..acf61d787 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/items_implicit.h @@ -28,9 +28,10 @@ class ItemsImplicit final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6})) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "array" && !schema.defines("items")); + schema.is_object() && !schema.defines("items")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "array"); ONLY_CONTINUE_IF( !(schema.defines("unevaluatedItems") && vocabularies.contains_any( diff --git a/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h b/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h index c7da52033..c80cf1fa9 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h +++ b/vendor/blaze/src/alterschema/canonicalizer/max_contains_covered_by_max_items.h @@ -22,11 +22,13 @@ class MaxContainsCoveredByMaxItems final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("maxContains") && - schema.at("maxContains").is_integer() && schema.defines("maxItems") && - schema.at("maxItems").is_integer() && - schema.at("maxContains").to_integer() > - schema.at("maxItems").to_integer()); + schema.is_object()); + + const auto *max_contains{schema.try_at("maxContains")}; + ONLY_CONTINUE_IF(max_contains && max_contains->is_integer()); + const auto *max_items{schema.try_at("maxItems")}; + ONLY_CONTINUE_IF(max_items && max_items->is_integer() && + max_contains->to_integer() > max_items->to_integer()); return APPLIES_TO_KEYWORDS("maxContains", "maxItems"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h index 05375e37d..a4cf397b1 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/max_decimal_implicit.h @@ -16,10 +16,11 @@ class MaxDecimalImplicit final : public SchemaTransformRule { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - !schema.defines("maxDecimal")); + schema.is_object() && !schema.defines("maxDecimal")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h b/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h index 0c2826ba8..1758a1d9c 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h +++ b/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_integer_fold.h @@ -18,13 +18,16 @@ class MaximumCanEqualIntegerFold final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("maximumCanEqual") && - schema.at("maximumCanEqual").is_boolean() && - !schema.at("maximumCanEqual").to_boolean() && - schema.defines("maximum") && schema.at("maximum").is_number()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; + ONLY_CONTINUE_IF(maximum_can_equal && maximum_can_equal->is_boolean() && + !maximum_can_equal->to_boolean()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h b/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h index 3104fcb36..f5b2068a0 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/maximum_can_equal_true_drop.h @@ -18,13 +18,15 @@ class MaximumCanEqualTrueDrop final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "integer" || - schema.at("type").to_string() == "number") && - schema.defines("maximumCanEqual") && - schema.at("maximumCanEqual").is_boolean() && - schema.at("maximumCanEqual").to_boolean()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; + ONLY_CONTINUE_IF(maximum_can_equal && maximum_can_equal->is_boolean() && + maximum_can_equal->to_boolean()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h b/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h index 73d2c3413..4c8950599 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h +++ b/vendor/blaze/src/alterschema/canonicalizer/min_items_given_min_contains.h @@ -21,10 +21,10 @@ class MinItemsGivenMinContains final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "array" && - !schema.defines("minItems")); + schema.is_object() && !schema.defines("minItems")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "array"); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h index 8f16f8f5d..91932fb99 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/min_length_implicit.h @@ -26,10 +26,11 @@ class MinLengthImplicit final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_0}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "string" && - !schema.defines("minLength")); + schema.is_object() && !schema.defines("minLength")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "string"); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h b/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h index 2f1cb3063..532162b77 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h +++ b/vendor/blaze/src/alterschema/canonicalizer/min_properties_covered_by_required.h @@ -17,19 +17,21 @@ class MinPropertiesCoveredByRequired final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *min_properties{schema.try_at("minProperties")}; + ONLY_CONTINUE_IF(min_properties && min_properties->is_integer()); + const auto *required{schema.try_at("required")}; ONLY_CONTINUE_IF( - vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Validation, - Vocabularies::Known::JSON_Schema_2019_09_Validation, - Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("minProperties") && - schema.at("minProperties").is_integer() && schema.defines("required") && - schema.at("required").is_array() && schema.at("required").unique() && - std::cmp_greater(schema.at("required").size(), - static_cast( - schema.at("minProperties").to_integer()))); + required && required->is_array() && required->unique() && + std::cmp_greater(required->size(), static_cast( + min_properties->to_integer()))); return APPLIES_TO_KEYWORDS("minProperties", "required"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h b/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h index 3ea2bab3c..216d24721 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h +++ b/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_integer_fold.h @@ -18,13 +18,16 @@ class MinimumCanEqualIntegerFold final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("minimumCanEqual") && - schema.at("minimumCanEqual").is_boolean() && - !schema.at("minimumCanEqual").to_boolean() && - schema.defines("minimum") && schema.at("minimum").is_number()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; + ONLY_CONTINUE_IF(minimum_can_equal && minimum_can_equal->is_boolean() && + !minimum_can_equal->to_boolean()); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h b/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h index 32c775a48..0bd3c2e08 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/minimum_can_equal_true_drop.h @@ -18,13 +18,15 @@ class MinimumCanEqualTrueDrop final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "integer" || - schema.at("type").to_string() == "number") && - schema.defines("minimumCanEqual") && - schema.at("minimumCanEqual").is_boolean() && - schema.at("minimumCanEqual").to_boolean()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; + ONLY_CONTINUE_IF(minimum_can_equal && minimum_can_equal->is_boolean() && + minimum_can_equal->to_boolean()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h index 330578be0..934f21127 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/multiple_of_implicit.h @@ -21,11 +21,12 @@ class MultipleOfImplicit final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - // Applying this to numbers would be a semantic problem - schema.at("type").to_string() == "integer" && - !schema.defines("multipleOf")); + schema.is_object() && !schema.defines("multipleOf")); + + const auto *type{schema.try_at("type")}; + // Applying this to numbers would be a semantic problem + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h index c3807414a..91b22f7ec 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/optional_property_implicit.h @@ -18,12 +18,15 @@ class OptionalPropertyImplicit final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "object" && - schema.defines("properties") && schema.at("properties").is_object()); + schema.is_object()); - for (const auto &entry : schema.at("properties").as_object()) { + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "object"); + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object()); + + for (const auto &entry : properties->as_object()) { if (entry.second.is_object() && !entry.second.empty() && !entry.second.defines("optional")) { return true; diff --git a/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h b/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h index a973fcf0b..ba2b12c3f 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h +++ b/vendor/blaze/src/alterschema/canonicalizer/recursive_anchor_false_drop.h @@ -16,9 +16,11 @@ class RecursiveAnchorFalseDrop final : public SchemaTransformRule { -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && - schema.is_object() && schema.defines("$recursiveAnchor") && - schema.at("$recursiveAnchor").is_boolean() && - !schema.at("$recursiveAnchor").to_boolean()); + schema.is_object()); + + const auto *recursive_anchor{schema.try_at("$recursiveAnchor")}; + ONLY_CONTINUE_IF(recursive_anchor && recursive_anchor->is_boolean() && + !recursive_anchor->to_boolean()); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h index 862912d3e..520c53160 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/required_property_implicit.h @@ -16,12 +16,15 @@ class RequiredPropertyImplicit final : public SchemaTransformRule { -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "object" && - schema.defines("properties") && schema.at("properties").is_object()); + schema.is_object()); - for (const auto &entry : schema.at("properties").as_object()) { + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "object"); + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object()); + + for (const auto &entry : properties->as_object()) { if (entry.second.is_object() && !entry.second.empty() && !entry.second.defines("$ref") && !entry.second.defines("required")) { return true; diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h b/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h index 79f8c7b8b..4745cb7ca 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h +++ b/vendor/blaze/src/alterschema/canonicalizer/single_branch_allof.h @@ -20,9 +20,10 @@ class SingleBranchAllOf final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() == 1); + schema.is_object()); + + const auto *all_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(all_of && all_of->is_array() && all_of->size() == 1); ONLY_CONTINUE_IF( !(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, @@ -31,11 +32,11 @@ class SingleBranchAllOf final : public SchemaTransformRule { schema.defines("unevaluatedItems")))); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); - const auto &branch{schema.at(KEYWORD).at(0)}; - if (branch.is_object() && branch.size() == 1) { - const auto &key{branch.as_object().cbegin()->first}; - ONLY_CONTINUE_IF(key != "$ref" && key != "$dynamicRef" && - key != "$recursiveRef"); + const auto &branch{all_of->at(0)}; + if (branch.is_object()) { + ONLY_CONTINUE_IF(!branch.defines("$ref") && + !branch.defines("$dynamicRef") && + !branch.defines("$recursiveRef")); } return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h b/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h index a7862d08f..e3fc7dedf 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h +++ b/vendor/blaze/src/alterschema/canonicalizer/single_branch_anyof.h @@ -20,9 +20,10 @@ class SingleBranchAnyOf final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() == 1); + schema.is_object()); + + const auto *any_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(any_of && any_of->is_array() && any_of->size() == 1); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); this->has_unevaluated_ = diff --git a/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h b/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h index 158501f3a..93627c169 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h +++ b/vendor/blaze/src/alterschema/canonicalizer/single_branch_oneof.h @@ -20,9 +20,10 @@ class SingleBranchOneOf final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() == 1); + schema.is_object()); + + const auto *one_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(one_of && one_of->is_array() && one_of->size() == 1); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); this->has_unevaluated_ = diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h index 6155618d5..c17b91cfb 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_array_to_any_of.h @@ -26,8 +26,10 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_array()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array()); this->keyword_instances_.clear(); diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h b/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h index f1c21b005..9909d78ef 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_boolean_as_enum.h @@ -27,10 +27,12 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_0}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "boolean" && - !schema.defines("enum") && !schema.defines("const")); + schema.is_object() && !schema.defines("enum") && + !schema.defines("const")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "boolean"); return APPLIES_TO_KEYWORDS("type"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h b/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h index d4dcd1925..1a7612eff 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_inherit_in_place.h @@ -59,9 +59,95 @@ class TypeInheritInPlace final : public SchemaTransformRule { return ancestor_schema.defines("type"); })}; - ONLY_CONTINUE_IF(ancestor.has_value()); - this->inherited_type_ = get(root, ancestor.value().get()).at("type"); - return true; + if (ancestor.has_value()) { + const auto &ancestor_type{get(root, ancestor.value().get()).at("type")}; + if (ancestor_type.is_array()) { + for (const auto &element : ancestor_type.as_array()) { + if (!element.is_string()) { + return false; + } + } + } + this->inherited_type_ = ancestor_type; + return true; + } + + auto walk_pointer{location.pointer}; + auto walk_parent{location.parent}; + while (walk_parent.has_value()) { + const auto &wp{walk_parent.value()}; + const auto walk_relative{walk_pointer.resolve_from(wp)}; + if (walk_relative.empty() || !walk_relative.at(0).is_property()) { + break; + } + const auto walk_entry{frame.traverse(frame.uri(wp).value().get())}; + if (!walk_entry.has_value()) { + break; + } + const auto walk_vocabularies{ + frame.vocabularies(walk_entry.value().get(), resolver)}; + const auto walk_keyword_type{ + walker(walk_relative.at(0).to_property(), walk_vocabularies).type}; + + if (!IS_IN_PLACE_APPLICATOR(walk_keyword_type)) { + break; + } + + if (walk_keyword_type == SchemaKeywordType::ApplicatorElementsInPlace && + walk_relative.size() >= 2 && walk_relative.at(1).is_index()) { + const auto branch_index{walk_relative.at(1).to_index()}; + const auto &allof_parent{get(root, wp)}; + const auto &keyword_name{walk_relative.at(0).to_property()}; + const auto *branches{allof_parent.is_object() + ? allof_parent.try_at(keyword_name) + : nullptr}; + if (branches && branches->is_array()) { + for (std::size_t index = 0; index < branches->size(); ++index) { + if (index == branch_index) { + continue; + } + const auto &sibling{branches->at(index)}; + if (!sibling.is_object()) { + continue; + } + const auto *sibling_type{sibling.try_at("type")}; + if (sibling_type && sibling_type->is_string()) { + this->inherited_type_ = *sibling_type; + return true; + } + const auto *sibling_enum{sibling.try_at("enum")}; + if (sibling_enum && sibling_enum->is_array() && + !sibling_enum->empty()) { + const auto inferred{infer_type_from_enum(*sibling_enum)}; + if (!inferred.empty()) { + this->inherited_type_ = JSON{inferred}; + return true; + } + } + const auto *sibling_ref{sibling.try_at("$ref")}; + if (sibling_ref && sibling_ref->is_string()) { + const auto ref_target{frame.traverse(sibling_ref->to_string())}; + if (ref_target.has_value()) { + const auto &ref_schema{ + get(root, ref_target.value().get().pointer)}; + const auto *ref_type{ref_schema.is_object() + ? ref_schema.try_at("type") + : nullptr}; + if (ref_type && ref_type->is_string()) { + this->inherited_type_ = *ref_type; + return true; + } + } + } + } + } + } + + walk_pointer = wp; + walk_parent = walk_entry.value().get().parent; + } + + return false; } auto transform(JSON &schema, const Result &) const -> void override { @@ -69,6 +155,66 @@ class TypeInheritInPlace final : public SchemaTransformRule { } private: + static auto infer_type_from_enum(const sourcemeta::core::JSON &enum_array) + -> sourcemeta::core::JSON::String { + using Type = sourcemeta::core::JSON::Type; + bool all_null{true}; + bool all_boolean{true}; + bool all_integer{true}; + bool all_number{true}; + bool all_string{true}; + bool all_array{true}; + bool all_object{true}; + + for (const auto &value : enum_array.as_array()) { + const auto value_type{value.type()}; + if (value_type != Type::Null) { + all_null = false; + } + if (value_type != Type::Boolean) { + all_boolean = false; + } + if (value_type != Type::Integer) { + all_integer = false; + } + if (value_type != Type::Integer && value_type != Type::Real) { + all_number = false; + } + if (value_type != Type::String) { + all_string = false; + } + if (value_type != Type::Array) { + all_array = false; + } + if (value_type != Type::Object) { + all_object = false; + } + } + + if (all_string) { + return "string"; + } + if (all_integer) { + return "integer"; + } + if (all_number) { + return "number"; + } + if (all_object) { + return "object"; + } + if (all_array) { + return "array"; + } + if (all_null) { + return "null"; + } + if (all_boolean) { + return "boolean"; + } + return ""; + } + mutable sourcemeta::core::JSON inherited_type_{ sourcemeta::core::JSON{nullptr}}; }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h b/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h index 93b6af341..47c0998b4 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_null_as_enum.h @@ -27,10 +27,11 @@ class TypeNullAsEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_0}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "null" && - !schema.defines("enum") && !schema.defines("const")); + schema.is_object() && !schema.defines("enum") && + !schema.defines("const")); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && type->to_string() == "null"); return APPLIES_TO_KEYWORDS("type"); } diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h b/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h index 8d1c35361..b2d13af3a 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_union_implicit.h @@ -9,12 +9,12 @@ class TypeUnionImplicit final : public SchemaTransformRule { [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &, + const sourcemeta::core::JSON &root, const sourcemeta::core::Vocabularies &vocabularies, - const sourcemeta::core::SchemaFrame &, - const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, const sourcemeta::core::SchemaWalker &walker, - const sourcemeta::core::SchemaResolver &) const + const sourcemeta::core::SchemaResolver &resolver) const -> SchemaTransformRule::Result override { using namespace sourcemeta::core; ONLY_CONTINUE_IF(schema.is_object() && !schema.empty()); @@ -54,6 +54,9 @@ class TypeUnionImplicit final : public SchemaTransformRule { !IS_IN_PLACE_APPLICATOR(keyword_type)); } + ONLY_CONTINUE_IF(!this->allof_sibling_constrains_type(root, frame, location, + walker, resolver)); + return true; } @@ -71,4 +74,70 @@ class TypeUnionImplicit final : public SchemaTransformRule { schema.assign("type", std::move(types)); } + +private: + static auto allof_sibling_constrains_type( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) -> bool { + using namespace sourcemeta::core; + auto walk_pointer{location.pointer}; + auto walk_parent{location.parent}; + while (walk_parent.has_value()) { + const auto &wp{walk_parent.value()}; + const auto walk_relative{walk_pointer.resolve_from(wp)}; + if (walk_relative.empty() || !walk_relative.at(0).is_property()) { + break; + } + const auto walk_entry{frame.traverse(frame.uri(wp).value().get())}; + if (!walk_entry.has_value()) { + break; + } + const auto walk_vocabularies{ + frame.vocabularies(walk_entry.value().get(), resolver)}; + const auto walk_keyword_type{ + walker(walk_relative.at(0).to_property(), walk_vocabularies).type}; + + if (!IS_IN_PLACE_APPLICATOR(walk_keyword_type)) { + break; + } + + if (walk_keyword_type == SchemaKeywordType::ApplicatorElementsInPlace && + walk_relative.size() >= 2 && walk_relative.at(1).is_index()) { + const auto branch_index{walk_relative.at(1).to_index()}; + const auto &allof_parent{get(root, wp)}; + const auto &keyword_name{walk_relative.at(0).to_property()}; + const auto *branches{allof_parent.is_object() + ? allof_parent.try_at(keyword_name) + : nullptr}; + if (branches && branches->is_array()) { + for (std::size_t index = 0; index < branches->size(); ++index) { + if (index == branch_index) { + continue; + } + const auto &sibling{branches->at(index)}; + if (!sibling.is_object()) { + continue; + } + + if (sibling.defines("type")) { + return true; + } + + const auto *sibling_enum{sibling.try_at("enum")}; + if (sibling_enum && sibling_enum->is_array() && + !sibling_enum->empty()) { + return true; + } + } + } + } + + walk_pointer = wp; + walk_parent = walk_entry.value().get().parent; + } + return false; + } }; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h b/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h index 086c34999..5a488516b 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_union_to_schemas.h @@ -18,10 +18,12 @@ class TypeUnionToSchemas final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_array()); + schema.is_object()); - for (const auto &element : schema.at("type").as_array()) { + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array()); + + for (const auto &element : type->as_array()) { if (element.is_string()) { return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h b/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h index d74833a0c..4022ae9e3 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_allof.h @@ -34,8 +34,8 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2020_12_Applicator}) && schema.defines("if")}; this->has_if_then_else_ = has_if; - const bool has_type{schema.defines("type") && - schema.at("type").is_string()}; + const auto *type_value{schema.try_at("type")}; + const bool has_type{type_value && type_value->is_string()}; const bool has_enum{schema.defines("enum")}; const bool is_modern{ vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) || @@ -56,8 +56,10 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { const bool has_structural{has_type || has_enum || has_ref}; bool modern_ref_needs_wrapping{false}; + this->ref_annotations_only_ = false; if (this->has_modern_ref_ || this->has_dynamic_ref_ || this->has_recursive_ref_) { + this->ref_annotations_only_ = true; for (const auto &entry : schema.as_object()) { if (entry.first == "$ref" || entry.first == "$dynamicRef" || entry.first == "$recursiveRef") { @@ -68,7 +70,12 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { keyword_type != sourcemeta::core::SchemaKeywordType::Annotation && keyword_type != sourcemeta::core::SchemaKeywordType::Comment) { modern_ref_needs_wrapping = true; - break; + if (keyword_type != sourcemeta::core::SchemaKeywordType::Reference && + keyword_type != sourcemeta::core::SchemaKeywordType::Other && + keyword_type != + sourcemeta::core::SchemaKeywordType::LocationMembers) { + this->ref_annotations_only_ = false; + } } } } @@ -94,9 +101,10 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { } } - ONLY_CONTINUE_IF((has_structural && applicator_count >= 1) || - applicator_count >= 2 || modern_ref_needs_wrapping || - has_orphaned_typed_keywords); + ONLY_CONTINUE_IF( + (has_structural && applicator_count >= 1) || applicator_count >= 2 || + modern_ref_needs_wrapping || + (has_orphaned_typed_keywords && !this->ref_annotations_only_)); this->strategy_ = Strategy::FullRestructure; this->applicators_with_refs_ = 0; @@ -222,8 +230,10 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { entry.first == "allOf" || entry.first == "oneOf" || entry.first == "$schema" || entry.first == "id" || entry.first == "$id" || entry.first == "definitions" || - entry.first == "$defs" || entry.first == "dependencies" || - entry.first == "dependentSchemas" || + entry.first == "$defs" || entry.first == "$anchor" || + entry.first == "$dynamicAnchor" || + entry.first == "$recursiveAnchor" || entry.first == "$vocabulary" || + entry.first == "dependencies" || entry.first == "dependentSchemas" || (this->has_if_then_else_ && (entry.first == "if" || entry.first == "then" || entry.first == "else")) || @@ -253,6 +263,25 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { schema.assign("allOf", std::move(new_allof)); } + if (this->has_modern_ref_ && schema.defines("$ref")) { + auto branch{JSON::make_object()}; + branch.assign("$ref", schema.at("$ref")); + schema.at("allOf").push_back(std::move(branch)); + schema.erase("$ref"); + } + if (this->has_dynamic_ref_ && schema.defines("$dynamicRef")) { + auto branch{JSON::make_object()}; + branch.assign("$dynamicRef", schema.at("$dynamicRef")); + schema.at("allOf").push_back(std::move(branch)); + schema.erase("$dynamicRef"); + } + if (this->has_recursive_ref_ && schema.defines("$recursiveRef")) { + auto branch{JSON::make_object()}; + branch.assign("$recursiveRef", schema.at("$recursiveRef")); + schema.at("allOf").push_back(std::move(branch)); + schema.erase("$recursiveRef"); + } + for (const auto &applicator : APPLICATORS_WITHOUT_ALLOF) { if (!schema.defines(applicator)) { continue; @@ -291,16 +320,34 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (this->has_modern_ref_ && schema.defines("$ref")) { auto branch{JSON::make_object()}; branch.assign("$ref", schema.at("$ref")); + if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { + for (const auto &entry : typed_branch.as_object()) { + branch.assign(entry.first, entry.second); + } + this->typed_keywords_.clear(); + } new_allof.push_back(std::move(branch)); } if (this->has_dynamic_ref_ && schema.defines("$dynamicRef")) { auto branch{JSON::make_object()}; branch.assign("$dynamicRef", schema.at("$dynamicRef")); + if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { + for (const auto &entry : typed_branch.as_object()) { + branch.assign(entry.first, entry.second); + } + this->typed_keywords_.clear(); + } new_allof.push_back(std::move(branch)); } if (this->has_recursive_ref_ && schema.defines("$recursiveRef")) { auto branch{JSON::make_object()}; branch.assign("$recursiveRef", schema.at("$recursiveRef")); + if (this->ref_annotations_only_ && !this->typed_keywords_.empty()) { + for (const auto &entry : typed_branch.as_object()) { + branch.assign(entry.first, entry.second); + } + this->typed_keywords_.clear(); + } new_allof.push_back(std::move(branch)); } @@ -342,6 +389,18 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { if (schema.defines("$defs")) { new_schema.assign("$defs", schema.at("$defs")); } + if (schema.defines("$anchor")) { + new_schema.assign("$anchor", schema.at("$anchor")); + } + if (schema.defines("$dynamicAnchor")) { + new_schema.assign("$dynamicAnchor", schema.at("$dynamicAnchor")); + } + if (schema.defines("$recursiveAnchor")) { + new_schema.assign("$recursiveAnchor", schema.at("$recursiveAnchor")); + } + if (schema.defines("$vocabulary")) { + new_schema.assign("$vocabulary", schema.at("$vocabulary")); + } if (schema.defines("dependencies")) { new_schema.assign("dependencies", schema.at("dependencies")); } @@ -455,6 +514,7 @@ class TypeWithApplicatorToAllOf final : public SchemaTransformRule { mutable bool has_dynamic_ref_{false}; mutable bool has_recursive_ref_{false}; mutable bool has_unevaluated_{false}; + mutable bool ref_annotations_only_{false}; mutable std::vector typed_keywords_; mutable std::uint8_t applicator_indices_{0}; mutable std::uint8_t applicators_with_refs_{0}; diff --git a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h b/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h index c4a11bd88..5d4ee3696 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h +++ b/vendor/blaze/src/alterschema/canonicalizer/type_with_applicator_to_extends.h @@ -21,14 +21,13 @@ class TypeWithApplicatorToExtends final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_3}) && schema.is_object()); - const bool has_extends{schema.defines("extends") && - schema.at("extends").is_array()}; - const bool has_disallow{schema.defines("disallow") && - schema.at("disallow").is_array()}; - const bool has_type_array{schema.defines("type") && - schema.at("type").is_array()}; - const bool has_type{schema.defines("type") && - schema.at("type").is_string()}; + const auto *extends_value{schema.try_at("extends")}; + const bool has_extends{extends_value && extends_value->is_array()}; + const auto *disallow_value{schema.try_at("disallow")}; + const bool has_disallow{disallow_value && disallow_value->is_array()}; + const auto *type_value{schema.try_at("type")}; + const bool has_type_array{type_value && type_value->is_array()}; + const bool has_type{type_value && type_value->is_string()}; const bool has_enum{schema.defines("enum")}; const unsigned int applicator_count{(has_extends ? 1U : 0U) + (has_disallow ? 1U : 0U) + diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h index b39ca0d8e..0b537c240 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h +++ b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_can_equal_bounds.h @@ -16,20 +16,25 @@ class UnsatisfiableCanEqualBounds final : public SchemaTransformRule { -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF( vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_2) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "number" || - schema.at("type").to_string() == "integer") && - schema.defines("minimum") && schema.at("minimum").is_number() && - schema.defines("maximum") && schema.at("maximum").is_number() && - schema.at("minimum") == schema.at("maximum")); + schema.is_object()); - const bool min_exclusive{schema.defines("minimumCanEqual") && - schema.at("minimumCanEqual").is_boolean() && - !schema.at("minimumCanEqual").to_boolean()}; - const bool max_exclusive{schema.defines("maximumCanEqual") && - schema.at("maximumCanEqual").is_boolean() && - !schema.at("maximumCanEqual").to_boolean()}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "number" || type->to_string() == "integer")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; + const bool min_exclusive{minimum_can_equal && + minimum_can_equal->is_boolean() && + !minimum_can_equal->to_boolean()}; + const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; + const bool max_exclusive{maximum_can_equal && + maximum_can_equal->is_boolean() && + !maximum_can_equal->to_boolean()}; ONLY_CONTINUE_IF(min_exclusive || max_exclusive); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h index 6da4b2dfb..4cd4e33a9 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h +++ b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_exclusive_equal_bounds.h @@ -17,20 +17,25 @@ class UnsatisfiableExclusiveEqualBounds final : public SchemaTransformRule { ONLY_CONTINUE_IF( vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "number" || - schema.at("type").to_string() == "integer") && - schema.defines("minimum") && schema.at("minimum").is_number() && - schema.defines("maximum") && schema.at("maximum").is_number() && - schema.at("minimum") == schema.at("maximum")); + schema.is_object()); - const bool exclusive_min{schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_boolean() && - schema.at("exclusiveMinimum").to_boolean()}; - const bool exclusive_max{schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_boolean() && - schema.at("exclusiveMaximum").to_boolean()}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "number" || type->to_string() == "integer")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + const bool exclusive_min{exclusive_minimum && + exclusive_minimum->is_boolean() && + exclusive_minimum->to_boolean()}; + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + const bool exclusive_max{exclusive_maximum && + exclusive_maximum->is_boolean() && + exclusive_maximum->to_boolean()}; ONLY_CONTINUE_IF(exclusive_min || exclusive_max); return true; } diff --git a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h index 1c6168540..d7990b0e3 100644 --- a/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h +++ b/vendor/blaze/src/alterschema/canonicalizer/unsatisfiable_type_and_enum.h @@ -25,11 +25,16 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_2020_12_Validation}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && schema.defines("enum") && - schema.at("enum").is_array() && !schema.at("enum").empty()); + schema.is_object()); - const auto declared_types{parse_schema_type(schema.at("type"))}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string()); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + !enum_value->empty()); + + const auto declared_types{parse_schema_type(*type)}; + ONLY_CONTINUE_IF(declared_types.any()); const bool integer_matches_integral{ vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_6, @@ -38,7 +43,7 @@ class UnsatisfiableTypeAndEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2020_12_Validation}) && declared_types.test(std::to_underlying(JSON::Type::Integer))}; ONLY_CONTINUE_IF(std::ranges::none_of( - schema.at("enum").as_array(), + enum_value->as_array(), [&declared_types, integer_matches_integral](const auto &value) { return declared_types.test(std::to_underlying(value.type())) || (integer_matches_integral && value.is_integral()); diff --git a/vendor/blaze/src/alterschema/common/allof_false_simplify.h b/vendor/blaze/src/alterschema/common/allof_false_simplify.h index 3abef5b39..935100201 100644 --- a/vendor/blaze/src/alterschema/common/allof_false_simplify.h +++ b/vendor/blaze/src/alterschema/common/allof_false_simplify.h @@ -22,12 +22,13 @@ class AllOfFalseSimplify final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines(KEYWORD) && - !schema.defines("not") && schema.at(KEYWORD).is_array()); + schema.is_object() && !schema.defines("not")); - const auto &allof{schema.at(KEYWORD)}; - for (std::size_t index = 0; index < allof.size(); ++index) { - const auto &entry{allof.at(index)}; + const auto *all_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(all_of && all_of->is_array()); + + for (std::size_t index = 0; index < all_of->size(); ++index) { + const auto &entry{all_of->at(index)}; if (entry.is_boolean() && !entry.to_boolean()) { ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); diff --git a/vendor/blaze/src/alterschema/common/anyof_false_simplify.h b/vendor/blaze/src/alterschema/common/anyof_false_simplify.h index 387f3086a..7069a38bf 100644 --- a/vendor/blaze/src/alterschema/common/anyof_false_simplify.h +++ b/vendor/blaze/src/alterschema/common/anyof_false_simplify.h @@ -22,11 +22,12 @@ class AnyOfFalseSimplify final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines(KEYWORD) && - !schema.defines("not") && schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() == 1); + schema.is_object() && !schema.defines("not")); - const auto &entry{schema.at(KEYWORD).front()}; + const auto *any_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(any_of && any_of->is_array() && any_of->size() == 1); + + const auto &entry{any_of->front()}; ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); diff --git a/vendor/blaze/src/alterschema/common/const_in_enum.h b/vendor/blaze/src/alterschema/common/const_in_enum.h index 7bf39ea50..fed367cf5 100644 --- a/vendor/blaze/src/alterschema/common/const_in_enum.h +++ b/vendor/blaze/src/alterschema/common/const_in_enum.h @@ -22,9 +22,13 @@ class ConstInEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("const") && - schema.defines("enum") && schema.at("enum").is_array() && - schema.at("enum").contains(schema.at("const"))); + schema.is_object()); + + const auto *const_value{schema.try_at("const")}; + ONLY_CONTINUE_IF(const_value); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + enum_value->contains(*const_value)); return APPLIES_TO_KEYWORDS("const", "enum"); } diff --git a/vendor/blaze/src/alterschema/common/const_with_type.h b/vendor/blaze/src/alterschema/common/const_with_type.h index 025444119..f7031330b 100644 --- a/vendor/blaze/src/alterschema/common/const_with_type.h +++ b/vendor/blaze/src/alterschema/common/const_with_type.h @@ -22,12 +22,17 @@ class ConstWithType final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("type") && - schema.defines("const")); + schema.is_object()); - const auto current_types{parse_schema_type(schema.at("type"))}; + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type); + const auto *const_value{schema.try_at("const")}; + ONLY_CONTINUE_IF(const_value); + + const auto current_types{parse_schema_type(*type)}; + ONLY_CONTINUE_IF(current_types.any()); ONLY_CONTINUE_IF( - current_types.test(std::to_underlying(schema.at("const").type()))); + current_types.test(std::to_underlying(const_value->type()))); return APPLIES_TO_KEYWORDS("const", "type"); } diff --git a/vendor/blaze/src/alterschema/common/dependencies_property_tautology.h b/vendor/blaze/src/alterschema/common/dependencies_property_tautology.h index e8a6f06b7..03bd70677 100644 --- a/vendor/blaze/src/alterschema/common/dependencies_property_tautology.h +++ b/vendor/blaze/src/alterschema/common/dependencies_property_tautology.h @@ -1,7 +1,7 @@ class DependenciesPropertyTautology final : public SchemaTransformRule { public: using mutates = std::true_type; - using reframe_after_transform = std::false_type; + using reframe_after_transform = std::true_type; DependenciesPropertyTautology() : SchemaTransformRule{ "dependencies_property_tautology", @@ -18,29 +18,71 @@ class DependenciesPropertyTautology final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && schema.defines("dependencies") && - schema.at("dependencies").is_object() && schema.defines("required") && - schema.at("required").is_array()); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *dependencies{schema.try_at("dependencies")}; + ONLY_CONTINUE_IF(dependencies && dependencies->is_object()); + + const bool is_draft_3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + + if (is_draft_3) { + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object()); + + ONLY_CONTINUE_IF(std::ranges::any_of( + properties->as_object(), [dependencies](const auto &entry) { + if (!entry.second.is_object()) { + return false; + } + const auto *required{entry.second.try_at("required")}; + if (!required || !required->is_boolean() || + !required->to_boolean()) { + return false; + } + const auto *dependent{dependencies->try_at(entry.first)}; + return dependent && + (dependent->is_array() || dependent->is_string()); + })); + return APPLIES_TO_KEYWORDS("dependencies", "properties"); + } + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array()); + ONLY_CONTINUE_IF(std::ranges::any_of( - schema.at("required").as_array(), [&schema](const auto &element) { - return element.is_string() && - schema.at("dependencies").defines(element.to_string()) && - (schema.at("dependencies") - .at(element.to_string()) - .is_array() || - schema.at("dependencies") - .at(element.to_string()) - .is_string()); + required->as_array(), [dependencies](const auto &element) { + if (!element.is_string()) { + return false; + } + const auto *dependent{dependencies->try_at(element.to_string())}; + return dependent && (dependent->is_array() || dependent->is_string()); })); return APPLIES_TO_KEYWORDS("dependencies", "required"); } - auto transform(JSON &schema, const Result &) const -> void override { + auto transform(JSON &schema, const Result &result) const -> void override { + const bool is_draft_3_path{ + std::ranges::any_of(result.locations, [](const auto &pointer) { + return pointer.size() == 1 && pointer.at(0).is_property() && + pointer.at(0).to_property() == "properties"; + })}; + if (is_draft_3_path) { + this->transform_boolean(schema); + } else { + this->transform_array(schema); + } + } + +private: + static auto transform_array(JSON &schema) -> void { auto requirements{schema.at("required")}; while (true) { bool match{false}; @@ -76,4 +118,65 @@ class DependenciesPropertyTautology final : public SchemaTransformRule { schema.assign("required", requirements); } + + static auto transform_boolean(JSON &schema) -> void { + while (true) { + bool match{false}; + + std::vector snapshot; + for (const auto &entry : schema.at("properties").as_object()) { + if (!entry.second.is_object()) { + continue; + } + const auto *required{entry.second.try_at("required")}; + if (required && required->is_boolean() && required->to_boolean()) { + snapshot.push_back(entry.first); + } + } + + for (const auto &name : snapshot) { + if (!schema.at("dependencies").defines(name)) { + continue; + } + + const auto dependents_copy{schema.at("dependencies").at(name)}; + std::vector new_required; + if (dependents_copy.is_string()) { + new_required.push_back(dependents_copy.to_string()); + } else if (dependents_copy.is_array()) { + for (const auto &dependent : dependents_copy.as_array()) { + if (dependent.is_string()) { + new_required.push_back(dependent.to_string()); + } + } + } else { + continue; + } + + for (const auto &dependency_name : new_required) { + if (!schema.at("properties").defines(dependency_name)) { + auto new_property{JSON::make_object()}; + new_property.assign("required", JSON{true}); + schema.at("properties") + .assign(dependency_name, std::move(new_property)); + match = true; + } else if (schema.at("properties").at(dependency_name).is_object()) { + auto &existing{schema.at("properties").at(dependency_name)}; + const auto *current_required{existing.try_at("required")}; + if (!current_required || !current_required->is_boolean() || + !current_required->to_boolean()) { + existing.assign("required", JSON{true}); + match = true; + } + } + } + + schema.at("dependencies").erase(name); + } + + if (!match) { + break; + } + } + } }; diff --git a/vendor/blaze/src/alterschema/common/dependent_required_tautology.h b/vendor/blaze/src/alterschema/common/dependent_required_tautology.h index 5aff7a3b9..1740b8dae 100644 --- a/vendor/blaze/src/alterschema/common/dependent_required_tautology.h +++ b/vendor/blaze/src/alterschema/common/dependent_required_tautology.h @@ -22,16 +22,19 @@ class DependentRequiredTautology final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("dependentRequired") && - schema.at("dependentRequired").is_object() && - schema.defines("required") && schema.at("required").is_array()); - ONLY_CONTINUE_IF(std::any_of( - schema.at("required").as_array().cbegin(), - schema.at("required").as_array().cend(), - [&schema](const auto &element) { - return element.is_string() && - schema.at("dependentRequired").defines(element.to_string()); - })); + schema.is_object()); + + const auto *dependent_required{schema.try_at("dependentRequired")}; + ONLY_CONTINUE_IF(dependent_required && dependent_required->is_object()); + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array()); + + ONLY_CONTINUE_IF( + std::any_of(required->as_array().cbegin(), required->as_array().cend(), + [dependent_required](const auto &element) { + return element.is_string() && + dependent_required->defines(element.to_string()); + })); return APPLIES_TO_KEYWORDS("dependentRequired", "required"); } diff --git a/vendor/blaze/src/alterschema/common/disallow_narrows_type.h b/vendor/blaze/src/alterschema/common/disallow_narrows_type.h new file mode 100644 index 000000000..6157ee939 --- /dev/null +++ b/vendor/blaze/src/alterschema/common/disallow_narrows_type.h @@ -0,0 +1,138 @@ +class DisallowNarrowsType final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowNarrowsType() + : SchemaTransformRule{ + "disallow_narrows_type", + "When `disallow` excludes types that are also in the parent " + "`type`, those types can be removed from `type` and the " + "corresponding `disallow` entries dropped"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"disallow"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->empty()); + + const auto *parent_type{schema.try_at("type")}; + ONLY_CONTINUE_IF(parent_type && parent_type->is_array() && + parent_type->size() > 1); + + std::unordered_set parent_type_names; + for (const auto &entry : parent_type->as_array()) { + ONLY_CONTINUE_IF(entry.is_string() && entry.to_string() != "any"); + parent_type_names.insert(entry.to_string()); + } + + std::vector locations; + std::unordered_set narrowed_types; + for (std::size_t index = 0; index < disallow->size(); ++index) { + const auto entry_types{extract_type_names(disallow->at(index))}; + if (entry_types.empty()) { + continue; + } + + const bool all_in_parent{std::ranges::all_of( + entry_types, [&parent_type_names](const auto &type_name) { + return parent_type_names.contains(type_name); + })}; + if (!all_in_parent) { + continue; + } + + locations.push_back(Pointer{KEYWORD, index}); + narrowed_types.insert(entry_types.cbegin(), entry_types.cend()); + } + + ONLY_CONTINUE_IF(!locations.empty()); + ONLY_CONTINUE_IF(narrowed_types.size() < parent_type_names.size()); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + return APPLIES_TO_POINTERS(std::move(locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + std::unordered_set narrowed_types; + std::vector dead_indices; + dead_indices.reserve(result.locations.size()); + const auto &disallow{schema.at("disallow")}; + for (const auto &location : result.locations) { + assert(location.size() == 2); + const auto index{location.at(1).to_index()}; + dead_indices.push_back(index); + const auto entry_types{extract_type_names(disallow.at(index))}; + narrowed_types.insert(entry_types.cbegin(), entry_types.cend()); + } + + auto new_type{JSON::make_array()}; + for (const auto &entry : schema.at("type").as_array()) { + if (entry.is_string() && !narrowed_types.contains(entry.to_string())) { + new_type.push_back(entry); + } + } + schema.assign("type", std::move(new_type)); + + auto new_disallow{JSON::make_array()}; + for (std::size_t index = 0; index < disallow.size(); ++index) { + if (std::ranges::find(dead_indices, index) == dead_indices.end()) { + new_disallow.push_back(disallow.at(index)); + } + } + if (new_disallow.empty()) { + schema.erase("disallow"); + } else { + schema.assign("disallow", std::move(new_disallow)); + } + } + +private: + static auto extract_type_names(const sourcemeta::core::JSON &entry) + -> std::unordered_set { + std::unordered_set result; + if (entry.is_string()) { + if (entry.to_string() != "any") { + result.insert(entry.to_string()); + } + return result; + } + if (!entry.is_object() || entry.size() != 1) { + return result; + } + const auto *entry_type{entry.try_at("type")}; + if (!entry_type) { + return result; + } + if (entry_type->is_string()) { + if (entry_type->to_string() != "any") { + result.insert(entry_type->to_string()); + } + return result; + } + if (!entry_type->is_array()) { + return result; + } + for (const auto &type_entry : entry_type->as_array()) { + if (!type_entry.is_string() || type_entry.to_string() == "any") { + return std::unordered_set{}; + } + result.insert(type_entry.to_string()); + } + return result; + } +}; diff --git a/vendor/blaze/src/alterschema/common/double_negation_elimination.h b/vendor/blaze/src/alterschema/common/double_negation_elimination.h index f00f2d104..50b86fb8e 100644 --- a/vendor/blaze/src/alterschema/common/double_negation_elimination.h +++ b/vendor/blaze/src/alterschema/common/double_negation_elimination.h @@ -24,12 +24,14 @@ class DoubleNegationElimination final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_object() && - schema.at(KEYWORD).size() == 1 && - schema.at(KEYWORD).defines(KEYWORD) && - !(schema.at(KEYWORD).at(KEYWORD).is_boolean() && - !schema.at(KEYWORD).at(KEYWORD).to_boolean())); + schema.is_object()); + + const auto *outer_not{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(outer_not && outer_not->is_object() && + outer_not->size() == 1); + const auto *inner_not{outer_not->try_at(KEYWORD)}; + ONLY_CONTINUE_IF(inner_not && + !(inner_not->is_boolean() && !inner_not->to_boolean())); ONLY_CONTINUE_IF( !(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, diff --git a/vendor/blaze/src/alterschema/common/draft_official_dialect_with_https.h b/vendor/blaze/src/alterschema/common/draft_official_dialect_with_https.h index 0f1540c0c..f3eb13a2d 100644 --- a/vendor/blaze/src/alterschema/common/draft_official_dialect_with_https.h +++ b/vendor/blaze/src/alterschema/common/draft_official_dialect_with_https.h @@ -30,9 +30,10 @@ class DraftOfficialDialectWithHttps final : public SchemaTransformRule { location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_2_Hyper || location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_1_Hyper || location.base_dialect == SchemaBaseDialect::JSON_Schema_Draft_0_Hyper); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$schema") && - schema.at("$schema").is_string()); - const auto &dialect{schema.at("$schema").to_string()}; + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; ONLY_CONTINUE_IF(dialect.starts_with("https://json-schema.org/")); ONLY_CONTINUE_IF( dialect == "https://json-schema.org/draft-07/schema" || diff --git a/vendor/blaze/src/alterschema/common/draft_official_dialect_without_empty_fragment.h b/vendor/blaze/src/alterschema/common/draft_official_dialect_without_empty_fragment.h index 22950766a..dab56c1f2 100644 --- a/vendor/blaze/src/alterschema/common/draft_official_dialect_without_empty_fragment.h +++ b/vendor/blaze/src/alterschema/common/draft_official_dialect_without_empty_fragment.h @@ -16,9 +16,10 @@ class DraftOfficialDialectWithoutEmptyFragment final const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$schema") && - schema.at("$schema").is_string()); - const auto &dialect{schema.at("$schema").to_string()}; + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; ONLY_CONTINUE_IF( dialect == "http://json-schema.org/draft-07/schema" || dialect == "http://json-schema.org/draft-07/hyper-schema" || diff --git a/vendor/blaze/src/alterschema/common/draft_ref_siblings.h b/vendor/blaze/src/alterschema/common/draft_ref_siblings.h index 938407287..5559f7916 100644 --- a/vendor/blaze/src/alterschema/common/draft_ref_siblings.h +++ b/vendor/blaze/src/alterschema/common/draft_ref_siblings.h @@ -16,14 +16,15 @@ class DraftRefSiblings final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &walker, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3, - Vocabularies::Known::JSON_Schema_Draft_2, - Vocabularies::Known::JSON_Schema_Draft_1, - Vocabularies::Known::JSON_Schema_Draft_0})); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$ref")); std::vector locations; diff --git a/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h b/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h index 346844fa8..f256f03dd 100644 --- a/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h +++ b/vendor/blaze/src/alterschema/common/drop_allof_empty_schemas.h @@ -18,11 +18,10 @@ class DropAllOfEmptySchemas final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4})); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("allOf") && - schema.at("allOf").is_array() && - !schema.at("allOf").empty()); - ONLY_CONTINUE_IF( - std::ranges::any_of(schema.at("allOf").as_array(), is_empty_schema)); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *all_of{schema.try_at("allOf")}; + ONLY_CONTINUE_IF(all_of && all_of->is_array() && !all_of->empty()); + ONLY_CONTINUE_IF(std::ranges::any_of(all_of->as_array(), is_empty_schema)); return APPLIES_TO_KEYWORDS("allOf"); } diff --git a/vendor/blaze/src/alterschema/common/drop_extends_empty_schemas.h b/vendor/blaze/src/alterschema/common/drop_extends_empty_schemas.h new file mode 100644 index 000000000..8ec9dd8d6 --- /dev/null +++ b/vendor/blaze/src/alterschema/common/drop_extends_empty_schemas.h @@ -0,0 +1,69 @@ +class DropExtendsEmptySchemas final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DropExtendsEmptySchemas() + : SchemaTransformRule{ + "drop_extends_empty_schemas", + "Empty schemas in `extends` are redundant and can be removed"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"extends"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *extends{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(extends); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + if (sourcemeta::core::is_empty_schema(*extends)) { + return APPLIES_TO_POINTERS({Pointer{KEYWORD}}); + } + + if (extends->is_array() && !extends->empty()) { + std::vector locations; + for (std::size_t index = 0; index < extends->size(); ++index) { + if (sourcemeta::core::is_empty_schema(extends->at(index))) { + locations.push_back(Pointer{KEYWORD, index}); + } + } + ONLY_CONTINUE_IF(!locations.empty()); + return APPLIES_TO_POINTERS(std::move(locations)); + } + + return false; + } + + auto transform(JSON &schema, const Result &result) const -> void override { + if (result.locations.size() == 1 && result.locations.at(0).size() == 1) { + schema.erase("extends"); + return; + } + + auto new_extends{JSON::make_array()}; + for (const auto &entry : schema.at("extends").as_array()) { + if (!sourcemeta::core::is_empty_schema(entry)) { + new_extends.push_back(entry); + } + } + + if (new_extends.empty()) { + schema.erase("extends"); + } else { + schema.assign("extends", std::move(new_extends)); + } + } +}; diff --git a/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h b/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h index da5921e0f..3ee911be0 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h +++ b/vendor/blaze/src/alterschema/common/duplicate_allof_branches.h @@ -25,9 +25,10 @@ class DuplicateAllOfBranches final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("allOf") && - schema.at("allOf").is_array() && - !schema.at("allOf").unique()); + schema.is_object()); + + const auto *all_of{schema.try_at("allOf")}; + ONLY_CONTINUE_IF(all_of && all_of->is_array() && !all_of->unique()); // TODO: Highlight which specific entries in `allOf` are duplicated return APPLIES_TO_KEYWORDS("allOf"); } diff --git a/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h b/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h index 3f0e5b4a9..678d47237 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h +++ b/vendor/blaze/src/alterschema/common/duplicate_anyof_branches.h @@ -25,9 +25,10 @@ class DuplicateAnyOfBranches final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("anyOf") && - schema.at("anyOf").is_array() && - !schema.at("anyOf").unique()); + schema.is_object()); + + const auto *any_of{schema.try_at("anyOf")}; + ONLY_CONTINUE_IF(any_of && any_of->is_array() && !any_of->unique()); // TODO: Highlight which specific entries in `anyOf` are duplicated return APPLIES_TO_KEYWORDS("anyOf"); } diff --git a/vendor/blaze/src/alterschema/common/duplicate_enum_values.h b/vendor/blaze/src/alterschema/common/duplicate_enum_values.h index 7246b5575..165071be6 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_enum_values.h +++ b/vendor/blaze/src/alterschema/common/duplicate_enum_values.h @@ -23,11 +23,14 @@ class DuplicateEnumValues final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1}) && - schema.is_object() && schema.defines("enum") && - schema.at("enum").is_array() && - !schema.at("enum").unique()); + schema.is_object()); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + !enum_value->unique()); // TODO: Highlight which specific entries in `enum` are duplicated return APPLIES_TO_KEYWORDS("enum"); } diff --git a/vendor/blaze/src/alterschema/common/duplicate_required_values.h b/vendor/blaze/src/alterschema/common/duplicate_required_values.h index 6d1d7d07d..ecf7347f0 100644 --- a/vendor/blaze/src/alterschema/common/duplicate_required_values.h +++ b/vendor/blaze/src/alterschema/common/duplicate_required_values.h @@ -23,9 +23,10 @@ class DuplicateRequiredValues final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("required") && - schema.at("required").is_array() && - !schema.at("required").unique()); + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array() && !required->unique()); // TODO: Highlight which specific entries in `required` are duplicated return APPLIES_TO_KEYWORDS("required"); } diff --git a/vendor/blaze/src/alterschema/common/dynamic_ref_to_static_ref.h b/vendor/blaze/src/alterschema/common/dynamic_ref_to_static_ref.h index 499d03763..481298286 100644 --- a/vendor/blaze/src/alterschema/common/dynamic_ref_to_static_ref.h +++ b/vendor/blaze/src/alterschema/common/dynamic_ref_to_static_ref.h @@ -14,7 +14,7 @@ class DynamicRefToStaticRef final : public SchemaTransformRule { [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, - const sourcemeta::core::JSON &, + const sourcemeta::core::JSON &root, const sourcemeta::core::Vocabularies &vocabularies, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, @@ -27,24 +27,82 @@ class DynamicRefToStaticRef final : public SchemaTransformRule { schema.defines("$dynamicRef")) { auto reference_pointer{location.pointer}; reference_pointer.push_back(std::cref(KEYWORD_DYNAMIC_REF)); - const auto reference_entry{frame.reference( + + auto reference_entry{frame.reference( sourcemeta::core::SchemaReferenceType::Static, reference_pointer)}; - if (reference_entry.has_value()) { - this->keyword_ = &KEYWORD_DYNAMIC_REF; - return APPLIES_TO_KEYWORDS("$dynamicRef"); + if (!reference_entry.has_value()) { + reference_entry = frame.reference( + sourcemeta::core::SchemaReferenceType::Dynamic, reference_pointer); + } + if (!reference_entry.has_value()) { + return false; + } + + const auto destination{ + frame.traverse(reference_entry->get().destination)}; + if (!destination.has_value()) { + return false; } + + if (destination->get().type == + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + const auto &subschema{sourcemeta::core::get( + root, sourcemeta::core::to_pointer(destination->get().pointer))}; + if (subschema.is_object()) { + const auto *dynamic_anchor{subschema.try_at("$dynamicAnchor")}; + if (dynamic_anchor != nullptr && dynamic_anchor->is_string()) { + const auto &destination_uri{reference_entry->get().destination}; + const auto fragment_position{destination_uri.find('#')}; + const std::string_view fragment{ + fragment_position == std::string::npos + ? std::string_view{destination_uri} + : std::string_view{ + destination_uri.data() + fragment_position + 1, + destination_uri.size() - fragment_position - 1}}; + if (fragment == dynamic_anchor->to_string()) { + return false; + } + } + } + } + + this->keyword_ = &KEYWORD_DYNAMIC_REF; + return APPLIES_TO_KEYWORDS("$dynamicRef"); } if (vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && schema.defines("$recursiveRef")) { auto reference_pointer{location.pointer}; reference_pointer.push_back(std::cref(KEYWORD_RECURSIVE_REF)); - const auto reference_entry{frame.reference( + + auto reference_entry{frame.reference( sourcemeta::core::SchemaReferenceType::Static, reference_pointer)}; - if (reference_entry.has_value()) { - this->keyword_ = &KEYWORD_RECURSIVE_REF; - return APPLIES_TO_KEYWORDS("$recursiveRef"); + if (!reference_entry.has_value()) { + reference_entry = frame.reference( + sourcemeta::core::SchemaReferenceType::Dynamic, reference_pointer); + } + if (!reference_entry.has_value()) { + return false; + } + + const auto destination{ + frame.traverse(reference_entry->get().destination)}; + if (!destination.has_value()) { + return false; } + + const auto &subschema{sourcemeta::core::get( + root, sourcemeta::core::to_pointer(destination->get().pointer))}; + if (subschema.is_object()) { + const auto *recursive_anchor{subschema.try_at("$recursiveAnchor")}; + if (recursive_anchor != nullptr && recursive_anchor->is_boolean() && + recursive_anchor->to_boolean()) { + return false; + } + } + + this->keyword_ = &KEYWORD_RECURSIVE_REF; + return APPLIES_TO_KEYWORDS("$recursiveRef"); } return false; diff --git a/vendor/blaze/src/alterschema/common/enum_with_type.h b/vendor/blaze/src/alterschema/common/enum_with_type.h index 6e0031a4d..2307456a3 100644 --- a/vendor/blaze/src/alterschema/common/enum_with_type.h +++ b/vendor/blaze/src/alterschema/common/enum_with_type.h @@ -24,18 +24,57 @@ class EnumWithType final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("type") && - schema.defines("enum") && schema.at("enum").is_array()); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); - const auto current_types{parse_schema_type(schema.at("type"))}; + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})) { + if (type->is_string() && type->to_string() == "any") { + return APPLIES_TO_KEYWORDS("enum", "type"); + } + + if (type->is_array()) { + bool has_tautology{false}; + bool has_unknown_subschema{false}; + for (const auto &entry : type->as_array()) { + if (entry.is_string() && entry.to_string() == "any") { + has_tautology = true; + break; + } + if (entry.is_object()) { + if (entry.empty()) { + has_tautology = true; + break; + } + has_unknown_subschema = true; + } + } + + if (has_tautology) { + return APPLIES_TO_KEYWORDS("enum", "type"); + } + + if (has_unknown_subschema) { + return false; + } + } + } + + const auto current_types{parse_schema_type(*type)}; + ONLY_CONTINUE_IF(current_types.any()); const bool integer_matches_integral{ vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_7}) && current_types.test(std::to_underlying(JSON::Type::Integer))}; ONLY_CONTINUE_IF(std::ranges::all_of( - schema.at("enum").as_array(), + enum_value->as_array(), [¤t_types, integer_matches_integral](const auto &item) { return current_types.test(std::to_underlying(item.type())) || (integer_matches_integral && item.is_integral()); diff --git a/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h b/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h index 5b16a16b2..b73d0d801 100644 --- a/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h +++ b/vendor/blaze/src/alterschema/common/equal_numeric_bounds_to_enum.h @@ -17,31 +17,36 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1, + Vocabularies::Known::JSON_Schema_Draft_0}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF( - vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3, - Vocabularies::Known::JSON_Schema_Draft_2, - Vocabularies::Known::JSON_Schema_Draft_1, - Vocabularies::Known::JSON_Schema_Draft_0}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "integer" || - schema.at("type").to_string() == "number") && - schema.defines("minimum") && schema.at("minimum").is_number() && - schema.defines("maximum") && schema.at("maximum").is_number() && - schema.at("minimum") == schema.at("maximum") && - !(schema.defines("exclusiveMinimum") && - schema.at("exclusiveMinimum").is_boolean() && - schema.at("exclusiveMinimum").to_boolean()) && - !(schema.defines("exclusiveMaximum") && - schema.at("exclusiveMaximum").is_boolean() && - schema.at("exclusiveMaximum").to_boolean()) && - !(schema.defines("minimumCanEqual") && - schema.at("minimumCanEqual").is_boolean() && - !schema.at("minimumCanEqual").to_boolean()) && - !(schema.defines("maximumCanEqual") && - schema.at("maximumCanEqual").is_boolean() && - !schema.at("maximumCanEqual").to_boolean())); + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(!(exclusive_minimum && exclusive_minimum->is_boolean() && + exclusive_minimum->to_boolean())); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(!(exclusive_maximum && exclusive_maximum->is_boolean() && + exclusive_maximum->to_boolean())); + const auto *minimum_can_equal{schema.try_at("minimumCanEqual")}; + ONLY_CONTINUE_IF(!(minimum_can_equal && minimum_can_equal->is_boolean() && + !minimum_can_equal->to_boolean())); + const auto *maximum_can_equal{schema.try_at("maximumCanEqual")}; + ONLY_CONTINUE_IF(!(maximum_can_equal && maximum_can_equal->is_boolean() && + !maximum_can_equal->to_boolean())); return APPLIES_TO_KEYWORDS("minimum", "maximum"); } diff --git a/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h b/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h new file mode 100644 index 000000000..ab0862d19 --- /dev/null +++ b/vendor/blaze/src/alterschema/common/exclusive_bounds_false_drop.h @@ -0,0 +1,52 @@ +class ExclusiveBoundsFalseDrop final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + ExclusiveBoundsFalseDrop() + : SchemaTransformRule{ + "exclusive_bounds_false_drop", + "Setting `exclusiveMinimum` or `exclusiveMaximum` to `false` " + "adds no constraint"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF( + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + + std::vector locations; + const auto *exclusive_min{schema.try_at("exclusiveMinimum")}; + if (exclusive_min && exclusive_min->is_boolean() && + !exclusive_min->to_boolean()) { + locations.push_back(Pointer{"exclusiveMinimum"}); + } + const auto *exclusive_max{schema.try_at("exclusiveMaximum")}; + if (exclusive_max && exclusive_max->is_boolean() && + !exclusive_max->to_boolean()) { + locations.push_back(Pointer{"exclusiveMaximum"}); + } + + ONLY_CONTINUE_IF(!locations.empty()); + return APPLIES_TO_POINTERS(std::move(locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + for (const auto &location : result.locations) { + schema.erase(location.at(0).to_property()); + } + } +}; diff --git a/vendor/blaze/src/alterschema/common/exclusive_maximum_number_and_maximum.h b/vendor/blaze/src/alterschema/common/exclusive_maximum_number_and_maximum.h index cd44c6ace..1e226301b 100644 --- a/vendor/blaze/src/alterschema/common/exclusive_maximum_number_and_maximum.h +++ b/vendor/blaze/src/alterschema/common/exclusive_maximum_number_and_maximum.h @@ -22,10 +22,12 @@ class ExclusiveMaximumNumberAndMaximum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("maximum") && - schema.defines("exclusiveMaximum") && - schema.at("maximum").is_number() && - schema.at("exclusiveMaximum").is_number()); + schema.is_object()); + + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number()); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(exclusive_maximum && exclusive_maximum->is_number()); return APPLIES_TO_KEYWORDS("exclusiveMaximum", "maximum"); } diff --git a/vendor/blaze/src/alterschema/common/exclusive_minimum_number_and_minimum.h b/vendor/blaze/src/alterschema/common/exclusive_minimum_number_and_minimum.h index 58a5626ed..3e940564f 100644 --- a/vendor/blaze/src/alterschema/common/exclusive_minimum_number_and_minimum.h +++ b/vendor/blaze/src/alterschema/common/exclusive_minimum_number_and_minimum.h @@ -22,10 +22,12 @@ class ExclusiveMinimumNumberAndMinimum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("minimum") && - schema.defines("exclusiveMinimum") && - schema.at("minimum").is_number() && - schema.at("exclusiveMinimum").is_number()); + schema.is_object()); + + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(exclusive_minimum && exclusive_minimum->is_number()); return APPLIES_TO_KEYWORDS("exclusiveMinimum", "minimum"); } diff --git a/vendor/blaze/src/alterschema/common/flatten_nested_extends.h b/vendor/blaze/src/alterschema/common/flatten_nested_extends.h new file mode 100644 index 000000000..4b6deb4ed --- /dev/null +++ b/vendor/blaze/src/alterschema/common/flatten_nested_extends.h @@ -0,0 +1,121 @@ +class FlattenNestedExtends final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + FlattenNestedExtends() + : SchemaTransformRule{ + "flatten_nested_extends", + "An `extends` branch that only contains another `extends` can " + "be flattened into the parent `extends`"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"extends"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + this->flatten_indices_.clear(); + const auto &branches{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < branches.size(); ++index) { + const auto &branch{branches.at(index)}; + if (branch.is_object() && branch.size() == 1 && branch.defines(KEYWORD) && + branch.at(KEYWORD).is_array()) { + this->flatten_indices_.push_back(index); + } + } + + ONLY_CONTINUE_IF(!this->flatten_indices_.empty()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return APPLIES_TO_KEYWORDS(KEYWORD); + } + + auto transform(JSON &schema, const Result &) const -> void override { + static const JSON::String KEYWORD{"extends"}; + this->index_mapping_.clear(); + const auto &original{schema.at(KEYWORD)}; + auto result{JSON::make_array()}; + std::size_t new_index{0}; + std::size_t flatten_cursor{0}; + + for (std::size_t index = 0; index < original.size(); ++index) { + if (flatten_cursor < this->flatten_indices_.size() && + this->flatten_indices_[flatten_cursor] == index) { + this->collect_leaves_(original.at(index), KEYWORD, index, result, + new_index); + ++flatten_cursor; + } else { + this->index_mapping_.emplace_back(index, std::nullopt, new_index); + result.push_back(original.at(index)); + ++new_index; + } + } + + schema.assign(KEYWORD, std::move(result)); + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + static const JSON::String KEYWORD{"extends"}; + const auto prefix{current.concat({KEYWORD})}; + if (!target.starts_with(prefix)) { + return target; + } + const auto relative{target.resolve_from(prefix)}; + if (relative.empty() || !relative.at(0).is_index()) { + return target; + } + const auto old_index{relative.at(0).to_index()}; + for (const auto &[outer, inner, mapped] : this->index_mapping_) { + if (outer == old_index && inner.has_value()) { + const Pointer old_prefix{ + prefix.concat({old_index, KEYWORD, inner.value()})}; + if (target.starts_with(old_prefix)) { + const Pointer new_prefix{prefix.concat({mapped})}; + return target.rebase(old_prefix, new_prefix); + } + } else if (outer == old_index) { + const Pointer old_prefix{prefix.concat({old_index})}; + const Pointer new_prefix{prefix.concat({mapped})}; + return target.rebase(old_prefix, new_prefix); + } + } + return target; + } + +private: + auto collect_leaves_(const JSON &node, const JSON::String &keyword, + std::size_t outer_index, JSON &result, + std::size_t &new_index) const -> void { + const auto &inner{node.at(keyword)}; + for (std::size_t inner_index = 0; inner_index < inner.size(); + ++inner_index) { + const auto &child{inner.at(inner_index)}; + if (child.is_object() && child.size() == 1 && child.defines(keyword) && + child.at(keyword).is_array()) { + this->collect_leaves_(child, keyword, outer_index, result, new_index); + } else { + this->index_mapping_.emplace_back(outer_index, inner_index, new_index); + result.push_back(child); + ++new_index; + } + } + } + + mutable std::vector flatten_indices_; + mutable std::vector< + std::tuple, std::size_t>> + index_mapping_; +}; diff --git a/vendor/blaze/src/alterschema/common/ignored_metaschema.h b/vendor/blaze/src/alterschema/common/ignored_metaschema.h index 1d37d6bf5..24766cc58 100644 --- a/vendor/blaze/src/alterschema/common/ignored_metaschema.h +++ b/vendor/blaze/src/alterschema/common/ignored_metaschema.h @@ -17,8 +17,9 @@ class IgnoredMetaschema final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$schema") && - schema.at("$schema").is_string()); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); const auto dialect{sourcemeta::core::dialect(schema)}; ONLY_CONTINUE_IF(!dialect.empty()); ONLY_CONTINUE_IF(dialect != location.dialect); diff --git a/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h b/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h index ac99fab38..6329a75b1 100644 --- a/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h +++ b/vendor/blaze/src/alterschema/common/maximum_real_for_integer.h @@ -24,15 +24,17 @@ class MaximumRealForInteger final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("maximum") && - (schema.at("maximum").is_real() || - (schema.at("maximum").is_decimal() && - !schema.at("maximum").to_decimal().is_integer()))); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && + !maximum->is_integral()); return APPLIES_TO_KEYWORDS("maximum"); } diff --git a/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h b/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h index 20180fa02..d142cd7e7 100644 --- a/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h +++ b/vendor/blaze/src/alterschema/common/minimum_real_for_integer.h @@ -24,15 +24,17 @@ class MinimumRealForInteger final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - schema.at("type").to_string() == "integer" && - schema.defines("minimum") && - (schema.at("minimum").is_real() || - (schema.at("minimum").is_decimal() && - !schema.at("minimum").to_decimal().is_integer()))); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number() && + !minimum->is_integral()); return APPLIES_TO_KEYWORDS("minimum"); } diff --git a/vendor/blaze/src/alterschema/common/modern_official_dialect_with_empty_fragment.h b/vendor/blaze/src/alterschema/common/modern_official_dialect_with_empty_fragment.h index 849d70bca..e3e8daf3c 100644 --- a/vendor/blaze/src/alterschema/common/modern_official_dialect_with_empty_fragment.h +++ b/vendor/blaze/src/alterschema/common/modern_official_dialect_with_empty_fragment.h @@ -17,9 +17,10 @@ class ModernOfficialDialectWithEmptyFragment final const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$schema") && - schema.at("$schema").is_string()); - const auto &dialect{schema.at("$schema").to_string()}; + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; ONLY_CONTINUE_IF( dialect == "https://json-schema.org/draft/2019-09/schema#" || dialect == "https://json-schema.org/draft/2019-09/hyper-schema#" || diff --git a/vendor/blaze/src/alterschema/common/modern_official_dialect_with_http.h b/vendor/blaze/src/alterschema/common/modern_official_dialect_with_http.h index facad8625..b98b62252 100644 --- a/vendor/blaze/src/alterschema/common/modern_official_dialect_with_http.h +++ b/vendor/blaze/src/alterschema/common/modern_official_dialect_with_http.h @@ -23,9 +23,10 @@ class ModernOfficialDialectWithHttp final : public SchemaTransformRule { location.base_dialect == SchemaBaseDialect::JSON_Schema_2020_12_Hyper || location.base_dialect == SchemaBaseDialect::JSON_Schema_2019_09 || location.base_dialect == SchemaBaseDialect::JSON_Schema_2019_09_Hyper); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("$schema") && - schema.at("$schema").is_string()); - const auto &dialect{schema.at("$schema").to_string()}; + ONLY_CONTINUE_IF(schema.is_object()); + const auto *schema_keyword{schema.try_at("$schema")}; + ONLY_CONTINUE_IF(schema_keyword && schema_keyword->is_string()); + const auto &dialect{schema_keyword->to_string()}; ONLY_CONTINUE_IF(dialect.starts_with("http://json-schema.org/")); ONLY_CONTINUE_IF( dialect == "http://json-schema.org/draft/2020-12/schema" || diff --git a/vendor/blaze/src/alterschema/common/non_applicable_additional_items.h b/vendor/blaze/src/alterschema/common/non_applicable_additional_items.h index 7d6d356a2..90d4c7a4d 100644 --- a/vendor/blaze/src/alterschema/common/non_applicable_additional_items.h +++ b/vendor/blaze/src/alterschema/common/non_applicable_additional_items.h @@ -25,14 +25,16 @@ class NonApplicableAdditionalItems final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3}) && + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && schema.is_object() && schema.defines(KEYWORD)); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); - if (schema.defines("items") && is_schema(schema.at("items"))) { + const auto *items{schema.try_at("items")}; + if (items && is_schema(*items)) { return APPLIES_TO_KEYWORDS(KEYWORD, "items"); - } else if (!schema.defines("items")) { + } else if (!items) { return APPLIES_TO_KEYWORDS(KEYWORD); } else { return false; diff --git a/vendor/blaze/src/alterschema/common/non_applicable_disallow_types.h b/vendor/blaze/src/alterschema/common/non_applicable_disallow_types.h new file mode 100644 index 000000000..f70963b98 --- /dev/null +++ b/vendor/blaze/src/alterschema/common/non_applicable_disallow_types.h @@ -0,0 +1,98 @@ +class NonApplicableDisallowTypes final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + NonApplicableDisallowTypes() + : SchemaTransformRule{ + "non_applicable_disallow_types", + "`disallow` entries whose type cannot overlap with the parent " + "`type` can never match and can be dropped"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"disallow"}; + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && !disallow->empty()); + + const auto *parent_type_value{schema.try_at("type")}; + ONLY_CONTINUE_IF(parent_type_value && + is_known_type_form(*parent_type_value)); + + const auto parent_types{parse_schema_type(*parent_type_value)}; + ONLY_CONTINUE_IF(parent_types.any()); + + std::vector locations; + for (std::size_t index = 0; index < disallow->size(); ++index) { + const auto &entry{disallow->at(index)}; + JSON::TypeSet entry_types; + if (entry.is_string() && entry.to_string() != "any") { + entry_types = parse_schema_type(entry); + } else if (entry.is_object()) { + const auto *entry_type{entry.try_at("type")}; + if (entry_type && is_known_type_form(*entry_type)) { + entry_types = parse_schema_type(*entry_type); + } + } + + if (entry_types.any() && (parent_types & entry_types).none()) { + locations.push_back(Pointer{KEYWORD, index}); + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + + auto keyword_pointer{location.pointer}; + keyword_pointer.push_back(std::cref(KEYWORD)); + ONLY_CONTINUE_IF(!frame.has_references_through(keyword_pointer)); + + return APPLIES_TO_POINTERS(std::move(locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + std::vector dead_indices; + dead_indices.reserve(result.locations.size()); + for (const auto &location : result.locations) { + assert(location.size() == 2); + dead_indices.push_back(location.at(1).to_index()); + } + + auto new_disallow{JSON::make_array()}; + const auto &disallow{schema.at("disallow")}; + for (std::size_t index = 0; index < disallow.size(); ++index) { + if (std::ranges::find(dead_indices, index) == dead_indices.end()) { + new_disallow.push_back(disallow.at(index)); + } + } + + if (new_disallow.empty()) { + schema.erase("disallow"); + } else { + schema.assign("disallow", std::move(new_disallow)); + } + } + +private: + static auto is_known_type_form(const sourcemeta::core::JSON &type) -> bool { + if (type.is_string()) { + return type.to_string() != "any"; + } + if (!type.is_array()) { + return false; + } + return std::ranges::all_of(type.as_array(), [](const auto &entry) { + return entry.is_string() && entry.to_string() != "any"; + }); + } +}; diff --git a/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h b/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h index 249f33ddd..e1076d2ce 100644 --- a/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h +++ b/vendor/blaze/src/alterschema/common/non_applicable_enum_validation_keywords.h @@ -28,16 +28,22 @@ class NonApplicableEnumValidationKeywords final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_2_Hyper, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_1_Hyper}) && - schema.is_object() && schema.defines("enum") && - schema.at("enum").is_array() && !schema.defines("type")); + schema.is_object() && !schema.defines("type")); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array()); sourcemeta::core::JSON::TypeSet enum_types; - for (const auto &value : schema.at("enum").as_array()) { + for (const auto &value : enum_value->as_array()) { enum_types.set(std::to_underlying(value.type())); } ONLY_CONTINUE_IF(enum_types.any()); + const bool is_draft3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + std::vector positions; for (const auto &entry : schema.as_object()) { const auto &metadata = walker(entry.first, vocabularies); @@ -47,6 +53,10 @@ class NonApplicableEnumValidationKeywords final : public SchemaTransformRule { continue; } + if (is_draft3 && entry.first == "required" && entry.second.is_boolean()) { + continue; + } + // Check if there's any overlap between keyword's applicable types and // enum types if ((metadata.instances & enum_types).none()) { diff --git a/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h b/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h index d67e15817..edab3ec0c 100644 --- a/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h +++ b/vendor/blaze/src/alterschema/common/non_applicable_type_specific_keywords.h @@ -18,6 +18,7 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { -> SchemaTransformRule::Result override { ONLY_CONTINUE_IF(schema.is_object()); + const auto *type_value{schema.try_at("type")}; auto current_types{vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, @@ -31,8 +32,8 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_1_Hyper, Vocabularies::Known::JSON_Schema_Draft_0, Vocabularies::Known::JSON_Schema_Draft_0_Hyper}) && - schema.defines("type") - ? parse_schema_type(schema.at("type")) + type_value + ? parse_schema_type(*type_value) : sourcemeta::core::JSON::TypeSet{}}; if (vocabularies.contains_any( @@ -43,10 +44,12 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_2, - Vocabularies::Known::JSON_Schema_Draft_1}) && - schema.defines("enum") && schema.at("enum").is_array()) { - for (const auto &entry : schema.at("enum").as_array()) { - current_types.set(std::to_underlying(entry.type())); + Vocabularies::Known::JSON_Schema_Draft_1})) { + const auto *enum_value{schema.try_at("enum")}; + if (enum_value && enum_value->is_array()) { + for (const auto &entry : enum_value->as_array()) { + current_types.set(std::to_underlying(entry.type())); + } } } @@ -54,9 +57,11 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.defines("const")) { - current_types.set(std::to_underlying(schema.at("const").type())); + Vocabularies::Known::JSON_Schema_Draft_6})) { + const auto *const_value{schema.try_at("const")}; + if (const_value) { + current_types.set(std::to_underlying(const_value->type())); + } } // This means that the schema has no explicit type constraints, diff --git a/vendor/blaze/src/alterschema/common/oneof_false_simplify.h b/vendor/blaze/src/alterschema/common/oneof_false_simplify.h index 258700f61..befd95f4b 100644 --- a/vendor/blaze/src/alterschema/common/oneof_false_simplify.h +++ b/vendor/blaze/src/alterschema/common/oneof_false_simplify.h @@ -22,11 +22,12 @@ class OneOfFalseSimplify final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines(KEYWORD) && - !schema.defines("not") && schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() == 1); + schema.is_object() && !schema.defines("not")); - const auto &entry{schema.at(KEYWORD).front()}; + const auto *one_of{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(one_of && one_of->is_array() && one_of->size() == 1); + + const auto &entry{one_of->front()}; ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); diff --git a/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h b/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h index 2e403cf77..a500acf10 100644 --- a/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h +++ b/vendor/blaze/src/alterschema/common/oneof_to_anyof_disjoint_types.h @@ -24,9 +24,11 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_array() && - schema.at(KEYWORD).size() > 1); + schema.is_object()); + + const auto *oneof_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(oneof_value && oneof_value->is_array() && + oneof_value->size() > 1); const auto has_validation_vocabulary{vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, @@ -34,7 +36,6 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})}; @@ -44,27 +45,30 @@ class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6})}; - const auto &oneof{schema.at(KEYWORD)}; std::vector type_sets; - type_sets.reserve(oneof.size()); + type_sets.reserve(oneof_value->size()); - for (const auto &branch : oneof.as_array()) { + for (const auto &branch : oneof_value->as_array()) { ONLY_CONTINUE_IF(branch.is_object()); - const auto has_type{branch.defines("type")}; - const auto has_const{has_const_vocabulary && branch.defines("const")}; - const auto has_enum{has_validation_vocabulary && branch.defines("enum") && - branch.at("enum").is_array()}; + const auto *type_value{branch.try_at("type")}; + const auto *const_value{has_const_vocabulary ? branch.try_at("const") + : nullptr}; + const auto *enum_value{has_validation_vocabulary ? branch.try_at("enum") + : nullptr}; + const auto has_enum{enum_value && enum_value->is_array()}; - if (has_type) { - type_sets.push_back(parse_schema_type(branch.at("type"))); - } else if (has_const && !has_enum) { + if (type_value) { + const auto branch_types{parse_schema_type(*type_value)}; + ONLY_CONTINUE_IF(branch_types.any()); + type_sets.push_back(branch_types); + } else if (const_value && !has_enum) { JSON::TypeSet branch_types; - branch_types.set(std::to_underlying(branch.at("const").type())); + branch_types.set(std::to_underlying(const_value->type())); type_sets.push_back(branch_types); - } else if (has_enum && !has_const) { + } else if (has_enum && !const_value) { JSON::TypeSet branch_types; - for (const auto &item : branch.at("enum").as_array()) { + for (const auto &item : enum_value->as_array()) { branch_types.set(std::to_underlying(item.type())); } type_sets.push_back(branch_types); diff --git a/vendor/blaze/src/alterschema/common/orphan_definitions.h b/vendor/blaze/src/alterschema/common/orphan_definitions.h index fe7a7faf6..faed95009 100644 --- a/vendor/blaze/src/alterschema/common/orphan_definitions.h +++ b/vendor/blaze/src/alterschema/common/orphan_definitions.h @@ -52,16 +52,36 @@ class OrphanDefinitions final : public SchemaTransformRule { schema.at(container).erase(pointer.at(1).to_property()); } - if (schema.defines("$defs") && schema.at("$defs").empty()) { + const auto *defs{schema.try_at("$defs")}; + if (defs && defs->empty()) { schema.erase("$defs"); } - if (schema.defines("definitions") && schema.at("definitions").empty()) { + const auto *definitions{schema.try_at("definitions")}; + if (definitions && definitions->empty()) { schema.erase("definitions"); } } private: + static auto + subtree_has_dynamic_anchor(const sourcemeta::core::SchemaFrame &frame, + const WeakPointer &entry_pointer) -> bool { + for (const auto &[key, location] : frame.locations()) { + if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic) { + continue; + } + if (location.type != + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + if (location.pointer.starts_with(entry_pointer)) { + return true; + } + } + return false; + } + static auto has_reachable_reference_through( const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &base, @@ -117,7 +137,9 @@ class OrphanDefinitions final : public SchemaTransformRule { if (entry_location.has_value() && !frame.is_reachable(base, entry_location->get(), walker, resolver) && !has_reachable_reference_through(frame, base, walker, resolver, - absolute_entry_pointer)) { + absolute_entry_pointer) && + !(!frame.standalone() && + subtree_has_dynamic_anchor(frame, absolute_entry_pointer))) { orphans.push_back(Pointer{container, entry.first}); } } diff --git a/vendor/blaze/src/alterschema/common/required_properties_in_properties.h b/vendor/blaze/src/alterschema/common/required_properties_in_properties.h index 00335df08..f4c56142a 100644 --- a/vendor/blaze/src/alterschema/common/required_properties_in_properties.h +++ b/vendor/blaze/src/alterschema/common/required_properties_in_properties.h @@ -29,15 +29,20 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3})) && - schema.is_object() && schema.defines("required") && - schema.at("required").is_array() && !schema.at("required").empty() && - !schema.defines("additionalProperties")); + Vocabularies::Known::JSON_Schema_Draft_4})) && + schema.is_object()); + + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array() && !required->empty()); + + const auto *additional_properties{schema.try_at("additionalProperties")}; + ONLY_CONTINUE_IF(!additional_properties || + (additional_properties->is_boolean() && + additional_properties->to_boolean())); std::vector locations; std::size_t index{0}; - for (const auto &property : schema.at("required").as_array()) { + for (const auto &property : required->as_array()) { if (property.is_string() && !this->defined_in_properties_sibling(schema, property.to_string()) && !WALK_UP_IN_PLACE_APPLICATORS( @@ -72,8 +77,8 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { defined_in_properties_sibling(const JSON &schema, const JSON::String &property) const -> bool { assert(schema.is_object()); - return schema.defines("properties") && - schema.at("properties").is_object() && - schema.at("properties").defines(property); + const auto *properties{schema.try_at("properties")}; + return properties && properties->is_object() && + properties->defines(property); }; }; diff --git a/vendor/blaze/src/alterschema/common/single_type_array.h b/vendor/blaze/src/alterschema/common/single_type_array.h index 310f82d34..5f9b0a61a 100644 --- a/vendor/blaze/src/alterschema/common/single_type_array.h +++ b/vendor/blaze/src/alterschema/common/single_type_array.h @@ -23,13 +23,15 @@ class SingleTypeArray final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_0}) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_array() && - schema.at("type").size() == 1 && - schema.at("type").front().is_string()); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_array() && type->size() == 1 && + type->front().is_string()); return APPLIES_TO_KEYWORDS("type"); } diff --git a/vendor/blaze/src/alterschema/common/unknown_local_ref.h b/vendor/blaze/src/alterschema/common/unknown_local_ref.h index 1901e91a4..7bc76a391 100644 --- a/vendor/blaze/src/alterschema/common/unknown_local_ref.h +++ b/vendor/blaze/src/alterschema/common/unknown_local_ref.h @@ -26,7 +26,8 @@ class UnknownLocalRef final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3})); + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_string()); diff --git a/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h b/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h index 6aed8a6ce..e7f9f9235 100644 --- a/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h +++ b/vendor/blaze/src/alterschema/common/unnecessary_allof_ref_wrapper_draft.h @@ -20,16 +20,15 @@ class UnnecessaryAllOfRefWrapperDraft final : public SchemaTransformRule { vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4})); - ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1 && - schema.defines("allOf") && schema.at("allOf").is_array()); - - const auto &all_of{schema.at("allOf")}; + ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); + const auto *all_of{schema.try_at("allOf")}; + ONLY_CONTINUE_IF(all_of && all_of->is_array()); // In Draft 7 and older, `$ref` overrides sibling keywords, so we can only // elevate it if it is the only keyword of the only branch, and the outer // subschema only declares `allOf` - ONLY_CONTINUE_IF(all_of.size() == 1); - const auto &entry{all_of.at(0)}; + ONLY_CONTINUE_IF(all_of->size() == 1); + const auto &entry{all_of->at(0)}; ONLY_CONTINUE_IF(entry.is_object()); ONLY_CONTINUE_IF(entry.size() == 1 && entry.defines("$ref")); diff --git a/vendor/blaze/src/alterschema/common/unnecessary_extends_ref_wrapper.h b/vendor/blaze/src/alterschema/common/unnecessary_extends_ref_wrapper.h new file mode 100644 index 000000000..bbe81a48f --- /dev/null +++ b/vendor/blaze/src/alterschema/common/unnecessary_extends_ref_wrapper.h @@ -0,0 +1,56 @@ +class UnnecessaryExtendsRefWrapper final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UnnecessaryExtendsRefWrapper() + : SchemaTransformRule{"unnecessary_extends_ref_wrapper", + "Wrapping `$ref` in `extends` is only necessary if " + "there are other sibling keywords"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); + ONLY_CONTINUE_IF(schema.is_object() && schema.size() == 1); + const auto *extends{schema.try_at("extends")}; + ONLY_CONTINUE_IF(extends); + + // In Draft 3, `$ref` overrides sibling keywords, so we can only elevate + // it if it is the only keyword of the only branch, and the outer + // subschema only declares `extends` + if (extends->is_object()) { + ONLY_CONTINUE_IF(extends->size() == 1 && extends->defines("$ref")); + return APPLIES_TO_POINTERS({{"extends", "$ref"}}); + } + + if (extends->is_array()) { + ONLY_CONTINUE_IF(extends->size() == 1); + const auto &branch{extends->at(0)}; + ONLY_CONTINUE_IF(branch.is_object()); + ONLY_CONTINUE_IF(branch.size() == 1 && branch.defines("$ref")); + return APPLIES_TO_POINTERS({{"extends", 0, "$ref"}}); + } + + return false; + } + + auto transform(JSON &schema, const Result &result) const -> void override { + const auto &location{result.locations.at(0)}; + if (location.size() == 3) { + auto value{schema.at("extends").at(0).at("$ref")}; + schema.at("extends").into(std::move(value)); + } else { + auto value{schema.at("extends").at("$ref")}; + schema.at("extends").into(std::move(value)); + } + schema.rename("extends", "$ref"); + } +}; diff --git a/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h b/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h index 2c5fb8373..67220a845 100644 --- a/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h +++ b/vendor/blaze/src/alterschema/common/unsatisfiable_drop_validation.h @@ -20,14 +20,33 @@ class UnsatisfiableDropValidation final : public SchemaTransformRule { {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator, Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("not") && - schema.at("not").is_boolean() && - schema.at("not").to_boolean()); + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const bool is_draft_3{vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})}; + + std::string_view trigger_keyword; + if (is_draft_3) { + const auto *disallow_value{schema.try_at("disallow")}; + if (disallow_value && is_disallow_tautology(*disallow_value)) { + trigger_keyword = "disallow"; + } + } else { + const auto *not_value{schema.try_at("not")}; + if (not_value && sourcemeta::core::is_empty_schema(*not_value)) { + trigger_keyword = "not"; + } + } + + ONLY_CONTINUE_IF(!trigger_keyword.empty()); std::vector positions; for (const auto &entry : schema.as_object()) { - if (entry.first == "not") { + if (entry.first == trigger_keyword) { continue; } @@ -55,6 +74,25 @@ class UnsatisfiableDropValidation final : public SchemaTransformRule { } private: + static auto is_disallow_tautology(const sourcemeta::core::JSON &value) + -> bool { + if (value.is_string()) { + return value.to_string() == "any"; + } + if (sourcemeta::core::is_empty_schema(value)) { + return true; + } + if (value.is_array()) { + return std::ranges::any_of(value.as_array(), [](const auto &entry) { + if (entry.is_string()) { + return entry.to_string() == "any"; + } + return sourcemeta::core::is_empty_schema(entry); + }); + } + return false; + } + static auto is_removable_keyword_type(const SchemaKeywordType type) -> bool { switch (type) { case SchemaKeywordType::Assertion: diff --git a/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h b/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h index 72967d7f0..cd9685a5d 100644 --- a/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h +++ b/vendor/blaze/src/alterschema/common/unsatisfiable_in_place_applicator_type.h @@ -25,10 +25,12 @@ class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_0})); const auto parent_types{parse_schema_type(schema.at("type"))}; + ONLY_CONTINUE_IF(parent_types.any()); std::vector locations; @@ -45,23 +47,33 @@ class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { const auto &branches{entry.second}; for (std::size_t index = 0; index < branches.size(); ++index) { const auto &branch{branches.at(index)}; - if (!branch.is_object() || !branch.defines("type")) { + if (!branch.is_object()) { + continue; + } + const auto *branch_type{branch.try_at("type")}; + if (!branch_type) { continue; } - const auto branch_types{parse_schema_type(branch.at("type"))}; - if ((parent_types & branch_types).none()) { + const auto branch_types{parse_schema_type(*branch_type)}; + if (branch_types.any() && (parent_types & branch_types).none()) { locations.push_back(Pointer{keyword, index}); } } } else if (keyword_type == - SchemaKeywordType::ApplicatorValueInPlaceMaybe) { - if (!entry.second.is_object() || !entry.second.defines("type")) { + SchemaKeywordType::ApplicatorValueInPlaceMaybe || + keyword_type == + SchemaKeywordType::ApplicatorValueInPlaceNegate) { + if (!entry.second.is_object()) { + continue; + } + const auto *branch_type{entry.second.try_at("type")}; + if (!branch_type) { continue; } - const auto branch_types{parse_schema_type(entry.second.at("type"))}; - if ((parent_types & branch_types).none()) { + const auto branch_types{parse_schema_type(*branch_type)}; + if (branch_types.any() && (parent_types & branch_types).none()) { locations.push_back(Pointer{keyword}); } } diff --git a/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h b/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h index beee4defc..077c992d6 100644 --- a/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h +++ b/vendor/blaze/src/alterschema/include/sourcemeta/blaze/alterschema.h @@ -45,6 +45,21 @@ enum class AlterSchemaMode : std::uint8_t { /// are syntax sugar to other keywords, potentially decreasing human /// readability in favor of explicitness Canonicalizer, + + /// Rules that upgrade a JSON Schema document up to JSON Schema Draft 4 + UpgradeDraft4, + + /// Rules that upgrade a JSON Schema document up to JSON Schema Draft 6 + UpgradeDraft6, + + /// Rules that upgrade a JSON Schema document up to JSON Schema Draft 7 + UpgradeDraft7, + + /// Rules that upgrade a JSON Schema document up to JSON Schema 2019-09 + Upgrade201909, + + /// Rules that upgrade a JSON Schema document up to JSON Schema 2020-12 + Upgrade202012, }; /// @ingroup alterschema diff --git a/vendor/blaze/src/alterschema/linter/const_not_in_enum.h b/vendor/blaze/src/alterschema/linter/const_not_in_enum.h index d0245d896..4412c12ab 100644 --- a/vendor/blaze/src/alterschema/linter/const_not_in_enum.h +++ b/vendor/blaze/src/alterschema/linter/const_not_in_enum.h @@ -22,9 +22,13 @@ class ConstNotInEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("const") && - schema.defines("enum") && schema.at("enum").is_array() && - !schema.at("enum").contains(schema.at("const"))); + schema.is_object()); + + const auto *const_value{schema.try_at("const")}; + ONLY_CONTINUE_IF(const_value); + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + !enum_value->contains(*const_value)); return APPLIES_TO_KEYWORDS("const", "enum"); } }; diff --git a/vendor/blaze/src/alterschema/linter/dependencies_default.h b/vendor/blaze/src/alterschema/linter/dependencies_default.h index ced56f8a7..3b7baa8b6 100644 --- a/vendor/blaze/src/alterschema/linter/dependencies_default.h +++ b/vendor/blaze/src/alterschema/linter/dependencies_default.h @@ -20,13 +20,15 @@ class DependenciesDefault final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_object() && schema.at(KEYWORD).empty()); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_object() && + schema.at(KEYWORD).empty()); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); return APPLIES_TO_KEYWORDS(KEYWORD); diff --git a/vendor/blaze/src/alterschema/linter/dependent_required_default.h b/vendor/blaze/src/alterschema/linter/dependent_required_default.h index 5fdcd1127..8e5bdd3fe 100644 --- a/vendor/blaze/src/alterschema/linter/dependent_required_default.h +++ b/vendor/blaze/src/alterschema/linter/dependent_required_default.h @@ -21,9 +21,11 @@ class DependentRequiredDefault final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("dependentRequired") && - schema.at("dependentRequired").is_object() && - schema.at("dependentRequired").empty()); + schema.is_object()); + + const auto *dependent_required{schema.try_at("dependentRequired")}; + ONLY_CONTINUE_IF(dependent_required && dependent_required->is_object() && + dependent_required->empty()); return APPLIES_TO_KEYWORDS("dependentRequired"); } diff --git a/vendor/blaze/src/alterschema/linter/description_trailing_period.h b/vendor/blaze/src/alterschema/linter/description_trailing_period.h index 64dbcdb35..287515857 100644 --- a/vendor/blaze/src/alterschema/linter/description_trailing_period.h +++ b/vendor/blaze/src/alterschema/linter/description_trailing_period.h @@ -24,6 +24,7 @@ class DescriptionTrailingPeriod final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); diff --git a/vendor/blaze/src/alterschema/linter/description_trim.h b/vendor/blaze/src/alterschema/linter/description_trim.h index a3ab17668..5be9dfdd3 100644 --- a/vendor/blaze/src/alterschema/linter/description_trim.h +++ b/vendor/blaze/src/alterschema/linter/description_trim.h @@ -24,6 +24,7 @@ class DescriptionTrim final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); diff --git a/vendor/blaze/src/alterschema/linter/disallow_default.h b/vendor/blaze/src/alterschema/linter/disallow_default.h new file mode 100644 index 000000000..51a812c99 --- /dev/null +++ b/vendor/blaze/src/alterschema/linter/disallow_default.h @@ -0,0 +1,32 @@ +class DisallowDefault final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DisallowDefault() + : SchemaTransformRule{"disallow_default", + "Setting the `disallow` keyword to the empty " + "array does not add any further constraint"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *disallow{schema.try_at("disallow")}; + ONLY_CONTINUE_IF(disallow && disallow->is_array() && disallow->empty()); + return APPLIES_TO_KEYWORDS("disallow"); + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("disallow"); + } +}; diff --git a/vendor/blaze/src/alterschema/linter/divisible_by_default.h b/vendor/blaze/src/alterschema/linter/divisible_by_default.h new file mode 100644 index 000000000..f4eb8c2d0 --- /dev/null +++ b/vendor/blaze/src/alterschema/linter/divisible_by_default.h @@ -0,0 +1,42 @@ +class DivisibleByDefault final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + DivisibleByDefault() + : SchemaTransformRule{ + "divisible_by_default", + "Setting `divisibleBy` to 1 does not add any further constraint"} { + }; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + + const auto *divisible_by{schema.try_at("divisibleBy")}; + ONLY_CONTINUE_IF( + divisible_by && + ((divisible_by->is_integer() && divisible_by->to_integer() == 1) || + (divisible_by->is_real() && divisible_by->to_real() == 1.0) || + (divisible_by->is_decimal() && + divisible_by->to_decimal() == sourcemeta::core::Decimal{1}))); + return APPLIES_TO_KEYWORDS("divisibleBy"); + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("divisibleBy"); + } +}; diff --git a/vendor/blaze/src/alterschema/linter/duplicate_examples.h b/vendor/blaze/src/alterschema/linter/duplicate_examples.h index fadda50ef..eaeca1ea3 100644 --- a/vendor/blaze/src/alterschema/linter/duplicate_examples.h +++ b/vendor/blaze/src/alterschema/linter/duplicate_examples.h @@ -21,9 +21,10 @@ class DuplicateExamples final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Meta_Data, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("examples") && - schema.at("examples").is_array() && - !schema.at("examples").unique()); + schema.is_object()); + + const auto *examples{schema.try_at("examples")}; + ONLY_CONTINUE_IF(examples && examples->is_array() && !examples->unique()); return APPLIES_TO_KEYWORDS("examples"); } diff --git a/vendor/blaze/src/alterschema/linter/else_empty.h b/vendor/blaze/src/alterschema/linter/else_empty.h index c4f64a0b3..cddc20c21 100644 --- a/vendor/blaze/src/alterschema/linter/else_empty.h +++ b/vendor/blaze/src/alterschema/linter/else_empty.h @@ -15,16 +15,18 @@ class ElseEmpty final : public SchemaTransformRule { const SchemaFrame &frame, const SchemaFrame::Location &location, const SchemaWalker &, const SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Applicator, - Vocabularies::Known::JSON_Schema_2019_09_Applicator, - Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines(KEYWORD) && - is_schema(schema.at(KEYWORD)) && is_empty_schema(schema.at(KEYWORD)) && - (schema.at(KEYWORD).is_object() || - (!schema.defines("if") || - !(schema.at("if").is_boolean() && schema.at("if").to_boolean())))); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object()); + + const auto *else_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(else_value && is_schema(*else_value) && + is_empty_schema(*else_value)); + const auto *if_value{schema.try_at("if")}; + ONLY_CONTINUE_IF(else_value->is_object() || !if_value || + !(if_value->is_boolean() && if_value->to_boolean())); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); return APPLIES_TO_KEYWORDS(KEYWORD); diff --git a/vendor/blaze/src/alterschema/linter/enum_to_const.h b/vendor/blaze/src/alterschema/linter/enum_to_const.h index e147622ec..9d3c7fc5b 100644 --- a/vendor/blaze/src/alterschema/linter/enum_to_const.h +++ b/vendor/blaze/src/alterschema/linter/enum_to_const.h @@ -21,9 +21,11 @@ class EnumToConst final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && !schema.defines("const") && - schema.defines("enum") && schema.at("enum").is_array() && - schema.at("enum").size() == 1); + schema.is_object() && !schema.defines("const")); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + enum_value->size() == 1); return APPLIES_TO_KEYWORDS("enum"); } diff --git a/vendor/blaze/src/alterschema/linter/equal_numeric_bounds_to_const.h b/vendor/blaze/src/alterschema/linter/equal_numeric_bounds_to_const.h index 9b45a5db8..0f2dab921 100644 --- a/vendor/blaze/src/alterschema/linter/equal_numeric_bounds_to_const.h +++ b/vendor/blaze/src/alterschema/linter/equal_numeric_bounds_to_const.h @@ -17,20 +17,30 @@ class EqualNumericBoundsToConst final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any({ + Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + }) && + schema.is_object()); + + const auto *type{schema.try_at("type")}; ONLY_CONTINUE_IF( - vocabularies.contains_any({ - Vocabularies::Known::JSON_Schema_2020_12_Validation, - Vocabularies::Known::JSON_Schema_2019_09_Validation, - Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - }) && - schema.is_object() && schema.defines("type") && - schema.at("type").is_string() && - (schema.at("type").to_string() == "integer" || - schema.at("type").to_string() == "number") && - schema.defines("minimum") && schema.at("minimum").is_number() && - schema.defines("maximum") && schema.at("maximum").is_number() && - schema.at("minimum") == schema.at("maximum")); + type && type->is_string() && + (type->to_string() == "integer" || type->to_string() == "number")); + const auto *minimum{schema.try_at("minimum")}; + ONLY_CONTINUE_IF(minimum && minimum->is_number()); + const auto *maximum{schema.try_at("maximum")}; + ONLY_CONTINUE_IF(maximum && maximum->is_number() && *minimum == *maximum); + + const auto *exclusive_minimum{schema.try_at("exclusiveMinimum")}; + ONLY_CONTINUE_IF(!(exclusive_minimum && exclusive_minimum->is_number() && + *exclusive_minimum >= *minimum)); + const auto *exclusive_maximum{schema.try_at("exclusiveMaximum")}; + ONLY_CONTINUE_IF(!(exclusive_maximum && exclusive_maximum->is_number() && + *exclusive_maximum <= *maximum)); + return APPLIES_TO_KEYWORDS("minimum", "maximum"); } diff --git a/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h b/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h index 44b315516..10a2319f4 100644 --- a/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h +++ b/vendor/blaze/src/alterschema/linter/forbid_empty_enum.h @@ -22,9 +22,11 @@ class ForbidEmptyEnum final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && !schema.defines("not") && - schema.defines("enum") && schema.at("enum").is_array() && - schema.at("enum").empty()); + schema.is_object() && !schema.defines("not")); + + const auto *enum_value{schema.try_at("enum")}; + ONLY_CONTINUE_IF(enum_value && enum_value->is_array() && + enum_value->empty()); ONLY_CONTINUE_IF(!frame.has_references_through(location.pointer)); return APPLIES_TO_KEYWORDS("enum"); } diff --git a/vendor/blaze/src/alterschema/linter/incoherent_min_max_contains.h b/vendor/blaze/src/alterschema/linter/incoherent_min_max_contains.h index 9c8915515..960e2bb73 100644 --- a/vendor/blaze/src/alterschema/linter/incoherent_min_max_contains.h +++ b/vendor/blaze/src/alterschema/linter/incoherent_min_max_contains.h @@ -21,13 +21,13 @@ class IncoherentMinMaxContains final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("contains") && - schema.defines("minContains") && - schema.at("minContains").is_integer() && - schema.defines("maxContains") && - schema.at("maxContains").is_integer() && - schema.at("minContains").to_integer() > - schema.at("maxContains").to_integer()); + schema.is_object() && schema.defines("contains")); + + const auto *min_contains{schema.try_at("minContains")}; + ONLY_CONTINUE_IF(min_contains && min_contains->is_integer()); + const auto *max_contains{schema.try_at("maxContains")}; + ONLY_CONTINUE_IF(max_contains && max_contains->is_integer() && + min_contains->to_integer() > max_contains->to_integer()); return APPLIES_TO_KEYWORDS("minContains", "maxContains"); } }; diff --git a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h index d0dbcf504..7828ebc0e 100644 --- a/vendor/blaze/src/alterschema/linter/invalid_external_ref.h +++ b/vendor/blaze/src/alterschema/linter/invalid_external_ref.h @@ -20,7 +20,8 @@ class InvalidExternalRef final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3})); + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && schema.at(KEYWORD).is_string()); diff --git a/vendor/blaze/src/alterschema/linter/items_array_default.h b/vendor/blaze/src/alterschema/linter/items_array_default.h index 3c4fbf540..f6dbafbf2 100644 --- a/vendor/blaze/src/alterschema/linter/items_array_default.h +++ b/vendor/blaze/src/alterschema/linter/items_array_default.h @@ -22,13 +22,15 @@ class ItemsArrayDefault final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_2_Hyper, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_1_Hyper}) && - schema.is_object() && schema.defines("items") && - schema.at("items").is_array() && - schema.at("items").empty()); + schema.is_object()); + + const auto *items{schema.try_at("items")}; + ONLY_CONTINUE_IF(items && items->is_array() && items->empty()); return APPLIES_TO_KEYWORDS("items"); } diff --git a/vendor/blaze/src/alterschema/linter/items_schema_default.h b/vendor/blaze/src/alterschema/linter/items_schema_default.h index 22be12b82..c67e6909d 100644 --- a/vendor/blaze/src/alterschema/linter/items_schema_default.h +++ b/vendor/blaze/src/alterschema/linter/items_schema_default.h @@ -27,6 +27,7 @@ class ItemsSchemaDefault final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_2_Hyper, Vocabularies::Known::JSON_Schema_Draft_1, diff --git a/vendor/blaze/src/alterschema/linter/multiple_of_default.h b/vendor/blaze/src/alterschema/linter/multiple_of_default.h index 2d123845f..09844d894 100644 --- a/vendor/blaze/src/alterschema/linter/multiple_of_default.h +++ b/vendor/blaze/src/alterschema/linter/multiple_of_default.h @@ -22,14 +22,19 @@ class MultipleOfDefault final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("multipleOf") && - ((schema.at("multipleOf").is_integer() && - schema.at("multipleOf").to_integer() == 1) || - (schema.at("multipleOf").is_real() && - schema.at("multipleOf").to_real() == 1.0) || - (schema.at("multipleOf").is_decimal() && - schema.at("multipleOf").to_decimal() == - sourcemeta::core::Decimal{1}))); + schema.is_object()); + + const auto *type{schema.try_at("type")}; + ONLY_CONTINUE_IF(type && type->is_string() && + type->to_string() == "integer"); + + const auto *multiple_of{schema.try_at("multipleOf")}; + ONLY_CONTINUE_IF( + multiple_of && + ((multiple_of->is_integer() && multiple_of->to_integer() == 1) || + (multiple_of->is_real() && multiple_of->to_real() == 1.0) || + (multiple_of->is_decimal() && + multiple_of->to_decimal() == sourcemeta::core::Decimal{1}))); return APPLIES_TO_KEYWORDS("multipleOf"); } diff --git a/vendor/blaze/src/alterschema/linter/pattern_properties_default.h b/vendor/blaze/src/alterschema/linter/pattern_properties_default.h index 2e6fb1315..4ed4b3563 100644 --- a/vendor/blaze/src/alterschema/linter/pattern_properties_default.h +++ b/vendor/blaze/src/alterschema/linter/pattern_properties_default.h @@ -23,11 +23,13 @@ class PatternPropertiesDefault final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3}) && - schema.is_object() && - schema.defines("patternProperties") && - schema.at("patternProperties").is_object() && - schema.at("patternProperties").empty()); + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper}) && + schema.is_object()); + + const auto *pattern_properties{schema.try_at("patternProperties")}; + ONLY_CONTINUE_IF(pattern_properties && pattern_properties->is_object() && + pattern_properties->empty()); return APPLIES_TO_KEYWORDS("patternProperties"); } diff --git a/vendor/blaze/src/alterschema/linter/portable_anchor_names.h b/vendor/blaze/src/alterschema/linter/portable_anchor_names.h new file mode 100644 index 000000000..7be1ccc2f --- /dev/null +++ b/vendor/blaze/src/alterschema/linter/portable_anchor_names.h @@ -0,0 +1,105 @@ +class PortableAnchorNames final : public SchemaTransformRule { +public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; + PortableAnchorNames() + : SchemaTransformRule{ + "portable_anchor_names", + "Keep anchors within the safe allowed character set across JSON " + "Schema dialects (`^[A-Za-z][A-Za-z0-9_.-]*$`)"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})); + ONLY_CONTINUE_IF(schema.is_object()); + + std::vector offenders; + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2019_09_Core})) { + this->check_anchor_keyword(schema, ANCHOR, offenders); + } + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Core})) { + this->check_anchor_keyword(schema, DYNAMIC_ANCHOR, offenders); + } + + if (vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4})) { + const auto &id_keyword{ + vocabularies.contains_any({Vocabularies::Known::JSON_Schema_Draft_4}) + ? ID_DRAFT_4 + : ID_MODERN}; + this->check_id_fragment(schema, id_keyword, offenders); + } + + ONLY_CONTINUE_IF(!offenders.empty()); + return APPLIES_TO_POINTERS(std::move(offenders)); + } + +private: + static inline const sourcemeta::core::JSON::String ANCHOR{"$anchor"}; + static inline const sourcemeta::core::JSON::String DYNAMIC_ANCHOR{ + "$dynamicAnchor"}; + static inline const sourcemeta::core::JSON::String ID_MODERN{"$id"}; + static inline const sourcemeta::core::JSON::String ID_DRAFT_4{"id"}; + static inline const Regex SAFE_ANCHOR_PATTERN{ + to_regex("^[A-Za-z][A-Za-z0-9_.-]*$").value()}; + + static auto + check_anchor_keyword(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &keyword, + std::vector &offenders) -> void { + if (!schema.defines(keyword) || !schema.at(keyword).is_string()) { + return; + } + + const auto &value{schema.at(keyword).to_string()}; + if (value.empty()) { + return; + } + + if (!matches(SAFE_ANCHOR_PATTERN, value)) { + offenders.push_back(Pointer{keyword}); + } + } + + static auto check_id_fragment(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &keyword, + std::vector &offenders) -> void { + if (!schema.defines(keyword) || !schema.at(keyword).is_string()) { + return; + } + + const auto &value{schema.at(keyword).to_string()}; + if (value.find('#') == sourcemeta::core::JSON::String::npos) { + return; + } + + const sourcemeta::core::URI uri{value}; + const auto fragment{uri.fragment()}; + if (!fragment.has_value() || fragment.value().empty()) { + return; + } + + if (!matches(SAFE_ANCHOR_PATTERN, + sourcemeta::core::JSON::String{fragment.value()})) { + offenders.push_back(Pointer{keyword}); + } + } +}; diff --git a/vendor/blaze/src/alterschema/linter/properties_default.h b/vendor/blaze/src/alterschema/linter/properties_default.h index 5e8489477..35ac20c1c 100644 --- a/vendor/blaze/src/alterschema/linter/properties_default.h +++ b/vendor/blaze/src/alterschema/linter/properties_default.h @@ -24,13 +24,16 @@ class PropertiesDefault final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_2_Hyper, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_1_Hyper}) && - schema.is_object() && schema.defines("properties") && - schema.at("properties").is_object() && - schema.at("properties").empty()); + schema.is_object()); + + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object() && + properties->empty()); return APPLIES_TO_KEYWORDS("properties"); } diff --git a/vendor/blaze/src/alterschema/linter/property_names_type_default.h b/vendor/blaze/src/alterschema/linter/property_names_type_default.h index 46626e282..7aa9f2670 100644 --- a/vendor/blaze/src/alterschema/linter/property_names_type_default.h +++ b/vendor/blaze/src/alterschema/linter/property_names_type_default.h @@ -17,23 +17,24 @@ class PropertyNamesTypeDefault final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6}) && + schema.is_object()); + + const auto *property_names{schema.try_at("propertyNames")}; + ONLY_CONTINUE_IF(property_names && property_names->is_object()); + const auto *type{property_names->try_at("type")}; ONLY_CONTINUE_IF( - vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Applicator, - Vocabularies::Known::JSON_Schema_2019_09_Applicator, - Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("propertyNames") && - schema.at("propertyNames").is_object() && - schema.at("propertyNames").defines("type") && - ((schema.at("propertyNames").at("type").is_string() && - schema.at("propertyNames").at("type").to_string() == "string") || - (schema.at("propertyNames").at("type").is_array() && - std::all_of(schema.at("propertyNames").at("type").as_array().begin(), - schema.at("propertyNames").at("type").as_array().end(), - [](const auto &item) { - return item.is_string() && item.to_string() == "string"; - })))); + type && ((type->is_string() && type->to_string() == "string") || + (type->is_array() && + std::all_of(type->as_array().begin(), type->as_array().end(), + [](const auto &item) { + return item.is_string() && + item.to_string() == "string"; + })))); return APPLIES_TO_POINTERS({{"propertyNames", "type"}}); } diff --git a/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h b/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h index 35661f1ca..61cbaa12d 100644 --- a/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h +++ b/vendor/blaze/src/alterschema/linter/simple_properties_identifiers.h @@ -26,13 +26,15 @@ class SimplePropertiesIdentifiers final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_2_Hyper, Vocabularies::Known::JSON_Schema_Draft_1, Vocabularies::Known::JSON_Schema_Draft_1_Hyper})); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("properties") && - schema.at("properties").is_object() && - !schema.at("properties").empty()); + ONLY_CONTINUE_IF(schema.is_object()); + const auto *properties{schema.try_at("properties")}; + ONLY_CONTINUE_IF(properties && properties->is_object() && + !properties->empty()); if (vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Core, @@ -55,7 +57,7 @@ class SimplePropertiesIdentifiers final : public SchemaTransformRule { } std::vector offenders; - for (const auto &entry : schema.at("properties").as_object()) { + for (const auto &entry : properties->as_object()) { static const Regex IDENTIFIER_PATTERN{ to_regex("^[A-Za-z_][A-Za-z0-9_]*$").value()}; if (!matches(IDENTIFIER_PATTERN, entry.first)) { diff --git a/vendor/blaze/src/alterschema/linter/then_empty.h b/vendor/blaze/src/alterschema/linter/then_empty.h index eacf299a4..647da3ee1 100644 --- a/vendor/blaze/src/alterschema/linter/then_empty.h +++ b/vendor/blaze/src/alterschema/linter/then_empty.h @@ -15,16 +15,18 @@ class ThenEmpty final : public SchemaTransformRule { const SchemaFrame &frame, const SchemaFrame::Location &location, const SchemaWalker &, const SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Applicator, - Vocabularies::Known::JSON_Schema_2019_09_Applicator, - Vocabularies::Known::JSON_Schema_Draft_7}) && - schema.is_object() && schema.defines(KEYWORD) && - is_schema(schema.at(KEYWORD)) && is_empty_schema(schema.at(KEYWORD)) && - (schema.at(KEYWORD).is_object() || - (!schema.defines("if") || - !(schema.at("if").is_boolean() && schema.at("if").to_boolean())))); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7}) && + schema.is_object()); + + const auto *then_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(then_value && is_schema(*then_value) && + is_empty_schema(*then_value)); + const auto *if_value{schema.try_at("if")}; + ONLY_CONTINUE_IF(then_value->is_object() || !if_value || + !(if_value->is_boolean() && if_value->to_boolean())); ONLY_CONTINUE_IF(!frame.has_references_through( location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); return APPLIES_TO_KEYWORDS(KEYWORD); diff --git a/vendor/blaze/src/alterschema/linter/title_description_equal.h b/vendor/blaze/src/alterschema/linter/title_description_equal.h index 79699098d..4ef00961e 100644 --- a/vendor/blaze/src/alterschema/linter/title_description_equal.h +++ b/vendor/blaze/src/alterschema/linter/title_description_equal.h @@ -24,6 +24,7 @@ class TitleDescriptionEqual final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); diff --git a/vendor/blaze/src/alterschema/linter/title_trailing_period.h b/vendor/blaze/src/alterschema/linter/title_trailing_period.h index 14d932646..806403842 100644 --- a/vendor/blaze/src/alterschema/linter/title_trailing_period.h +++ b/vendor/blaze/src/alterschema/linter/title_trailing_period.h @@ -24,6 +24,7 @@ class TitleTrailingPeriod final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); diff --git a/vendor/blaze/src/alterschema/linter/title_trim.h b/vendor/blaze/src/alterschema/linter/title_trim.h index 40d9e0982..94d2824df 100644 --- a/vendor/blaze/src/alterschema/linter/title_trim.h +++ b/vendor/blaze/src/alterschema/linter/title_trim.h @@ -23,6 +23,7 @@ class TitleTrim final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); diff --git a/vendor/blaze/src/alterschema/linter/top_level_description.h b/vendor/blaze/src/alterschema/linter/top_level_description.h index d26d076e9..69a68c0a9 100644 --- a/vendor/blaze/src/alterschema/linter/top_level_description.h +++ b/vendor/blaze/src/alterschema/linter/top_level_description.h @@ -25,14 +25,15 @@ class TopLevelDescription final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); - if (schema.defines("description") && schema.at("description").is_string() && - schema.at("description").empty()) { + const auto *description{schema.try_at("description")}; + if (description && description->is_string() && description->empty()) { return APPLIES_TO_KEYWORDS("description"); } else { - return !schema.defines("description"); + return !description; } } }; diff --git a/vendor/blaze/src/alterschema/linter/top_level_examples.h b/vendor/blaze/src/alterschema/linter/top_level_examples.h index f4ae68bbd..ff6fd27fa 100644 --- a/vendor/blaze/src/alterschema/linter/top_level_examples.h +++ b/vendor/blaze/src/alterschema/linter/top_level_examples.h @@ -24,11 +24,11 @@ class TopLevelExamples final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6})); ONLY_CONTINUE_IF(schema.is_object()); - if (schema.defines("examples") && schema.at("examples").is_array() && - schema.at("examples").empty()) { + const auto *examples{schema.try_at("examples")}; + if (examples && examples->is_array() && examples->empty()) { return APPLIES_TO_KEYWORDS("examples"); } else { - return !schema.defines("examples"); + return !examples; } } }; diff --git a/vendor/blaze/src/alterschema/linter/top_level_title.h b/vendor/blaze/src/alterschema/linter/top_level_title.h index c6360dab2..8e2abe71a 100644 --- a/vendor/blaze/src/alterschema/linter/top_level_title.h +++ b/vendor/blaze/src/alterschema/linter/top_level_title.h @@ -25,14 +25,15 @@ class TopLevelTitle final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4, Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper, Vocabularies::Known::JSON_Schema_Draft_2, Vocabularies::Known::JSON_Schema_Draft_1})); ONLY_CONTINUE_IF(schema.is_object()); - if (schema.defines("title") && schema.at("title").is_string() && - schema.at("title").empty()) { + const auto *title{schema.try_at("title")}; + if (title && title->is_string() && title->empty()) { return APPLIES_TO_KEYWORDS("title"); } else { - return !schema.defines("title"); + return !title; } } }; diff --git a/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h b/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h new file mode 100644 index 000000000..94ab66771 --- /dev/null +++ b/vendor/blaze/src/alterschema/linter/unknown_format_prefix.h @@ -0,0 +1,111 @@ +class UnknownFormatPrefix final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UnknownFormatPrefix() + : SchemaTransformRule{ + "unknown_format_prefix", + "For interoperability purposes, the JSON Schema specification " + "advises against the use of " + "`format` values that are not explicitly defined by the " + "specification"} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(schema.is_object() && schema.defines("format")); + const auto &format_value{schema.at("format")}; + ONLY_CONTINUE_IF(format_value.is_string()); + + const auto *recognized{recognized_formats_for(vocabularies)}; + if (recognized == nullptr) { + return false; + } + + if (recognized->contains(format_value.to_string())) { + return false; + } + + return APPLIES_TO_KEYWORDS("format"); + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + std::string prefixed_name{"x-format"}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + schema.rename("format", std::move(prefixed_name)); + } + +private: + static auto + recognized_formats_for(const sourcemeta::core::Vocabularies &vocabularies) + -> const std::unordered_set * { + using Known = sourcemeta::core::Vocabularies::Known; + if (vocabularies.contains_any( + {Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper})) { + return &DRAFT_3_FORMATS; + } + if (vocabularies.contains_any( + {Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper})) { + return &DRAFT_4_FORMATS; + } + if (vocabularies.contains_any( + {Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper})) { + return &DRAFT_6_FORMATS; + } + if (vocabularies.contains_any( + {Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper})) { + return &DRAFT_7_FORMATS; + } + if (vocabularies.contains(Known::JSON_Schema_2019_09_Format)) { + return &DRAFT_2019_09_FORMATS; + } + if (vocabularies.contains(Known::JSON_Schema_2020_12_Format_Annotation) || + vocabularies.contains(Known::JSON_Schema_2020_12_Format_Assertion)) { + return &DRAFT_2020_12_FORMATS; + } + return nullptr; + } + + static inline const std::unordered_set DRAFT_3_FORMATS{ + "date-time", "date", "time", "utc-millisec", "regex", + "color", "style", "phone", "uri", "email", + "ip-address", "ipv6", "host-name"}; + static inline const std::unordered_set DRAFT_4_FORMATS{ + "date-time", "email", "hostname", "ipv4", "ipv6", "uri"}; + static inline const std::unordered_set DRAFT_6_FORMATS{ + "date-time", "email", "hostname", "ipv4", "ipv6", + "uri", "uri-reference", "uri-template", "json-pointer"}; + static inline const std::unordered_set DRAFT_7_FORMATS{ + "date-time", "date", "time", "email", + "idn-email", "hostname", "idn-hostname", "ipv4", + "ipv6", "uri", "uri-reference", "iri", + "iri-reference", "uri-template", "json-pointer", "relative-json-pointer", + "regex"}; + static inline const std::unordered_set + DRAFT_2019_09_FORMATS{ + "date-time", "date", "time", + "duration", "email", "idn-email", + "hostname", "idn-hostname", "ipv4", + "ipv6", "uri", "uri-reference", + "iri", "iri-reference", "uuid", + "uri-template", "json-pointer", "relative-json-pointer", + "regex"}; + static inline const std::unordered_set + DRAFT_2020_12_FORMATS{ + "date-time", "date", "time", + "duration", "email", "idn-email", + "hostname", "idn-hostname", "ipv4", + "ipv6", "uri", "uri-reference", + "iri", "iri-reference", "uuid", + "uri-template", "json-pointer", "relative-json-pointer", + "regex"}; +}; diff --git a/vendor/blaze/src/alterschema/linter/unnecessary_allof_ref_wrapper_modern.h b/vendor/blaze/src/alterschema/linter/unnecessary_allof_ref_wrapper_modern.h index 2615b2d97..183266b46 100644 --- a/vendor/blaze/src/alterschema/linter/unnecessary_allof_ref_wrapper_modern.h +++ b/vendor/blaze/src/alterschema/linter/unnecessary_allof_ref_wrapper_modern.h @@ -19,10 +19,12 @@ class UnnecessaryAllOfRefWrapperModern final : public SchemaTransformRule { ONLY_CONTINUE_IF(vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Applicator, Vocabularies::Known::JSON_Schema_2019_09_Applicator})); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines("allOf") && - schema.at("allOf").is_array()); + ONLY_CONTINUE_IF(schema.is_object()); - const auto &all_of{schema.at("allOf")}; + const auto *all_of_value{schema.try_at("allOf")}; + ONLY_CONTINUE_IF(all_of_value && all_of_value->is_array()); + + const auto &all_of{*all_of_value}; // Don't do anything if there is more than one branch and ALL branches // define `$ref` (a common multiple composition pattern) diff --git a/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h b/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h index 8762db1e4..faa1fb8d3 100644 --- a/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h +++ b/vendor/blaze/src/alterschema/linter/unnecessary_allof_wrapper.h @@ -21,9 +21,26 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4})); - ONLY_CONTINUE_IF(schema.is_object() && schema.defines(KEYWORD) && - schema.at(KEYWORD).is_array() && - !schema.at(KEYWORD).empty()); + ONLY_CONTINUE_IF(schema.is_object()); + + const auto *all_of_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(all_of_value && all_of_value->is_array() && + !all_of_value->empty()); + + std::unordered_map keyword_frequency; + for (const auto &entry : all_of_value->as_array()) { + if (!entry.is_object()) { + continue; + } + for (const auto &property : entry.as_object()) { + const auto &metadata{walker(property.first, vocabularies)}; + if (metadata.type == SchemaKeywordType::Annotation || + metadata.type == SchemaKeywordType::Comment) { + continue; + } + keyword_frequency[property.first]++; + } + } std::unordered_set dependency_blocked; for (const auto &entry : schema.as_object()) { @@ -41,18 +58,19 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { } } + const auto *parent_type_value{schema.try_at("type")}; const JSON::TypeSet parent_types{ - schema.defines("type") && + parent_type_value && vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation, Vocabularies::Known::JSON_Schema_Draft_7, Vocabularies::Known::JSON_Schema_Draft_6, Vocabularies::Known::JSON_Schema_Draft_4}) - ? parse_schema_type(schema.at("type")) + ? parse_schema_type(*parent_type_value) : JSON::TypeSet{}}; - const auto &all_of{schema.at(KEYWORD)}; + const auto &all_of{*all_of_value}; std::vector locations; std::unordered_set elevated; @@ -86,32 +104,36 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { continue; } - for (const auto &keyword_entry : entry.as_object()) { + const auto try_elevate_keyword = [&](const auto &keyword_entry) -> bool { const auto &keyword{keyword_entry.first}; const auto &metadata{walker(keyword, vocabularies)}; if (elevated.contains(keyword) || (schema.defines(keyword) && schema.at(keyword) != keyword_entry.second)) { - continue; + return false; } if (dependency_blocked.contains(keyword)) { - continue; + return false; + } + + if (keyword_frequency[keyword] > 1) { + return false; } if (metadata.instances.any() && parent_types.any() && (metadata.instances & parent_types).none()) { - continue; + return false; } - if (std::ranges::any_of( - metadata.dependencies, [&](const auto &dependency) { - return !entry.defines(std::string{dependency}) && - (schema.defines(std::string{dependency}) || - elevated.contains(dependency)); - })) { - continue; + if (std::ranges::any_of(metadata.dependencies, + [&](const auto &dependency) { + return !entry.defines(dependency) && + (schema.defines(dependency) || + elevated.contains(dependency)); + })) { + return false; } locations.push_back(Pointer{KEYWORD, index - 1, keyword}); @@ -123,11 +145,38 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { (keyword == "unevaluatedProperties" || keyword == "unevaluatedItems"))) { for (const auto &dependency : metadata.dependencies) { - if (!entry.defines(std::string{dependency})) { + if (!entry.defines(dependency)) { dependency_blocked.emplace(dependency); } } } + + return true; + }; + + bool entry_has_non_annotation = false; + bool non_annotation_elevated = false; + for (const auto &keyword_entry : entry.as_object()) { + const auto &metadata{walker(keyword_entry.first, vocabularies)}; + if (metadata.type == SchemaKeywordType::Annotation || + metadata.type == SchemaKeywordType::Comment) { + continue; + } + entry_has_non_annotation = true; + if (try_elevate_keyword(keyword_entry)) { + non_annotation_elevated = true; + } + } + + if (!entry_has_non_annotation || non_annotation_elevated) { + for (const auto &keyword_entry : entry.as_object()) { + const auto &metadata{walker(keyword_entry.first, vocabularies)}; + if (metadata.type != SchemaKeywordType::Annotation && + metadata.type != SchemaKeywordType::Comment) { + continue; + } + try_elevate_keyword(keyword_entry); + } } } diff --git a/vendor/blaze/src/alterschema/linter/unnecessary_extends_wrapper.h b/vendor/blaze/src/alterschema/linter/unnecessary_extends_wrapper.h new file mode 100644 index 000000000..af175e5d6 --- /dev/null +++ b/vendor/blaze/src/alterschema/linter/unnecessary_extends_wrapper.h @@ -0,0 +1,146 @@ +class UnnecessaryExtendsWrapper final : public SchemaTransformRule { +private: + static inline const std::string KEYWORD{"extends"}; + +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UnnecessaryExtendsWrapper() + : SchemaTransformRule{"unnecessary_extends_wrapper", + "Keywords inside `extends` that do not conflict " + "with the parent schema can be elevated"} {}; + + [[nodiscard]] auto + condition(const JSON &schema, const JSON &, const Vocabularies &vocabularies, + const SchemaFrame &frame, const SchemaFrame::Location &location, + const SchemaWalker &walker, const SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_3_Hyper})); + ONLY_CONTINUE_IF(schema.is_object()); + + const auto *extends_value{schema.try_at(KEYWORD)}; + ONLY_CONTINUE_IF(extends_value && extends_value->is_array() && + !extends_value->empty()); + + std::unordered_set dependency_blocked; + for (const auto &entry : schema.as_object()) { + for (const auto &dependency : + walker(entry.first, vocabularies).dependencies) { + dependency_blocked.emplace(dependency); + } + } + + const auto *parent_type_value{schema.try_at("type")}; + const JSON::TypeSet parent_types{ + parent_type_value && is_known_type_form(*parent_type_value) + ? parse_schema_type(*parent_type_value) + : JSON::TypeSet{}}; + + const auto &extends{*extends_value}; + std::vector locations; + std::unordered_set elevated; + + for (auto index = extends.size(); index > 0; index--) { + const auto &entry{extends.at(index - 1)}; + if (!entry.is_object() || entry.empty() || + // We separately handle this case via + // `unnecessary_extends_ref_wrapper` + entry.defines("$ref")) { + continue; + } + + auto entry_pointer{location.pointer}; + entry_pointer.push_back(std::cref(KEYWORD)); + entry_pointer.push_back(index - 1); + if (frame.has_references_to(entry_pointer)) { + continue; + } + + // Skip entries that define their own identity, as elevating keywords + // from them could break references that target those identifiers + if (entry.defines("id")) { + continue; + } + + for (const auto &keyword_entry : entry.as_object()) { + const auto &keyword{keyword_entry.first}; + const auto &metadata{walker(keyword, vocabularies)}; + + if (elevated.contains(keyword) || + (schema.defines(keyword) && + schema.at(keyword) != keyword_entry.second)) { + continue; + } + + if (dependency_blocked.contains(keyword)) { + continue; + } + + if (metadata.instances.any() && parent_types.any() && + (metadata.instances & parent_types).none()) { + continue; + } + + if (std::ranges::any_of( + metadata.dependencies, [&](const auto &dependency) { + return !entry.defines(std::string{dependency}) && + (schema.defines(std::string{dependency}) || + elevated.contains(dependency)); + })) { + continue; + } + + locations.push_back(Pointer{KEYWORD, index - 1, keyword}); + elevated.emplace(keyword); + + for (const auto &dependency : metadata.dependencies) { + if (!entry.defines(std::string{dependency})) { + dependency_blocked.emplace(dependency); + } + } + } + } + + ONLY_CONTINUE_IF(!locations.empty()); + return APPLIES_TO_POINTERS(std::move(locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + for (const auto &location : result.locations) { + assert(location.size() == 3); + const auto extends_index{location.at(1).to_index()}; + const auto &keyword{location.at(2).to_property()}; + schema.try_assign_before( + keyword, schema.at(KEYWORD).at(extends_index).at(keyword), KEYWORD); + schema.at(KEYWORD).at(extends_index).erase(keyword); + } + } + + [[nodiscard]] auto rereference(const std::string_view, const Pointer &, + const Pointer &target, + const Pointer ¤t) const + -> Pointer override { + // The rule moves keywords from /extends// to / + const auto extends_prefix{current.concat({KEYWORD})}; + const auto relative{target.resolve_from(extends_prefix)}; + const auto &keyword{relative.at(1).to_property()}; + const Pointer old_prefix{extends_prefix.concat({relative.at(0), keyword})}; + const Pointer new_prefix{current.concat({keyword})}; + return target.rebase(old_prefix, new_prefix); + } + +private: + static auto is_known_type_form(const sourcemeta::core::JSON &type) -> bool { + if (type.is_string()) { + return type.to_string() != "any"; + } + if (!type.is_array()) { + return false; + } + return std::ranges::all_of(type.as_array(), [](const auto &entry) { + return entry.is_string() && entry.to_string() != "any"; + }); + } +}; diff --git a/vendor/blaze/src/alterschema/linter/unsatisfiable_max_contains.h b/vendor/blaze/src/alterschema/linter/unsatisfiable_max_contains.h index 29522c9c9..9d02a2ce1 100644 --- a/vendor/blaze/src/alterschema/linter/unsatisfiable_max_contains.h +++ b/vendor/blaze/src/alterschema/linter/unsatisfiable_max_contains.h @@ -22,11 +22,13 @@ class UnsatisfiableMaxContains final : public SchemaTransformRule { vocabularies.contains_any( {Vocabularies::Known::JSON_Schema_2020_12_Validation, Vocabularies::Known::JSON_Schema_2019_09_Validation}) && - schema.is_object() && schema.defines("maxContains") && - schema.at("maxContains").is_integer() && schema.defines("maxItems") && - schema.at("maxItems").is_integer() && - schema.at("maxContains").to_integer() >= - schema.at("maxItems").to_integer()); + schema.is_object()); + + const auto *max_contains{schema.try_at("maxContains")}; + ONLY_CONTINUE_IF(max_contains && max_contains->is_integer()); + const auto *max_items{schema.try_at("maxItems")}; + ONLY_CONTINUE_IF(max_items && max_items->is_integer() && + max_contains->to_integer() >= max_items->to_integer()); return APPLIES_TO_KEYWORDS("maxContains", "maxItems"); } diff --git a/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h b/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h index 4616ca8ee..42ab40e82 100644 --- a/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h +++ b/vendor/blaze/src/alterschema/linter/unsatisfiable_min_properties.h @@ -17,19 +17,21 @@ class UnsatisfiableMinProperties final : public SchemaTransformRule { const sourcemeta::core::SchemaWalker &, const sourcemeta::core::SchemaResolver &) const -> SchemaTransformRule::Result override { - ONLY_CONTINUE_IF( - vocabularies.contains_any( - {Vocabularies::Known::JSON_Schema_2020_12_Validation, - Vocabularies::Known::JSON_Schema_2019_09_Validation, - Vocabularies::Known::JSON_Schema_Draft_7, - Vocabularies::Known::JSON_Schema_Draft_6, - Vocabularies::Known::JSON_Schema_Draft_4}) && - schema.is_object() && schema.defines("minProperties") && - schema.at("minProperties").is_integer() && schema.defines("required") && - schema.at("required").is_array() && schema.at("required").unique() && - std::cmp_greater_equal(schema.at("required").size(), - static_cast( - schema.at("minProperties").to_integer()))); + ONLY_CONTINUE_IF(vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}) && + schema.is_object()); + + const auto *min_properties{schema.try_at("minProperties")}; + ONLY_CONTINUE_IF(min_properties && min_properties->is_integer()); + const auto *required{schema.try_at("required")}; + ONLY_CONTINUE_IF(required && required->is_array() && required->unique() && + std::cmp_greater_equal(required->size(), + static_cast( + min_properties->to_integer()))); return APPLIES_TO_KEYWORDS("minProperties", "required"); } diff --git a/vendor/blaze/src/alterschema/linter/valid_default.h b/vendor/blaze/src/alterschema/linter/valid_default.h index 75af8258f..7485c7013 100644 --- a/vendor/blaze/src/alterschema/linter/valid_default.h +++ b/vendor/blaze/src/alterschema/linter/valid_default.h @@ -18,17 +18,19 @@ class ValidDefault final : public SchemaTransformRule { -> SchemaTransformRule::Result override { using Known = Vocabularies::Known; // Technically, the `default` keyword goes back to Draft 1, but Blaze - // only supports Draft 4 and later + // only supports Draft 3 and later ONLY_CONTINUE_IF( vocabularies.contains_any( {Known::JSON_Schema_2020_12_Meta_Data, Known::JSON_Schema_2019_09_Meta_Data, Known::JSON_Schema_Draft_7, - Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_4}) && + Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_4, + Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper}) && schema.is_object() && schema.defines("default")); - if (vocabularies.contains_any({Known::JSON_Schema_Draft_7, - Known::JSON_Schema_Draft_6, - Known::JSON_Schema_Draft_4})) { + if (vocabularies.contains_any( + {Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_6, + Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_3, + Known::JSON_Schema_Draft_3_Hyper})) { ONLY_CONTINUE_IF(!schema.defines("$ref")); } @@ -41,6 +43,10 @@ class ValidDefault final : public SchemaTransformRule { try { schema_template = compile(root, walker, resolver, this->compiler_, frame, base.value().get(), Mode::Exhaustive); + } catch (const CompilerReferenceTargetNotSchemaError &) { + throw; + } catch (const sourcemeta::core::SchemaVocabularyError &) { + throw; } catch (...) { return false; } @@ -81,6 +87,10 @@ class ValidDefault final : public SchemaTransformRule { try { schema_template = compile(subschema, walker, resolver, this->compiler_, Mode::Exhaustive, location.dialect, default_id); + } catch (const CompilerReferenceTargetNotSchemaError &) { + throw; + } catch (const sourcemeta::core::SchemaVocabularyError &) { + throw; } catch (...) { return false; } diff --git a/vendor/blaze/src/alterschema/linter/valid_examples.h b/vendor/blaze/src/alterschema/linter/valid_examples.h index a1e039eca..738daf353 100644 --- a/vendor/blaze/src/alterschema/linter/valid_examples.h +++ b/vendor/blaze/src/alterschema/linter/valid_examples.h @@ -23,8 +23,10 @@ class ValidExamples final : public SchemaTransformRule { Known::JSON_Schema_2019_09_Meta_Data, Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_6}) && - schema.is_object() && schema.defines("examples") && - schema.at("examples").is_array() && !schema.at("examples").empty()); + schema.is_object()); + + const auto *examples{schema.try_at("examples")}; + ONLY_CONTINUE_IF(examples && examples->is_array() && !examples->empty()); if (vocabularies.contains_any({Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_6, @@ -42,11 +44,15 @@ class ValidExamples final : public SchemaTransformRule { try { schema_template = compile(root, walker, resolver, this->compiler_, frame, base.value().get(), Mode::Exhaustive); + } catch (const CompilerReferenceTargetNotSchemaError &) { + throw; + } catch (const sourcemeta::core::SchemaVocabularyError &) { + throw; } catch (...) { return false; } - for (const auto &example : schema.at("examples").as_array()) { + for (const auto &example : examples->as_array()) { SimpleOutput output{example}; Evaluator evaluator; const auto result{ @@ -90,6 +96,10 @@ class ValidExamples final : public SchemaTransformRule { try { schema_template = compile(subschema, walker, resolver, this->compiler_, Mode::Exhaustive, location.dialect, default_id); + } catch (const CompilerReferenceTargetNotSchemaError &) { + throw; + } catch (const sourcemeta::core::SchemaVocabularyError &) { + throw; } catch (...) { return false; } diff --git a/vendor/blaze/src/alterschema/schema_rule.cc b/vendor/blaze/src/alterschema/schema_rule.cc index 6c71c38ee..adbdb7228 100644 --- a/vendor/blaze/src/alterschema/schema_rule.cc +++ b/vendor/blaze/src/alterschema/schema_rule.cc @@ -18,16 +18,14 @@ namespace sourcemeta::blaze { static constexpr std::string_view NAME_PATTERN{"^[a-z0-9_/]+$"}; static auto validate_name(const std::string_view name) -> void { - static const auto pattern{ - sourcemeta::core::to_regex(std::string{NAME_PATTERN})}; + static const auto pattern{sourcemeta::core::to_regex(NAME_PATTERN)}; assert(pattern.has_value()); if (name.empty()) [[unlikely]] { throw SchemaRuleInvalidNameError(name, "The schema rule name must not be empty"); } - if (!sourcemeta::core::matches(pattern.value(), std::string{name})) - [[unlikely]] { + if (!sourcemeta::core::matches(pattern.value(), name)) [[unlikely]] { throw SchemaRuleInvalidNamePatternError(name, NAME_PATTERN); } } diff --git a/vendor/blaze/src/alterschema/transformer.cc b/vendor/blaze/src/alterschema/transformer.cc index beb917b04..8769e9326 100644 --- a/vendor/blaze/src/alterschema/transformer.cc +++ b/vendor/blaze/src/alterschema/transformer.cc @@ -300,33 +300,40 @@ auto SchemaTransformer::apply(core::JSON &schema, frame.traverse(core::to_weak_pointer(entry_pointer))}; assert(new_location.has_value()); - const auto new_vocabularies{ - frame.vocabularies(new_location.value().get(), resolver)}; - - if (rule->check(current, schema, new_vocabularies, walker, resolver, - frame, new_location.value().get(), exclude_keyword) - .applies) { - std::ostringstream error; - error << "Rule condition holds after application: " << rule->name(); - throw std::runtime_error(error.str()); - } - + // Fix broken references before re-checking the condition, + // as the re-check may mutate rule state that rereference needs bool references_fixed{false}; + const auto resource_offset{new_location.value().get().relative_pointer}; + const auto current_slice{entry_pointer.slice(resource_offset)}; for (const auto &saved_reference : potentially_broken_references) { if (core::try_get(schema, saved_reference.target_pointer)) { continue; } + // If the origin was also relocated, resolve its new location + auto effective_origin{saved_reference.origin}; if (!core::try_get(schema, saved_reference.origin.initial())) { - continue; + try { + const auto new_origin{rule->rereference( + saved_reference.destination, saved_reference.origin, + saved_reference.origin.slice(resource_offset), + current_slice)}; + effective_origin = + saved_reference.origin.slice(0, resource_offset) + .concat(new_origin); + } catch (...) { + continue; + } + if (!core::try_get(schema, effective_origin.initial())) { + continue; + } } const auto new_relative{rule->rereference( saved_reference.destination, saved_reference.origin, saved_reference.target_pointer.slice( saved_reference.target_relative_pointer), - entry_pointer.slice( - new_location.value().get().relative_pointer))}; + current_slice)}; const auto new_fragment{ saved_reference.fragment == core::to_string(saved_reference.target_pointer) @@ -337,11 +344,21 @@ auto SchemaTransformer::apply(core::JSON &schema, core::URI original{saved_reference.original}; original.fragment(core::to_string(new_fragment)); - core::set(schema, saved_reference.origin, - core::JSON{original.recompose()}); + core::set(schema, effective_origin, core::JSON{original.recompose()}); references_fixed = true; } + const auto new_vocabularies{ + frame.vocabularies(new_location.value().get(), resolver)}; + + if (rule->check(current, schema, new_vocabularies, walker, resolver, + frame, new_location.value().get(), exclude_keyword) + .applies) { + std::ostringstream error; + error << "Rule condition holds after application: " << rule->name(); + throw std::runtime_error(error.str()); + } + std::tuple mark{ entry_pointer, rule->name(), current}; if (processed_rules.contains(mark)) { diff --git a/vendor/blaze/src/alterschema/upgrade/helpers.h b/vendor/blaze/src/alterschema/upgrade/helpers.h new file mode 100644 index 000000000..eec42a5aa --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/helpers.h @@ -0,0 +1,84 @@ +static const std::string DIALECT_OVERRIDE_KEYWORD{ + "x-sourcemeta-dialect-override-subschema"}; + +static auto mark_dialect_override(sourcemeta::core::JSON &schema, + const std::string_view dialect) -> void { + schema.assign(DIALECT_OVERRIDE_KEYWORD, sourcemeta::core::JSON{dialect}); +} + +static auto current_dialect_or_override(const sourcemeta::core::JSON &schema) + -> std::string_view { + if (!schema.is_object()) { + return {}; + } + const auto *override_value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + if (override_value != nullptr && override_value->is_string()) { + return override_value->to_string(); + } + if (schema.defines("$schema") && schema.at("$schema").is_string()) { + return schema.at("$schema").to_string(); + } + return {}; +} + +static auto +subschema_at_dialect(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame::Location &location, + const std::string_view dialect) -> bool { + const auto current{current_dialect_or_override(schema)}; + if (!current.empty()) { + return current == dialect; + } + return schema.is_object() && location.pointer.empty(); +} + +static auto drop_dialect_overrides(sourcemeta::core::JSON &schema, + const bool is_root) -> void { + if (schema.is_array()) { + for (auto &item : schema.as_array()) { + drop_dialect_overrides(item, false); + } + return; + } + + if (!schema.is_object()) { + return; + } + + if (!is_root && schema.defines("$schema") && + schema.at("$schema").is_string()) { + return; + } + + schema.erase(DIALECT_OVERRIDE_KEYWORD); + + std::vector keys; + keys.reserve(schema.size()); + for (const auto &entry : schema.as_object()) { + keys.push_back(entry.first); + } + for (const auto &key : keys) { + drop_dialect_overrides(schema.at(key), false); + } +} + +struct AnchorCharPolicy { + std::function is_valid_first; + std::function is_valid_body; +}; + +static auto sanitize_anchor_with_policy(const std::string_view original, + const std::set &in_use, + const AnchorCharPolicy &policy) + -> std::string { + std::string sanitized; + sanitized.reserve(original.size()); + for (const char character : original) { + sanitized.push_back(policy.is_valid_body(character) ? character : '-'); + } + while (sanitized.empty() || !policy.is_valid_first(sanitized.front()) || + in_use.contains(sanitized)) { + sanitized.insert(0, "x-"); + } + return sanitized; +} diff --git a/vendor/blaze/src/alterschema/upgrade/prefix_promoted_2020_12_keywords.h b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_2020_12_keywords.h new file mode 100644 index 000000000..a95c05900 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_2020_12_keywords.h @@ -0,0 +1,65 @@ +class PrefixPromoted202012Keywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + PrefixPromoted202012Keywords() + : SchemaTransformRule{"prefix_promoted_2020_12_keywords", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && + schema.is_object()); + + return schema.defines_any({"prefixItems", "$dynamicAnchor", "$dynamicRef"}); + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + for (const auto &keyword : KEYWORDS) { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + continue; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_name, new_name] : this->renames_) { + const auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static inline const std::array KEYWORDS{ + {"prefixItems", "$dynamicAnchor", "$dynamicRef"}}; + + mutable std::unordered_map renames_; +}; diff --git a/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_2019_09_keywords.h b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_2019_09_keywords.h new file mode 100644 index 000000000..0e67f10aa --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_2019_09_keywords.h @@ -0,0 +1,74 @@ +class PrefixPromoted201909Keywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + PrefixPromoted201909Keywords() + : SchemaTransformRule{"prefix_promoted_2019_09_keywords", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_7) && + schema.is_object()); + + for (const auto &keyword : KEYWORDS) { + if (schema.defines(keyword)) { + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + for (const auto &keyword : KEYWORDS) { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + continue; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_name, new_name] : this->renames_) { + const auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static inline const std::array KEYWORDS{ + {"$anchor", "$recursiveAnchor", "$recursiveRef", "$vocabulary", "$defs", + "dependentSchemas", "dependentRequired", "unevaluatedItems", + "unevaluatedProperties", "maxContains", "minContains", "contentSchema", + "deprecated"}}; + + mutable std::unordered_map renames_; +}; diff --git a/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_4_keywords.h b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_4_keywords.h new file mode 100644 index 000000000..921cbba99 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_4_keywords.h @@ -0,0 +1,72 @@ +class PrefixPromotedDraft4Keywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + PrefixPromotedDraft4Keywords() + : SchemaTransformRule{"prefix_promoted_draft_4_keywords", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && + schema.is_object()); + + for (const auto &keyword : KEYWORDS) { + if (schema.defines(keyword)) { + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + for (const auto &keyword : KEYWORDS) { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + continue; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_name, new_name] : this->renames_) { + const auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static inline const std::array KEYWORDS{ + {"multipleOf", "maxProperties", "minProperties", "allOf", "anyOf", + "oneOf", "not"}}; + + mutable std::unordered_map renames_; +}; diff --git a/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_6_keywords.h b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_6_keywords.h new file mode 100644 index 000000000..f2bcae220 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_6_keywords.h @@ -0,0 +1,71 @@ +class PrefixPromotedDraft6Keywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + PrefixPromotedDraft6Keywords() + : SchemaTransformRule{"prefix_promoted_draft_6_keywords", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4) && + schema.is_object()); + + for (const auto &keyword : KEYWORDS) { + if (schema.defines(keyword)) { + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + for (const auto &keyword : KEYWORDS) { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + continue; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_name, new_name] : this->renames_) { + const auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static inline const std::array KEYWORDS{ + {"const", "contains", "propertyNames", "examples"}}; + + mutable std::unordered_map renames_; +}; diff --git a/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_7_keywords.h b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_7_keywords.h new file mode 100644 index 000000000..597d2eee6 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/prefix_promoted_draft_7_keywords.h @@ -0,0 +1,72 @@ +class PrefixPromotedDraft7Keywords final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + PrefixPromotedDraft7Keywords() + : SchemaTransformRule{"prefix_promoted_draft_7_keywords", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_6) && + schema.is_object()); + + for (const auto &keyword : KEYWORDS) { + if (schema.defines(keyword)) { + return true; + } + } + + return false; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + for (const auto &keyword : KEYWORDS) { + const std::string keyword_name{keyword}; + if (!schema.defines(keyword_name)) { + continue; + } + + std::string prefixed_name{"x-" + keyword_name}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace(keyword_name, prefixed_name); + schema.rename(keyword_name, std::move(prefixed_name)); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_name, new_name] : this->renames_) { + const auto result{ + target.rebase(current.concat(sourcemeta::core::Pointer{old_name}), + current.concat(sourcemeta::core::Pointer{new_name}))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static inline const std::array KEYWORDS{ + {"$comment", "if", "then", "else", "readOnly", "writeOnly", + "contentMediaType", "contentEncoding"}}; + + mutable std::unordered_map renames_; +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_2019_09_to_2020_12.h b/vendor/blaze/src/alterschema/upgrade/upgrade_2019_09_to_2020_12.h new file mode 100644 index 000000000..18d907b71 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_2019_09_to_2020_12.h @@ -0,0 +1,580 @@ +class Upgrade201909To202012 final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + Upgrade201909To202012() + : SchemaTransformRule{"upgrade_2019_09_to_2020_12", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_2019_09_Core) && + schema.is_object()); + + const bool is_resource_scope{ + location.type == + sourcemeta::core::SchemaFrame::LocationType::Resource || + location.pointer.empty()}; + + if (is_resource_scope) { + compute_anchor_sanitization(root, frame, location); + if (!this->anchor_renames_.empty() || + !this->anchor_ref_rewrites_.empty()) { + this->descendant_has_pending_pattern_ = + any_descendant_has_pending_pattern(root, frame, location); + this->resource_has_recursive_anchor_ = + compute_resource_has_recursive_anchor(root, frame, location); + this->document_has_unevaluated_items_ = + compute_document_has_unevaluated_items(root, frame, walker, + resolver); + this->is_inside_contains_wrapper_ = false; + return Result{std::vector{}, + std::string{"sanitize"}}; + } + } + + if (enclosing_resource_has_pending_sanitization(root, frame, location)) { + return false; + } + + this->is_inside_contains_wrapper_ = + location_inside_contains_wrapper(location); + + if (any_descendant_has_pending_pattern(root, frame, location)) { + return false; + } + + this->resource_has_recursive_anchor_ = + compute_resource_has_recursive_anchor(root, frame, location); + this->document_has_unevaluated_items_ = + compute_document_has_unevaluated_items(root, frame, walker, resolver); + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &result) const + -> void override { + const bool is_sanitize_phase{result.description.has_value() && + result.description.value() == "sanitize"}; + if (is_sanitize_phase) { + apply_anchor_sanitization(schema); + if (this->descendant_has_pending_pattern_) { + return; + } + } + + this->renames_.clear(); + + if (schema.defines("$recursiveAnchor") && + schema.at("$recursiveAnchor").is_boolean()) { + if (schema.at("$recursiveAnchor").to_boolean()) { + schema.rename("$recursiveAnchor", "$dynamicAnchor"); + schema.at("$dynamicAnchor").into(sourcemeta::core::JSON{"meta"}); + } else { + schema.erase("$recursiveAnchor"); + } + } + + if (schema.defines("$recursiveRef")) { + schema.rename("$recursiveRef", "$dynamicRef"); + if (this->resource_has_recursive_anchor_) { + schema.at("$dynamicRef").into(sourcemeta::core::JSON{"#meta"}); + } + } + + if (schema.defines("items") && schema.at("items").is_array()) { + if (schema.at("items").empty()) { + schema.erase("items"); + } else { + this->renames_.emplace_back(sourcemeta::core::Pointer{"items"}, + sourcemeta::core::Pointer{"prefixItems"}); + schema.rename("items", "prefixItems"); + } + if (schema.defines("additionalItems")) { + this->renames_.emplace_back( + sourcemeta::core::Pointer{"additionalItems"}, + sourcemeta::core::Pointer{"items"}); + schema.rename("additionalItems", "items"); + } + } else if (schema.defines("additionalItems")) { + schema.erase("additionalItems"); + } + + if (schema.defines("contains") && !this->is_inside_contains_wrapper_ && + this->document_has_unevaluated_items_) { + auto wrapper_inner{sourcemeta::core::JSON::make_object()}; + wrapper_inner.assign("contains", schema.at("contains")); + if (schema.defines("minContains")) { + wrapper_inner.assign("minContains", schema.at("minContains")); + schema.erase("minContains"); + } + if (schema.defines("maxContains")) { + wrapper_inner.assign("maxContains", schema.at("maxContains")); + schema.erase("maxContains"); + } + + auto inner_not{sourcemeta::core::JSON::make_object()}; + inner_not.assign("not", std::move(wrapper_inner)); + + if (!schema.defines("not")) { + schema.rename("contains", "not"); + schema.at("not").into(std::move(inner_not)); + this->renames_.emplace_back( + sourcemeta::core::Pointer{"contains"}, + sourcemeta::core::Pointer{"not", "not", "contains"}); + } else { + schema.erase("contains"); + auto outer_not{sourcemeta::core::JSON::make_object()}; + outer_not.assign("not", std::move(inner_not)); + if (schema.defines("allOf") && schema.at("allOf").is_array()) { + const auto allof_index{schema.at("allOf").size()}; + schema.at("allOf").push_back(std::move(outer_not)); + this->renames_.emplace_back( + sourcemeta::core::Pointer{"contains"}, + sourcemeta::core::Pointer{ + "allOf", + static_cast( + allof_index), + "not", "not", "contains"}); + } else { + auto allof_array{sourcemeta::core::JSON::make_array()}; + allof_array.push_back(std::move(outer_not)); + schema.assign("allOf", std::move(allof_array)); + this->renames_.emplace_back( + sourcemeta::core::Pointer{"contains"}, + sourcemeta::core::Pointer{ + "allOf", + static_cast(0), + "not", "not", "contains"}); + } + } + } + + if (schema.defines("$schema") && schema.at("$schema").is_string() && + schema.at("$schema").to_string() == DRAFT_2019_09_URL) { + schema.assign("$schema", sourcemeta::core::JSON{DRAFT_2020_12_URL}); + drop_dialect_overrides(schema, true); + } else { + mark_dialect_override(schema, DRAFT_2020_12_URL); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_pointer, new_pointer] : this->renames_) { + const auto result{target.rebase(current.concat(old_pointer), + current.concat(new_pointer))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static constexpr std::string_view DRAFT_2019_09_URL{ + "https://json-schema.org/draft/2019-09/schema"}; + static constexpr std::string_view DRAFT_2020_12_URL{ + "https://json-schema.org/draft/2020-12/schema"}; + + struct AnchorRename { + sourcemeta::core::Pointer subschema_pointer; + std::string new_name; + }; + + struct AnchorRefRewrite { + sourcemeta::core::Pointer ref_pointer; + std::string new_value; + }; + + mutable std::vector< + std::pair> + renames_; + mutable bool resource_has_recursive_anchor_{false}; + mutable bool is_inside_contains_wrapper_{false}; + mutable bool descendant_has_pending_pattern_{false}; + mutable bool document_has_unevaluated_items_{false}; + mutable std::vector anchor_renames_; + mutable std::vector anchor_ref_rewrites_; + + static auto compute_document_has_unevaluated_items( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) -> bool { + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + const auto absolute{sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &subschema{sourcemeta::core::get(root, absolute)}; + if (!subschema.is_object() || !subschema.defines("unevaluatedItems")) { + continue; + } + const auto location_vocabularies{ + frame.vocabularies(entry.second, resolver)}; + const auto &keyword_metadata{ + walker("unevaluatedItems", location_vocabularies)}; + if (keyword_metadata.type != + sourcemeta::core::SchemaKeywordType::Unknown) { + return true; + } + } + return false; + } + + static auto any_descendant_has_pending_pattern( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location) -> bool { + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + if (entry.second.pointer.size() <= location.pointer.size() || + !entry.second.pointer.starts_with(location.pointer)) { + continue; + } + const auto absolute{sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &descendant{sourcemeta::core::get(root, absolute)}; + if (has_pending_pattern(descendant, entry.second)) { + return true; + } + } + return false; + } + + static auto is_2020_12_anchor_first_char(const char character) -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || character == '_'; + } + + static auto is_2020_12_anchor_body_char(const char character) -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '_' || + character == '.' || character == '-'; + } + + static auto is_valid_2020_12_anchor(const std::string_view name) -> bool { + if (name.empty()) { + return false; + } + if (!is_2020_12_anchor_first_char(name.front())) { + return false; + } + for (std::size_t index{1}; index < name.size(); ++index) { + if (!is_2020_12_anchor_body_char(name[index])) { + return false; + } + } + return true; + } + + static auto location_inside_contains_wrapper( + const sourcemeta::core::SchemaFrame::Location &location) -> bool { + if (location.pointer.size() < 2) { + return false; + } + const auto &last{location.pointer.back()}; + if (!last.is_property() || last.to_property() != "not") { + return false; + } + const auto &second_last{location.pointer.at(location.pointer.size() - 2)}; + if (!second_last.is_property() || second_last.to_property() != "not") { + return false; + } + return true; + } + + static auto + has_pending_pattern(const sourcemeta::core::JSON &subschema, + const sourcemeta::core::SchemaFrame::Location &location) + -> bool { + if (!subschema.is_object()) { + return false; + } + if (!subschema.defines_any({"$schema", "$recursiveAnchor", "$recursiveRef", + "items", "additionalItems", "contains"})) { + return false; + } + if (subschema.defines("$schema") && subschema.at("$schema").is_string() && + subschema.at("$schema").to_string() == DRAFT_2019_09_URL) { + return true; + } + if (subschema.defines_any( + {"$recursiveAnchor", "$recursiveRef", "additionalItems"})) { + return true; + } + if (subschema.defines("items") && subschema.at("items").is_array()) { + return true; + } + if (subschema.defines("contains") && + !location_inside_contains_wrapper(location)) { + return true; + } + return false; + } + + static auto find_enclosing_resource( + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location ¤t_location) + -> std::optional> { + std::optional< + std::reference_wrapper> + closest; + for (const auto &entry : frame.locations()) { + const bool entry_is_resource_scope{ + entry.second.type == + sourcemeta::core::SchemaFrame::LocationType::Resource || + entry.second.pointer.empty()}; + if (!entry_is_resource_scope) { + continue; + } + if (entry.second.pointer.size() > current_location.pointer.size()) { + continue; + } + if (!current_location.pointer.starts_with(entry.second.pointer)) { + continue; + } + if (!closest.has_value() || + entry.second.pointer.size() > closest.value().get().pointer.size()) { + closest = std::cref(entry.second); + } + } + return closest; + } + + static auto + pointer_within_resource(const sourcemeta::core::WeakPointer &candidate, + const sourcemeta::core::WeakPointer &resource_pointer) + -> bool { + if (!candidate.starts_with(resource_pointer)) { + return false; + } + return true; + } + + auto compute_anchor_sanitization( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &resource_location) const + -> void { + this->anchor_renames_.clear(); + this->anchor_ref_rewrites_.clear(); + + const auto &resource_pointer{resource_location.pointer}; + + std::set existing_valid; + std::vector> invalid; + + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + if (entry.first.first != sourcemeta::core::SchemaReferenceType::Static) { + continue; + } + if (!pointer_within_resource(entry.second.pointer, resource_pointer)) { + continue; + } + const sourcemeta::core::URI anchor_uri{entry.first.second}; + const auto fragment{anchor_uri.fragment()}; + if (!fragment.has_value() || fragment.value().empty()) { + continue; + } + const std::string anchor_name{fragment.value()}; + if (is_valid_2020_12_anchor(anchor_name)) { + existing_valid.insert(anchor_name); + } else { + invalid.emplace_back(anchor_name, entry.second.pointer); + } + } + + if (invalid.empty()) { + return; + } + + static const AnchorCharPolicy POLICY{ + .is_valid_first = &is_2020_12_anchor_first_char, + .is_valid_body = &is_2020_12_anchor_body_char}; + + std::map rename_map; + std::set in_use{existing_valid}; + for (const auto &[original, pointer] : invalid) { + if (rename_map.contains(original)) { + continue; + } + in_use.erase(original); + const auto sanitized{ + sanitize_anchor_with_policy(original, in_use, POLICY)}; + rename_map.emplace(original, sanitized); + in_use.insert(sanitized); + } + + std::set processed_pointers; + for (const auto &[original, pointer] : invalid) { + const auto pointer_str{sourcemeta::core::to_string(pointer)}; + if (processed_pointers.contains(pointer_str)) { + continue; + } + processed_pointers.insert(pointer_str); + + const auto absolute{sourcemeta::core::to_pointer(pointer)}; + const auto &subschema{sourcemeta::core::get(root, absolute)}; + if (!subschema.is_object() || !subschema.defines("$anchor") || + !subschema.at("$anchor").is_string()) { + continue; + } + const auto current_value{subschema.at("$anchor").to_string()}; + const auto rename_iter{rename_map.find(current_value)}; + if (rename_iter == rename_map.end()) { + continue; + } + + const auto relative_weak{pointer.resolve_from(resource_pointer)}; + this->anchor_renames_.push_back( + {sourcemeta::core::to_pointer(relative_weak), rename_iter->second}); + } + + for (const auto &reference : frame.references()) { + if (!pointer_within_resource(reference.first.second, resource_pointer)) { + continue; + } + if (!reference.second.fragment.has_value()) { + continue; + } + const auto &fragment{reference.second.fragment.value()}; + if (fragment.empty() || fragment.front() == '/') { + continue; + } + const auto rename_iter{rename_map.find(std::string{fragment})}; + if (rename_iter == rename_map.end()) { + continue; + } + + sourcemeta::core::URI ref_uri{reference.second.original}; + ref_uri.fragment(rename_iter->second); + const auto new_value{ref_uri.recompose()}; + + const auto relative_weak{ + reference.first.second.resolve_from(resource_pointer)}; + this->anchor_ref_rewrites_.push_back( + {sourcemeta::core::to_pointer(relative_weak), new_value}); + } + } + + static auto enclosing_resource_has_pending_sanitization( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location ¤t_location) -> bool { + const auto closest{find_enclosing_resource(frame, current_location)}; + if (!closest.has_value()) { + return false; + } + const auto &resource_pointer{closest.value().get().pointer}; + + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + if (entry.first.first != sourcemeta::core::SchemaReferenceType::Static) { + continue; + } + if (!pointer_within_resource(entry.second.pointer, resource_pointer)) { + continue; + } + const sourcemeta::core::URI anchor_uri{entry.first.second}; + const auto fragment{anchor_uri.fragment()}; + if (!fragment.has_value() || fragment.value().empty()) { + continue; + } + if (!is_valid_2020_12_anchor(fragment.value())) { + const auto absolute{sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &subschema{sourcemeta::core::get(root, absolute)}; + if (subschema.is_object() && subschema.defines("$anchor") && + subschema.at("$anchor").is_string() && + subschema.at("$anchor").to_string() == fragment.value()) { + return true; + } + } + } + return false; + } + + auto apply_anchor_sanitization(sourcemeta::core::JSON &schema) const -> void { + for (const auto &rename : this->anchor_renames_) { + auto &target{sourcemeta::core::get(schema, rename.subschema_pointer)}; + target.at("$anchor").into(sourcemeta::core::JSON{rename.new_name}); + } + for (const auto &rewrite : this->anchor_ref_rewrites_) { + auto &target{sourcemeta::core::get(schema, rewrite.ref_pointer)}; + target.into(sourcemeta::core::JSON{rewrite.new_value}); + } + } + + static auto compute_resource_has_recursive_anchor( + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location ¤t_location) -> bool { + const auto closest{find_enclosing_resource(frame, current_location)}; + if (!closest.has_value()) { + return false; + } + + const auto &resource_pointer{closest.value().get().pointer}; + std::set seen; + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + if (!entry.second.pointer.starts_with(resource_pointer)) { + continue; + } + if (entry.second.type == + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.pointer.size() > resource_pointer.size()) { + continue; + } + const auto pointer_str{sourcemeta::core::to_string(entry.second.pointer)}; + if (seen.contains(pointer_str)) { + continue; + } + seen.insert(pointer_str); + + const auto absolute{sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &subschema{sourcemeta::core::get(root, absolute)}; + if (!subschema.is_object()) { + continue; + } + if (subschema.defines("$recursiveAnchor") && + subschema.at("$recursiveAnchor").is_boolean() && + subschema.at("$recursiveAnchor").to_boolean()) { + return true; + } + } + return false; + } +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_dialect_override_cleanup.h b/vendor/blaze/src/alterschema/upgrade/upgrade_dialect_override_cleanup.h new file mode 100644 index 000000000..5dbbd6540 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_dialect_override_cleanup.h @@ -0,0 +1,47 @@ +class UpgradeDialectOverrideCleanup final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UpgradeDialectOverrideCleanup() + : SchemaTransformRule{"upgrade_dialect_override_cleanup", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaFrame &, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF(location.pointer.empty() && schema.is_object()); + + const auto *override_value{schema.try_at(DIALECT_OVERRIDE_KEYWORD)}; + ONLY_CONTINUE_IF(override_value != nullptr && override_value->is_string()); + + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + if (!schema.defines("$schema")) { + sourcemeta::core::JSON dialect_value{ + schema.at(DIALECT_OVERRIDE_KEYWORD).to_string()}; + + bool placed{false}; + for (const auto &entry : schema.as_object()) { + if (entry.first != DIALECT_OVERRIDE_KEYWORD) { + schema.try_assign_before("$schema", dialect_value, entry.first); + placed = true; + break; + } + } + + if (!placed) { + schema.assign("$schema", std::move(dialect_value)); + } + } + + drop_dialect_overrides(schema, true); + } +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_draft_3_to_draft_4.h b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_3_to_draft_4.h new file mode 100644 index 000000000..49fce4775 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_3_to_draft_4.h @@ -0,0 +1,431 @@ +class UpgradeDraft3ToDraft4 final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UpgradeDraft3ToDraft4() + : SchemaTransformRule{"upgrade_draft_3_to_draft_4", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_3) && + schema.is_object()); + + const bool root_via_default_dialect = + location.pointer.empty() && !schema.defines("$schema"); + + ONLY_CONTINUE_IF(has_pending_draft_3_pattern(schema) || + root_via_default_dialect); + + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + if (!is_strict_descendant(location.pointer, entry.second.pointer)) { + continue; + } + + const auto entry_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &entry_schema{sourcemeta::core::get(root, entry_pointer)}; + + if (entry_schema.is_object() && entry_schema.defines("$ref")) { + continue; + } + + if (has_pending_draft_3_pattern(entry_schema)) { + return false; + } + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + rewrite_type_any(schema); + rewrite_type_array_with_subschemas(schema); + rewrite_disallow(schema); + rewrite_extends(schema); + rewrite_divisible_by(schema); + rewrite_required_property_booleans(schema); + rewrite_dependencies_string_form(schema); + rewrite_format(schema); + + if (schema.defines("$schema") && schema.at("$schema").is_string() && + schema.at("$schema").to_string() == DRAFT_3_URL) { + schema.assign("$schema", sourcemeta::core::JSON{DRAFT_4_URL}); + drop_dialect_overrides(schema, true); + } else { + mark_dialect_override(schema, DRAFT_4_URL); + } + } + +private: + static inline const std::string DRAFT_3_URL{ + "http://json-schema.org/draft-03/schema#"}; + static inline const std::string DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + + static auto + has_pending_draft_3_pattern(const sourcemeta::core::JSON &subschema) -> bool { + if (!subschema.is_object()) { + return false; + } + + if (subschema.defines("$schema") && subschema.at("$schema").is_string() && + subschema.at("$schema").to_string() == DRAFT_3_URL) { + return true; + } + + const auto *type_value{subschema.try_at("type")}; + if (type_value != nullptr) { + if (type_value->is_string() && type_value->to_string() == "any") { + return true; + } + if (type_value->is_array()) { + for (const auto &element : type_value->as_array()) { + if (element.is_string() && element.to_string() == "any") { + return true; + } + if (element.is_object()) { + return true; + } + } + } + } + + const auto *disallow_value{subschema.try_at("disallow")}; + if (disallow_value != nullptr && + (disallow_value->is_string() || disallow_value->is_array() || + disallow_value->is_object())) { + return true; + } + + const auto *extends_value{subschema.try_at("extends")}; + if (extends_value != nullptr && + (extends_value->is_array() || extends_value->is_object())) { + return true; + } + + if (subschema.defines("divisibleBy")) { + return true; + } + + const auto *properties{subschema.try_at("properties")}; + if (properties != nullptr && properties->is_object()) { + for (const auto &entry : properties->as_object()) { + if (entry.second.is_object() && entry.second.defines("required") && + entry.second.at("required").is_boolean()) { + return true; + } + } + } + + const auto *dependencies{subschema.try_at("dependencies")}; + if (dependencies != nullptr && dependencies->is_object()) { + for (const auto &entry : dependencies->as_object()) { + if (entry.second.is_string()) { + return true; + } + } + } + + if (has_renamable_draft_3_format(subschema)) { + return true; + } + + return false; + } + + static auto + has_renamable_draft_3_format(const sourcemeta::core::JSON &subschema) + -> bool { + if (!subschema.is_object()) { + return false; + } + const auto *format_value{subschema.try_at("format")}; + if (format_value == nullptr || !format_value->is_string()) { + return false; + } + const auto &name{format_value->to_string()}; + return name == "host-name" || name == "ip-address"; + } + + static auto rewrite_type_any(sourcemeta::core::JSON &schema) -> void { + if (!schema.defines("type")) { + return; + } + auto &type_value{schema.at("type")}; + if (type_value.is_string()) { + if (type_value.to_string() == "any") { + schema.erase("type"); + } + return; + } + if (!type_value.is_array()) { + return; + } + bool collapses{false}; + for (const auto &element : type_value.as_array()) { + if (element.is_string() && element.to_string() == "any") { + collapses = true; + break; + } + } + if (collapses) { + schema.erase("type"); + } + } + + static auto rewrite_type_array_with_subschemas(sourcemeta::core::JSON &schema) + -> void { + if (!schema.defines("type")) { + return; + } + auto &type_value{schema.at("type")}; + if (!type_value.is_array()) { + return; + } + bool has_subschema{false}; + for (const auto &element : type_value.as_array()) { + if (element.is_object()) { + has_subschema = true; + break; + } + } + if (!has_subschema) { + return; + } + + auto branches{sourcemeta::core::JSON::make_array()}; + for (const auto &element : type_value.as_array()) { + if (element.is_string()) { + auto branch{sourcemeta::core::JSON::make_object()}; + branch.assign("type", element); + branches.push_back(std::move(branch)); + } else if (element.is_object()) { + branches.push_back(element); + } + } + schema.erase("type"); + schema.assign("anyOf", std::move(branches)); + } + + static auto type_string_to_branch(const std::string &type_name) + -> sourcemeta::core::JSON { + auto branch{sourcemeta::core::JSON::make_object()}; + branch.assign("type", sourcemeta::core::JSON{type_name}); + return branch; + } + + static auto rewrite_disallow(sourcemeta::core::JSON &schema) -> void { + if (!schema.defines("disallow") || schema.defines("not")) { + return; + } + + const auto &disallow{schema.at("disallow")}; + if (!disallow.is_string() && !disallow.is_array() && + !disallow.is_object()) { + return; + } + + if (disallow.is_string() && disallow.to_string() == "any") { + schema.erase("disallow"); + schema.assign("not", sourcemeta::core::JSON::make_object()); + return; + } + + if (disallow.is_array()) { + for (const auto &element : disallow.as_array()) { + if (element.is_string() && element.to_string() == "any") { + schema.erase("disallow"); + schema.assign("not", sourcemeta::core::JSON::make_object()); + return; + } + } + } + + auto negated{sourcemeta::core::JSON::make_object()}; + if (disallow.is_string()) { + negated.assign("type", disallow); + } else if (disallow.is_array()) { + bool has_subschema{false}; + for (const auto &element : disallow.as_array()) { + if (element.is_object()) { + has_subschema = true; + break; + } + } + if (!has_subschema) { + negated.assign("type", disallow); + } else { + auto branches{sourcemeta::core::JSON::make_array()}; + for (const auto &element : disallow.as_array()) { + if (element.is_string()) { + branches.push_back(type_string_to_branch(element.to_string())); + } else if (element.is_object()) { + branches.push_back(element); + } + } + negated.assign("anyOf", std::move(branches)); + } + } else { + negated = disallow; + } + + schema.erase("disallow"); + schema.assign("not", std::move(negated)); + } + + static auto rewrite_extends(sourcemeta::core::JSON &schema) -> void { + if (!schema.defines("extends") || schema.defines("allOf")) { + return; + } + + const auto &extends{schema.at("extends")}; + if (!extends.is_array() && !extends.is_object()) { + return; + } + + auto value{extends}; + schema.erase("extends"); + + if (value.is_array()) { + schema.assign("allOf", std::move(value)); + return; + } + + auto array{sourcemeta::core::JSON::make_array()}; + array.push_back(std::move(value)); + schema.assign("allOf", std::move(array)); + } + + static auto rewrite_divisible_by(sourcemeta::core::JSON &schema) -> void { + if (!schema.defines("divisibleBy") || schema.defines("multipleOf")) { + return; + } + schema.rename("divisibleBy", "multipleOf"); + } + + static auto rewrite_required_property_booleans(sourcemeta::core::JSON &schema) + -> void { + if (!schema.defines("properties") || !schema.at("properties").is_object()) { + return; + } + + std::vector newly_required; + auto &properties{schema.at("properties")}; + std::vector property_keys; + property_keys.reserve(properties.size()); + for (const auto &entry : properties.as_object()) { + property_keys.push_back(entry.first); + } + + for (const auto &key : property_keys) { + auto &property{properties.at(key)}; + if (!property.is_object() || !property.defines("required")) { + continue; + } + const auto &required_value{property.at("required")}; + if (!required_value.is_boolean()) { + continue; + } + const bool is_required{required_value.to_boolean()}; + property.erase("required"); + if (is_required) { + newly_required.push_back(key); + } + } + + if (newly_required.empty()) { + return; + } + + if (!schema.defines("required") || !schema.at("required").is_array()) { + auto fresh{sourcemeta::core::JSON::make_array()}; + for (const auto &name : newly_required) { + fresh.push_back(sourcemeta::core::JSON{name}); + } + schema.try_assign_before("required", fresh, "properties"); + return; + } + + auto &existing{schema.at("required")}; + std::set already; + for (const auto &item : existing.as_array()) { + if (item.is_string()) { + already.insert(item.to_string()); + } + } + for (const auto &name : newly_required) { + if (already.contains(name)) { + continue; + } + existing.push_back(sourcemeta::core::JSON{name}); + already.insert(name); + } + } + + static auto rewrite_dependencies_string_form(sourcemeta::core::JSON &schema) + -> void { + if (!schema.defines("dependencies") || + !schema.at("dependencies").is_object()) { + return; + } + auto &dependencies{schema.at("dependencies")}; + std::vector string_keys; + for (const auto &entry : dependencies.as_object()) { + if (entry.second.is_string()) { + string_keys.push_back(entry.first); + } + } + for (const auto &key : string_keys) { + const auto value{dependencies.at(key).to_string()}; + auto array{sourcemeta::core::JSON::make_array()}; + array.push_back(sourcemeta::core::JSON{value}); + dependencies.assign(key, std::move(array)); + } + } + + static auto rewrite_format(sourcemeta::core::JSON &schema) -> void { + if (!schema.defines("format")) { + return; + } + auto &format_value{schema.at("format")}; + if (!format_value.is_string()) { + return; + } + const auto &name{format_value.to_string()}; + if (name == "host-name") { + schema.assign("format", sourcemeta::core::JSON{"hostname"}); + } else if (name == "ip-address") { + schema.assign("format", sourcemeta::core::JSON{"ipv4"}); + } + } + + static auto + is_strict_descendant(const sourcemeta::core::WeakPointer &ancestor, + const sourcemeta::core::WeakPointer &candidate) -> bool { + if (candidate.size() <= ancestor.size()) { + return false; + } + for (std::size_t index{0}; index < ancestor.size(); ++index) { + if (!(ancestor.at(index) == candidate.at(index))) { + return false; + } + } + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_draft_4_to_draft_6.h b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_4_to_draft_6.h new file mode 100644 index 000000000..e4e4e7b93 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_4_to_draft_6.h @@ -0,0 +1,613 @@ +class UpgradeDraft4ToDraft6 final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UpgradeDraft4ToDraft6() + : SchemaTransformRule{"upgrade_draft_4_to_draft_6", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_4) && + schema.is_object()); + + const bool is_resource_scope = + location.type == + sourcemeta::core::SchemaFrame::LocationType::Resource || + location.pointer.empty(); + + const bool sanitization_branch = + is_resource_scope && resource_needs_anchor_sanitization(schema); + + const bool other_branch = has_pending_draft_4_pattern(schema); + + const bool root_via_default_dialect = + location.pointer.empty() && !schema.defines("$schema"); + + ONLY_CONTINUE_IF(sanitization_branch || other_branch || + root_via_default_dialect); + + if (!sanitization_branch && other_branch && + enclosing_resource_has_pending_sanitization(location, root, frame)) { + return false; + } + + if (!sanitization_branch) { + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + if (!is_strict_descendant(location.pointer, entry.second.pointer)) { + continue; + } + + const auto entry_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &entry_schema{sourcemeta::core::get(root, entry_pointer)}; + + if (entry_schema.is_object() && entry_schema.defines("$ref")) { + continue; + } + + if (has_pending_draft_4_pattern(entry_schema)) { + return false; + } + } + } + + if (sanitization_branch) { + return Result{std::vector{}, + std::string{"sanitize"}}; + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &result) const + -> void override { + if (result.description.has_value()) { + const auto renames{build_resource_rename_map(schema)}; + std::optional resource_base; + if (schema.defines("id") && schema.at("id").is_string()) { + const sourcemeta::core::URI uri{schema.at("id").to_string()}; + const auto without_fragment{uri.recompose_without_fragment()}; + if (without_fragment.has_value() && !without_fragment.value().empty()) { + resource_base = without_fragment.value(); + } + } + apply_anchor_renames_in_resource(schema, true, renames, resource_base); + if (resource_has_descendant_with_pending_pattern(schema, true)) { + return; + } + } + + if (schema.defines("id") && schema.at("id").is_string() && + !schema.defines("$id")) { + schema.rename("id", "$id"); + } + + if (schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_boolean()) { + const bool exclusive{schema.at("exclusiveMinimum").to_boolean()}; + schema.erase("exclusiveMinimum"); + if (exclusive && schema.defines("minimum") && + schema.at("minimum").is_number()) { + schema.rename("minimum", "exclusiveMinimum"); + } + } + + if (schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_boolean()) { + const bool exclusive{schema.at("exclusiveMaximum").to_boolean()}; + schema.erase("exclusiveMaximum"); + if (exclusive && schema.defines("maximum") && + schema.at("maximum").is_number()) { + schema.rename("maximum", "exclusiveMaximum"); + } + } + + if (schema.defines("$schema") && schema.at("$schema").is_string() && + schema.at("$schema").to_string() == DRAFT_4_URL) { + schema.assign("$schema", sourcemeta::core::JSON{DRAFT_6_URL}); + drop_dialect_overrides(schema, true); + } else { + mark_dialect_override(schema, DRAFT_6_URL); + } + } + +private: + static inline const std::string DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + static inline const std::string DRAFT_6_URL{ + "http://json-schema.org/draft-06/schema#"}; + static inline const std::array PROMOTED_KEYWORDS{ + {"const", "contains", "propertyNames", "examples"}}; + + static auto + has_pending_draft_4_pattern(const sourcemeta::core::JSON &subschema) -> bool { + if (!subschema.is_object()) { + return false; + } + + if (subschema.defines("$schema") && subschema.at("$schema").is_string() && + subschema.at("$schema").to_string() == DRAFT_4_URL) { + return true; + } + + if (subschema.defines("id") && subschema.at("id").is_string() && + !subschema.defines("$id")) { + const auto fragment{extract_id_fragment(subschema.at("id"))}; + if (!fragment.has_value() || fragment.value().empty() || + is_strict_plain_name(fragment.value())) { + return true; + } + } + + const auto *exclusive_minimum{subschema.try_at("exclusiveMinimum")}; + if (exclusive_minimum != nullptr && exclusive_minimum->is_boolean()) { + return true; + } + + const auto *exclusive_maximum{subschema.try_at("exclusiveMaximum")}; + if (exclusive_maximum != nullptr && exclusive_maximum->is_boolean()) { + return true; + } + + for (const auto &keyword : PROMOTED_KEYWORDS) { + if (subschema.defines(keyword)) { + return true; + } + } + + return false; + } + + static auto + is_strict_descendant(const sourcemeta::core::WeakPointer &ancestor, + const sourcemeta::core::WeakPointer &candidate) -> bool { + if (candidate.size() <= ancestor.size()) { + return false; + } + for (std::size_t index{0}; index < ancestor.size(); ++index) { + if (!(ancestor.at(index) == candidate.at(index))) { + return false; + } + } + return true; + } + + static auto is_strict_plain_name_first_char(const char character) -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z'); + } + + static auto is_strict_plain_name_body_char(const char character) -> bool { + return (character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z') || + (character >= '0' && character <= '9') || character == '_' || + character == ':' || character == '.' || character == '-'; + } + + static auto is_strict_plain_name(const std::string_view fragment) -> bool { + if (fragment.empty()) { + return false; + } + if (!is_strict_plain_name_first_char(fragment.front())) { + return false; + } + for (std::size_t index{1}; index < fragment.size(); ++index) { + if (!is_strict_plain_name_body_char(fragment[index])) { + return false; + } + } + return true; + } + + static auto sanitize_anchor_name(const std::string_view original, + const std::set &existing_names) + -> std::string { + static const AnchorCharPolicy POLICY{ + .is_valid_first = &is_strict_plain_name_first_char, + .is_valid_body = &is_strict_plain_name_body_char}; + return sanitize_anchor_with_policy(original, existing_names, POLICY); + } + + static auto extract_id_fragment(const sourcemeta::core::JSON &id_value) + -> std::optional { + if (!id_value.is_string()) { + return std::nullopt; + } + const sourcemeta::core::URI uri{id_value.to_string()}; + const auto fragment{uri.fragment()}; + if (!fragment.has_value()) { + return std::nullopt; + } + return std::string{fragment.value()}; + } + + static auto + subschema_id_fragment_is_invalid(const sourcemeta::core::JSON &subschema) + -> bool { + if (!subschema.is_object() || !subschema.defines("id") || + !subschema.at("id").is_string()) { + return false; + } + const auto fragment{extract_id_fragment(subschema.at("id"))}; + if (!fragment.has_value() || fragment.value().empty()) { + return false; + } + return !is_strict_plain_name(fragment.value()); + } + + static auto + subschema_starts_sub_resource(const sourcemeta::core::JSON &subschema) + -> bool { + if (!subschema.is_object() || !subschema.defines("id") || + !subschema.at("id").is_string()) { + return false; + } + const sourcemeta::core::URI uri{subschema.at("id").to_string()}; + if (uri.is_fragment_only()) { + return false; + } + const auto without_fragment{uri.recompose_without_fragment()}; + return without_fragment.has_value() && !without_fragment.value().empty(); + } + + static auto collect_resource_anchors(const sourcemeta::core::JSON &subschema, + const bool is_root, + std::set &result) -> void { + if (!subschema.is_object()) { + return; + } + + if (!is_root && subschema_starts_sub_resource(subschema)) { + return; + } + + if (subschema.defines("id") && subschema.at("id").is_string()) { + const auto fragment{extract_id_fragment(subschema.at("id"))}; + if (fragment.has_value() && !fragment.value().empty()) { + result.insert(fragment.value()); + } + } + + for (const std::string_view object_keyword : + {"definitions", "properties", "patternProperties", "dependencies"}) { + if (subschema.defines(object_keyword) && + subschema.at(object_keyword).is_object()) { + for (const auto &entry : subschema.at(object_keyword).as_object()) { + collect_resource_anchors(entry.second, false, result); + } + } + } + + for (const std::string_view array_keyword : {"allOf", "anyOf", "oneOf"}) { + if (subschema.defines(array_keyword) && + subschema.at(array_keyword).is_array()) { + for (const auto &item : subschema.at(array_keyword).as_array()) { + collect_resource_anchors(item, false, result); + } + } + } + + for (const std::string_view single_keyword : + {"additionalProperties", "additionalItems", "not"}) { + if (subschema.defines(single_keyword)) { + collect_resource_anchors(subschema.at(single_keyword), false, result); + } + } + + if (subschema.defines("items")) { + const auto &items{subschema.at("items")}; + if (items.is_array()) { + for (const auto &item : items.as_array()) { + collect_resource_anchors(item, false, result); + } + } else { + collect_resource_anchors(items, false, result); + } + } + } + + static auto collect_invalid_anchors(const sourcemeta::core::JSON &subschema, + const bool is_root, + std::vector &result) + -> void { + if (!subschema.is_object()) { + return; + } + + if (!is_root && subschema_starts_sub_resource(subschema)) { + return; + } + + if (subschema_id_fragment_is_invalid(subschema)) { + const auto fragment{extract_id_fragment(subschema.at("id"))}; + result.push_back(fragment.value()); + } + + for (const std::string_view object_keyword : + {"definitions", "properties", "patternProperties", "dependencies"}) { + if (subschema.defines(object_keyword) && + subschema.at(object_keyword).is_object()) { + for (const auto &entry : subschema.at(object_keyword).as_object()) { + collect_invalid_anchors(entry.second, false, result); + } + } + } + + for (const std::string_view array_keyword : {"allOf", "anyOf", "oneOf"}) { + if (subschema.defines(array_keyword) && + subschema.at(array_keyword).is_array()) { + for (const auto &item : subschema.at(array_keyword).as_array()) { + collect_invalid_anchors(item, false, result); + } + } + } + + for (const std::string_view single_keyword : + {"additionalProperties", "additionalItems", "not"}) { + if (subschema.defines(single_keyword)) { + collect_invalid_anchors(subschema.at(single_keyword), false, result); + } + } + + if (subschema.defines("items")) { + const auto &items{subschema.at("items")}; + if (items.is_array()) { + for (const auto &item : items.as_array()) { + collect_invalid_anchors(item, false, result); + } + } else { + collect_invalid_anchors(items, false, result); + } + } + } + + static auto + build_resource_rename_map(const sourcemeta::core::JSON &resource_root) + -> std::map { + std::set existing; + collect_resource_anchors(resource_root, true, existing); + + std::vector invalid; + collect_invalid_anchors(resource_root, true, invalid); + + std::map renames; + std::set in_use{existing}; + for (const auto &original : invalid) { + if (renames.contains(original)) { + continue; + } + in_use.erase(original); + const auto sanitized{sanitize_anchor_name(original, in_use)}; + renames.emplace(original, sanitized); + in_use.insert(sanitized); + } + return renames; + } + + static auto resource_has_descendant_with_pending_pattern( + const sourcemeta::core::JSON &subschema, const bool is_root) -> bool { + if (!subschema.is_object()) { + return false; + } + if (!is_root && subschema_starts_sub_resource(subschema)) { + return false; + } + if (!is_root && has_pending_draft_4_pattern(subschema)) { + return true; + } + + for (const std::string_view object_keyword : + {"definitions", "properties", "patternProperties", "dependencies"}) { + if (subschema.defines(object_keyword) && + subschema.at(object_keyword).is_object()) { + for (const auto &entry : subschema.at(object_keyword).as_object()) { + if (resource_has_descendant_with_pending_pattern(entry.second, + false)) { + return true; + } + } + } + } + + for (const std::string_view array_keyword : {"allOf", "anyOf", "oneOf"}) { + if (subschema.defines(array_keyword) && + subschema.at(array_keyword).is_array()) { + for (const auto &item : subschema.at(array_keyword).as_array()) { + if (resource_has_descendant_with_pending_pattern(item, false)) { + return true; + } + } + } + } + + for (const std::string_view single_keyword : + {"additionalProperties", "additionalItems", "not"}) { + if (subschema.defines(single_keyword)) { + if (resource_has_descendant_with_pending_pattern( + subschema.at(single_keyword), false)) { + return true; + } + } + } + + if (subschema.defines("items")) { + const auto &items{subschema.at("items")}; + if (items.is_array()) { + for (const auto &item : items.as_array()) { + if (resource_has_descendant_with_pending_pattern(item, false)) { + return true; + } + } + } else if (resource_has_descendant_with_pending_pattern(items, false)) { + return true; + } + } + + return false; + } + + static auto apply_anchor_renames_in_resource( + sourcemeta::core::JSON &subschema, const bool is_root, + const std::map &renames, + const std::optional &resource_base) -> void { + if (!subschema.is_object()) { + return; + } + + if (!is_root && subschema_starts_sub_resource(subschema)) { + return; + } + + if (subschema.defines("id") && subschema.at("id").is_string()) { + const auto &id_string{subschema.at("id").to_string()}; + const sourcemeta::core::URI uri{id_string}; + const auto fragment{uri.fragment()}; + if (fragment.has_value() && !fragment.value().empty()) { + const auto rename_iter{renames.find(std::string{fragment.value()})}; + if (rename_iter != renames.end()) { + if (uri.is_fragment_only()) { + subschema.assign("id", + sourcemeta::core::JSON{"#" + rename_iter->second}); + } else { + const auto without_fragment{uri.recompose_without_fragment()}; + subschema.assign( + "id", sourcemeta::core::JSON{(without_fragment.has_value() + ? without_fragment.value() + : std::string{}) + + "#" + rename_iter->second}); + } + } + } + } + + if (subschema.defines("$ref") && subschema.at("$ref").is_string()) { + const auto &ref_string{subschema.at("$ref").to_string()}; + const sourcemeta::core::URI ref_uri{ref_string}; + const auto fragment{ref_uri.fragment()}; + if (fragment.has_value() && + renames.contains(std::string{fragment.value()})) { + const auto without_fragment{ref_uri.recompose_without_fragment()}; + const bool same_base = + ref_uri.is_fragment_only() || + (resource_base.has_value() && without_fragment.has_value() && + without_fragment.value() == resource_base.value()); + if (same_base) { + const auto &new_name{renames.at(std::string{fragment.value()})}; + subschema.assign( + "$ref", sourcemeta::core::JSON{ + ref_uri.is_fragment_only() + ? "#" + new_name + : (without_fragment.value() + "#" + new_name)}); + } + } + } + + for (const std::string_view object_keyword : + {"definitions", "properties", "patternProperties", "dependencies"}) { + if (subschema.defines(object_keyword) && + subschema.at(object_keyword).is_object()) { + std::vector keys; + keys.reserve(subschema.at(object_keyword).size()); + for (const auto &entry : subschema.at(object_keyword).as_object()) { + keys.push_back(entry.first); + } + for (const auto &key : keys) { + apply_anchor_renames_in_resource(subschema.at(object_keyword).at(key), + false, renames, resource_base); + } + } + } + + for (const std::string_view array_keyword : {"allOf", "anyOf", "oneOf"}) { + if (subschema.defines(array_keyword) && + subschema.at(array_keyword).is_array()) { + auto &array_value{subschema.at(array_keyword)}; + for (std::size_t index{0}; index < array_value.size(); ++index) { + apply_anchor_renames_in_resource(array_value.at(index), false, + renames, resource_base); + } + } + } + + for (const std::string_view single_keyword : + {"additionalProperties", "additionalItems", "not"}) { + if (subschema.defines(single_keyword)) { + apply_anchor_renames_in_resource(subschema.at(single_keyword), false, + renames, resource_base); + } + } + + if (subschema.defines("items")) { + auto &items{subschema.at("items")}; + if (items.is_array()) { + for (std::size_t index{0}; index < items.size(); ++index) { + apply_anchor_renames_in_resource(items.at(index), false, renames, + resource_base); + } + } else { + apply_anchor_renames_in_resource(items, false, renames, resource_base); + } + } + } + + static auto resource_needs_anchor_sanitization( + const sourcemeta::core::JSON &resource_root) -> bool { + std::vector invalid; + collect_invalid_anchors(resource_root, true, invalid); + return !invalid.empty(); + } + + static auto enclosing_resource_has_pending_sanitization( + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::JSON &root, + const sourcemeta::core::SchemaFrame &frame) -> bool { + std::optional closest; + for (const auto &entry : frame.locations()) { + const bool entry_is_resource_scope = + entry.second.type == + sourcemeta::core::SchemaFrame::LocationType::Resource || + entry.second.pointer.empty(); + if (!entry_is_resource_scope) { + continue; + } + if (entry.second.pointer.size() > location.pointer.size()) { + continue; + } + bool is_ancestor{true}; + for (std::size_t index{0}; index < entry.second.pointer.size(); ++index) { + if (!(entry.second.pointer.at(index) == location.pointer.at(index))) { + is_ancestor = false; + break; + } + } + if (!is_ancestor) { + continue; + } + if (!closest.has_value() || + entry.second.pointer.size() > closest.value().size()) { + closest = entry.second.pointer; + } + } + if (!closest.has_value()) { + return false; + } + const auto closest_pointer{sourcemeta::core::to_pointer(closest.value())}; + const auto &resource_schema{sourcemeta::core::get(root, closest_pointer)}; + return resource_needs_anchor_sanitization(resource_schema); + } +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_draft_6_to_draft_7.h b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_6_to_draft_7.h new file mode 100644 index 000000000..a3cf617c0 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_6_to_draft_7.h @@ -0,0 +1,106 @@ +class UpgradeDraft6ToDraft7 final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UpgradeDraft6ToDraft7() + : SchemaTransformRule{"upgrade_draft_6_to_draft_7", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_6) && + subschema_at_dialect(schema, location, DRAFT_6_URL)); + + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + if (!is_strict_descendant(location.pointer, entry.second.pointer)) { + continue; + } + + const auto entry_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &entry_schema{sourcemeta::core::get(root, entry_pointer)}; + + if (entry_schema.is_object() && entry_schema.defines("$ref")) { + continue; + } + + if (has_pending_pattern(entry_schema)) { + return false; + } + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + if (schema.defines("$schema") && schema.at("$schema").is_string() && + schema.at("$schema").to_string() == DRAFT_6_URL) { + schema.assign("$schema", sourcemeta::core::JSON{DRAFT_7_URL}); + drop_dialect_overrides(schema, true); + } else { + mark_dialect_override(schema, DRAFT_7_URL); + } + } + +private: + static inline const std::string DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + static inline const std::string DRAFT_6_URL{ + "http://json-schema.org/draft-06/schema#"}; + static inline const std::string DRAFT_7_URL{ + "http://json-schema.org/draft-07/schema#"}; + static inline const std::array PROMOTED_KEYWORDS{ + {"$comment", "if", "then", "else", "readOnly", "writeOnly", + "contentMediaType", "contentEncoding"}}; + + static auto has_pending_pattern(const sourcemeta::core::JSON &subschema) + -> bool { + if (!subschema.is_object()) { + return false; + } + + if (subschema.defines("$schema") && subschema.at("$schema").is_string()) { + const auto &dialect{subschema.at("$schema").to_string()}; + if (dialect == DRAFT_4_URL || dialect == DRAFT_6_URL) { + return true; + } + } + + for (const auto &keyword : PROMOTED_KEYWORDS) { + if (subschema.defines(keyword)) { + return true; + } + } + + return false; + } + + static auto + is_strict_descendant(const sourcemeta::core::WeakPointer &ancestor, + const sourcemeta::core::WeakPointer &candidate) -> bool { + if (candidate.size() <= ancestor.size()) { + return false; + } + for (std::size_t index{0}; index < ancestor.size(); ++index) { + if (!(ancestor.at(index) == candidate.at(index))) { + return false; + } + } + return true; + } +}; diff --git a/vendor/blaze/src/alterschema/upgrade/upgrade_draft_7_to_draft_2019_09.h b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_7_to_draft_2019_09.h new file mode 100644 index 000000000..187be7660 --- /dev/null +++ b/vendor/blaze/src/alterschema/upgrade/upgrade_draft_7_to_draft_2019_09.h @@ -0,0 +1,390 @@ +class UpgradeDraft7To201909 final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UpgradeDraft7To201909() + : SchemaTransformRule{"upgrade_draft_7_to_2019_09", ""} {}; + + [[nodiscard]] auto + condition(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &root, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &, + const sourcemeta::core::SchemaResolver &) const + -> SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + vocabularies.contains(Vocabularies::Known::JSON_Schema_Draft_7) && + schema.is_object()); + + ONLY_CONTINUE_IF(subschema_at_dialect(schema, location, DRAFT_7_URL) || + has_actionable_id_fragment(schema) || + has_actionable_dependencies(schema) || + has_actionable_ref_siblings(schema)); + + for (const auto &entry : frame.locations()) { + if (entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + entry.second.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + if (entry.second.pointer.size() <= location.pointer.size() || + !entry.second.pointer.starts_with(location.pointer)) { + continue; + } + + const auto entry_pointer{ + sourcemeta::core::to_pointer(entry.second.pointer)}; + const auto &entry_schema{sourcemeta::core::get(root, entry_pointer)}; + + if (has_descendant_pending_pattern(entry_schema, entry.second.dialect)) { + return false; + } + } + + return true; + } + + auto transform(sourcemeta::core::JSON &schema, const Result &) const + -> void override { + this->renames_.clear(); + this->prefix_ref_siblings(schema); + this->split_id_fragment(schema); + this->split_dependencies(schema); + if (bump_schema(schema)) { + drop_dialect_overrides(schema, true); + } else { + mark_dialect_override(schema, DRAFT_2019_09_URL); + } + } + + [[nodiscard]] auto rereference(const std::string_view, + const sourcemeta::core::Pointer &, + const sourcemeta::core::Pointer &target, + const sourcemeta::core::Pointer ¤t) const + -> sourcemeta::core::Pointer override { + for (const auto &[old_pointer, new_pointer] : this->renames_) { + const auto result{target.rebase(current.concat(old_pointer), + current.concat(new_pointer))}; + if (result != target) { + return result; + } + } + + return target; + } + +private: + static constexpr std::string_view DRAFT_4_URL{ + "http://json-schema.org/draft-04/schema#"}; + static constexpr std::string_view DRAFT_6_URL{ + "http://json-schema.org/draft-06/schema#"}; + static constexpr std::string_view DRAFT_7_URL{ + "http://json-schema.org/draft-07/schema#"}; + static constexpr std::string_view DRAFT_2019_09_URL{ + "https://json-schema.org/draft/2019-09/schema"}; + + static inline const std::array SHADOW_EXEMPT_KEYWORDS{ + {"$schema", "$id", "title", "description", "default", "examples", + "$comment", "readOnly", "writeOnly", "deprecated", "contentMediaType", + "contentEncoding"}}; + + static inline const std::array + PROMOTED_2019_09_KEYWORDS{{"$anchor", "$recursiveAnchor", "$recursiveRef", + "$vocabulary", "$defs", "dependentSchemas", + "dependentRequired", "unevaluatedItems", + "unevaluatedProperties", "maxContains", + "minContains", "contentSchema", "deprecated"}}; + + static inline const std::array PROMOTED_DRAFT_7_KEYWORDS{ + {"$comment", "if", "then", "else", "readOnly", "writeOnly", + "contentMediaType", "contentEncoding"}}; + + static inline const std::array PROMOTED_DRAFT_6_KEYWORDS{ + {"const", "contains", "propertyNames", "examples"}}; + + mutable std::vector< + std::pair> + renames_; + + static auto is_shadow_exempt(const std::string_view keyword) -> bool { + return std::ranges::any_of( + SHADOW_EXEMPT_KEYWORDS, + [&keyword](const auto &candidate) { return candidate == keyword; }); + } + + static auto is_plain_name_fragment(const std::string_view fragment) -> bool { + if (fragment.empty()) { + return false; + } + + for (const auto character : fragment) { + const bool is_alpha{(character >= 'A' && character <= 'Z') || + (character >= 'a' && character <= 'z')}; + const bool is_digit{character >= '0' && character <= '9'}; + const bool is_punct{character == '_' || character == ':' || + character == '.' || character == '-'}; + if (!is_alpha && !is_digit && !is_punct) { + return false; + } + } + + return true; + } + + static auto + has_actionable_id_fragment(const sourcemeta::core::JSON &subschema) -> bool { + if (!(subschema.defines("$id") && subschema.at("$id").is_string())) { + return false; + } + + const sourcemeta::core::URI uri{subschema.at("$id").to_string()}; + const auto fragment{uri.fragment()}; + return fragment.has_value() && (fragment.value().empty() || + is_plain_name_fragment(fragment.value())); + } + + static auto + has_actionable_dependencies(const sourcemeta::core::JSON &subschema) -> bool { + if (!(subschema.defines("dependencies") && + subschema.at("dependencies").is_object())) { + return false; + } + + if (subschema.defines("dependentRequired") || + subschema.defines("dependentSchemas")) { + return false; + } + + for (const auto &entry : subschema.at("dependencies").as_object()) { + if (!entry.second.is_array() && !entry.second.is_object() && + !entry.second.is_boolean()) { + return false; + } + } + + return true; + } + + static auto + has_actionable_ref_siblings(const sourcemeta::core::JSON &subschema) -> bool { + if (!subschema.defines("$ref")) { + return false; + } + + for (const auto &entry : subschema.as_object()) { + if (entry.first == "$ref" || is_shadow_exempt(entry.first) || + entry.first.starts_with("x-")) { + continue; + } + return true; + } + + return false; + } + + auto prefix_ref_siblings(sourcemeta::core::JSON &schema) const -> void { + if (!schema.defines("$ref")) { + return; + } + + std::vector siblings_to_prefix; + for (const auto &entry : schema.as_object()) { + if (entry.first == "$ref" || is_shadow_exempt(entry.first) || + entry.first.starts_with("x-")) { + continue; + } + + siblings_to_prefix.push_back(entry.first); + } + + for (const auto &keyword : siblings_to_prefix) { + std::string prefixed_name{"x-" + keyword}; + while (schema.defines(prefixed_name)) { + prefixed_name.insert(0, "x-"); + } + + this->renames_.emplace_back(sourcemeta::core::Pointer{keyword}, + sourcemeta::core::Pointer{prefixed_name}); + schema.rename(keyword, std::move(prefixed_name)); + } + } + + static auto split_id_fragment(sourcemeta::core::JSON &schema) -> void { + if (!(schema.defines("$id") && schema.at("$id").is_string())) { + return; + } + + const sourcemeta::core::URI uri{schema.at("$id").to_string()}; + const auto fragment{uri.fragment()}; + if (!fragment.has_value()) { + return; + } + + const auto &fragment_value{fragment.value()}; + const bool plain_name{is_plain_name_fragment(fragment_value)}; + + if (uri.is_fragment_only()) { + if (plain_name) { + schema.rename("$id", "$anchor"); + schema.at("$anchor").into(sourcemeta::core::JSON{fragment_value}); + } else if (fragment_value.empty()) { + schema.erase("$id"); + } + return; + } + + if (!plain_name && !fragment_value.empty()) { + return; + } + + const auto without_fragment{uri.recompose_without_fragment()}; + if (!without_fragment.has_value()) { + return; + } + + schema.assign("$id", sourcemeta::core::JSON{without_fragment.value()}); + if (plain_name) { + schema.assign("$anchor", sourcemeta::core::JSON{fragment_value}); + } + } + + auto split_dependencies(sourcemeta::core::JSON &schema) const -> void { + if (!has_actionable_dependencies(schema)) { + return; + } + + auto dependent_required{sourcemeta::core::JSON::make_object()}; + auto dependent_schemas{sourcemeta::core::JSON::make_object()}; + + for (const auto &entry : schema.at("dependencies").as_object()) { + if (entry.second.is_array()) { + dependent_required.assign(entry.first, entry.second); + } else { + dependent_schemas.assign(entry.first, entry.second); + } + } + + if (dependent_required.empty() && dependent_schemas.empty()) { + schema.erase("dependencies"); + return; + } + + if (!dependent_required.empty() && !dependent_schemas.empty()) { + for (const auto &entry : dependent_schemas.as_object()) { + this->renames_.emplace_back( + sourcemeta::core::Pointer{"dependencies", entry.first}, + sourcemeta::core::Pointer{"dependentSchemas", entry.first}); + } + for (const auto &entry : dependent_required.as_object()) { + this->renames_.emplace_back( + sourcemeta::core::Pointer{"dependencies", entry.first}, + sourcemeta::core::Pointer{"dependentRequired", entry.first}); + } + schema.try_assign_before("dependentSchemas", dependent_schemas, + "dependencies"); + schema.rename("dependencies", "dependentRequired"); + schema.at("dependentRequired").into(std::move(dependent_required)); + return; + } + + if (!dependent_schemas.empty()) { + this->renames_.emplace_back( + sourcemeta::core::Pointer{"dependencies"}, + sourcemeta::core::Pointer{"dependentSchemas"}); + schema.rename("dependencies", "dependentSchemas"); + schema.at("dependentSchemas").into(std::move(dependent_schemas)); + return; + } + + this->renames_.emplace_back(sourcemeta::core::Pointer{"dependencies"}, + sourcemeta::core::Pointer{"dependentRequired"}); + schema.rename("dependencies", "dependentRequired"); + schema.at("dependentRequired").into(std::move(dependent_required)); + } + + static auto bump_schema(sourcemeta::core::JSON &schema) -> bool { + if (schema.defines("$schema") && schema.at("$schema").is_string() && + schema.at("$schema").to_string() == DRAFT_7_URL) { + schema.assign("$schema", sourcemeta::core::JSON{DRAFT_2019_09_URL}); + return true; + } + return false; + } + + static auto has_pending_pattern(const sourcemeta::core::JSON &subschema) + -> bool { + if (!subschema.is_object()) { + return false; + } + + if (current_dialect_or_override(subschema) == DRAFT_7_URL) { + return true; + } + + return has_actionable_id_fragment(subschema) || + has_actionable_dependencies(subschema) || + has_actionable_ref_siblings(subschema); + } + + static auto + has_descendant_pending_pattern(const sourcemeta::core::JSON &subschema, + const std::string_view descendant_dialect) + -> bool { + if (!subschema.is_object()) { + return false; + } + + if (subschema.defines("$schema") && subschema.at("$schema").is_string()) { + const auto &dialect{subschema.at("$schema").to_string()}; + if (dialect == DRAFT_4_URL || dialect == DRAFT_6_URL || + dialect == DRAFT_7_URL) { + return true; + } + } + + if (subschema.defines("id") && subschema.at("id").is_string() && + !subschema.defines("$id")) { + return true; + } + + const auto *exclusive_minimum{subschema.try_at("exclusiveMinimum")}; + if (exclusive_minimum != nullptr && exclusive_minimum->is_boolean()) { + return true; + } + + const auto *exclusive_maximum{subschema.try_at("exclusiveMaximum")}; + if (exclusive_maximum != nullptr && exclusive_maximum->is_boolean()) { + return true; + } + + if (descendant_dialect == DRAFT_4_URL) { + for (const auto &keyword : PROMOTED_DRAFT_6_KEYWORDS) { + if (subschema.defines(keyword)) { + return true; + } + } + } + + if (descendant_dialect == DRAFT_6_URL) { + for (const auto &keyword : PROMOTED_DRAFT_7_KEYWORDS) { + if (subschema.defines(keyword)) { + return true; + } + } + } + + if (descendant_dialect == DRAFT_7_URL) { + for (const auto &keyword : PROMOTED_2019_09_KEYWORDS) { + if (subschema.defines(keyword)) { + return true; + } + } + } + + return has_pending_pattern(subschema); + } +}; diff --git a/vendor/blaze/src/codegen/CMakeLists.txt b/vendor/blaze/src/codegen/CMakeLists.txt new file mode 100644 index 000000000..099c0cda9 --- /dev/null +++ b/vendor/blaze/src/codegen/CMakeLists.txt @@ -0,0 +1,20 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME codegen + FOLDER "Blaze/Codegen" + PRIVATE_HEADERS error.h typescript.h + SOURCES + codegen.cc + codegen_symbol.cc + codegen_default_compiler.h + codegen_typescript.cc + codegen_mangle.cc) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME codegen) +endif() + +target_link_libraries(sourcemeta_blaze_codegen PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_codegen PUBLIC + sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_codegen PRIVATE + sourcemeta::blaze::alterschema) diff --git a/vendor/blaze/src/codegen/codegen.cc b/vendor/blaze/src/codegen/codegen.cc new file mode 100644 index 000000000..03d309415 --- /dev/null +++ b/vendor/blaze/src/codegen/codegen.cc @@ -0,0 +1,138 @@ +#include +#include + +#include // std::ranges::sort +#include // assert +#include // std::unordered_set + +#include "codegen_default_compiler.h" + +namespace { + +auto is_validation_subschema( + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) -> bool { + if (!location.parent.has_value()) { + return false; + } + + const auto &parent{location.parent.value()}; + if (parent.size() >= location.pointer.size()) { + return false; + } + + const auto &keyword_token{location.pointer.at(parent.size())}; + if (!keyword_token.is_property()) { + return false; + } + + const auto parent_location{frame.traverse(parent)}; + if (!parent_location.has_value()) { + return false; + } + + const auto vocabularies{ + frame.vocabularies(parent_location.value().get(), resolver)}; + const auto &walker_result{walker(keyword_token.to_property(), vocabularies)}; + using Type = sourcemeta::core::SchemaKeywordType; + if (walker_result.type == Type::ApplicatorValueTraverseAnyPropertyKey || + walker_result.type == Type::ApplicatorValueTraverseAnyItem) { + return true; + } + + return is_validation_subschema(frame, parent_location.value().get(), walker, + resolver); +} + +} // anonymous namespace + +namespace sourcemeta::blaze { + +auto compile(const sourcemeta::core::JSON &input, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const CodegenCompiler &compiler, + const std::string_view default_dialect, + const std::string_view default_id) -> CodegenIRResult { + // -------------------------------------------------------------------------- + // (1) Bundle the schema to resolve external references + // -------------------------------------------------------------------------- + + auto schema{sourcemeta::core::bundle(input, walker, resolver, default_dialect, + default_id)}; + + // -------------------------------------------------------------------------- + // (2) Canonicalize the schema for easier analysis + // -------------------------------------------------------------------------- + + sourcemeta::blaze::SchemaTransformer canonicalizer; + sourcemeta::blaze::add(canonicalizer, + sourcemeta::blaze::AlterSchemaMode::Canonicalizer); + [[maybe_unused]] const auto canonicalized{canonicalizer.apply( + schema, walker, resolver, + [](const auto &, const auto, const auto, const auto &, + [[maybe_unused]] const auto applied) { assert(applied); }, + default_dialect, default_id)}; + assert(canonicalized.first); + + // -------------------------------------------------------------------------- + // (3) Frame the resulting schema with instance location information + // -------------------------------------------------------------------------- + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(schema, walker, resolver, default_dialect, default_id); + + // -------------------------------------------------------------------------- + // (4) Convert every subschema into a code generation object + // -------------------------------------------------------------------------- + + std::unordered_set + visited; + CodegenIRResult result; + for (const auto &[key, location] : frame.locations()) { + if (location.type != + sourcemeta::core::SchemaFrame::LocationType::Resource && + location.type != + sourcemeta::core::SchemaFrame::LocationType::Subschema) { + continue; + } + + // Framing may report resource twice or more given default identifiers and + // nested resources + const auto [visited_iterator, inserted] = visited.insert(location.pointer); + if (!inserted) { + continue; + } + + // Skip subschemas under validation-only keywords that do not contribute + // to the type structure (like `contains`) + if (is_validation_subschema(frame, location, walker, resolver)) { + continue; + } + + const auto &subschema{sourcemeta::core::get(schema, location.pointer)}; + result.push_back(compiler(schema, frame, location, resolver, subschema)); + } + + // -------------------------------------------------------------------------- + // (5) Sort entries so that dependencies come before dependents + // -------------------------------------------------------------------------- + + std::ranges::sort( + result, + [](const CodegenIREntity &left, const CodegenIREntity &right) -> bool { + return std::visit([](const auto &entry) { return entry.pointer; }, + right) < + std::visit([](const auto &entry) { return entry.pointer; }, + left); + }); + + return result; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/codegen/codegen_default_compiler.h b/vendor/blaze/src/codegen/codegen_default_compiler.h new file mode 100644 index 000000000..c92041bb8 --- /dev/null +++ b/vendor/blaze/src/codegen/codegen_default_compiler.h @@ -0,0 +1,784 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_DEFAULT_COMPILER_H_ +#define SOURCEMETA_BLAZE_CODEGEN_DEFAULT_COMPILER_H_ + +#include + +#include +#include +#include + +#include // assert +#include // std::string_view +#include // std::unordered_set + +// We do not check vocabularies here because the canonicaliser ensures +// we never get an official keyword when its vocabulary is not present +// NOLINTNEXTLINE(cppcoreguidelines-macro-usage) +#define ONLY_WHITELIST_KEYWORDS(schema, subschema, pointer, ...) \ + { \ + static const std::unordered_set allowed{__VA_ARGS__}; \ + for (const auto &entry : (subschema).as_object()) { \ + if (!allowed.contains(entry.first)) { \ + throw sourcemeta::blaze::CodegenUnsupportedKeywordError( \ + (schema), (pointer), entry.first, \ + "Unsupported keyword in subschema"); \ + } \ + } \ + } + +namespace sourcemeta::blaze { + +auto handle_impossible(const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &) -> CodegenIRImpossible { + return CodegenIRImpossible{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}}; +} + +auto handle_any(const sourcemeta::core::JSON &, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &) -> CodegenIRAny { + return CodegenIRAny{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}}; +} + +auto handle_string(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", + "$id", + "$anchor", + "$dynamicAnchor", + "$defs", + "$vocabulary", + "type", + "minLength", + "maxLength", + "pattern", + "format", + "title", + "description", + "default", + "deprecated", + "readOnly", + "writeOnly", + "examples", + "contentEncoding", + "contentMediaType", + "contentSchema"}); + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::String}; +} + +auto handle_object(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIRObject { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$defs", "$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", + "type", "properties", "required", + // Note that most programming languages CANNOT represent the idea + // of additional properties, mainly if they differ from the types of the + // other properties. Therefore, we whitelist this, but we consider it to + // be the responsability of the validator + "additionalProperties", "minProperties", "maxProperties", + "propertyNames", "patternProperties", "title", "description", "default", + "deprecated", "readOnly", "writeOnly", "examples"}); + + std::vector> + members; + + // Guaranteed by canonicalisation + assert(subschema.defines("properties")); + + const auto &properties{subschema.at("properties")}; + + std::unordered_set required_set; + if (subschema.defines("required")) { + const auto &required{subschema.at("required")}; + for (const auto &item : required.as_array()) { + // Guaranteed by canonicalisation + assert(properties.defines(item.to_string())); + required_set.insert(item.to_string()); + } + } + + for (const auto &entry : properties.as_object()) { + auto property_pointer{sourcemeta::core::to_pointer(location.pointer)}; + property_pointer.push_back("properties"); + property_pointer.push_back(entry.first); + + const auto property_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(property_pointer))}; + assert(property_location.has_value()); + + CodegenIRObjectValue member_value{ + {.pointer = std::move(property_pointer), + .symbol = symbol(frame, property_location.value().get())}, + required_set.contains(entry.first), + false}; + + members.emplace_back(entry.first, std::move(member_value)); + } + + std::variant additional{true}; + if (subschema.defines("additionalProperties")) { + const auto &additional_schema{subschema.at("additionalProperties")}; + if (additional_schema.is_boolean()) { + additional = additional_schema.to_boolean(); + } else { + auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; + additional_pointer.push_back("additionalProperties"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + } + + std::vector pattern; + if (subschema.defines("patternProperties")) { + const auto &pattern_props{subschema.at("patternProperties")}; + for (const auto &entry : pattern_props.as_object()) { + auto pattern_pointer{sourcemeta::core::to_pointer(location.pointer)}; + pattern_pointer.push_back("patternProperties"); + pattern_pointer.push_back(entry.first); + + const auto pattern_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(pattern_pointer))}; + assert(pattern_location.has_value()); + + std::optional prefix{std::nullopt}; + const auto regex{sourcemeta::core::to_regex(entry.first)}; + if (regex.has_value() && + std::holds_alternative( + regex.value())) { + prefix = std::get(regex.value()); + } + + pattern.push_back(CodegenIRObjectPatternProperty{ + {.pointer = std::move(pattern_pointer), + .symbol = symbol(frame, pattern_location.value().get())}, + std::move(prefix)}); + } + } + + return CodegenIRObject{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(members), + std::move(additional), + std::move(pattern)}; +} + +auto handle_integer(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) + -> CodegenIRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", "multipleOf", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples"}); + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Integer}; +} + +auto handle_number(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIRScalar { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", "multipleOf", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples"}); + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Number}; +} + +auto handle_array(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &vocabularies, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", + "$dynamicAnchor", "$defs", "$vocabulary", + "type", "items", "minItems", + "maxItems", "uniqueItems", "contains", + "minContains", "maxContains", "additionalItems", + "prefixItems", "title", "description", + "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + using Vocabularies = sourcemeta::core::Vocabularies; + + if (vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Applicator) && + subschema.defines("prefixItems")) { + const auto &prefix_items{subschema.at("prefixItems")}; + assert(prefix_items.is_array()); + + std::vector tuple_items; + for (std::size_t index = 0; index < prefix_items.size(); ++index) { + auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; + item_pointer.push_back("prefixItems"); + item_pointer.push_back(index); + + const auto item_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(item_pointer))}; + assert(item_location.has_value()); + + tuple_items.push_back( + {.pointer = std::move(item_pointer), + .symbol = symbol(frame, item_location.value().get())}); + } + + std::optional additional{std::nullopt}; + if (subschema.defines("items")) { + auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; + additional_pointer.push_back("items"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + return CodegenIRTuple{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; + } + + if (vocabularies.contains_any( + {Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4, + Vocabularies::Known::JSON_Schema_Draft_3}) && + subschema.defines("items") && subschema.at("items").is_array()) { + const auto &items_array{subschema.at("items")}; + + std::vector tuple_items; + for (std::size_t index = 0; index < items_array.size(); ++index) { + auto item_pointer{sourcemeta::core::to_pointer(location.pointer)}; + item_pointer.push_back("items"); + item_pointer.push_back(index); + + const auto item_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(item_pointer))}; + assert(item_location.has_value()); + + tuple_items.push_back( + {.pointer = std::move(item_pointer), + .symbol = symbol(frame, item_location.value().get())}); + } + + std::optional additional{std::nullopt}; + if (subschema.defines("additionalItems")) { + auto additional_pointer{sourcemeta::core::to_pointer(location.pointer)}; + additional_pointer.push_back("additionalItems"); + + const auto additional_location{frame.traverse( + sourcemeta::core::to_weak_pointer(additional_pointer))}; + assert(additional_location.has_value()); + + additional = CodegenIRType{ + .pointer = std::move(additional_pointer), + .symbol = symbol(frame, additional_location.value().get())}; + } + + return CodegenIRTuple{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(tuple_items), + std::move(additional)}; + } + + std::optional items_type{std::nullopt}; + if (subschema.defines("items")) { + auto items_pointer{sourcemeta::core::to_pointer(location.pointer)}; + items_pointer.push_back("items"); + + const auto items_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(items_pointer))}; + assert(items_location.has_value()); + + items_type = + CodegenIRType{.pointer = std::move(items_pointer), + .symbol = symbol(frame, items_location.value().get())}; + } + + return CodegenIRArray{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(items_type)}; +} + +auto handle_enum(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "enum", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + const auto &enum_json{subschema.at("enum")}; + + // Boolean and null special cases + if (enum_json.size() == 1 && enum_json.at(0).is_null()) { + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Null}; + } else if (enum_json.size() == 2) { + const auto &first{enum_json.at(0)}; + const auto &second{enum_json.at(1)}; + if ((first.is_boolean() && second.is_boolean()) && + (first.to_boolean() != second.to_boolean())) { + return CodegenIRScalar{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + CodegenIRScalarType::Boolean}; + } + } + + std::vector values{enum_json.as_array().cbegin(), + enum_json.as_array().cend()}; + return CodegenIREnumeration{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(values)}; +} + +auto handle_anyof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "anyOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &any_of{subschema.at("anyOf")}; + assert(any_of.is_array()); + assert(any_of.size() >= 2); + + std::vector branches; + for (std::size_t index = 0; index < any_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("anyOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_oneof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "oneOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &one_of{subschema.at("oneOf")}; + assert(one_of.is_array()); + assert(one_of.size() >= 2); + + std::vector branches; + for (std::size_t index = 0; index < one_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("oneOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$ref", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; + ref_pointer.push_back("$ref"); + const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)}; + + const auto &references{frame.references()}; + const auto reference{references.find( + {sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})}; + assert(reference != references.cend()); + + const auto &destination{reference->second.destination}; + const auto target{frame.traverse(destination)}; + if (!target.has_value()) { + throw CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return CodegenIRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = sourcemeta::core::to_pointer(target_location.pointer), + .symbol = symbol(frame, target_location)}}; +} + +auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$dynamicRef", "title", + "description", "default", "deprecated", "readOnly", + "writeOnly", "examples"}); + + auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; + ref_pointer.push_back("$dynamicRef"); + const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)}; + + const auto &references{frame.references()}; + + // Note: The frame internally converts single-target dynamic references to + // static reference + const auto static_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})}; + if (static_reference != references.cend()) { + const auto &destination{static_reference->second.destination}; + const auto target{frame.traverse(destination)}; + if (!target.has_value()) { + throw CodegenUnexpectedSchemaError( + schema, location.pointer, "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return CodegenIRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = sourcemeta::core::to_pointer(target_location.pointer), + .symbol = symbol(frame, target_location)}}; + } + + // Multi-target dynamic reference: find all dynamic anchors with the matching + // fragment and emit a union of all possible targets + const auto dynamic_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Dynamic, ref_weak_pointer})}; + assert(dynamic_reference != references.cend()); + assert(dynamic_reference->second.fragment.has_value()); + const auto &fragment{dynamic_reference->second.fragment.value()}; + + std::vector branches; + for (const auto &[key, entry] : frame.locations()) { + if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic || + entry.type != sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + + const sourcemeta::core::URI anchor_uri{key.second}; + const auto anchor_fragment{anchor_uri.fragment()}; + if (!anchor_fragment.has_value() || anchor_fragment.value() != fragment) { + continue; + } + + branches.push_back({.pointer = sourcemeta::core::to_pointer(entry.pointer), + .symbol = symbol(frame, entry)}); + } + + assert(!branches.empty()); + return CodegenIRUnion{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_allof(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS( + schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", "$defs", "$vocabulary", + "allOf", "title", "description", "default", "deprecated", "readOnly", + "writeOnly", "examples", "unevaluatedProperties", "unevaluatedItems"}); + + const auto &all_of{subschema.at("allOf")}; + assert(all_of.is_array()); + + if (all_of.size() == 1) { + auto target_pointer{sourcemeta::core::to_pointer(location.pointer)}; + target_pointer.push_back("allOf"); + target_pointer.push_back(static_cast(0)); + + const auto target_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(target_pointer))}; + assert(target_location.has_value()); + + return CodegenIRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = std::move(target_pointer), + .symbol = symbol(frame, target_location.value().get())}}; + } + + std::vector branches; + for (std::size_t index = 0; index < all_of.size(); ++index) { + auto branch_pointer{sourcemeta::core::to_pointer(location.pointer)}; + branch_pointer.push_back("allOf"); + branch_pointer.push_back(index); + + const auto branch_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(branch_pointer))}; + assert(branch_location.has_value()); + + branches.push_back( + {.pointer = std::move(branch_pointer), + .symbol = symbol(frame, branch_location.value().get())}); + } + + return CodegenIRIntersection{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + +auto handle_if_then_else( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> CodegenIREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "if", "then", "else", + "title", "description", "default", "deprecated", + "readOnly", "writeOnly", "examples", + "unevaluatedProperties", "unevaluatedItems"}); + + assert(subschema.defines("if")); + assert(subschema.defines("then")); + assert(subschema.defines("else")); + + auto if_pointer{sourcemeta::core::to_pointer(location.pointer)}; + if_pointer.push_back("if"); + const auto if_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(if_pointer))}; + assert(if_location.has_value()); + + auto then_pointer{sourcemeta::core::to_pointer(location.pointer)}; + then_pointer.push_back("then"); + const auto then_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(then_pointer))}; + assert(then_location.has_value()); + + auto else_pointer{sourcemeta::core::to_pointer(location.pointer)}; + else_pointer.push_back("else"); + const auto else_location{ + frame.traverse(sourcemeta::core::to_weak_pointer(else_pointer))}; + assert(else_location.has_value()); + + return CodegenIRConditional{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = std::move(if_pointer), + .symbol = symbol(frame, if_location.value().get())}, + {.pointer = std::move(then_pointer), + .symbol = symbol(frame, then_location.value().get())}, + {.pointer = std::move(else_pointer), + .symbol = symbol(frame, else_location.value().get())}}; +} + +auto default_compiler(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity { + const auto vocabularies{frame.vocabularies(location, resolver)}; + assert(!vocabularies.empty()); + + // Be strict with vocabulary support + using Vocabularies = sourcemeta::core::Vocabularies; + static const std::unordered_set supported{ + Vocabularies::Known::JSON_Schema_2020_12_Core, + Vocabularies::Known::JSON_Schema_2020_12_Applicator, + Vocabularies::Known::JSON_Schema_2020_12_Validation, + Vocabularies::Known::JSON_Schema_2020_12_Unevaluated, + Vocabularies::Known::JSON_Schema_2020_12_Content, + Vocabularies::Known::JSON_Schema_2020_12_Meta_Data, + Vocabularies::Known::JSON_Schema_2020_12_Format_Annotation, + Vocabularies::Known::JSON_Schema_2020_12_Format_Assertion, + Vocabularies::Known::JSON_Schema_2019_09_Core, + Vocabularies::Known::JSON_Schema_2019_09_Applicator, + Vocabularies::Known::JSON_Schema_2019_09_Validation, + Vocabularies::Known::JSON_Schema_2019_09_Content, + Vocabularies::Known::JSON_Schema_2019_09_Meta_Data, + Vocabularies::Known::JSON_Schema_2019_09_Format, + Vocabularies::Known::JSON_Schema_Draft_7, + Vocabularies::Known::JSON_Schema_Draft_6, + Vocabularies::Known::JSON_Schema_Draft_4}; + vocabularies.throw_if_any_unsupported(supported, + "Unsupported required vocabulary"); + + // The canonicaliser ensures that every subschema schema is only in one of the + // following shapes + + if (subschema.is_boolean()) { + if (subschema.to_boolean()) { + return handle_any(schema, frame, location, vocabularies, resolver, + subschema); + } else { + return handle_impossible(schema, frame, location, vocabularies, resolver, + subschema); + } + } else if (subschema.defines("type")) { + const auto &type_value{subschema.at("type")}; + if (!type_value.is_string()) { + throw CodegenUnsupportedKeywordValueError( + schema, location.pointer, "type", "Expected a string value"); + } + + const auto &type_string{type_value.to_string()}; + + // The canonicaliser transforms any other type + if (type_string == "string") { + return handle_string(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "object") { + return handle_object(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "integer") { + return handle_integer(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "number") { + return handle_number(schema, frame, location, vocabularies, resolver, + subschema); + } else if (type_string == "array") { + return handle_array(schema, frame, location, vocabularies, resolver, + subschema); + } else { + throw CodegenUnsupportedKeywordValueError( + schema, location.pointer, "type", "Unsupported type value"); + } + } else if (subschema.defines("enum")) { + return handle_enum(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("anyOf")) { + return handle_anyof(schema, frame, location, vocabularies, resolver, + subschema); + // This is usually a good enough approximation. We usually can't check that + // the other types DO NOT match, but that is in a way a validation concern + } else if (subschema.defines("oneOf")) { + return handle_oneof(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("allOf")) { + return handle_allof(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("$dynamicRef")) { + return handle_dynamic_ref(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("$ref")) { + return handle_ref(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("if")) { + return handle_if_then_else(schema, frame, location, vocabularies, resolver, + subschema); + } else if (subschema.defines("not")) { + throw CodegenUnsupportedKeywordError(schema, location.pointer, "not", + "Unsupported keyword in subschema"); + } else { + throw CodegenUnexpectedSchemaError(schema, location.pointer, + "Unsupported schema"); + } +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/codegen/codegen_mangle.cc b/vendor/blaze/src/codegen/codegen_mangle.cc new file mode 100644 index 000000000..0d6a4a388 --- /dev/null +++ b/vendor/blaze/src/codegen/codegen_mangle.cc @@ -0,0 +1,90 @@ +#include + +namespace { + +auto is_alpha(char character) -> bool { + return (character >= 'a' && character <= 'z') || + (character >= 'A' && character <= 'Z'); +} + +auto is_digit(char character) -> bool { + return character >= '0' && character <= '9'; +} + +auto to_upper(char character) -> char { + if (character >= 'a' && character <= 'z') { + return static_cast(character - 'a' + 'A'); + } + return character; +} + +auto symbol_to_identifier(const std::string_view prefix, + const std::vector &symbol) + -> std::string { + std::string result{prefix}; + + for (const auto &segment : symbol) { + if (segment.empty()) { + continue; + } + + bool first_in_segment{true}; + for (const auto character : segment) { + if (is_alpha(character)) { + if (first_in_segment) { + result += to_upper(character); + first_in_segment = false; + } else { + result += character; + } + } else if (is_digit(character)) { + if (first_in_segment) { + result += '_'; + } + result += character; + first_in_segment = false; + } else if (character == '_' || character == '$') { + result += character; + first_in_segment = false; + } + } + } + + if (result.empty()) { + return "_"; + } + + if (is_digit(result[0])) { + result.insert(0, "_"); + } + + return result; +} + +} // namespace + +namespace sourcemeta::blaze { + +auto mangle(const std::string_view prefix, + const sourcemeta::core::Pointer &pointer, + const std::vector &symbol, + std::map &cache) + -> const std::string & { + auto name{symbol_to_identifier(prefix, symbol)}; + + while (true) { + auto iterator{cache.find(name)}; + if (iterator != cache.end()) { + if (iterator->second == pointer) { + return iterator->first; + } + + name.insert(0, "_"); + } else { + auto result{cache.insert({std::move(name), pointer})}; + return result.first->first; + } + } +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/codegen/codegen_symbol.cc b/vendor/blaze/src/codegen/codegen_symbol.cc new file mode 100644 index 000000000..6003dba22 --- /dev/null +++ b/vendor/blaze/src/codegen/codegen_symbol.cc @@ -0,0 +1,119 @@ +#include + +#include + +#include // std::ranges::reverse +#include // assert +#include // std::filesystem::path +#include // std::istringstream +#include // std::string, std::getline +#include // std::vector + +namespace { + +// Strip all extensions from a filename (e.g., "user.schema.json" -> "user") +auto strip_extensions(const std::string &filename) -> std::string { + std::filesystem::path path{filename}; + while (path.has_extension()) { + path = path.stem(); + } + return path.string(); +} + +// If the input looks like an absolute URI, extract its path segments. +// For file URIs, only the filename (without extensions) is used. +// For other URIs, all path segments are used with extensions stripped from +// the last segment. +// Otherwise, add the input as a single segment. +// Note: segments are added in reverse order because the caller reverses +// the entire result at the end. +auto push_token_segments(std::vector &result, + const std::string &value) -> void { + try { + const sourcemeta::core::URI uri{value}; + if (uri.is_absolute()) { + const auto path{uri.path()}; + if (path.has_value() && !path->empty()) { + std::vector segments; + std::istringstream stream{std::string{path.value()}}; + std::string segment; + while (std::getline(stream, segment, '/')) { + if (!segment.empty()) { + segments.emplace_back(segment); + } + } + + if (!segments.empty()) { + // Strip extensions from the last segment + segments.back() = strip_extensions(segments.back()); + + // For file URIs, only use the filename + if (uri.is_file()) { + result.emplace_back(segments.back()); + } else { + // Reverse segments since the caller will reverse the entire result + std::ranges::reverse(segments); + for (const auto &path_segment : segments) { + result.emplace_back(path_segment); + } + } + + return; + } + } + } + // NOLINTNEXTLINE(bugprone-empty-catch) + } catch (const sourcemeta::core::URIParseError &) { + // Not a valid URI, fall through to default behavior + } + + result.emplace_back(value); +} + +} // namespace + +namespace sourcemeta::blaze { + +auto symbol(const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location) + -> std::vector { + std::vector result; + + auto current_pointer{location.pointer}; + + while (true) { + const auto current_location{frame.traverse(current_pointer)}; + assert(current_location.has_value()); + + if (!current_location->get().parent.has_value()) { + break; + } + + const auto &parent_pointer{current_location->get().parent.value()}; + const auto segments_skipped{current_pointer.size() - parent_pointer.size()}; + assert(segments_skipped >= 1); + + if (segments_skipped >= 2) { + const auto &last_token{current_pointer.back()}; + if (last_token.is_property()) { + push_token_segments(result, last_token.to_property()); + } else { + result.emplace_back(std::to_string(last_token.to_index())); + } + } else { + const auto &token{current_pointer.back()}; + if (token.is_property()) { + push_token_segments(result, token.to_property()); + } else { + result.emplace_back(std::to_string(token.to_index())); + } + } + + current_pointer = parent_pointer; + } + + std::ranges::reverse(result); + return result; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/codegen/codegen_typescript.cc b/vendor/blaze/src/codegen/codegen_typescript.cc new file mode 100644 index 000000000..c632372c3 --- /dev/null +++ b/vendor/blaze/src/codegen/codegen_typescript.cc @@ -0,0 +1,328 @@ +#include + +#include // std::ranges::any_of +#include // std::hex, std::setfill, std::setw +#include // std::ostringstream + +namespace { + +// TODO: Move to Core +auto escape_string(const std::string &input) -> std::string { + std::ostringstream result; + for (const auto character : input) { + switch (character) { + case '\\': + result << "\\\\"; + break; + case '"': + result << "\\\""; + break; + case '\b': + result << "\\b"; + break; + case '\f': + result << "\\f"; + break; + case '\n': + result << "\\n"; + break; + case '\r': + result << "\\r"; + break; + case '\t': + result << "\\t"; + break; + default: + // Escape other control characters (< 0x20) using \uXXXX format + if (static_cast(character) < 0x20) { + result << "\\u" << std::hex << std::setfill('0') << std::setw(4) + << static_cast(static_cast(character)); + } else { + result << character; + } + break; + } + } + + return result.str(); +} + +} // namespace + +namespace sourcemeta::blaze { + +TypeScript::TypeScript(std::ostream &stream, const std::string_view type_prefix) + : output{stream}, prefix{type_prefix} {} + +auto TypeScript::operator()(const CodegenIRScalar &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + switch (entry.value) { + case CodegenIRScalarType::String: + this->output << "string"; + break; + case CodegenIRScalarType::Number: + case CodegenIRScalarType::Integer: + this->output << "number"; + break; + case CodegenIRScalarType::Boolean: + this->output << "boolean"; + break; + case CodegenIRScalarType::Null: + this->output << "null"; + break; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIREnumeration &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator; + sourcemeta::core::prettify(value, this->output); + separator = " | "; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIRObject &entry) -> void { + const auto type_name{ + mangle(this->prefix, entry.pointer, entry.symbol, this->cache)}; + const auto has_typed_additional{ + std::holds_alternative(entry.additional)}; + const auto allows_any_additional{ + std::holds_alternative(entry.additional) && + std::get(entry.additional)}; + + if (has_typed_additional && entry.members.empty() && entry.pattern.empty()) { + const auto &additional_type{std::get(entry.additional)}; + this->output << "export type " << type_name << " = Recordprefix, additional_type.pointer, + additional_type.symbol, this->cache) + << ">;\n"; + return; + } + + if (allows_any_additional && entry.members.empty() && entry.pattern.empty()) { + this->output << "export type " << type_name + << " = Record;\n"; + return; + } + + this->output << "export interface " << type_name << " {\n"; + + // We always quote property names for safety. JSON Schema allows any string + // as a property name, but unquoted TypeScript/ECMAScript property names + // must be valid IdentifierName productions (see ECMA-262 section 12.7). + // Quoting allows any string to be used as a property name. + // See: https://tc39.es/ecma262/#sec-names-and-keywords + // See: https://mathiasbynens.be/notes/javascript-properties + for (const auto &[member_name, member_value] : entry.members) { + const auto optional_marker{member_value.required ? "" : "?"}; + const auto readonly_marker{member_value.immutable ? "readonly " : ""}; + + this->output << " " << readonly_marker << "\"" + << escape_string(member_name) << "\"" << optional_marker + << ": " + << mangle(this->prefix, member_value.pointer, + member_value.symbol, this->cache) + << ";\n"; + } + + for (const auto &pattern_property : entry.pattern) { + if (!pattern_property.prefix.has_value()) { + continue; + } + + this->output << " [key: `" << pattern_property.prefix.value() + << "${string}`]: " + << mangle(this->prefix, pattern_property.pointer, + pattern_property.symbol, this->cache); + + // TypeScript requires that a more specific index signature type is + // assignable to any less specific one that overlaps it. When a prefix + // is a sub-prefix of another (i.e. "x-data-" starts with "x-"), + // intersect the types so the constraint is satisfied + for (const auto &other : entry.pattern) { + if (&other == &pattern_property || !other.prefix.has_value()) { + continue; + } + + if (pattern_property.prefix.value().starts_with(other.prefix.value())) { + this->output << " & " + << mangle(this->prefix, other.pointer, other.symbol, + this->cache); + } + } + + this->output << ";\n"; + } + + const auto has_non_prefix_pattern{ + std::ranges::any_of(entry.pattern, [](const auto &pattern_property) { + return !pattern_property.prefix.has_value(); + })}; + + if (allows_any_additional) { + this->output << " [key: string]: unknown | undefined;\n"; + } else if (has_typed_additional || has_non_prefix_pattern) { + // TypeScript index signatures must be a supertype of all property value + // types. We use a union of all member types plus the additional properties + // type plus undefined (for optional properties). + this->output << " [key: string]:\n"; + this->output << " // As a notable limitation, TypeScript requires index " + "signatures\n"; + this->output << " // to also include the types of all of its " + "properties, so we must\n"; + this->output << " // match a superset of what JSON Schema allows\n"; + for (const auto &[member_name, member_value] : entry.members) { + this->output << " " + << mangle(this->prefix, member_value.pointer, + member_value.symbol, this->cache) + << " |\n"; + } + + for (const auto &pattern_property : entry.pattern) { + this->output << " " + << mangle(this->prefix, pattern_property.pointer, + pattern_property.symbol, this->cache) + << " |\n"; + } + + if (has_typed_additional) { + const auto &additional_type{std::get(entry.additional)}; + this->output << " " + << mangle(this->prefix, additional_type.pointer, + additional_type.symbol, this->cache) + << " |\n"; + } + + this->output << " undefined;\n"; + } + + this->output << "}\n"; +} + +auto TypeScript::operator()(const CodegenIRImpossible &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = never;\n"; +} + +auto TypeScript::operator()(const CodegenIRAny &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = unknown;\n"; +} + +auto TypeScript::operator()(const CodegenIRArray &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = "; + + if (entry.items.has_value()) { + this->output << mangle(this->prefix, entry.items->pointer, + entry.items->symbol, this->cache) + << "[]"; + } else { + this->output << "unknown[]"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIRReference &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = " + << mangle(this->prefix, entry.target.pointer, + entry.target.symbol, this->cache) + << ";\n"; +} + +auto TypeScript::operator()(const CodegenIRTuple &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = ["; + + const char *separator{""}; + for (const auto &item : entry.items) { + this->output << separator + << mangle(this->prefix, item.pointer, item.symbol, + this->cache); + separator = ", "; + } + + if (entry.additional.has_value()) { + this->output << separator << "..." + << mangle(this->prefix, entry.additional->pointer, + entry.additional->symbol, this->cache) + << "[]"; + } + + this->output << "];\n"; +} + +auto TypeScript::operator()(const CodegenIRUnion &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n"; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator << " " + << mangle(this->prefix, value.pointer, value.symbol, + this->cache); + separator = " |\n"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIRIntersection &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n"; + + const char *separator{""}; + for (const auto &value : entry.values) { + this->output << separator << " " + << mangle(this->prefix, value.pointer, value.symbol, + this->cache); + separator = " &\n"; + } + + this->output << ";\n"; +} + +auto TypeScript::operator()(const CodegenIRConditional &entry) -> void { + // As a notable limitation, TypeScript cannot express the negation of an + // if/then/else condition, so the else branch is wider than what JSON + // Schema allows + this->output << "// (if & then) | else approximation: the else branch is " + "wider than what\n"; + this->output << "// JSON Schema allows, as TypeScript cannot express type " + "negation\n"; + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " =\n (" + << mangle(this->prefix, entry.condition.pointer, + entry.condition.symbol, this->cache) + << " & " + << mangle(this->prefix, entry.consequent.pointer, + entry.consequent.symbol, this->cache) + << ") | " + << mangle(this->prefix, entry.alternative.pointer, + entry.alternative.symbol, this->cache) + << ";\n"; +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen.h b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen.h new file mode 100644 index 000000000..db14db1c2 --- /dev/null +++ b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen.h @@ -0,0 +1,180 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_H_ +#define SOURCEMETA_BLAZE_CODEGEN_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +// NOLINTBEGIN(misc-include-cleaner) +#include +#include +// NOLINTEND(misc-include-cleaner) + +#include +#include +#include + +#include // std::uint8_t +#include // std::function +#include // std::map +#include // std::optional, std::nullopt +#include // std::ostream +#include // std::string +#include // std::string_view +#include // std::pair +#include // std::variant, std::visit +#include // std::vector + +/// @defgroup codegen Codegen +/// @brief A code generation utility built on top of Blaze + +namespace sourcemeta::blaze { + +/// @ingroup codegen +enum class CodegenIRScalarType : std::uint8_t { + String, + Number, + Integer, + Boolean, + Null +}; + +/// @ingroup codegen +struct CodegenIRType { + sourcemeta::core::Pointer pointer; + std::vector symbol; +}; + +/// @ingroup codegen +struct CodegenIRScalar : CodegenIRType { + CodegenIRScalarType value; +}; + +/// @ingroup codegen +struct CodegenIREnumeration : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRUnion : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRIntersection : CodegenIRType { + std::vector values; +}; + +/// @ingroup codegen +struct CodegenIRObjectValue : CodegenIRType { + bool required; + bool immutable; +}; + +/// @ingroup codegen +struct CodegenIRObjectPatternProperty : CodegenIRType { + std::optional prefix; +}; + +/// @ingroup codegen +struct CodegenIRObject : CodegenIRType { + // To preserve the user's ordering + std::vector> + members; + std::variant additional; + std::vector pattern; +}; + +/// @ingroup codegen +struct CodegenIRArray : CodegenIRType { + std::optional items; +}; + +/// @ingroup codegen +struct CodegenIRTuple : CodegenIRType { + std::vector items; + std::optional additional; +}; + +/// @ingroup codegen +struct CodegenIRImpossible : CodegenIRType {}; + +/// @ingroup codegen +struct CodegenIRAny : CodegenIRType {}; + +/// @ingroup codegen +struct CodegenIRConditional : CodegenIRType { + CodegenIRType condition; + CodegenIRType consequent; + CodegenIRType alternative; +}; + +/// @ingroup codegen +struct CodegenIRReference : CodegenIRType { + CodegenIRType target; +}; + +/// @ingroup codegen +using CodegenIREntity = + std::variant; + +/// @ingroup codegen +using CodegenIRResult = std::vector; + +/// @ingroup codegen +using CodegenCompiler = std::function; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto default_compiler(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::JSON &subschema) + -> CodegenIREntity; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto compile(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const CodegenCompiler &compiler, + const std::string_view default_dialect = "", + const std::string_view default_id = "") -> CodegenIRResult; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto symbol(const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location) + -> std::vector; + +/// @ingroup codegen +SOURCEMETA_BLAZE_CODEGEN_EXPORT +auto mangle(const std::string_view prefix, + const sourcemeta::core::Pointer &pointer, + const std::vector &symbol, + std::map &cache) + -> const std::string &; + +/// @ingroup codegen +template +auto generate(std::ostream &output, const CodegenIRResult &result, + const std::string_view prefix = "Schema") -> void { + T visitor{output, prefix}; + const char *separator{""}; + for (const auto &entity : result) { + output << separator; + separator = "\n"; + std::visit(visitor, entity); + } +} + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_error.h b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_error.h new file mode 100644 index 000000000..bb5fc76f7 --- /dev/null +++ b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_error.h @@ -0,0 +1,181 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_ERROR_H_ +#define SOURCEMETA_BLAZE_CODEGEN_ERROR_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +#include +#include + +#include // std::exception +#include // std::string +#include // std::string_view +#include // std::move + +namespace sourcemeta::blaze { + +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251 4275) +#endif + +/// @ingroup codegen +/// An error that represents an unsupported keyword during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnsupportedKeywordError + : public std::exception { +public: + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + keyword_{std::move(keyword)}, message_{message} {} + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : CodegenUnsupportedKeywordError{std::move(json), + sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string message) = delete; + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string &&message) = delete; + CodegenUnsupportedKeywordError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + + [[nodiscard]] auto keyword() const noexcept -> std::string_view { + return this->keyword_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + std::string keyword_; + const char *message_; +}; + +/// @ingroup codegen +/// An error that represents an unsupported keyword value during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnsupportedKeywordValueError + : public std::exception { +public: + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + keyword_{std::move(keyword)}, message_{message} {} + CodegenUnsupportedKeywordValueError( + sourcemeta::core::JSON json, const sourcemeta::core::WeakPointer &pointer, + std::string keyword, const char *message) + : CodegenUnsupportedKeywordValueError{ + std::move(json), sourcemeta::core::to_pointer(pointer), + std::move(keyword), message} {} + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string message) = delete; + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string &&message) = delete; + CodegenUnsupportedKeywordValueError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string keyword, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + + [[nodiscard]] auto keyword() const noexcept -> std::string_view { + return this->keyword_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + std::string keyword_; + const char *message_; +}; + +/// @ingroup codegen +/// An error that represents an unexpected schema during IR compilation +class SOURCEMETA_BLAZE_CODEGEN_EXPORT CodegenUnexpectedSchemaError + : public std::exception { +public: + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + const char *message) + : json_{std::move(json)}, pointer_{std::move(pointer)}, + message_{message} {} + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + const sourcemeta::core::WeakPointer &pointer, + const char *message) + : CodegenUnexpectedSchemaError{ + std::move(json), sourcemeta::core::to_pointer(pointer), message} {} + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string message) = delete; + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string &&message) = delete; + CodegenUnexpectedSchemaError(sourcemeta::core::JSON json, + sourcemeta::core::Pointer pointer, + std::string_view message) = delete; + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto json() const noexcept -> const sourcemeta::core::JSON & { + return this->json_; + } + + [[nodiscard]] auto pointer() const noexcept + -> const sourcemeta::core::Pointer & { + return this->pointer_; + } + +private: + sourcemeta::core::JSON json_; + sourcemeta::core::Pointer pointer_; + const char *message_; +}; + +#if defined(_MSC_VER) +#pragma warning(default : 4251 4275) +#endif + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_typescript.h b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_typescript.h new file mode 100644 index 000000000..cac0000f1 --- /dev/null +++ b/vendor/blaze/src/codegen/include/sourcemeta/blaze/codegen_typescript.h @@ -0,0 +1,63 @@ +#ifndef SOURCEMETA_BLAZE_CODEGEN_TYPESCRIPT_H_ +#define SOURCEMETA_BLAZE_CODEGEN_TYPESCRIPT_H_ + +#ifndef SOURCEMETA_BLAZE_CODEGEN_EXPORT +#include +#endif + +#include + +#include // std::map +#include // std::ostream +#include // std::string +#include // std::string_view + +namespace sourcemeta::blaze { + +struct CodegenIRScalar; +struct CodegenIREnumeration; +struct CodegenIRObject; +struct CodegenIRImpossible; +struct CodegenIRAny; +struct CodegenIRArray; +struct CodegenIRReference; +struct CodegenIRTuple; +struct CodegenIRUnion; +struct CodegenIRIntersection; +struct CodegenIRConditional; + +/// @ingroup codegen +class SOURCEMETA_BLAZE_CODEGEN_EXPORT TypeScript { +public: + TypeScript(std::ostream &stream, std::string_view type_prefix); + auto operator()(const CodegenIRScalar &entry) -> void; + auto operator()(const CodegenIREnumeration &entry) -> void; + auto operator()(const CodegenIRObject &entry) -> void; + auto operator()(const CodegenIRImpossible &entry) -> void; + auto operator()(const CodegenIRAny &entry) -> void; + auto operator()(const CodegenIRArray &entry) -> void; + auto operator()(const CodegenIRReference &entry) -> void; + auto operator()(const CodegenIRTuple &entry) -> void; + auto operator()(const CodegenIRUnion &entry) -> void; + auto operator()(const CodegenIRIntersection &entry) -> void; + auto operator()(const CodegenIRConditional &entry) -> void; + +private: +// Exporting symbols that depends on the standard C++ library is considered +// safe. +// https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-level-2-c4275?view=msvc-170&redirectedfrom=MSDN +#if defined(_MSC_VER) +#pragma warning(disable : 4251) +#endif + // NOLINTNEXTLINE(cppcoreguidelines-avoid-const-or-ref-data-members) + std::ostream &output; + std::string_view prefix; + std::map cache; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif +}; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/compiler/compile_helpers.h b/vendor/blaze/src/compiler/compile_helpers.h index 6de78735c..03984f6d9 100644 --- a/vendor/blaze/src/compiler/compile_helpers.h +++ b/vendor/blaze/src/compiler/compile_helpers.h @@ -355,6 +355,54 @@ is_circular(const sourcemeta::core::SchemaFrame &frame, return false; } +// The set of property names that this schema declares as required at this +// level +inline auto required_properties(const SchemaContext &schema_context) + -> ValueStringSet { + using Known = sourcemeta::core::Vocabularies::Known; + const auto imports_validation_vocabulary{ + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_4) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_6) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_7) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2019_09_Validation) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2020_12_Validation)}; + + ValueStringSet result; + + if (imports_validation_vocabulary && schema_context.schema.is_object() && + schema_context.schema.defines("required") && + schema_context.schema.at("required").is_array()) { + for (const auto &entry : schema_context.schema.at("required").as_array()) { + if (entry.is_string()) { + result.insert(entry.to_string()); + } + } + + return result; + } + + const auto imports_draft3_vocabulary{ + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3_Hyper)}; + + if (imports_draft3_vocabulary && schema_context.schema.is_object() && + schema_context.schema.defines("properties") && + schema_context.schema.at("properties").is_object()) { + for (const auto &entry : + schema_context.schema.at("properties").as_object()) { + if (entry.second.is_object() && entry.second.defines("required") && + entry.second.at("required").is_boolean() && + entry.second.at("required").to_boolean()) { + result.insert(entry.first); + } + } + } + + return result; +} + } // namespace sourcemeta::blaze #endif diff --git a/vendor/blaze/src/compiler/default_compiler.cc b/vendor/blaze/src/compiler/default_compiler.cc index 54c66f381..c4e23cfc3 100644 --- a/vendor/blaze/src/compiler/default_compiler.cc +++ b/vendor/blaze/src/compiler/default_compiler.cc @@ -2,6 +2,7 @@ #include "default_compiler_2019_09.h" #include "default_compiler_2020_12.h" +#include "default_compiler_draft3.h" #include "default_compiler_draft4.h" #include "default_compiler_draft6.h" #include "default_compiler_draft7.h" @@ -41,6 +42,8 @@ auto sourcemeta::blaze::default_schema_compiler( Known::JSON_Schema_Draft_6_Hyper, Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, + Known::JSON_Schema_Draft_3, + Known::JSON_Schema_Draft_3_Hyper, Known::OpenAPI_3_1_Base, Known::OpenAPI_3_2_Base}; @@ -138,9 +141,9 @@ auto sourcemeta::blaze::default_schema_compiler( // As per compatibility optional test COMPILE(Known::JSON_Schema_2020_12_Applicator, "dependencies", - compiler_draft4_applicator_dependencies); + compiler_draft3_applicator_dependencies); - COMPILE(Known::JSON_Schema_2020_12_Core, "$ref", compiler_draft4_core_ref); + COMPILE(Known::JSON_Schema_2020_12_Core, "$ref", compiler_draft3_core_ref); COMPILE(Known::JSON_Schema_2020_12_Applicator, "allOf", compiler_draft4_applicator_allof); @@ -152,13 +155,13 @@ auto sourcemeta::blaze::default_schema_compiler( compiler_draft4_applicator_not); COMPILE(Known::JSON_Schema_2020_12_Validation, "enum", - compiler_draft4_validation_enum); + compiler_draft3_validation_enum); COMPILE(Known::JSON_Schema_2020_12_Validation, "uniqueItems", - compiler_draft4_validation_uniqueitems); + compiler_draft3_validation_uniqueitems); COMPILE(Known::JSON_Schema_2020_12_Validation, "maxItems", - compiler_draft4_validation_maxitems); + compiler_draft3_validation_maxitems); COMPILE(Known::JSON_Schema_2020_12_Validation, "minItems", - compiler_draft4_validation_minitems); + compiler_draft3_validation_minitems); COMPILE(Known::JSON_Schema_2020_12_Validation, "required", compiler_draft4_validation_required); COMPILE(Known::JSON_Schema_2020_12_Validation, "maxProperties", @@ -166,17 +169,17 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE(Known::JSON_Schema_2020_12_Validation, "minProperties", compiler_draft4_validation_minproperties); COMPILE(Known::JSON_Schema_2020_12_Validation, "maximum", - compiler_draft4_validation_maximum); + compiler_draft3_validation_maximum); COMPILE(Known::JSON_Schema_2020_12_Validation, "minimum", - compiler_draft4_validation_minimum); + compiler_draft3_validation_minimum); COMPILE(Known::JSON_Schema_2020_12_Validation, "multipleOf", - compiler_draft4_validation_multipleof); + compiler_draft3_validation_divisibleby); COMPILE(Known::JSON_Schema_2020_12_Validation, "maxLength", - compiler_draft4_validation_maxlength); + compiler_draft3_validation_maxlength); COMPILE(Known::JSON_Schema_2020_12_Validation, "minLength", - compiler_draft4_validation_minlength); + compiler_draft3_validation_minlength); COMPILE(Known::JSON_Schema_2020_12_Validation, "pattern", - compiler_draft4_validation_pattern); + compiler_draft3_validation_pattern); // ******************************************** // 2019-09 @@ -239,9 +242,9 @@ auto sourcemeta::blaze::default_schema_compiler( // As per compatibility optional test COMPILE(Known::JSON_Schema_2019_09_Applicator, "dependencies", - compiler_draft4_applicator_dependencies); + compiler_draft3_applicator_dependencies); - COMPILE(Known::JSON_Schema_2019_09_Core, "$ref", compiler_draft4_core_ref); + COMPILE(Known::JSON_Schema_2019_09_Core, "$ref", compiler_draft3_core_ref); COMPILE(Known::JSON_Schema_2019_09_Applicator, "allOf", compiler_draft4_applicator_allof); @@ -253,13 +256,13 @@ auto sourcemeta::blaze::default_schema_compiler( compiler_draft4_applicator_not); COMPILE(Known::JSON_Schema_2019_09_Validation, "enum", - compiler_draft4_validation_enum); + compiler_draft3_validation_enum); COMPILE(Known::JSON_Schema_2019_09_Validation, "uniqueItems", - compiler_draft4_validation_uniqueitems); + compiler_draft3_validation_uniqueitems); COMPILE(Known::JSON_Schema_2019_09_Validation, "maxItems", - compiler_draft4_validation_maxitems); + compiler_draft3_validation_maxitems); COMPILE(Known::JSON_Schema_2019_09_Validation, "minItems", - compiler_draft4_validation_minitems); + compiler_draft3_validation_minitems); COMPILE(Known::JSON_Schema_2019_09_Validation, "required", compiler_draft4_validation_required); COMPILE(Known::JSON_Schema_2019_09_Validation, "maxProperties", @@ -267,24 +270,24 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE(Known::JSON_Schema_2019_09_Validation, "minProperties", compiler_draft4_validation_minproperties); COMPILE(Known::JSON_Schema_2019_09_Validation, "maximum", - compiler_draft4_validation_maximum); + compiler_draft3_validation_maximum); COMPILE(Known::JSON_Schema_2019_09_Validation, "minimum", - compiler_draft4_validation_minimum); + compiler_draft3_validation_minimum); COMPILE(Known::JSON_Schema_2019_09_Validation, "multipleOf", - compiler_draft4_validation_multipleof); + compiler_draft3_validation_divisibleby); COMPILE(Known::JSON_Schema_2019_09_Validation, "maxLength", - compiler_draft4_validation_maxlength); + compiler_draft3_validation_maxlength); COMPILE(Known::JSON_Schema_2019_09_Validation, "minLength", - compiler_draft4_validation_minlength); + compiler_draft3_validation_minlength); COMPILE(Known::JSON_Schema_2019_09_Validation, "pattern", - compiler_draft4_validation_pattern); + compiler_draft3_validation_pattern); // ******************************************** // DRAFT 7 // ******************************************** COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "$ref", compiler_draft4_core_ref); + "$ref", compiler_draft3_core_ref); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_7, "$ref"); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_7_Hyper, "$ref"); @@ -322,18 +325,18 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, "not", compiler_draft4_applicator_not); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "enum", compiler_draft4_validation_enum); + "enum", compiler_draft3_validation_enum); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "items", compiler_draft4_applicator_items); + "items", compiler_draft3_applicator_items); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "additionalItems", compiler_draft4_applicator_additionalitems); + "additionalItems", compiler_draft3_applicator_additionalitems); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "uniqueItems", compiler_draft4_validation_uniqueitems); + "uniqueItems", compiler_draft3_validation_uniqueitems); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "maxItems", compiler_draft4_validation_maxitems); + "maxItems", compiler_draft3_validation_maxitems); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "minItems", compiler_draft4_validation_minitems); + "minItems", compiler_draft3_validation_minitems); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, "required", compiler_draft4_validation_required); @@ -342,36 +345,36 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, "minProperties", compiler_draft4_validation_minproperties); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "properties", compiler_draft4_applicator_properties); + "properties", compiler_draft3_applicator_properties); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft3_applicator_patternproperties); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, "additionalProperties", - compiler_draft4_applicator_additionalproperties); + compiler_draft3_applicator_additionalproperties); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "dependencies", compiler_draft4_applicator_dependencies); + "dependencies", compiler_draft3_applicator_dependencies); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "maximum", compiler_draft4_validation_maximum); + "maximum", compiler_draft3_validation_maximum); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "minimum", compiler_draft4_validation_minimum); + "minimum", compiler_draft3_validation_minimum); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "multipleOf", compiler_draft4_validation_multipleof); + "multipleOf", compiler_draft3_validation_divisibleby); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "maxLength", compiler_draft4_validation_maxlength); + "maxLength", compiler_draft3_validation_maxlength); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "minLength", compiler_draft4_validation_minlength); + "minLength", compiler_draft3_validation_minlength); COMPILE_ANY(Known::JSON_Schema_Draft_7, Known::JSON_Schema_Draft_7_Hyper, - "pattern", compiler_draft4_validation_pattern); + "pattern", compiler_draft3_validation_pattern); // ******************************************** // DRAFT 6 // ******************************************** COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "$ref", compiler_draft4_core_ref); + "$ref", compiler_draft3_core_ref); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_6, "$ref"); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_6_Hyper, "$ref"); @@ -406,18 +409,18 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, "not", compiler_draft4_applicator_not); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "enum", compiler_draft4_validation_enum); + "enum", compiler_draft3_validation_enum); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "items", compiler_draft4_applicator_items); + "items", compiler_draft3_applicator_items); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "additionalItems", compiler_draft4_applicator_additionalitems); + "additionalItems", compiler_draft3_applicator_additionalitems); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "uniqueItems", compiler_draft4_validation_uniqueitems); + "uniqueItems", compiler_draft3_validation_uniqueitems); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "maxItems", compiler_draft4_validation_maxitems); + "maxItems", compiler_draft3_validation_maxitems); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "minItems", compiler_draft4_validation_minitems); + "minItems", compiler_draft3_validation_minitems); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, "required", compiler_draft4_validation_required); @@ -426,36 +429,36 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, "minProperties", compiler_draft4_validation_minproperties); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "properties", compiler_draft4_applicator_properties); + "properties", compiler_draft3_applicator_properties); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft3_applicator_patternproperties); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, "additionalProperties", - compiler_draft4_applicator_additionalproperties); + compiler_draft3_applicator_additionalproperties); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "dependencies", compiler_draft4_applicator_dependencies); + "dependencies", compiler_draft3_applicator_dependencies); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "maximum", compiler_draft4_validation_maximum); + "maximum", compiler_draft3_validation_maximum); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "minimum", compiler_draft4_validation_minimum); + "minimum", compiler_draft3_validation_minimum); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "multipleOf", compiler_draft4_validation_multipleof); + "multipleOf", compiler_draft3_validation_divisibleby); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "maxLength", compiler_draft4_validation_maxlength); + "maxLength", compiler_draft3_validation_maxlength); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "minLength", compiler_draft4_validation_minlength); + "minLength", compiler_draft3_validation_minlength); COMPILE_ANY(Known::JSON_Schema_Draft_6, Known::JSON_Schema_Draft_6_Hyper, - "pattern", compiler_draft4_validation_pattern); + "pattern", compiler_draft3_validation_pattern); // ******************************************** // DRAFT 4 // ******************************************** COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "$ref", compiler_draft4_core_ref); + "$ref", compiler_draft3_core_ref); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_4, "$ref"); STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_4_Hyper, "$ref"); @@ -469,25 +472,25 @@ auto sourcemeta::blaze::default_schema_compiler( COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, "not", compiler_draft4_applicator_not); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "properties", compiler_draft4_applicator_properties); + "properties", compiler_draft3_applicator_properties); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, "patternProperties", - compiler_draft4_applicator_patternproperties); + compiler_draft3_applicator_patternproperties); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, "additionalProperties", - compiler_draft4_applicator_additionalproperties); + compiler_draft3_applicator_additionalproperties); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "items", compiler_draft4_applicator_items); + "items", compiler_draft3_applicator_items); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "additionalItems", compiler_draft4_applicator_additionalitems); + "additionalItems", compiler_draft3_applicator_additionalitems); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "dependencies", compiler_draft4_applicator_dependencies); + "dependencies", compiler_draft3_applicator_dependencies); // Any COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "type", compiler_draft4_validation_type); + "type", compiler_draft3_validation_type); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "enum", compiler_draft4_validation_enum); + "enum", compiler_draft3_validation_enum); // Object COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, @@ -499,27 +502,86 @@ auto sourcemeta::blaze::default_schema_compiler( // Array COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "uniqueItems", compiler_draft4_validation_uniqueitems); + "uniqueItems", compiler_draft3_validation_uniqueitems); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "maxItems", compiler_draft4_validation_maxitems); + "maxItems", compiler_draft3_validation_maxitems); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "minItems", compiler_draft4_validation_minitems); + "minItems", compiler_draft3_validation_minitems); // String COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "pattern", compiler_draft4_validation_pattern); + "pattern", compiler_draft3_validation_pattern); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "maxLength", compiler_draft4_validation_maxlength); + "maxLength", compiler_draft3_validation_maxlength); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "minLength", compiler_draft4_validation_minlength); + "minLength", compiler_draft3_validation_minlength); // Number COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "maximum", compiler_draft4_validation_maximum); + "maximum", compiler_draft3_validation_maximum); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "minimum", compiler_draft4_validation_minimum); + "minimum", compiler_draft3_validation_minimum); COMPILE_ANY(Known::JSON_Schema_Draft_4, Known::JSON_Schema_Draft_4_Hyper, - "multipleOf", compiler_draft4_validation_multipleof); + "multipleOf", compiler_draft3_validation_divisibleby); + + // ******************************************** + // DRAFT 3 + // ******************************************** + + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "$ref", compiler_draft3_core_ref); + STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_3, "$ref"); + STOP_IF_SIBLING_KEYWORD(Known::JSON_Schema_Draft_3_Hyper, "$ref"); + + // Applicators + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "extends", compiler_draft3_applicator_extends); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "properties", compiler_draft3_applicator_properties); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "patternProperties", + compiler_draft3_applicator_patternproperties); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "additionalProperties", + compiler_draft3_applicator_additionalproperties); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "items", compiler_draft3_applicator_items); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "additionalItems", compiler_draft3_applicator_additionalitems); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "dependencies", compiler_draft3_applicator_dependencies); + + // Any + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "type", compiler_draft3_validation_type); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "disallow", compiler_draft3_validation_disallow); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "enum", compiler_draft3_validation_enum); + + // Array + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "uniqueItems", compiler_draft3_validation_uniqueitems); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "maxItems", compiler_draft3_validation_maxitems); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "minItems", compiler_draft3_validation_minitems); + + // String + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "pattern", compiler_draft3_validation_pattern); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "maxLength", compiler_draft3_validation_maxlength); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "minLength", compiler_draft3_validation_minlength); + + // Number + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "maximum", compiler_draft3_validation_maximum); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "minimum", compiler_draft3_validation_minimum); + COMPILE_ANY(Known::JSON_Schema_Draft_3, Known::JSON_Schema_Draft_3_Hyper, + "divisibleBy", compiler_draft3_validation_divisibleby); // ******************************************** // OpenAPI diff --git a/vendor/blaze/src/compiler/default_compiler_2019_09.h b/vendor/blaze/src/compiler/default_compiler_2019_09.h index 8b76fd0aa..32ebf57b9 100644 --- a/vendor/blaze/src/compiler/default_compiler_2019_09.h +++ b/vendor/blaze/src/compiler/default_compiler_2019_09.h @@ -202,7 +202,7 @@ auto compiler_2019_09_applicator_additionalproperties( const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - return compiler_draft4_applicator_additionalproperties_with_options( + return compiler_draft3_applicator_additionalproperties_with_options( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, requires_evaluation(context, schema_context)); @@ -220,12 +220,12 @@ auto compiler_2019_09_applicator_items(const Context &context, })}; if (schema_context.schema.at(dynamic_context.keyword).is_array()) { - return compiler_draft4_applicator_items_with_options( + return compiler_draft3_applicator_items_with_options( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, track); } - return compiler_draft4_applicator_items_with_options( + return compiler_draft3_applicator_items_with_options( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, track && !schema_context.schema.defines("unevaluatedItems")); @@ -242,7 +242,7 @@ auto compiler_2019_09_applicator_additionalitems( return dependency.first.ends_with("unevaluatedItems"); })}; - return compiler_draft4_applicator_additionalitems_with_options( + return compiler_draft3_applicator_additionalitems_with_options( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, track && !schema_context.schema.defines("unevaluatedItems")); @@ -419,7 +419,7 @@ auto compiler_2019_09_core_recursiveref(const Context &context, // In this case, just behave as a normal static reference if (!context.frame.references().contains( {sourcemeta::core::SchemaReferenceType::Dynamic, entry.pointer})) { - return compiler_draft4_core_ref(context, schema_context, dynamic_context, + return compiler_draft3_core_ref(context, schema_context, dynamic_context, current); } @@ -431,7 +431,7 @@ auto compiler_2019_09_applicator_properties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions ¤t) -> Instructions { - return compiler_draft4_applicator_properties_with_options( + return compiler_draft3_applicator_properties_with_options( context, schema_context, dynamic_context, current, context.mode == Mode::Exhaustive, requires_evaluation(context, schema_context)); @@ -441,7 +441,7 @@ auto compiler_2019_09_applicator_patternproperties( const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, const Instructions &) -> Instructions { - return compiler_draft4_applicator_patternproperties_with_options( + return compiler_draft3_applicator_patternproperties_with_options( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, requires_evaluation(context, schema_context)); diff --git a/vendor/blaze/src/compiler/default_compiler_2020_12.h b/vendor/blaze/src/compiler/default_compiler_2020_12.h index f91aba2e4..d4fe9695a 100644 --- a/vendor/blaze/src/compiler/default_compiler_2020_12.h +++ b/vendor/blaze/src/compiler/default_compiler_2020_12.h @@ -21,7 +21,7 @@ auto compiler_2020_12_applicator_prefixitems( return dependency.first.ends_with("unevaluatedItems"); })}; - return compiler_draft4_applicator_items_array( + return compiler_draft3_applicator_items_array( context, schema_context, dynamic_context, context.mode == Mode::Exhaustive, track); } @@ -42,7 +42,7 @@ auto compiler_2020_12_applicator_items(const Context &context, return dependency.first.ends_with("unevaluatedItems"); })}; - return compiler_draft4_applicator_additionalitems_from_cursor( + return compiler_draft3_applicator_additionalitems_from_cursor( context, schema_context, dynamic_context, cursor, context.mode == Mode::Exhaustive, track && !schema_context.schema.defines("unevaluatedItems")); @@ -74,7 +74,7 @@ auto compiler_2020_12_core_dynamicref(const Context &context, // In this case, just behave as a normal static reference if (!context.frame.references().contains( {sourcemeta::core::SchemaReferenceType::Dynamic, entry.pointer})) { - return compiler_draft4_core_ref(context, schema_context, dynamic_context, + return compiler_draft3_core_ref(context, schema_context, dynamic_context, current); } diff --git a/vendor/blaze/src/compiler/default_compiler_draft3.h b/vendor/blaze/src/compiler/default_compiler_draft3.h new file mode 100644 index 000000000..0b11d1f98 --- /dev/null +++ b/vendor/blaze/src/compiler/default_compiler_draft3.h @@ -0,0 +1,2443 @@ +#ifndef SOURCEMETA_BLAZE_COMPILER_DEFAULT_COMPILER_DRAFT3_H_ +#define SOURCEMETA_BLAZE_COMPILER_DEFAULT_COMPILER_DRAFT3_H_ + +#include +#include + +#include + +#include // std::sort, std::ranges::any_of, std::ranges::all_of, std::find_if, std::ranges::none_of +#include // assert +#include // std::set +#include // std::move, std::to_underlying + +#include "compile_helpers.h" + +static auto parse_regex(const std::string &pattern, + const sourcemeta::core::URI &base, + const sourcemeta::core::WeakPointer &schema_location) + -> sourcemeta::core::Regex { + const auto result{sourcemeta::core::to_regex(pattern)}; + if (!result.has_value()) [[unlikely]] { + throw sourcemeta::blaze::CompilerInvalidRegexError( + base, to_pointer(schema_location), pattern); + } + + return result.value(); +} + +static auto +relative_schema_location_size(const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::Instruction &step) + -> std::size_t { + return context.extra[step.extra_index].relative_schema_location.size(); +} + +static auto +defines_direct_enumeration(const sourcemeta::blaze::Instructions &steps) + -> std::optional { + const auto iterator{std::ranges::find_if(steps, [](const auto &step) { + return step.type == sourcemeta::blaze::InstructionIndex::AssertionEqual || + step.type == sourcemeta::blaze::InstructionIndex::AssertionEqualsAny; + })}; + + if (iterator == steps.cend()) { + return std::nullopt; + } + + return std::distance(steps.cbegin(), iterator); +} + +static auto is_inside_disjunctor(const sourcemeta::core::WeakPointer &pointer) + -> bool { + return pointer.size() > 2 && pointer.at(pointer.size() - 2).is_index() && + pointer.at(pointer.size() - 3).is_property() && + (pointer.at(pointer.size() - 3).to_property() == "oneOf" || + pointer.at(pointer.size() - 3).to_property() == "anyOf"); +} + +static auto json_array_to_string_set(const sourcemeta::core::JSON &document) + -> sourcemeta::blaze::ValueStringSet { + sourcemeta::blaze::ValueStringSet result; + for (const auto &value : document.as_array()) { + assert(value.is_string()); + result.insert(value.to_string()); + } + + return result; +} + +static auto +is_closed_properties_required(const sourcemeta::core::JSON &schema, + const sourcemeta::blaze::ValueStringSet &required) + -> bool { + return !schema.defines("patternProperties") && + schema.defines("additionalProperties") && + schema.at("additionalProperties").is_boolean() && + !schema.at("additionalProperties").to_boolean() && + schema.defines("properties") && schema.at("properties").is_object() && + schema.at("properties").size() == required.size() && + std::ranges::all_of(required, [&schema](const auto &property) { + return schema.at("properties") + .defines(property.first, property.second); + }); +} + +static auto +compile_properties(const sourcemeta::blaze::Context &context, + const sourcemeta::blaze::SchemaContext &schema_context, + const sourcemeta::blaze::DynamicContext &dynamic_context, + const sourcemeta::blaze::Instructions &) + -> std::vector> { + std::vector> + properties; + for (const auto &entry : schema_context.schema.at("properties").as_object()) { + properties.emplace_back( + entry.first, + compile(context, schema_context, dynamic_context, + sourcemeta::blaze::make_weak_pointer(entry.first), + sourcemeta::blaze::make_weak_pointer(entry.first))); + } + + // In many cases, `properties` have some subschemas that are small + // and some subschemas that are large. To attempt to improve performance, + // we prefer to evaluate smaller subschemas first, in the hope of failing + // earlier without spending a lot of time on other subschemas + if (context.tweaks.properties_reorder) { + std::ranges::sort(properties, [&context](const auto &left, + const auto &right) { + const auto left_size{recursive_template_size(left.second)}; + const auto right_size{recursive_template_size(right.second)}; + if (left_size == right_size) { + const auto left_direct_enumeration{ + defines_direct_enumeration(left.second)}; + const auto right_direct_enumeration{ + defines_direct_enumeration(right.second)}; + + // Enumerations always take precedence + if (left_direct_enumeration.has_value() && + right_direct_enumeration.has_value()) { + // If both options have a direct enumeration, we choose + // the one with the shorter relative schema location + return relative_schema_location_size( + context, left.second.at(left_direct_enumeration.value())) < + relative_schema_location_size( + context, + right.second.at(right_direct_enumeration.value())); + } else if (left_direct_enumeration.has_value()) { + return true; + } else if (right_direct_enumeration.has_value()) { + return false; + } + + return left.first < right.first; + } else { + return left_size < right_size; + } + }); + } + + return properties; +} + +static auto to_string_hashes( + std::vector> + &hashes) -> sourcemeta::blaze::ValueStringHashes { + assert(!hashes.empty()); + std::ranges::sort(hashes, [](const auto &left, const auto &right) { + return left.first.size() < right.first.size(); + }); + + sourcemeta::blaze::ValueStringHashes result; + // The idea with the table of contents is as follows: each index + // marks the starting and end positions for a string where the size + // is equal to the index. + result.second.resize(hashes.back().first.size() + 1, std::make_pair(0, 0)); + // TODO(C++23): Use std::views::enumerate when available in libc++ + for (std::size_t index = 0; index < hashes.size(); index++) { + result.first.emplace_back(hashes[index].second, hashes[index].first); + const auto string_size{hashes[index].first.size()}; + // We leave index 0 to represent the empty string + const auto position{index + 1}; + const auto lower_bound{ + result.second[string_size].first == 0 + ? position + : std::min(result.second[string_size].first, position)}; + const auto upper_bound{ + result.second[string_size].second == 0 + ? position + : std::max(result.second[string_size].second, position)}; + assert(lower_bound <= upper_bound); + assert(lower_bound > 0 && upper_bound > 0); + assert(string_size < result.second.size()); + result.second[string_size] = std::make_pair(lower_bound, upper_bound); + } + + assert(result.second.size() == hashes.back().first.size() + 1); + return result; +} + +namespace internal { +using namespace sourcemeta::blaze; + +auto compile_required_assertions(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions ¤t, + ValueStringSet properties_set) + -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; + } + + const auto assume_object{schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == + "object"}; + + if (properties_set.empty()) { + return {}; + } else if (properties_set.size() > 1) { + if (is_closed_properties_required(schema_context.schema, properties_set)) { + if (context.mode == Mode::FastValidation && assume_object) { + static const std::string properties_keyword{"properties"}; + const SchemaContext new_schema_context{ + .relative_pointer = + schema_context.relative_pointer.initial().concat( + sourcemeta::blaze::make_weak_pointer(properties_keyword)), + .schema = schema_context.schema, + .vocabularies = schema_context.vocabularies, + .base = schema_context.base, + .is_property_name = schema_context.is_property_name}; + const DynamicContext new_dynamic_context{ + .keyword = KEYWORD_PROPERTIES, + .base_schema_location = sourcemeta::core::empty_weak_pointer, + .base_instance_location = sourcemeta::core::empty_weak_pointer}; + auto properties{compile_properties(context, new_schema_context, + new_dynamic_context, current)}; + if (std::ranges::all_of(properties, [](const auto &property) { + return property.second.size() == 1 && + property.second.front().type == + InstructionIndex::AssertionTypeStrict; + })) { + std::set types; + for (const auto &property : properties) { + types.insert(std::get(property.second.front().value)); + } + + if (types.size() == 1) { + // Handled in `properties` + return {}; + } + } + + sourcemeta::core::PropertyHashJSON hasher; + if (context.mode == Mode::FastValidation && + properties_set.size() == 3 && + std::ranges::all_of(properties_set, + [&hasher](const auto &property) { + return hasher.is_perfect(property.second); + })) { + std::vector> hashes; + for (const auto &property : properties_set) { + hashes.emplace_back(property.first, property.second); + } + + return {make(sourcemeta::blaze::InstructionIndex:: + AssertionDefinesExactlyStrictHash3, + context, schema_context, dynamic_context, + to_string_hashes(hashes))}; + } + + return {make( + sourcemeta::blaze::InstructionIndex::AssertionDefinesExactlyStrict, + context, schema_context, dynamic_context, + std::move(properties_set))}; + } else { + return { + make(sourcemeta::blaze::InstructionIndex::AssertionDefinesExactly, + context, schema_context, dynamic_context, + std::move(properties_set))}; + } + } else if (assume_object) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionDefinesAllStrict, + context, schema_context, dynamic_context, std::move(properties_set))}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::AssertionDefinesAll, + context, schema_context, dynamic_context, + std::move(properties_set))}; + } + } else if (assume_object) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionDefinesStrict, + context, schema_context, dynamic_context, + make_property(properties_set.begin()->first))}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::AssertionDefines, context, + schema_context, dynamic_context, + make_property(properties_set.begin()->first))}; + } +} + +auto compiler_draft3_core_ref(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + const auto &entry{static_frame_entry(context, schema_context)}; + const auto type{sourcemeta::core::SchemaReferenceType::Static}; + const auto reference{context.frame.reference(type, entry.pointer)}; + if (!reference.has_value()) [[unlikely]] { + throw sourcemeta::core::SchemaReferenceError( + schema_context.schema.at(dynamic_context.keyword).to_string(), + to_pointer(schema_context.relative_pointer), + "Could not resolve schema reference"); + } + + const auto key{std::make_tuple(type, + std::string_view{reference->get().destination}, + schema_context.is_property_name)}; + assert(context.targets.contains(key)); + return {make(sourcemeta::blaze::InstructionIndex::ControlJump, context, + schema_context, dynamic_context, + ValueUnsignedInteger{context.targets.at(key).first})}; +} + +// There are two ways to compile `properties` depending on whether +// most of the properties are marked as required using `required` +// or whether most of the properties are optional. Each shines +// in the corresponding case. +auto properties_as_loop(const Context &context, + const SchemaContext &schema_context, + const sourcemeta::core::JSON &properties) -> bool { + if (context.tweaks.properties_always_unroll) { + return false; + } + + using Known = sourcemeta::core::Vocabularies::Known; + const auto size{properties.size()}; + const auto imports_validation_vocabulary = + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_4) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_6) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_7) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2019_09_Validation) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2020_12_Validation); + const auto imports_const = + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_6) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_7) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2019_09_Validation) || + schema_context.vocabularies.contains( + Known::JSON_Schema_2020_12_Validation); + ValueStringSet required; + for (const auto &entry : required_properties(schema_context)) { + // Only count the required property if its indeed in "properties" + if (properties.defines(entry.first)) { + required.insert(entry.first); + } + } + + const auto ¤t_entry{static_frame_entry(context, schema_context)}; + const auto inside_disjunctor{ + is_inside_disjunctor(schema_context.relative_pointer) || + // Check if any reference from `anyOf` or `oneOf` points to us + std::ranges::any_of( + context.frame.references(), + [&context, ¤t_entry](const auto &reference) { + if (!context.frame.locations().contains( + {sourcemeta::core::SchemaReferenceType::Static, + reference.second.destination})) { + return false; + } + + const auto &target{ + context.frame.locations() + .at({sourcemeta::core::SchemaReferenceType::Static, + reference.second.destination}) + .pointer}; + return is_inside_disjunctor(reference.first.second) && + current_entry.pointer.initial() == target; + })}; + + if (!inside_disjunctor && + schema_context.schema.defines("additionalProperties") && + schema_context.schema.at("additionalProperties").is_boolean() && + !schema_context.schema.at("additionalProperties").to_boolean() && + // If all properties are required, we should still unroll + required.size() < size) { + return true; + } + + return + // This strategy only makes sense if most of the properties are "optional" + required.size() <= (size / 4) && + // If `properties` only defines a relatively small amount of properties, + // then its probably still faster to unroll + size > 5 && + // Always unroll inside `oneOf` or `anyOf`, to have a + // better chance at quickly short-circuiting + (!inside_disjunctor || + std::ranges::none_of(properties.as_object(), [&](const auto &pair) { + return pair.second.is_object() && + ((imports_validation_vocabulary && + pair.second.defines("enum")) || + (imports_const && pair.second.defines("const"))); + })); +} + +auto draft3_any_type_instructions(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context) + -> Instructions { + if (context.mode != Mode::Exhaustive) { + return {}; + } + + using sourcemeta::core::JSON; + ValueTypes types{}; + types.set(std::to_underlying(JSON::Type::Null)); + types.set(std::to_underlying(JSON::Type::Boolean)); + types.set(std::to_underlying(JSON::Type::Object)); + types.set(std::to_underlying(JSON::Type::Array)); + types.set(std::to_underlying(JSON::Type::Integer)); + types.set(std::to_underlying(JSON::Type::Real)); + types.set(std::to_underlying(JSON::Type::Decimal)); + types.set(std::to_underlying(JSON::Type::String)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; +} + +auto draft3_set_type_bits(const std::string &name, ValueTypes &types) -> bool { + using sourcemeta::core::JSON; + if (name == "null") { + types.set(std::to_underlying(JSON::Type::Null)); + } else if (name == "boolean") { + types.set(std::to_underlying(JSON::Type::Boolean)); + } else if (name == "object") { + types.set(std::to_underlying(JSON::Type::Object)); + } else if (name == "array") { + types.set(std::to_underlying(JSON::Type::Array)); + } else if (name == "integer") { + types.set(std::to_underlying(JSON::Type::Integer)); + } else if (name == "string") { + types.set(std::to_underlying(JSON::Type::String)); + } else if (name == "number") { + types.set(std::to_underlying(JSON::Type::Real)); + types.set(std::to_underlying(JSON::Type::Integer)); + types.set(std::to_underlying(JSON::Type::Decimal)); + } else { + return false; + } + return true; +} + +auto is_integer_type_check(const Instruction &instruction) -> bool { + return (instruction.type == InstructionIndex::AssertionType || + instruction.type == InstructionIndex::AssertionTypeStrict) && + std::get(instruction.value) == + sourcemeta::core::JSON::Type::Integer; +} + +auto has_strict_integer_type(const Instructions &children) -> bool { + for (const auto &child : children) { + if (child.type == InstructionIndex::AssertionTypeStrict && + std::get(child.value) == + sourcemeta::core::JSON::Type::Integer) { + return true; + } + } + + return false; +} + +auto is_integer_type_bounded_pattern(const Instructions &children) -> bool { + if (children.size() != 3) { + return false; + } + + bool has_type{false}; + bool has_min{false}; + bool has_max{false}; + for (const auto &child : children) { + if (is_integer_type_check(child)) { + has_type = true; + } else if (child.type == InstructionIndex::AssertionGreaterEqual && + std::get(child.value).is_integer()) { + has_min = true; + } else if (child.type == InstructionIndex::AssertionLessEqual && + std::get(child.value).is_integer()) { + has_max = true; + } + } + + return has_type && has_min && has_max; +} + +auto is_integer_type_lower_bound_pattern(const Instructions &children) -> bool { + if (children.size() != 2) { + return false; + } + + bool has_type{false}; + bool has_min{false}; + for (const auto &child : children) { + if (is_integer_type_check(child)) { + has_type = true; + } else if (child.type == InstructionIndex::AssertionGreaterEqual && + std::get(child.value).is_integer()) { + has_min = true; + } + } + + return has_type && has_min; +} + +auto extract_integer_lower_bound(const Instructions &children) -> std::int64_t { + for (const auto &child : children) { + if (child.type == InstructionIndex::AssertionGreaterEqual) { + return std::get(child.value).to_integer(); + } + } + + return 0; +} + +auto extract_integer_bounds(const Instructions &children) + -> ValueIntegerBounds { + std::int64_t minimum{0}; + std::int64_t maximum{0}; + for (const auto &child : children) { + if (child.type == InstructionIndex::AssertionGreaterEqual) { + minimum = std::get(child.value).to_integer(); + } else if (child.type == InstructionIndex::AssertionLessEqual) { + maximum = std::get(child.value).to_integer(); + } + } + + return {minimum, maximum}; +} + +auto compiler_draft3_applicator_properties_with_options( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions ¤t, + const bool annotate, const bool track_evaluation) -> Instructions { + if (schema_context.is_property_name) { + return {}; + } + + if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { + return {}; + } + + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; + } + + if (properties_as_loop(context, schema_context, + schema_context.schema.at(dynamic_context.keyword))) { + ValueNamedIndexes indexes; + Instructions children; + std::size_t cursor = 0; + + for (auto &&[name, substeps] : compile_properties( + context, schema_context, relative_dynamic_context(), current)) { + indexes.emplace(name, cursor); + + if (track_evaluation) { + substeps.push_back(make( + sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, + schema_context, relative_dynamic_context(), ValuePointer{name})); + } + + if (annotate) { + substeps.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, + schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{name})); + } + + // Note that the evaluator completely ignores this wrapper anyway + children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, + context, schema_context, + relative_dynamic_context(), ValueNone{}, + std::move(substeps))); + cursor += 1; + } + + if (context.mode == Mode::FastValidation && !track_evaluation && + !schema_context.schema.defines("patternProperties") && + schema_context.schema.defines("additionalProperties") && + schema_context.schema.at("additionalProperties").is_boolean() && + !schema_context.schema.at("additionalProperties").to_boolean()) { + return { + make(sourcemeta::blaze::InstructionIndex::LoopPropertiesMatchClosed, + context, schema_context, dynamic_context, std::move(indexes), + std::move(children))}; + } + + return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesMatch, + context, schema_context, dynamic_context, std::move(indexes), + std::move(children))}; + } + + Instructions children; + + const auto effective_dynamic_context{context.mode == Mode::FastValidation + ? dynamic_context + : relative_dynamic_context()}; + + const auto assume_object{schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == + "object"}; + + ValueStringSet required{required_properties(schema_context)}; + + auto properties{compile_properties(context, schema_context, + effective_dynamic_context, current)}; + + if (context.mode == Mode::FastValidation && !required.empty() && + schema_context.schema.defines("additionalProperties") && + schema_context.schema.at("additionalProperties").is_boolean() && + !schema_context.schema.at("additionalProperties").to_boolean() && + required.size() == + schema_context.schema.at(dynamic_context.keyword).size() && + std::ranges::all_of(properties, [&required](const auto &property) { + return required.contains(property.first); + })) { + if (std::ranges::all_of(properties, [](const auto &property) { + return property.second.size() == 1 && + property.second.front().type == + InstructionIndex::AssertionTypeStrict; + })) { + std::set types; + for (const auto &property : properties) { + types.insert(std::get(property.second.front().value)); + } + + if (types.size() == 1 && + !schema_context.schema.defines("patternProperties")) { + if (assume_object) { + if (is_closed_properties_required(schema_context.schema, required)) { + sourcemeta::core::PropertyHashJSON hasher; + std::vector> + perfect_hashes; + for (const auto &entry : required) { + assert(required.contains(entry.first, entry.second)); + if (hasher.is_perfect(entry.second)) { + perfect_hashes.emplace_back(entry.first, entry.second); + } + } + + if (perfect_hashes.size() == required.size()) { + return {make(sourcemeta::blaze::InstructionIndex:: + LoopPropertiesExactlyTypeStrictHash, + context, schema_context, dynamic_context, + ValueTypedHashes{*types.cbegin(), + to_string_hashes(perfect_hashes)})}; + } + + return {make( + sourcemeta::blaze::InstructionIndex:: + LoopPropertiesExactlyTypeStrict, + context, schema_context, dynamic_context, + ValueTypedProperties{*types.cbegin(), std::move(required)})}; + } + } + + return { + make(sourcemeta::blaze::InstructionIndex::LoopPropertiesTypeStrict, + context, schema_context, dynamic_context, *types.cbegin())}; + } + } + + if (std::ranges::all_of(properties, [](const auto &property) { + return property.second.size() == 1 && + property.second.front().type == + InstructionIndex::AssertionType; + })) { + std::set types; + for (const auto &property : properties) { + types.insert(std::get(property.second.front().value)); + } + + if (types.size() == 1) { + return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesType, + context, schema_context, dynamic_context, + *types.cbegin())}; + } + } + } + + auto attempt_object_fusion{context.mode == Mode::FastValidation && + !annotate && !track_evaluation && assume_object}; + if (attempt_object_fusion) { + for (const auto &entry : schema_context.schema.as_object()) { + const auto &keyword{entry.first}; + if (keyword == "type" || keyword == "required" || + keyword == dynamic_context.keyword) { + continue; + } + + if (keyword == "additionalProperties" && entry.second.is_boolean() && + entry.second.to_boolean()) { + continue; + } + + const auto &keyword_type{ + context.walker(keyword, schema_context.vocabularies).type}; + using enum sourcemeta::core::SchemaKeywordType; + if (keyword_type == Assertion || keyword_type == Annotation || + keyword_type == Unknown || keyword_type == Comment || + keyword_type == Other || keyword_type == LocationMembers) { + continue; + } + + attempt_object_fusion = false; + break; + } + } + ValueObjectProperties fusion_entries; + Instructions fusion_children; + bool fusion_possible{attempt_object_fusion}; + + for (auto &&[name, substeps] : properties) { + if (annotate) { + substeps.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, + schema_context, effective_dynamic_context, + sourcemeta::core::JSON{name})); + } + + // Optimize `properties` where its subschemas just include a type check + + if (context.mode == Mode::FastValidation && track_evaluation && + substeps.size() == 1 && + substeps.front().type == InstructionIndex::AssertionTypeStrict) { + children.push_back(rephrase(context, + sourcemeta::blaze::InstructionIndex:: + AssertionPropertyTypeStrictEvaluate, + substeps.front())); + } else if (context.mode == Mode::FastValidation && track_evaluation && + substeps.size() == 1 && + substeps.front().type == InstructionIndex::AssertionType) { + children.push_back(rephrase( + context, + sourcemeta::blaze::InstructionIndex::AssertionPropertyTypeEvaluate, + substeps.front())); + } else if (context.mode == Mode::FastValidation && track_evaluation && + substeps.size() == 1 && + substeps.front().type == + InstructionIndex::AssertionTypeStrictAny) { + children.push_back(rephrase(context, + sourcemeta::blaze::InstructionIndex:: + AssertionPropertyTypeStrictAnyEvaluate, + substeps.front())); + + // NOLINTBEGIN(bugprone-branch-clone) + } else if (!fusion_possible && context.mode == Mode::FastValidation && + substeps.size() == 1 && + substeps.front().type == + InstructionIndex::AssertionPropertyTypeStrict) { + children.push_back( + unroll(context, substeps.front(), + effective_dynamic_context.base_instance_location)); + } else if (!fusion_possible && context.mode == Mode::FastValidation && + substeps.size() == 1 && + substeps.front().type == + InstructionIndex::AssertionPropertyType) { + children.push_back( + unroll(context, substeps.front(), + effective_dynamic_context.base_instance_location)); + } else if (!fusion_possible && context.mode == Mode::FastValidation && + substeps.size() == 1 && + substeps.front().type == + InstructionIndex::AssertionPropertyTypeStrictAny) { + children.push_back( + unroll(context, substeps.front(), + effective_dynamic_context.base_instance_location)); + // NOLINTEND(bugprone-branch-clone) + + } else { + if (track_evaluation) { + auto new_base_instance_location{ + effective_dynamic_context.base_instance_location}; + new_base_instance_location.push_back({name}); + substeps.push_back( + make(sourcemeta::blaze::InstructionIndex::Evaluate, context, + schema_context, + DynamicContext{ + .keyword = effective_dynamic_context.keyword, + .base_schema_location = + effective_dynamic_context.base_schema_location, + .base_instance_location = new_base_instance_location}, + ValueNone{})); + } + + if (context.mode == Mode::FastValidation && !substeps.empty()) { + if (is_integer_type_bounded_pattern(substeps)) { + auto bounds = extract_integer_bounds(substeps); + const auto index = + has_strict_integer_type(substeps) + ? InstructionIndex::AssertionTypeIntegerBoundedStrict + : InstructionIndex::AssertionTypeIntegerBounded; + auto instance_location = substeps.front().relative_instance_location; + substeps.clear(); + auto fused = make(index, context, schema_context, + relative_dynamic_context(), bounds); + fused.relative_instance_location = std::move(instance_location); + substeps.push_back(std::move(fused)); + } else if (is_integer_type_lower_bound_pattern(substeps)) { + const auto minimum = extract_integer_lower_bound(substeps); + const auto index = + has_strict_integer_type(substeps) + ? InstructionIndex::AssertionTypeIntegerLowerBoundStrict + : InstructionIndex::AssertionTypeIntegerLowerBound; + auto instance_location = substeps.front().relative_instance_location; + substeps.clear(); + auto fused = + make(index, context, schema_context, relative_dynamic_context(), + ValueIntegerBounds{minimum, 0}); + fused.relative_instance_location = std::move(instance_location); + substeps.push_back(std::move(fused)); + } else if (substeps.size() == 2) { + bool has_items_bounded{false}; + bool has_array_type{false}; + std::size_t items_index{0}; + std::size_t array_index{0}; + for (std::size_t step_index = 0; step_index < 2; step_index++) { + if (substeps[step_index].type == + InstructionIndex::LoopItemsIntegerBounded) { + has_items_bounded = true; + items_index = step_index; + } else if (substeps[step_index].type == + InstructionIndex::AssertionTypeArrayBounded) { + has_array_type = true; + array_index = step_index; + } + } + + if (has_items_bounded && has_array_type) { + auto integer_bounds{ + std::get(substeps[items_index].value)}; + auto range{std::get(substeps[array_index].value)}; + auto instance_location = + substeps[items_index].relative_instance_location; + Value fused_value{ + ValueIntegerBoundsWithSize{integer_bounds, std::move(range)}}; + substeps.clear(); + auto fused = + make(InstructionIndex::LoopItemsIntegerBoundedSized, context, + schema_context, effective_dynamic_context, fused_value); + fused.relative_instance_location = std::move(instance_location); + substeps.push_back(std::move(fused)); + } + } + } + + if (fusion_possible && substeps.size() >= 2 && + std::ranges::any_of(substeps, [](const auto &step) { + return step.type == + InstructionIndex::AssertionObjectPropertiesSimple; + })) { + std::erase_if(substeps, [](const auto &step) { + if (step.type == InstructionIndex::AssertionDefinesAllStrict || + step.type == InstructionIndex::AssertionDefinesAll) { + return true; + } + + if ((step.type == InstructionIndex::AssertionTypeStrict || + step.type == InstructionIndex::AssertionType) && + std::get(step.value) == + sourcemeta::core::JSON::Type::Object) { + return true; + } + + return false; + }); + } + + if (fusion_possible && substeps.size() == 1 && + substeps.front().type != InstructionIndex::ControlJump && + substeps.front().type != InstructionIndex::ControlDynamicAnchorJump) { + const auto is_required{assume_object && required.contains(name)}; + auto prop{make_property(name)}; + auto fusion_child{substeps.front()}; + fusion_child.relative_instance_location = {}; + auto fusion_extra{context.extra[fusion_child.extra_index]}; + fusion_extra.relative_schema_location = {}; + fusion_child.extra_index = context.extra.size(); + context.extra.push_back(std::move(fusion_extra)); + + fusion_entries.emplace_back(prop.first, prop.second, is_required); + fusion_children.push_back(std::move(fusion_child)); + } else { + fusion_possible = false; + } + + if (!substeps.empty()) { + // As a performance shortcut + if (effective_dynamic_context.base_instance_location.empty()) { + if (assume_object && required.contains(name)) { + for (auto &&step : substeps) { + children.push_back(std::move(step)); + } + } else { + children.push_back(make(sourcemeta::blaze::InstructionIndex:: + ControlGroupWhenDefinesDirect, + context, schema_context, + effective_dynamic_context, + make_property(name), std::move(substeps))); + } + } else { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlGroupWhenDefines, + context, schema_context, effective_dynamic_context, + make_property(name), std::move(substeps))); + } + } + } + } + + if (context.mode == Mode::FastValidation) { + if (fusion_possible && !fusion_entries.empty()) { + for (const auto &req : required) { + const auto &req_name{req.first}; + bool already_tracked{false}; + for (const auto &entry : fusion_entries) { + if (std::get<0>(entry) == req_name) { + already_tracked = true; + break; + } + } + if (!already_tracked) { + auto prop{make_property(req_name)}; + fusion_entries.emplace_back(prop.first, prop.second, true); + } + } + + if (fusion_entries.size() > 32) { + return children; + } + + return {make(InstructionIndex::AssertionObjectPropertiesSimple, context, + schema_context, dynamic_context, + Value{std::move(fusion_entries)}, + std::move(fusion_children))}; + } + + return children; + } else if (children.empty()) { + return {}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::LogicalWhenType, context, + schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Object, std::move(children))}; + } +} + +auto compiler_draft3_applicator_properties( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions ¤t) + -> Instructions { + auto property_instructions{compiler_draft3_applicator_properties_with_options( + context, schema_context, dynamic_context, current, false, false)}; + + using Known = sourcemeta::core::Vocabularies::Known; + const auto is_draft3{ + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3_Hyper)}; + if (!is_draft3) { + return property_instructions; + } + + ValueStringSet required{required_properties(schema_context)}; + if (required.empty()) { + return property_instructions; + } + + auto required_instructions{compile_required_assertions( + context, schema_context, dynamic_context, current, std::move(required))}; + if (required_instructions.empty()) { + return property_instructions; + } + + Instructions result{std::move(required_instructions)}; + for (auto &&step : property_instructions) { + result.push_back(std::move(step)); + } + return result; +} + +auto compiler_draft3_applicator_patternproperties_with_options( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { + return {}; + } + + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; + } + + Instructions children; + + // To guarantee ordering + std::vector patterns; + for (auto &entry : + schema_context.schema.at(dynamic_context.keyword).as_object()) { + patterns.push_back(entry.first); + } + + std::ranges::sort(patterns); + + // For each regular expression and corresponding subschema in the object + for (const auto &pattern : patterns) { + auto substeps{compile(context, schema_context, relative_dynamic_context(), + sourcemeta::blaze::make_weak_pointer(pattern))}; + + if (annotate) { + substeps.push_back(make( + sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, + context, schema_context, relative_dynamic_context(), ValueNone{})); + } + + if (track_evaluation) { + substeps.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, + schema_context, relative_dynamic_context(), ValuePointer{})); + } + + if (context.mode == Mode::FastValidation && !track_evaluation && + patterns.size() == 1 && + (!schema_context.schema.defines("properties") || + (schema_context.schema.at("properties").is_object() && + schema_context.schema.at("properties").empty())) && + schema_context.schema.defines("additionalProperties") && + schema_context.schema.at("additionalProperties").is_boolean() && + !schema_context.schema.at("additionalProperties").to_boolean()) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::LoopPropertiesRegexClosed, + context, schema_context, dynamic_context, + ValueRegex{.first = parse_regex(pattern, schema_context.base, + schema_context.relative_pointer), + .second = pattern}, + std::move(substeps))); + + // If the `patternProperties` subschema for the given pattern does + // nothing, then we can avoid generating an entire loop for it + } else if (!substeps.empty()) { + const auto maybe_prefix{pattern_as_prefix(pattern)}; + if (maybe_prefix.has_value()) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::LoopPropertiesStartsWith, + context, schema_context, dynamic_context, + ValueString{maybe_prefix.value()}, std::move(substeps))); + } else { + children.push_back(make( + sourcemeta::blaze::InstructionIndex::LoopPropertiesRegex, context, + schema_context, dynamic_context, + ValueRegex{.first = parse_regex(pattern, schema_context.base, + schema_context.relative_pointer), + .second = pattern}, + std::move(substeps))); + } + } + } + + return children; +} + +auto compiler_draft3_applicator_patternproperties( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + return compiler_draft3_applicator_patternproperties_with_options( + context, schema_context, dynamic_context, false, false); +} + +auto compiler_draft3_applicator_additionalproperties_with_options( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; + } + + Instructions children{compile(context, schema_context, + relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; + + if (annotate) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, + context, schema_context, relative_dynamic_context(), ValueNone{})); + } + + ValueStringSet filter_strings; + ValueStrings filter_prefixes; + std::vector filter_regexes; + + if (schema_context.schema.defines("properties") && + schema_context.schema.at("properties").is_object()) { + for (const auto &entry : + schema_context.schema.at("properties").as_object()) { + filter_strings.insert(entry.first); + } + } + + if (schema_context.schema.defines("patternProperties") && + schema_context.schema.at("patternProperties").is_object()) { + for (const auto &entry : + schema_context.schema.at("patternProperties").as_object()) { + const auto maybe_prefix{pattern_as_prefix(entry.first)}; + if (maybe_prefix.has_value()) { + filter_prefixes.push_back(maybe_prefix.value()); + } else { + static const std::string pattern_properties_keyword{ + "patternProperties"}; + filter_regexes.push_back( + {parse_regex(entry.first, schema_context.base, + schema_context.relative_pointer.initial().concat( + sourcemeta::blaze::make_weak_pointer( + pattern_properties_keyword))), + entry.first}); + } + } + } + + // For performance, if a schema sets `additionalProperties: true` (or its + // variants), we don't need to do anything + if (!track_evaluation && children.empty()) { + return {}; + } + + // When `additionalProperties: false` with only `properties` (no + // patternProperties), and `properties` is compiled as a loop + // (LoopPropertiesMatchClosed), that loop already handles rejecting unknown + // properties, so we don't need to emit anything for `additionalProperties` + if (context.mode == Mode::FastValidation && children.size() == 1 && + children.front().type == InstructionIndex::AssertionFail && + !filter_strings.empty() && filter_prefixes.empty() && + filter_regexes.empty() && + properties_as_loop(context, schema_context, + schema_context.schema.at("properties"))) { + return {}; + } + + // When all properties are required and `additionalProperties: false`, + // the `required` keyword compiles to `AssertionDefinesExactly` which already + // checks that the object has exactly the required properties, so we don't + // need to emit anything for `additionalProperties` + if (context.mode == Mode::FastValidation && children.size() == 1 && + children.front().type == InstructionIndex::AssertionFail && + !filter_strings.empty() && filter_prefixes.empty() && + filter_regexes.empty() && + is_closed_properties_required(schema_context.schema, + required_properties(schema_context))) { + return {}; + } + + if (context.mode == Mode::FastValidation && filter_strings.empty() && + filter_prefixes.empty() && filter_regexes.size() == 1 && + !track_evaluation && !children.empty() && + children.front().type == InstructionIndex::AssertionFail) { + return {}; + } + + if (!filter_strings.empty() || !filter_prefixes.empty() || + !filter_regexes.empty()) { + if (track_evaluation) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, + schema_context, relative_dynamic_context(), ValuePointer{})); + } + + return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesExcept, + context, schema_context, dynamic_context, + ValuePropertyFilter{std::move(filter_strings), + std::move(filter_prefixes), + std::move(filter_regexes)}, + std::move(children))}; + } else if (track_evaluation) { + if (children.empty()) { + return {make(sourcemeta::blaze::InstructionIndex::Evaluate, context, + schema_context, dynamic_context, ValueNone{})}; + } + + return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesEvaluate, + context, schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } else if (children.size() == 1 && + children.front().type == InstructionIndex::AssertionFail) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionObjectSizeLess, + context, schema_context, dynamic_context, + ValueUnsignedInteger{1})}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::LoopProperties, context, + schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } +} + +auto compiler_draft3_applicator_additionalproperties( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + return compiler_draft3_applicator_additionalproperties_with_options( + context, schema_context, dynamic_context, false, false); +} + +auto compiler_draft3_validation_pattern(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_string()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "string") { + return {}; + } + + const auto ®ex_string{ + schema_context.schema.at(dynamic_context.keyword).to_string()}; + return { + make(sourcemeta::blaze::InstructionIndex::AssertionRegex, context, + schema_context, dynamic_context, + ValueRegex{.first = parse_regex(regex_string, schema_context.base, + schema_context.relative_pointer), + .second = regex_string})}; +} + +auto compiler_draft3_applicator_items_array( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Instructions { + if (schema_context.is_property_name) { + return {}; + } + + if (!schema_context.schema.at(dynamic_context.keyword).is_array()) { + return {}; + } + + const auto items_size{ + schema_context.schema.at(dynamic_context.keyword).size()}; + if (items_size == 0) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + // Precompile subschemas + std::vector subschemas; + subschemas.reserve(items_size); + const auto &array{ + schema_context.schema.at(dynamic_context.keyword).as_array()}; + for (auto iterator{array.cbegin()}; iterator != array.cend(); ++iterator) { + subschemas.push_back(compile(context, schema_context, + relative_dynamic_context(), + {subschemas.size()}, {subschemas.size()})); + } + + Instructions children; + for (std::size_t cursor = 0; cursor < items_size; cursor++) { + Instructions subchildren; + for (std::size_t index = 0; index < cursor + 1; index++) { + for (const auto &substep : subschemas.at(index)) { + subchildren.push_back(substep); + } + } + + if (annotate) { + subchildren.push_back( + make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, + schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{cursor})); + } + + children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, + context, schema_context, relative_dynamic_context(), + ValueNone{}, std::move(subchildren))); + } + + Instructions tail; + for (const auto &subschema : subschemas) { + for (const auto &substep : subschema) { + tail.push_back(substep); + } + } + + if (annotate) { + tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{children.size() - 1})); + tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{true})); + } + + children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, + context, schema_context, relative_dynamic_context(), + ValueNone{}, std::move(tail))); + + if (track_evaluation) { + return { + make(sourcemeta::blaze::InstructionIndex::AssertionArrayPrefixEvaluate, + context, schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::AssertionArrayPrefix, + context, schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } +} + +auto is_number_type_check(const Instruction &instruction) -> bool { + if (instruction.type != InstructionIndex::AssertionTypeStrictAny) { + return false; + } + + const auto &value{std::get(instruction.value)}; + const auto numeric_count{ + static_cast(value.test( + std::to_underlying(sourcemeta::core::JSON::Type::Integer))) + + static_cast( + value.test(std::to_underlying(sourcemeta::core::JSON::Type::Real))) + + static_cast(value.test( + std::to_underlying(sourcemeta::core::JSON::Type::Decimal)))}; + return numeric_count >= 2 && value.count() == numeric_count; +} + +auto is_integer_bounded_pattern(const Instructions &children) -> bool { + if (children.size() != 3) { + return false; + } + + bool has_type{false}; + bool has_min{false}; + bool has_max{false}; + for (const auto &child : children) { + if (is_number_type_check(child)) { + has_type = true; + } else if (child.type == InstructionIndex::AssertionGreaterEqual) { + if (!std::get(child.value).is_integer()) { + return false; + } + has_min = true; + } else if (child.type == InstructionIndex::AssertionLessEqual) { + if (!std::get(child.value).is_integer()) { + return false; + } + has_max = true; + } + } + + return has_type && has_min && has_max; +} + +auto compiler_draft3_applicator_items_with_options( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + if (is_schema(schema_context.schema.at(dynamic_context.keyword))) { + if (annotate || track_evaluation) { + Instructions subchildren{compile(context, schema_context, + relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; + + Instructions children; + + if (!subchildren.empty()) { + children.push_back(make(sourcemeta::blaze::InstructionIndex::LoopItems, + context, schema_context, dynamic_context, + ValueNone{}, std::move(subchildren))); + } + + if (!annotate && !track_evaluation) { + return children; + } + + Instructions tail; + + if (annotate) { + tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{true})); + } + + if (track_evaluation) { + tail.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, + schema_context, relative_dynamic_context(), ValuePointer{})); + } + + children.push_back( + make(sourcemeta::blaze::InstructionIndex::LogicalWhenType, context, + schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Array, std::move(tail))); + + return children; + } + + Instructions children{compile(context, schema_context, + relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; + if (track_evaluation) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, + schema_context, relative_dynamic_context(), ValuePointer{})); + } + + if (children.empty()) { + return {}; + } + + if (context.mode == Mode::FastValidation && children.size() == 3 && + is_integer_bounded_pattern(children)) { + return {make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded, + context, schema_context, dynamic_context, + extract_integer_bounds(children))}; + } + + if (context.mode == Mode::FastValidation && children.size() == 1) { + if (children.front().type == InstructionIndex::AssertionTypeStrict) { + return {make(sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrict, + context, schema_context, dynamic_context, + children.front().value)}; + } else if (children.front().type == InstructionIndex::AssertionType) { + return {make(sourcemeta::blaze::InstructionIndex::LoopItemsType, + context, schema_context, dynamic_context, + children.front().value)}; + } else if (children.front().type == + InstructionIndex::AssertionTypeStrictAny) { + return {make( + sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrictAny, + context, schema_context, dynamic_context, children.front().value)}; + } else if (children.front().type == + InstructionIndex::LoopPropertiesExactlyTypeStrictHash) { + auto value_copy = children.front().value; + auto current{make(sourcemeta::blaze::InstructionIndex::LoopItems, + context, schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + if (std::get(value_copy).second.first.size() == 3) { + return {Instruction{.type = sourcemeta::blaze::InstructionIndex:: + LoopItemsPropertiesExactlyTypeStrictHash3, + .relative_instance_location = + current.relative_instance_location, + .value = std::move(value_copy), + .children = {}, + .extra_index = current.extra_index}}; + } + + return {Instruction{.type = sourcemeta::blaze::InstructionIndex:: + LoopItemsPropertiesExactlyTypeStrictHash, + .relative_instance_location = + current.relative_instance_location, + .value = std::move(value_copy), + .children = {}, + .extra_index = current.extra_index}}; + } + } + + return {make(sourcemeta::blaze::InstructionIndex::LoopItems, context, + schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } + + return compiler_draft3_applicator_items_array( + context, schema_context, dynamic_context, annotate, track_evaluation); +} + +auto compiler_draft3_applicator_items(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + return compiler_draft3_applicator_items_with_options( + context, schema_context, dynamic_context, false, false); +} + +auto compiler_draft3_applicator_additionalitems_from_cursor( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const std::size_t cursor, + const bool annotate, const bool track_evaluation) -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + Instructions subchildren{compile(context, schema_context, + relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; + + Instructions children; + + if (!subchildren.empty()) { + if (context.mode == Mode::FastValidation && cursor == 0 && !annotate && + !track_evaluation && is_integer_bounded_pattern(subchildren)) { + children.push_back( + make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded, + context, schema_context, dynamic_context, + extract_integer_bounds(subchildren))); + return children; + } + + children.push_back(make(sourcemeta::blaze::InstructionIndex::LoopItemsFrom, + context, schema_context, dynamic_context, + ValueUnsignedInteger{cursor}, + std::move(subchildren))); + } + + // Avoid one extra wrapper instruction if possible + if (!annotate && !track_evaluation) { + return children; + } + + Instructions tail; + + if (annotate) { + tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON{true})); + } + + if (track_evaluation) { + tail.push_back(make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, + context, schema_context, relative_dynamic_context(), + ValuePointer{})); + } + + assert(!tail.empty()); + children.push_back( + make(sourcemeta::blaze::InstructionIndex::LogicalWhenArraySizeGreater, + context, schema_context, dynamic_context, + ValueUnsignedInteger{cursor}, std::move(tail))); + + return children; +} + +auto compiler_draft3_applicator_additionalitems_with_options( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const bool annotate, + const bool track_evaluation) -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + assert(schema_context.schema.is_object()); + + // Nothing to do here + if (!schema_context.schema.defines("items") || + schema_context.schema.at("items").is_object()) { + return {}; + } + + const auto cursor{(schema_context.schema.defines("items") && + schema_context.schema.at("items").is_array()) + ? schema_context.schema.at("items").size() + : 0}; + + return compiler_draft3_applicator_additionalitems_from_cursor( + context, schema_context, dynamic_context, cursor, annotate, + track_evaluation); +} + +auto compiler_draft3_applicator_additionalitems( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + return compiler_draft3_applicator_additionalitems_with_options( + context, schema_context, dynamic_context, false, false); +} + +auto compiler_draft3_validation_enum(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_array()) { + return {}; + } + + if (schema_context.schema.at(dynamic_context.keyword).empty()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + + if (schema_context.schema.at(dynamic_context.keyword).size() == 1) { + return { + make(sourcemeta::blaze::InstructionIndex::AssertionEqual, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword).front()})}; + } + + std::vector> + perfect_string_hashes; + ValueSet options; + sourcemeta::core::PropertyHashJSON hasher; + for (const auto &option : + schema_context.schema.at(dynamic_context.keyword).as_array()) { + if (option.is_string()) { + const auto hash{hasher(option.to_string())}; + if (hasher.is_perfect(hash)) { + perfect_string_hashes.emplace_back(option.to_string(), hash); + } + } + + options.insert(option); + } + + // Only apply this optimisation on fast validation, as it + // can affect error messages + if (context.mode == Mode::FastValidation && + perfect_string_hashes.size() == options.size()) { + return { + make(sourcemeta::blaze::InstructionIndex::AssertionEqualsAnyStringHash, + context, schema_context, dynamic_context, + to_string_hashes(perfect_string_hashes))}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionEqualsAny, context, + schema_context, dynamic_context, std::move(options))}; +} + +auto compiler_draft3_validation_uniqueitems( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_boolean() || + !schema_context.schema.at(dynamic_context.keyword).to_boolean()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionUnique, context, + schema_context, dynamic_context, ValueNone{})}; +} + +auto compiler_draft3_validation_maxlength(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { + return {}; + } + + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "string") { + return {}; + } + + // We'll handle it at the type level as an optimization + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "string") { + return {}; + } + + return {make( + sourcemeta::blaze::InstructionIndex::AssertionStringSizeLess, context, + schema_context, dynamic_context, + ValueUnsignedInteger{ + static_cast( + schema_context.schema.at(dynamic_context.keyword).as_integer()) + + 1})}; +} + +auto compiler_draft3_validation_minlength(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { + return {}; + } + + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "string") { + return {}; + } + + // We'll handle it at the type level as an optimization + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "string") { + return {}; + } + + const auto value{ + schema_context.schema.at(dynamic_context.keyword).as_integer()}; + if (value <= 0) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionStringSizeGreater, + context, schema_context, dynamic_context, + ValueUnsignedInteger{static_cast(value - 1)})}; +} + +auto compiler_draft3_validation_maxitems(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { + return {}; + } + + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + // We'll handle it at the type level as an optimization + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "array") { + return {}; + } + + return {make( + sourcemeta::blaze::InstructionIndex::AssertionArraySizeLess, context, + schema_context, dynamic_context, + ValueUnsignedInteger{ + static_cast( + schema_context.schema.at(dynamic_context.keyword).as_integer()) + + 1})}; +} + +auto compiler_draft3_validation_minitems(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { + return {}; + } + + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "array") { + return {}; + } + + // We'll handle it at the type level as an optimization + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "array") { + return {}; + } + + const auto value{ + schema_context.schema.at(dynamic_context.keyword).as_integer()}; + if (value <= 0) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionArraySizeGreater, + context, schema_context, dynamic_context, + ValueUnsignedInteger{static_cast(value - 1)})}; +} + +auto compiler_draft3_validation_maximum(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "integer" && + schema_context.schema.at("type").to_string() != "number") { + return {}; + } + + // TODO: As an optimization, if `minimum` is set to the same number, do + // a single equality assertion + + assert(schema_context.schema.is_object()); + if (schema_context.schema.defines("exclusiveMaximum") && + schema_context.schema.at("exclusiveMaximum").is_boolean() && + schema_context.schema.at("exclusiveMaximum").to_boolean()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionLess, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::AssertionLessEqual, + context, schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})}; + } +} + +auto compiler_draft3_validation_minimum(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { + return {}; + } + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "integer" && + schema_context.schema.at("type").to_string() != "number") { + return {}; + } + + // TODO: As an optimization, if `maximum` is set to the same number, do + // a single equality assertion + + assert(schema_context.schema.is_object()); + if (schema_context.schema.defines("exclusiveMinimum") && + schema_context.schema.at("exclusiveMinimum").is_boolean() && + schema_context.schema.at("exclusiveMinimum").to_boolean()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionGreater, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::AssertionGreaterEqual, + context, schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})}; + } +} + +auto compiler_draft3_validation_type(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + const auto &value{schema_context.schema.at(dynamic_context.keyword)}; + + using Known = sourcemeta::core::Vocabularies::Known; + const auto is_draft3{ + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3_Hyper)}; + + if (is_draft3) { + if (value.is_string() && value.to_string() == "any") { + return draft3_any_type_instructions(context, schema_context, + dynamic_context); + } + + if (value.is_array()) { + bool has_object{false}; + for (const auto &element : value.as_array()) { + if (element.is_string() && element.to_string() == "any") { + return draft3_any_type_instructions(context, schema_context, + dynamic_context); + } + if (element.is_object()) { + has_object = true; + } + } + + if (has_object) { + if (context.mode == Mode::FastValidation && value.size() == 1) { + return compile( + context, schema_context, dynamic_context, + {static_cast(0)}); + } + + Instructions disjunctors; + for (std::uint64_t index = 0; index < value.size(); index++) { + const auto &element{value.at(index)}; + Instructions branch; + + if (element.is_object()) { + branch = compile( + context, schema_context, relative_dynamic_context(), + {static_cast(index)}); + } else if (element.is_string()) { + const auto &type_string{element.to_string()}; + if (type_string == "null") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::Null)); + } else if (type_string == "boolean") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::Boolean)); + } else if (type_string == "object") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::Object)); + } else if (type_string == "array") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::Array)); + } else if (type_string == "number") { + ValueTypes types{}; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set( + std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set( + std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + branch.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, relative_dynamic_context(), types)); + } else if (type_string == "integer") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::Integer)); + } else if (type_string == "string") { + branch.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, relative_dynamic_context(), + sourcemeta::core::JSON::Type::String)); + } else { + continue; + } + } else { + continue; + } + + disjunctors.push_back( + make(sourcemeta::blaze::InstructionIndex::ControlGroup, context, + schema_context, relative_dynamic_context(), ValueNone{}, + std::move(branch))); + } + + return {make(sourcemeta::blaze::InstructionIndex::LogicalOr, context, + schema_context, dynamic_context, ValueBoolean{false}, + std::move(disjunctors))}; + } + } + } + + if (value.is_string()) { + const auto &type{value.to_string()}; + if (type == "null") { + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_null(); })) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Null)}; + } else if (type == "boolean") { + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_boolean(); })) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Boolean)}; + } else if (type == "object") { + if (!is_draft3) { + const auto minimum{unsigned_integer_property(schema_context.schema, + "minProperties", 0)}; + const auto maximum{ + unsigned_integer_property(schema_context.schema, "maxProperties")}; + + if (context.mode == Mode::FastValidation) { + if (maximum.has_value() && minimum == 0) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionTypeObjectUpper, + context, schema_context, dynamic_context, + ValueUnsignedInteger{maximum.value()})}; + } else if (minimum > 0 || maximum.has_value()) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionTypeObjectBounded, + context, schema_context, dynamic_context, + ValueRange{minimum, maximum, false})}; + } + } + } + + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_object(); })) { + return {}; + } + + if (!is_draft3 && context.mode == Mode::FastValidation && + schema_context.schema.defines("required")) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Object)}; + } else if (type == "array") { + const auto minimum{ + unsigned_integer_property(schema_context.schema, "minItems", 0)}; + const auto maximum{ + unsigned_integer_property(schema_context.schema, "maxItems")}; + + if (context.mode == Mode::FastValidation) { + if (maximum.has_value() && minimum == 0) { + return { + make(sourcemeta::blaze::InstructionIndex::AssertionTypeArrayUpper, + context, schema_context, dynamic_context, + ValueUnsignedInteger{maximum.value()})}; + } else if (minimum > 0 || maximum.has_value()) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionTypeArrayBounded, + context, schema_context, dynamic_context, + ValueRange{minimum, maximum, false})}; + } + } + + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_array(); })) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Array)}; + } else if (type == "number") { + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_number(); })) { + return {}; + } + + ValueTypes types{}; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } else if (type == "integer") { + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_integer(); })) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Integer)}; + } else if (type == "string") { + const auto minimum{ + unsigned_integer_property(schema_context.schema, "minLength", 0)}; + const auto maximum{ + unsigned_integer_property(schema_context.schema, "maxLength")}; + + if (context.mode == Mode::FastValidation) { + if (maximum.has_value() && minimum == 0) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionTypeStringUpper, + context, schema_context, dynamic_context, + ValueUnsignedInteger{maximum.value()})}; + } else if (minimum > 0 || maximum.has_value()) { + return {make( + sourcemeta::blaze::InstructionIndex::AssertionTypeStringBounded, + context, schema_context, dynamic_context, + ValueRange{minimum, maximum, false})}; + } + } + + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("enum") && + schema_context.schema.at("enum").is_array() && + std::ranges::all_of( + schema_context.schema.at("enum").as_array(), + [](const auto &candidate) { return candidate.is_string(); })) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::String)}; + } else { + return {}; + } + } else if (value.is_array() && value.size() == 1 && + value.front().is_string()) { + const auto &type{value.front().to_string()}; + if (type == "null") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Null)}; + } else if (type == "boolean") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Boolean)}; + } else if (type == "object") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Object)}; + } else if (type == "array") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Array)}; + } else if (type == "number") { + ValueTypes types{}; + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } else if (type == "integer") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::Integer)}; + } else if (type == "string") { + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, + context, schema_context, dynamic_context, + sourcemeta::core::JSON::Type::String)}; + } else { + return {}; + } + } else if (value.is_array()) { + ValueTypes types{}; + for (const auto &element : value.as_array()) { + assert(element.is_string()); + const auto &type_string{element.to_string()}; + if (type_string == "null") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); + } else if (type_string == "boolean") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); + } else if (type_string == "object") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); + } else if (type_string == "array") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); + } else if (type_string == "number") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + } else if (type_string == "integer") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + } else if (type_string == "string") { + types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); + } + } + + if (!types.any()) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, + context, schema_context, dynamic_context, types)}; + } + + return {}; +} + +auto compiler_draft3_validation_disallow(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + const auto &value{schema_context.schema.at(dynamic_context.keyword)}; + + const auto contains_any{ + (value.is_string() && value.to_string() == "any") || + (value.is_array() && + std::ranges::any_of(value.as_array(), [](const auto &element) { + return element.is_string() && element.to_string() == "any"; + }))}; + if (contains_any) { + return {make(sourcemeta::blaze::InstructionIndex::AssertionFail, context, + schema_context, dynamic_context, ValueNone{})}; + } + + ValueTypes types{}; + Instructions subschema_nots; + + if (value.is_string()) { + draft3_set_type_bits(value.to_string(), types); + } else if (value.is_array()) { + for (std::uint64_t index = 0; index < value.size(); index++) { + const auto &element{value.at(index)}; + if (element.is_string()) { + draft3_set_type_bits(element.to_string(), types); + } else if (element.is_object()) { + sourcemeta::core::WeakPointer index_suffix; + index_suffix.push_back(index); + const auto element_uri{ + sourcemeta::core::to_uri( + schema_context.relative_pointer.concat(index_suffix), + schema_context.base) + .canonicalize() + .recompose()}; + + auto inner_instructions{ + compile(context, schema_context, relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer, element_uri)}; + + const auto element_relative_pointer{ + schema_context.relative_pointer.concat(index_suffix)}; + const SchemaContext element_schema_context{ + .relative_pointer = element_relative_pointer, + .schema = schema_context.schema, + .vocabularies = schema_context.vocabularies, + .base = schema_context.base, + .is_property_name = schema_context.is_property_name}; + + const auto element_base_schema_location{ + sourcemeta::blaze::make_weak_pointer(dynamic_context.keyword, + index)}; + const DynamicContext element_dynamic_context{ + .keyword = KEYWORD_EMPTY, + .base_schema_location = element_base_schema_location, + .base_instance_location = dynamic_context.base_instance_location}; + + subschema_nots.push_back( + make(sourcemeta::blaze::InstructionIndex::LogicalNot, context, + element_schema_context, element_dynamic_context, ValueNone{}, + std::move(inner_instructions))); + } + } + } + + Instructions result; + if (types.any()) { + result.push_back( + make(sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny, + context, schema_context, dynamic_context, types)); + } + for (auto &&instruction : subschema_nots) { + result.push_back(std::move(instruction)); + } + return result; +} + +auto compiler_draft3_applicator_extends(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + assert(!context.uses_dynamic_scopes); + + const auto &value{schema_context.schema.at(dynamic_context.keyword)}; + + if (value.is_object()) { + if (context.mode == Mode::FastValidation) { + return compile(context, schema_context, dynamic_context, + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer); + } + + auto inner{compile(context, schema_context, relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; + if (inner.empty()) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::LogicalAnd, context, + schema_context, dynamic_context, ValueNone{}, + std::move(inner))}; + } + + if (!value.is_array()) { + return {}; + } + + if (value.empty()) { + return {}; + } + + Instructions children; + + if (context.mode == Mode::FastValidation) { + for (std::uint64_t index = 0; index < value.size(); index++) { + for (auto &&step : compile( + context, schema_context, dynamic_context, + {static_cast(index)})) { + children.push_back(std::move(step)); + } + } + + return children; + } + + for (std::uint64_t index = 0; index < value.size(); index++) { + auto arm{ + compile(context, schema_context, relative_dynamic_context(), + {static_cast(index)})}; + if (arm.empty()) { + continue; + } + children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, + context, schema_context, relative_dynamic_context(), + ValueNone{}, std::move(arm))); + } + + if (children.empty()) { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::LogicalAnd, context, + schema_context, dynamic_context, ValueNone{}, + std::move(children))}; +} + +auto compiler_draft3_applicator_dependencies( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; + } + + if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { + return {}; + } + + using Known = sourcemeta::core::Vocabularies::Known; + const auto is_draft3{ + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3) || + schema_context.vocabularies.contains(Known::JSON_Schema_Draft_3_Hyper)}; + + Instructions children; + ValueStringMap dependencies; + + for (const auto &entry : + schema_context.schema.at(dynamic_context.keyword).as_object()) { + if (is_schema(entry.second)) { + if (!entry.second.is_boolean() || !entry.second.to_boolean()) { + children.push_back(make( + sourcemeta::blaze::InstructionIndex::LogicalWhenDefines, context, + schema_context, dynamic_context, make_property(entry.first), + compile(context, schema_context, relative_dynamic_context(), + sourcemeta::blaze::make_weak_pointer(entry.first)))); + } + } else if (entry.second.is_array()) { + std::vector properties; + for (const auto &property : entry.second.as_array()) { + assert(property.is_string()); + properties.push_back(property.to_string()); + } + + if (!properties.empty()) { + dependencies.emplace(entry.first, properties); + } + } else if (is_draft3 && entry.second.is_string()) { + dependencies.emplace(entry.first, + std::vector{ + entry.second.to_string()}); + } + } + + if (!dependencies.empty()) { + children.push_back(make( + sourcemeta::blaze::InstructionIndex::AssertionPropertyDependencies, + context, schema_context, dynamic_context, std::move(dependencies))); + } + + return children; +} + +auto compiler_draft3_validation_divisibleby( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { + return {}; + } + + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); + + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "integer" && + schema_context.schema.at("type").to_string() != "number") { + return {}; + } + + return {make(sourcemeta::blaze::InstructionIndex::AssertionDivisible, context, + schema_context, dynamic_context, + sourcemeta::core::JSON{ + schema_context.schema.at(dynamic_context.keyword)})}; +} + +} // namespace internal +#endif diff --git a/vendor/blaze/src/compiler/default_compiler_draft4.h b/vendor/blaze/src/compiler/default_compiler_draft4.h index be9e251c7..08f9a5517 100644 --- a/vendor/blaze/src/compiler/default_compiler_draft4.h +++ b/vendor/blaze/src/compiler/default_compiler_draft4.h @@ -4,446 +4,17 @@ #include #include -#include - -#include // std::sort, std::ranges::any_of, std::ranges::all_of, std::find_if, std::ranges::none_of -#include // assert -#include // std::set -#include // std::move, std::to_underlying +#include // std::ranges::any_of, std::ranges::all_of, std::ranges::none_of, std::find_if +#include // assert +#include // std::set +#include // std::move, std::to_underlying #include "compile_helpers.h" - -static auto parse_regex(const std::string &pattern, - const sourcemeta::core::URI &base, - const sourcemeta::core::WeakPointer &schema_location) - -> sourcemeta::core::Regex { - const auto result{sourcemeta::core::to_regex(pattern)}; - if (!result.has_value()) [[unlikely]] { - throw sourcemeta::blaze::CompilerInvalidRegexError( - base, to_pointer(schema_location), pattern); - } - - return result.value(); -} - -static auto -relative_schema_location_size(const sourcemeta::blaze::Context &context, - const sourcemeta::blaze::Instruction &step) - -> std::size_t { - return context.extra[step.extra_index].relative_schema_location.size(); -} - -static auto -defines_direct_enumeration(const sourcemeta::blaze::Instructions &steps) - -> std::optional { - const auto iterator{std::ranges::find_if(steps, [](const auto &step) { - return step.type == sourcemeta::blaze::InstructionIndex::AssertionEqual || - step.type == sourcemeta::blaze::InstructionIndex::AssertionEqualsAny; - })}; - - if (iterator == steps.cend()) { - return std::nullopt; - } - - return std::distance(steps.cbegin(), iterator); -} - -static auto is_inside_disjunctor(const sourcemeta::core::WeakPointer &pointer) - -> bool { - return pointer.size() > 2 && pointer.at(pointer.size() - 2).is_index() && - pointer.at(pointer.size() - 3).is_property() && - (pointer.at(pointer.size() - 3).to_property() == "oneOf" || - pointer.at(pointer.size() - 3).to_property() == "anyOf"); -} - -static auto json_array_to_string_set(const sourcemeta::core::JSON &document) - -> sourcemeta::blaze::ValueStringSet { - sourcemeta::blaze::ValueStringSet result; - for (const auto &value : document.as_array()) { - assert(value.is_string()); - result.insert(value.to_string()); - } - - return result; -} - -static auto -is_closed_properties_required(const sourcemeta::core::JSON &schema, - const sourcemeta::blaze::ValueStringSet &required) - -> bool { - return !schema.defines("patternProperties") && - schema.defines("additionalProperties") && - schema.at("additionalProperties").is_boolean() && - !schema.at("additionalProperties").to_boolean() && - schema.defines("properties") && schema.at("properties").is_object() && - schema.at("properties").size() == required.size() && - std::ranges::all_of(required, [&schema](const auto &property) { - return schema.at("properties") - .defines(property.first, property.second); - }); -} - -static auto -compile_properties(const sourcemeta::blaze::Context &context, - const sourcemeta::blaze::SchemaContext &schema_context, - const sourcemeta::blaze::DynamicContext &dynamic_context, - const sourcemeta::blaze::Instructions &) - -> std::vector> { - std::vector> - properties; - for (const auto &entry : schema_context.schema.at("properties").as_object()) { - properties.emplace_back( - entry.first, - compile(context, schema_context, dynamic_context, - sourcemeta::blaze::make_weak_pointer(entry.first), - sourcemeta::blaze::make_weak_pointer(entry.first))); - } - - // In many cases, `properties` have some subschemas that are small - // and some subschemas that are large. To attempt to improve performance, - // we prefer to evaluate smaller subschemas first, in the hope of failing - // earlier without spending a lot of time on other subschemas - if (context.tweaks.properties_reorder) { - std::ranges::sort(properties, [&context](const auto &left, - const auto &right) { - const auto left_size{recursive_template_size(left.second)}; - const auto right_size{recursive_template_size(right.second)}; - if (left_size == right_size) { - const auto left_direct_enumeration{ - defines_direct_enumeration(left.second)}; - const auto right_direct_enumeration{ - defines_direct_enumeration(right.second)}; - - // Enumerations always take precedence - if (left_direct_enumeration.has_value() && - right_direct_enumeration.has_value()) { - // If both options have a direct enumeration, we choose - // the one with the shorter relative schema location - return relative_schema_location_size( - context, left.second.at(left_direct_enumeration.value())) < - relative_schema_location_size( - context, - right.second.at(right_direct_enumeration.value())); - } else if (left_direct_enumeration.has_value()) { - return true; - } else if (right_direct_enumeration.has_value()) { - return false; - } - - return left.first < right.first; - } else { - return left_size < right_size; - } - }); - } - - return properties; -} - -static auto to_string_hashes( - std::vector> - &hashes) -> sourcemeta::blaze::ValueStringHashes { - assert(!hashes.empty()); - std::ranges::sort(hashes, [](const auto &left, const auto &right) { - return left.first.size() < right.first.size(); - }); - - sourcemeta::blaze::ValueStringHashes result; - // The idea with the table of contents is as follows: each index - // marks the starting and end positions for a string where the size - // is equal to the index. - result.second.resize(hashes.back().first.size() + 1, std::make_pair(0, 0)); - // TODO(C++23): Use std::views::enumerate when available in libc++ - for (std::size_t index = 0; index < hashes.size(); index++) { - result.first.emplace_back(hashes[index].second, hashes[index].first); - const auto string_size{hashes[index].first.size()}; - // We leave index 0 to represent the empty string - const auto position{index + 1}; - const auto lower_bound{ - result.second[string_size].first == 0 - ? position - : std::min(result.second[string_size].first, position)}; - const auto upper_bound{ - result.second[string_size].second == 0 - ? position - : std::max(result.second[string_size].second, position)}; - assert(lower_bound <= upper_bound); - assert(lower_bound > 0 && upper_bound > 0); - assert(string_size < result.second.size()); - result.second[string_size] = std::make_pair(lower_bound, upper_bound); - } - - assert(result.second.size() == hashes.back().first.size() + 1); - return result; -} +#include "default_compiler_draft3.h" namespace internal { using namespace sourcemeta::blaze; -auto compiler_draft4_core_ref(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - const auto &entry{static_frame_entry(context, schema_context)}; - const auto type{sourcemeta::core::SchemaReferenceType::Static}; - const auto reference{context.frame.reference(type, entry.pointer)}; - if (!reference.has_value()) [[unlikely]] { - throw sourcemeta::core::SchemaReferenceError( - schema_context.schema.at(dynamic_context.keyword).to_string(), - to_pointer(schema_context.relative_pointer), - "Could not resolve schema reference"); - } - - const auto key{std::make_tuple(type, - std::string_view{reference->get().destination}, - schema_context.is_property_name)}; - assert(context.targets.contains(key)); - return {make(sourcemeta::blaze::InstructionIndex::ControlJump, context, - schema_context, dynamic_context, - ValueUnsignedInteger{context.targets.at(key).first})}; -} - -auto compiler_draft4_validation_type(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (schema_context.schema.at(dynamic_context.keyword).is_string()) { - const auto &type{ - schema_context.schema.at(dynamic_context.keyword).to_string()}; - if (type == "null") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_null(); })) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Null)}; - } else if (type == "boolean") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_boolean(); })) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Boolean)}; - } else if (type == "object") { - const auto minimum{ - unsigned_integer_property(schema_context.schema, "minProperties", 0)}; - const auto maximum{ - unsigned_integer_property(schema_context.schema, "maxProperties")}; - - if (context.mode == Mode::FastValidation) { - if (maximum.has_value() && minimum == 0) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionTypeObjectUpper, - context, schema_context, dynamic_context, - ValueUnsignedInteger{maximum.value()})}; - } else if (minimum > 0 || maximum.has_value()) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionTypeObjectBounded, - context, schema_context, dynamic_context, - ValueRange{minimum, maximum, false})}; - } - } - - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_object(); })) { - return {}; - } - - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("required")) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Object)}; - } else if (type == "array") { - const auto minimum{ - unsigned_integer_property(schema_context.schema, "minItems", 0)}; - const auto maximum{ - unsigned_integer_property(schema_context.schema, "maxItems")}; - - if (context.mode == Mode::FastValidation) { - if (maximum.has_value() && minimum == 0) { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionTypeArrayUpper, - context, schema_context, dynamic_context, - ValueUnsignedInteger{maximum.value()})}; - } else if (minimum > 0 || maximum.has_value()) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionTypeArrayBounded, - context, schema_context, dynamic_context, - ValueRange{minimum, maximum, false})}; - } - } - - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_array(); })) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Array)}; - } else if (type == "number") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_number(); })) { - return {}; - } - - ValueTypes types{}; - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, - context, schema_context, dynamic_context, types)}; - } else if (type == "integer") { - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_integer(); })) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Integer)}; - } else if (type == "string") { - const auto minimum{ - unsigned_integer_property(schema_context.schema, "minLength", 0)}; - const auto maximum{ - unsigned_integer_property(schema_context.schema, "maxLength")}; - - if (context.mode == Mode::FastValidation) { - if (maximum.has_value() && minimum == 0) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionTypeStringUpper, - context, schema_context, dynamic_context, - ValueUnsignedInteger{maximum.value()})}; - } else if (minimum > 0 || maximum.has_value()) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionTypeStringBounded, - context, schema_context, dynamic_context, - ValueRange{minimum, maximum, false})}; - } - } - - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("enum") && - schema_context.schema.at("enum").is_array() && - std::ranges::all_of( - schema_context.schema.at("enum").as_array(), - [](const auto &value) { return value.is_string(); })) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::String)}; - } else { - return {}; - } - } else if (schema_context.schema.at(dynamic_context.keyword).is_array() && - schema_context.schema.at(dynamic_context.keyword).size() == 1 && - schema_context.schema.at(dynamic_context.keyword) - .front() - .is_string()) { - const auto &type{ - schema_context.schema.at(dynamic_context.keyword).front().to_string()}; - if (type == "null") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Null)}; - } else if (type == "boolean") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Boolean)}; - } else if (type == "object") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Object)}; - } else if (type == "array") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Array)}; - } else if (type == "number") { - ValueTypes types{}; - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, - context, schema_context, dynamic_context, types)}; - } else if (type == "integer") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Integer)}; - } else if (type == "string") { - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrict, - context, schema_context, dynamic_context, - sourcemeta::core::JSON::Type::String)}; - } else { - return {}; - } - } else if (schema_context.schema.at(dynamic_context.keyword).is_array()) { - ValueTypes types{}; - for (const auto &type : - schema_context.schema.at(dynamic_context.keyword).as_array()) { - assert(type.is_string()); - const auto &type_string{type.to_string()}; - if (type_string == "null") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Null)); - } else if (type_string == "boolean") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Boolean)); - } else if (type_string == "object") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Object)); - } else if (type_string == "array") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Array)); - } else if (type_string == "number") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Real)); - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); - } else if (type_string == "integer") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); - } else if (type_string == "string") { - types.set(std::to_underlying(sourcemeta::core::JSON::Type::String)); - } - } - - assert(types.any()); - return {make(sourcemeta::blaze::InstructionIndex::AssertionTypeStrictAny, - context, schema_context, dynamic_context, types)}; - } - - return {}; -} - auto compiler_draft4_validation_required(const Context &context, const SchemaContext &schema_context, const DynamicContext &dynamic_context, @@ -453,121 +24,10 @@ auto compiler_draft4_validation_required(const Context &context, return {}; } - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; - - if (schema_context.schema.at(dynamic_context.keyword).empty()) { - return {}; - } else if (schema_context.schema.at(dynamic_context.keyword).size() > 1) { - ValueStringSet properties_set{json_array_to_string_set( - schema_context.schema.at(dynamic_context.keyword))}; - if (properties_set.size() == 1) { - if (assume_object) { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionDefinesStrict, - context, schema_context, dynamic_context, - make_property(properties_set.begin()->first))}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::AssertionDefines, - context, schema_context, dynamic_context, - make_property(properties_set.begin()->first))}; - } - } else if (is_closed_properties_required(schema_context.schema, - properties_set)) { - if (context.mode == Mode::FastValidation && assume_object) { - static const std::string properties_keyword{"properties"}; - const SchemaContext new_schema_context{ - .relative_pointer = - schema_context.relative_pointer.initial().concat( - sourcemeta::blaze::make_weak_pointer(properties_keyword)), - .schema = schema_context.schema, - .vocabularies = schema_context.vocabularies, - .base = schema_context.base, - .is_property_name = schema_context.is_property_name}; - const DynamicContext new_dynamic_context{ - .keyword = KEYWORD_PROPERTIES, - .base_schema_location = sourcemeta::core::empty_weak_pointer, - .base_instance_location = sourcemeta::core::empty_weak_pointer}; - auto properties{compile_properties(context, new_schema_context, - new_dynamic_context, current)}; - if (std::ranges::all_of(properties, [](const auto &property) { - return property.second.size() == 1 && - property.second.front().type == - InstructionIndex::AssertionTypeStrict; - })) { - std::set types; - for (const auto &property : properties) { - types.insert(std::get(property.second.front().value)); - } - - if (types.size() == 1) { - // Handled in `properties` - return {}; - } - } - - sourcemeta::core::PropertyHashJSON hasher; - if (context.mode == Mode::FastValidation && - properties_set.size() == 3 && - std::ranges::all_of(properties_set, - [&hasher](const auto &property) { - return hasher.is_perfect(property.second); - })) { - std::vector> hashes; - for (const auto &property : properties_set) { - hashes.emplace_back(property.first, property.second); - } - - return {make(sourcemeta::blaze::InstructionIndex:: - AssertionDefinesExactlyStrictHash3, - context, schema_context, dynamic_context, - to_string_hashes(hashes))}; - } - - return {make( - sourcemeta::blaze::InstructionIndex::AssertionDefinesExactlyStrict, - context, schema_context, dynamic_context, - std::move(properties_set))}; - } else { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionDefinesExactly, - context, schema_context, dynamic_context, - std::move(properties_set))}; - } - } else if (assume_object) { - return {make( - sourcemeta::blaze::InstructionIndex::AssertionDefinesAllStrict, - context, schema_context, dynamic_context, std::move(properties_set))}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::AssertionDefinesAll, - context, schema_context, dynamic_context, - std::move(properties_set))}; - } - } else if (assume_object) { - assert( - schema_context.schema.at(dynamic_context.keyword).front().is_string()); - return {make(sourcemeta::blaze::InstructionIndex::AssertionDefinesStrict, - context, schema_context, dynamic_context, - make_property(schema_context.schema.at(dynamic_context.keyword) - .front() - .to_string()))}; - } else { - assert( - schema_context.schema.at(dynamic_context.keyword).front().is_string()); - return {make(sourcemeta::blaze::InstructionIndex::AssertionDefines, context, - schema_context, dynamic_context, - make_property(schema_context.schema.at(dynamic_context.keyword) - .front() - .to_string()))}; - } + return compile_required_assertions( + context, schema_context, dynamic_context, current, + json_array_to_string_set( + schema_context.schema.at(dynamic_context.keyword))); } auto compiler_draft4_applicator_allof(const Context &context, @@ -718,197 +178,90 @@ auto compiler_draft4_applicator_oneof(const Context &context, ValueBoolean{requires_exhaustive}, std::move(disjunctors))}; } -// There are two ways to compile `properties` depending on whether -// most of the properties are marked as required using `required` -// or whether most of the properties are optional. Each shines -// in the corresponding case. -auto properties_as_loop(const Context &context, - const SchemaContext &schema_context, - const sourcemeta::core::JSON &properties) -> bool { - if (context.tweaks.properties_always_unroll) { - return false; - } - - using Known = sourcemeta::core::Vocabularies::Known; - const auto size{properties.size()}; - const auto imports_validation_vocabulary = - schema_context.vocabularies.contains(Known::JSON_Schema_Draft_4) || - schema_context.vocabularies.contains(Known::JSON_Schema_Draft_6) || - schema_context.vocabularies.contains(Known::JSON_Schema_Draft_7) || - schema_context.vocabularies.contains( - Known::JSON_Schema_2019_09_Validation) || - schema_context.vocabularies.contains( - Known::JSON_Schema_2020_12_Validation); - const auto imports_const = - schema_context.vocabularies.contains(Known::JSON_Schema_Draft_6) || - schema_context.vocabularies.contains(Known::JSON_Schema_Draft_7) || - schema_context.vocabularies.contains( - Known::JSON_Schema_2019_09_Validation) || - schema_context.vocabularies.contains( - Known::JSON_Schema_2020_12_Validation); - std::set required; - if (imports_validation_vocabulary && - schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array()) { - for (const auto &property : - schema_context.schema.at("required").as_array()) { - if (property.is_string() && - // Only count the required property if its indeed in "properties" - properties.defines(property.to_string())) { - required.insert(property.to_string()); - } +auto compiler_draft4_applicator_not(const Context &context, + const SchemaContext &schema_context, + const DynamicContext &dynamic_context, + const Instructions &) -> Instructions { + std::size_t subschemas{0}; + for (const auto &subschema : + walk_subschemas(context, schema_context, dynamic_context)) { + if (subschema.pointer.empty()) { + continue; } - } - const auto ¤t_entry{static_frame_entry(context, schema_context)}; - const auto inside_disjunctor{ - is_inside_disjunctor(schema_context.relative_pointer) || - // Check if any reference from `anyOf` or `oneOf` points to us - std::ranges::any_of( - context.frame.references(), - [&context, ¤t_entry](const auto &reference) { - if (!context.frame.locations().contains( - {sourcemeta::core::SchemaReferenceType::Static, - reference.second.destination})) { - return false; - } - - const auto &target{ - context.frame.locations() - .at({sourcemeta::core::SchemaReferenceType::Static, - reference.second.destination}) - .pointer}; - return is_inside_disjunctor(reference.first.second) && - current_entry.pointer.initial() == target; - })}; - - if (!inside_disjunctor && - schema_context.schema.defines("additionalProperties") && - schema_context.schema.at("additionalProperties").is_boolean() && - !schema_context.schema.at("additionalProperties").to_boolean() && - // If all properties are required, we should still unroll - required.size() < size) { - return true; - } - - return - // This strategy only makes sense if most of the properties are "optional" - required.size() <= (size / 4) && - // If `properties` only defines a relatively small amount of properties, - // then its probably still faster to unroll - size > 5 && - // Always unroll inside `oneOf` or `anyOf`, to have a - // better chance at quickly short-circuiting - (!inside_disjunctor || - std::ranges::none_of(properties.as_object(), [&](const auto &pair) { - return pair.second.is_object() && - ((imports_validation_vocabulary && - pair.second.defines("enum")) || - (imports_const && pair.second.defines("const"))); - })); -} - -auto is_integer_type_check(const Instruction &instruction) -> bool { - return (instruction.type == InstructionIndex::AssertionType || - instruction.type == InstructionIndex::AssertionTypeStrict) && - std::get(instruction.value) == - sourcemeta::core::JSON::Type::Integer; -} - -auto has_strict_integer_type(const Instructions &children) -> bool { - for (const auto &child : children) { - if (child.type == InstructionIndex::AssertionTypeStrict && - std::get(child.value) == - sourcemeta::core::JSON::Type::Integer) { - return true; - } + subschemas += 1; } - return false; -} + Instructions children{compile(context, schema_context, + relative_dynamic_context(), + sourcemeta::core::empty_weak_pointer, + sourcemeta::core::empty_weak_pointer)}; -auto is_integer_type_bounded_pattern(const Instructions &children) -> bool { - if (children.size() != 3) { - return false; - } + // TODO: Be smarter about how we treat `unevaluatedItems` like how we do for + // `unevaluatedProperties` + const bool track_items{ + std::ranges::any_of(context.unevaluated, [](const auto &dependency) { + return dependency.first.ends_with("unevaluatedItems"); + })}; - bool has_type{false}; - bool has_min{false}; - bool has_max{false}; - for (const auto &child : children) { - if (is_integer_type_check(child)) { - has_type = true; - } else if (child.type == InstructionIndex::AssertionGreaterEqual && - std::get(child.value).is_integer()) { - has_min = true; - } else if (child.type == InstructionIndex::AssertionLessEqual && - std::get(child.value).is_integer()) { - has_max = true; - } + // Only emit a `not` instruction that keeps track of + // evaluation if we really need it. If the "not" subschema + // does not define applicators, then that's an easy case + // we can skip + if (subschemas > 0 && + (requires_evaluation(context, schema_context) || track_items)) { + return {make(sourcemeta::blaze::InstructionIndex::LogicalNotEvaluate, + context, schema_context, dynamic_context, ValueNone{}, + std::move(children))}; + } else { + return {make(sourcemeta::blaze::InstructionIndex::LogicalNot, context, + schema_context, dynamic_context, ValueNone{}, + std::move(children))}; } - - return has_type && has_min && has_max; } -auto is_integer_type_lower_bound_pattern(const Instructions &children) -> bool { - if (children.size() != 2) { - return false; - } - - bool has_type{false}; - bool has_min{false}; - for (const auto &child : children) { - if (is_integer_type_check(child)) { - has_type = true; - } else if (child.type == InstructionIndex::AssertionGreaterEqual && - std::get(child.value).is_integer()) { - has_min = true; - } +auto compiler_draft4_validation_maxproperties( + const Context &context, const SchemaContext &schema_context, + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { + return {}; } - return has_type && has_min; -} + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); -auto extract_integer_lower_bound(const Instructions &children) -> std::int64_t { - for (const auto &child : children) { - if (child.type == InstructionIndex::AssertionGreaterEqual) { - return std::get(child.value).to_integer(); - } + if (schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() != "object") { + return {}; } - return 0; -} - -auto extract_integer_bounds(const Instructions &children) - -> ValueIntegerBounds { - std::int64_t minimum{0}; - std::int64_t maximum{0}; - for (const auto &child : children) { - if (child.type == InstructionIndex::AssertionGreaterEqual) { - minimum = std::get(child.value).to_integer(); - } else if (child.type == InstructionIndex::AssertionLessEqual) { - maximum = std::get(child.value).to_integer(); - } + // We'll handle it at the type level as an optimization + if (context.mode == Mode::FastValidation && + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "object") { + return {}; } - return {minimum, maximum}; + return {make( + sourcemeta::blaze::InstructionIndex::AssertionObjectSizeLess, context, + schema_context, dynamic_context, + ValueUnsignedInteger{ + static_cast( + schema_context.schema.at(dynamic_context.keyword).as_integer()) + + 1})}; } -auto compiler_draft4_applicator_properties_with_options( +auto compiler_draft4_validation_minproperties( const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions ¤t, - const bool annotate, const bool track_evaluation) -> Instructions { - if (schema_context.is_property_name) { - return {}; - } - - if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { + const DynamicContext &dynamic_context, const Instructions &) + -> Instructions { + if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { return {}; } - if (schema_context.schema.at(dynamic_context.keyword).empty()) { - return {}; - } + assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); if (schema_context.schema.defines("type") && schema_context.schema.at("type").is_string() && @@ -916,1503 +269,24 @@ auto compiler_draft4_applicator_properties_with_options( return {}; } - if (properties_as_loop(context, schema_context, - schema_context.schema.at(dynamic_context.keyword))) { - ValueNamedIndexes indexes; - Instructions children; - std::size_t cursor = 0; - - for (auto &&[name, substeps] : compile_properties( - context, schema_context, relative_dynamic_context(), current)) { - indexes.emplace(name, cursor); - - if (track_evaluation) { - substeps.push_back(make( - sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, - schema_context, relative_dynamic_context(), ValuePointer{name})); - } - - if (annotate) { - substeps.push_back( - make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, - schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{name})); - } - - // Note that the evaluator completely ignores this wrapper anyway - children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, - context, schema_context, - relative_dynamic_context(), ValueNone{}, - std::move(substeps))); - cursor += 1; - } - - if (context.mode == Mode::FastValidation && !track_evaluation && - !schema_context.schema.defines("patternProperties") && - schema_context.schema.defines("additionalProperties") && - schema_context.schema.at("additionalProperties").is_boolean() && - !schema_context.schema.at("additionalProperties").to_boolean()) { - return { - make(sourcemeta::blaze::InstructionIndex::LoopPropertiesMatchClosed, - context, schema_context, dynamic_context, std::move(indexes), - std::move(children))}; - } - - return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesMatch, - context, schema_context, dynamic_context, std::move(indexes), - std::move(children))}; - } - - Instructions children; - - const auto effective_dynamic_context{context.mode == Mode::FastValidation - ? dynamic_context - : relative_dynamic_context()}; - - const auto assume_object{schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == - "object"}; - - auto properties{compile_properties(context, schema_context, - effective_dynamic_context, current)}; - + // We'll handle it at the type level as an optimization if (context.mode == Mode::FastValidation && - schema_context.schema.defines("additionalProperties") && - schema_context.schema.at("additionalProperties").is_boolean() && - !schema_context.schema.at("additionalProperties").to_boolean() && - // TODO: Check that the validation vocabulary is present - schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array() && - schema_context.schema.at("required").size() == - schema_context.schema.at(dynamic_context.keyword).size() && - std::ranges::all_of(properties, [&schema_context](const auto &property) { - return schema_context.schema.at("required") - .contains(sourcemeta::core::JSON{property.first}); - })) { - if (std::ranges::all_of(properties, [](const auto &property) { - return property.second.size() == 1 && - property.second.front().type == - InstructionIndex::AssertionTypeStrict; - })) { - std::set types; - for (const auto &property : properties) { - types.insert(std::get(property.second.front().value)); - } - - if (types.size() == 1 && - !schema_context.schema.defines("patternProperties")) { - if (schema_context.schema.defines("required") && assume_object) { - auto required_copy = schema_context.schema.at("required"); - std::ranges::sort(required_copy.as_array()); - ValueStringSet required{json_array_to_string_set(required_copy)}; - if (is_closed_properties_required(schema_context.schema, required)) { - sourcemeta::core::PropertyHashJSON hasher; - std::vector> - perfect_hashes; - for (const auto &entry : required) { - assert(required.contains(entry.first, entry.second)); - if (hasher.is_perfect(entry.second)) { - perfect_hashes.emplace_back(entry.first, entry.second); - } - } + schema_context.schema.defines("type") && + schema_context.schema.at("type").is_string() && + schema_context.schema.at("type").to_string() == "object") { + return {}; + } - if (perfect_hashes.size() == required.size()) { - return {make(sourcemeta::blaze::InstructionIndex:: - LoopPropertiesExactlyTypeStrictHash, - context, schema_context, dynamic_context, - ValueTypedHashes{*types.cbegin(), - to_string_hashes(perfect_hashes)})}; - } - - return {make( - sourcemeta::blaze::InstructionIndex:: - LoopPropertiesExactlyTypeStrict, - context, schema_context, dynamic_context, - ValueTypedProperties{*types.cbegin(), std::move(required)})}; - } - } - - return { - make(sourcemeta::blaze::InstructionIndex::LoopPropertiesTypeStrict, - context, schema_context, dynamic_context, *types.cbegin())}; - } - } - - if (std::ranges::all_of(properties, [](const auto &property) { - return property.second.size() == 1 && - property.second.front().type == - InstructionIndex::AssertionType; - })) { - std::set types; - for (const auto &property : properties) { - types.insert(std::get(property.second.front().value)); - } - - if (types.size() == 1) { - return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesType, - context, schema_context, dynamic_context, - *types.cbegin())}; - } - } - } - - auto attempt_object_fusion{context.mode == Mode::FastValidation && - !annotate && !track_evaluation && assume_object}; - if (attempt_object_fusion) { - for (const auto &entry : schema_context.schema.as_object()) { - const auto &keyword{entry.first}; - if (keyword == "type" || keyword == "required" || - keyword == dynamic_context.keyword) { - continue; - } - - if (keyword == "additionalProperties" && entry.second.is_boolean() && - entry.second.to_boolean()) { - continue; - } - - const auto &keyword_type{ - context.walker(keyword, schema_context.vocabularies).type}; - using enum sourcemeta::core::SchemaKeywordType; - if (keyword_type == Assertion || keyword_type == Annotation || - keyword_type == Unknown || keyword_type == Comment || - keyword_type == Other || keyword_type == LocationMembers) { - continue; - } - - attempt_object_fusion = false; - break; - } - } - ValueObjectProperties fusion_entries; - Instructions fusion_children; - bool fusion_possible{attempt_object_fusion}; - - for (auto &&[name, substeps] : properties) { - if (annotate) { - substeps.push_back( - make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, - schema_context, effective_dynamic_context, - sourcemeta::core::JSON{name})); - } - - // Optimize `properties` where its subschemas just include a type check - - if (context.mode == Mode::FastValidation && track_evaluation && - substeps.size() == 1 && - substeps.front().type == InstructionIndex::AssertionTypeStrict) { - children.push_back(rephrase(context, - sourcemeta::blaze::InstructionIndex:: - AssertionPropertyTypeStrictEvaluate, - substeps.front())); - } else if (context.mode == Mode::FastValidation && track_evaluation && - substeps.size() == 1 && - substeps.front().type == InstructionIndex::AssertionType) { - children.push_back(rephrase( - context, - sourcemeta::blaze::InstructionIndex::AssertionPropertyTypeEvaluate, - substeps.front())); - } else if (context.mode == Mode::FastValidation && track_evaluation && - substeps.size() == 1 && - substeps.front().type == - InstructionIndex::AssertionTypeStrictAny) { - children.push_back(rephrase(context, - sourcemeta::blaze::InstructionIndex:: - AssertionPropertyTypeStrictAnyEvaluate, - substeps.front())); - - // NOLINTBEGIN(bugprone-branch-clone) - } else if (!fusion_possible && context.mode == Mode::FastValidation && - substeps.size() == 1 && - substeps.front().type == - InstructionIndex::AssertionPropertyTypeStrict) { - children.push_back( - unroll(context, substeps.front(), - effective_dynamic_context.base_instance_location)); - } else if (!fusion_possible && context.mode == Mode::FastValidation && - substeps.size() == 1 && - substeps.front().type == - InstructionIndex::AssertionPropertyType) { - children.push_back( - unroll(context, substeps.front(), - effective_dynamic_context.base_instance_location)); - } else if (!fusion_possible && context.mode == Mode::FastValidation && - substeps.size() == 1 && - substeps.front().type == - InstructionIndex::AssertionPropertyTypeStrictAny) { - children.push_back( - unroll(context, substeps.front(), - effective_dynamic_context.base_instance_location)); - // NOLINTEND(bugprone-branch-clone) - - } else { - if (track_evaluation) { - auto new_base_instance_location{ - effective_dynamic_context.base_instance_location}; - new_base_instance_location.push_back({name}); - substeps.push_back( - make(sourcemeta::blaze::InstructionIndex::Evaluate, context, - schema_context, - DynamicContext{ - .keyword = effective_dynamic_context.keyword, - .base_schema_location = - effective_dynamic_context.base_schema_location, - .base_instance_location = new_base_instance_location}, - ValueNone{})); - } - - if (context.mode == Mode::FastValidation && !substeps.empty()) { - if (is_integer_type_bounded_pattern(substeps)) { - auto bounds = extract_integer_bounds(substeps); - const auto index = - has_strict_integer_type(substeps) - ? InstructionIndex::AssertionTypeIntegerBoundedStrict - : InstructionIndex::AssertionTypeIntegerBounded; - auto instance_location = substeps.front().relative_instance_location; - substeps.clear(); - auto fused = make(index, context, schema_context, - relative_dynamic_context(), bounds); - fused.relative_instance_location = std::move(instance_location); - substeps.push_back(std::move(fused)); - } else if (is_integer_type_lower_bound_pattern(substeps)) { - const auto minimum = extract_integer_lower_bound(substeps); - const auto index = - has_strict_integer_type(substeps) - ? InstructionIndex::AssertionTypeIntegerLowerBoundStrict - : InstructionIndex::AssertionTypeIntegerLowerBound; - auto instance_location = substeps.front().relative_instance_location; - substeps.clear(); - auto fused = - make(index, context, schema_context, relative_dynamic_context(), - ValueIntegerBounds{minimum, 0}); - fused.relative_instance_location = std::move(instance_location); - substeps.push_back(std::move(fused)); - } else if (substeps.size() == 2) { - bool has_items_bounded{false}; - bool has_array_type{false}; - std::size_t items_index{0}; - std::size_t array_index{0}; - for (std::size_t step_index = 0; step_index < 2; step_index++) { - if (substeps[step_index].type == - InstructionIndex::LoopItemsIntegerBounded) { - has_items_bounded = true; - items_index = step_index; - } else if (substeps[step_index].type == - InstructionIndex::AssertionTypeArrayBounded) { - has_array_type = true; - array_index = step_index; - } - } - - if (has_items_bounded && has_array_type) { - auto integer_bounds{ - std::get(substeps[items_index].value)}; - auto range{std::get(substeps[array_index].value)}; - auto instance_location = - substeps[items_index].relative_instance_location; - Value fused_value{ - ValueIntegerBoundsWithSize{integer_bounds, std::move(range)}}; - substeps.clear(); - auto fused = - make(InstructionIndex::LoopItemsIntegerBoundedSized, context, - schema_context, effective_dynamic_context, fused_value); - fused.relative_instance_location = std::move(instance_location); - substeps.push_back(std::move(fused)); - } - } - } - - if (fusion_possible && substeps.size() >= 2 && - std::ranges::any_of(substeps, [](const auto &step) { - return step.type == - InstructionIndex::AssertionObjectPropertiesSimple; - })) { - std::erase_if(substeps, [](const auto &step) { - if (step.type == InstructionIndex::AssertionDefinesAllStrict || - step.type == InstructionIndex::AssertionDefinesAll) { - return true; - } - - if ((step.type == InstructionIndex::AssertionTypeStrict || - step.type == InstructionIndex::AssertionType) && - std::get(step.value) == - sourcemeta::core::JSON::Type::Object) { - return true; - } - - return false; - }); - } - - if (fusion_possible && substeps.size() == 1 && - substeps.front().type != InstructionIndex::ControlJump && - substeps.front().type != InstructionIndex::ControlDynamicAnchorJump) { - const auto is_required{ - assume_object && schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array() && - schema_context.schema.at("required") - .contains(sourcemeta::core::JSON{name})}; - auto prop{make_property(name)}; - auto fusion_child{substeps.front()}; - fusion_child.relative_instance_location = {}; - auto fusion_extra{context.extra[fusion_child.extra_index]}; - fusion_extra.relative_schema_location = {}; - fusion_child.extra_index = context.extra.size(); - context.extra.push_back(std::move(fusion_extra)); - - fusion_entries.emplace_back(prop.first, prop.second, is_required); - fusion_children.push_back(std::move(fusion_child)); - } else { - fusion_possible = false; - } - - if (!substeps.empty()) { - // As a performance shortcut - if (effective_dynamic_context.base_instance_location.empty()) { - if (assume_object && - // TODO: Check that the validation vocabulary is present - schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array() && - schema_context.schema.at("required") - .contains(sourcemeta::core::JSON{name})) { - for (auto &&step : substeps) { - children.push_back(std::move(step)); - } - } else { - children.push_back(make(sourcemeta::blaze::InstructionIndex:: - ControlGroupWhenDefinesDirect, - context, schema_context, - effective_dynamic_context, - make_property(name), std::move(substeps))); - } - } else { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::ControlGroupWhenDefines, - context, schema_context, effective_dynamic_context, - make_property(name), std::move(substeps))); - } - } - } - } - - if (context.mode == Mode::FastValidation) { - if (fusion_possible && !fusion_entries.empty()) { - if (schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array()) { - for (const auto &req : - schema_context.schema.at("required").as_array()) { - if (!req.is_string()) { - continue; - } - const auto &req_name{req.to_string()}; - bool already_tracked{false}; - for (const auto &entry : fusion_entries) { - if (std::get<0>(entry) == req_name) { - already_tracked = true; - break; - } - } - if (!already_tracked) { - auto prop{make_property(req_name)}; - fusion_entries.emplace_back(prop.first, prop.second, true); - } - } - } - - if (fusion_entries.size() > 32) { - return children; - } - - return {make(InstructionIndex::AssertionObjectPropertiesSimple, context, - schema_context, dynamic_context, - Value{std::move(fusion_entries)}, - std::move(fusion_children))}; - } - - return children; - } else if (children.empty()) { - return {}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::LogicalWhenType, context, - schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Object, std::move(children))}; - } -} - -auto compiler_draft4_applicator_properties( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions ¤t) - -> Instructions { - return compiler_draft4_applicator_properties_with_options( - context, schema_context, dynamic_context, current, false, false); -} - -auto compiler_draft4_applicator_patternproperties_with_options( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate, - const bool track_evaluation) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { - return {}; - } - - if (schema_context.schema.at(dynamic_context.keyword).empty()) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - Instructions children; - - // To guarantee ordering - std::vector patterns; - for (auto &entry : - schema_context.schema.at(dynamic_context.keyword).as_object()) { - patterns.push_back(entry.first); - } - - std::ranges::sort(patterns); - - // For each regular expression and corresponding subschema in the object - for (const auto &pattern : patterns) { - auto substeps{compile(context, schema_context, relative_dynamic_context(), - sourcemeta::blaze::make_weak_pointer(pattern))}; - - if (annotate) { - substeps.push_back(make( - sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, - context, schema_context, relative_dynamic_context(), ValueNone{})); - } - - if (track_evaluation) { - substeps.push_back( - make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, - schema_context, relative_dynamic_context(), ValuePointer{})); - } - - if (context.mode == Mode::FastValidation && !track_evaluation && - patterns.size() == 1 && - (!schema_context.schema.defines("properties") || - (schema_context.schema.at("properties").is_object() && - schema_context.schema.at("properties").empty())) && - schema_context.schema.defines("additionalProperties") && - schema_context.schema.at("additionalProperties").is_boolean() && - !schema_context.schema.at("additionalProperties").to_boolean()) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::LoopPropertiesRegexClosed, - context, schema_context, dynamic_context, - ValueRegex{.first = parse_regex(pattern, schema_context.base, - schema_context.relative_pointer), - .second = pattern}, - std::move(substeps))); - - // If the `patternProperties` subschema for the given pattern does - // nothing, then we can avoid generating an entire loop for it - } else if (!substeps.empty()) { - const auto maybe_prefix{pattern_as_prefix(pattern)}; - if (maybe_prefix.has_value()) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::LoopPropertiesStartsWith, - context, schema_context, dynamic_context, - ValueString{maybe_prefix.value()}, std::move(substeps))); - } else { - children.push_back(make( - sourcemeta::blaze::InstructionIndex::LoopPropertiesRegex, context, - schema_context, dynamic_context, - ValueRegex{.first = parse_regex(pattern, schema_context.base, - schema_context.relative_pointer), - .second = pattern}, - std::move(substeps))); - } - } - } - - return children; -} - -auto compiler_draft4_applicator_patternproperties( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - return compiler_draft4_applicator_patternproperties_with_options( - context, schema_context, dynamic_context, false, false); -} - -auto compiler_draft4_applicator_additionalproperties_with_options( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate, - const bool track_evaluation) -> Instructions { - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - Instructions children{compile(context, schema_context, - relative_dynamic_context(), - sourcemeta::core::empty_weak_pointer, - sourcemeta::core::empty_weak_pointer)}; - - if (annotate) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::AnnotationBasenameToParent, - context, schema_context, relative_dynamic_context(), ValueNone{})); - } - - ValueStringSet filter_strings; - ValueStrings filter_prefixes; - std::vector filter_regexes; - - if (schema_context.schema.defines("properties") && - schema_context.schema.at("properties").is_object()) { - for (const auto &entry : - schema_context.schema.at("properties").as_object()) { - filter_strings.insert(entry.first); - } - } - - if (schema_context.schema.defines("patternProperties") && - schema_context.schema.at("patternProperties").is_object()) { - for (const auto &entry : - schema_context.schema.at("patternProperties").as_object()) { - const auto maybe_prefix{pattern_as_prefix(entry.first)}; - if (maybe_prefix.has_value()) { - filter_prefixes.push_back(maybe_prefix.value()); - } else { - static const std::string pattern_properties_keyword{ - "patternProperties"}; - filter_regexes.push_back( - {parse_regex(entry.first, schema_context.base, - schema_context.relative_pointer.initial().concat( - sourcemeta::blaze::make_weak_pointer( - pattern_properties_keyword))), - entry.first}); - } - } - } - - // For performance, if a schema sets `additionalProperties: true` (or its - // variants), we don't need to do anything - if (!track_evaluation && children.empty()) { - return {}; - } - - // When `additionalProperties: false` with only `properties` (no - // patternProperties), and `properties` is compiled as a loop - // (LoopPropertiesMatchClosed), that loop already handles rejecting unknown - // properties, so we don't need to emit anything for `additionalProperties` - if (context.mode == Mode::FastValidation && children.size() == 1 && - children.front().type == InstructionIndex::AssertionFail && - !filter_strings.empty() && filter_prefixes.empty() && - filter_regexes.empty() && - properties_as_loop(context, schema_context, - schema_context.schema.at("properties"))) { - return {}; - } - - // When all properties are required and `additionalProperties: false`, - // the `required` keyword compiles to `AssertionDefinesExactly` which already - // checks that the object has exactly the required properties, so we don't - // need to emit anything for `additionalProperties` - if (context.mode == Mode::FastValidation && children.size() == 1 && - children.front().type == InstructionIndex::AssertionFail && - !filter_strings.empty() && filter_prefixes.empty() && - filter_regexes.empty() && schema_context.schema.defines("required") && - schema_context.schema.at("required").is_array() && - is_closed_properties_required( - schema_context.schema, - json_array_to_string_set(schema_context.schema.at("required")))) { - return {}; - } - - if (context.mode == Mode::FastValidation && filter_strings.empty() && - filter_prefixes.empty() && filter_regexes.size() == 1 && - !track_evaluation && !children.empty() && - children.front().type == InstructionIndex::AssertionFail) { - return {}; - } - - if (!filter_strings.empty() || !filter_prefixes.empty() || - !filter_regexes.empty()) { - if (track_evaluation) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, - schema_context, relative_dynamic_context(), ValuePointer{})); - } - - return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesExcept, - context, schema_context, dynamic_context, - ValuePropertyFilter{std::move(filter_strings), - std::move(filter_prefixes), - std::move(filter_regexes)}, - std::move(children))}; - } else if (track_evaluation) { - if (children.empty()) { - return {make(sourcemeta::blaze::InstructionIndex::Evaluate, context, - schema_context, dynamic_context, ValueNone{})}; - } - - return {make(sourcemeta::blaze::InstructionIndex::LoopPropertiesEvaluate, - context, schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } else if (children.size() == 1 && - children.front().type == InstructionIndex::AssertionFail) { - return {make(sourcemeta::blaze::InstructionIndex::AssertionObjectSizeLess, - context, schema_context, dynamic_context, - ValueUnsignedInteger{1})}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::LoopProperties, context, - schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } -} - -auto compiler_draft4_applicator_additionalproperties( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - return compiler_draft4_applicator_additionalproperties_with_options( - context, schema_context, dynamic_context, false, false); -} - -auto compiler_draft4_validation_pattern(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_string()) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "string") { - return {}; - } - - const auto ®ex_string{ - schema_context.schema.at(dynamic_context.keyword).to_string()}; - return { - make(sourcemeta::blaze::InstructionIndex::AssertionRegex, context, - schema_context, dynamic_context, - ValueRegex{.first = parse_regex(regex_string, schema_context.base, - schema_context.relative_pointer), - .second = regex_string})}; -} - -auto compiler_draft4_applicator_not(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - std::size_t subschemas{0}; - for (const auto &subschema : - walk_subschemas(context, schema_context, dynamic_context)) { - if (subschema.pointer.empty()) { - continue; - } - - subschemas += 1; - } - - Instructions children{compile(context, schema_context, - relative_dynamic_context(), - sourcemeta::core::empty_weak_pointer, - sourcemeta::core::empty_weak_pointer)}; - - // TODO: Be smarter about how we treat `unevaluatedItems` like how we do for - // `unevaluatedProperties` - const bool track_items{ - std::ranges::any_of(context.unevaluated, [](const auto &dependency) { - return dependency.first.ends_with("unevaluatedItems"); - })}; - - // Only emit a `not` instruction that keeps track of - // evaluation if we really need it. If the "not" subschema - // does not define applicators, then that's an easy case - // we can skip - if (subschemas > 0 && - (requires_evaluation(context, schema_context) || track_items)) { - return {make(sourcemeta::blaze::InstructionIndex::LogicalNotEvaluate, - context, schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::LogicalNot, context, - schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } -} - -auto compiler_draft4_applicator_items_array( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate, - const bool track_evaluation) -> Instructions { - if (schema_context.is_property_name) { - return {}; - } - - if (!schema_context.schema.at(dynamic_context.keyword).is_array()) { - return {}; - } - - const auto items_size{ - schema_context.schema.at(dynamic_context.keyword).size()}; - if (items_size == 0) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - // Precompile subschemas - std::vector subschemas; - subschemas.reserve(items_size); - const auto &array{ - schema_context.schema.at(dynamic_context.keyword).as_array()}; - for (auto iterator{array.cbegin()}; iterator != array.cend(); ++iterator) { - subschemas.push_back(compile(context, schema_context, - relative_dynamic_context(), - {subschemas.size()}, {subschemas.size()})); - } - - Instructions children; - for (std::size_t cursor = 0; cursor < items_size; cursor++) { - Instructions subchildren; - for (std::size_t index = 0; index < cursor + 1; index++) { - for (const auto &substep : subschemas.at(index)) { - subchildren.push_back(substep); - } - } - - if (annotate) { - subchildren.push_back( - make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, context, - schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{cursor})); - } - - children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, - context, schema_context, relative_dynamic_context(), - ValueNone{}, std::move(subchildren))); - } - - Instructions tail; - for (const auto &subschema : subschemas) { - for (const auto &substep : subschema) { - tail.push_back(substep); - } - } - - if (annotate) { - tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, - context, schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{children.size() - 1})); - tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, - context, schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{true})); - } - - children.push_back(make(sourcemeta::blaze::InstructionIndex::ControlGroup, - context, schema_context, relative_dynamic_context(), - ValueNone{}, std::move(tail))); - - if (track_evaluation) { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionArrayPrefixEvaluate, - context, schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::AssertionArrayPrefix, - context, schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } -} - -auto is_number_type_check(const Instruction &instruction) -> bool { - if (instruction.type != InstructionIndex::AssertionTypeStrictAny) { - return false; - } - - const auto &value{std::get(instruction.value)}; - const auto numeric_count{ - static_cast(value.test( - std::to_underlying(sourcemeta::core::JSON::Type::Integer))) + - static_cast( - value.test(std::to_underlying(sourcemeta::core::JSON::Type::Real))) + - static_cast(value.test( - std::to_underlying(sourcemeta::core::JSON::Type::Decimal)))}; - return numeric_count >= 2 && value.count() == numeric_count; -} - -auto is_integer_bounded_pattern(const Instructions &children) -> bool { - if (children.size() != 3) { - return false; - } - - bool has_type{false}; - bool has_min{false}; - bool has_max{false}; - for (const auto &child : children) { - if (is_number_type_check(child)) { - has_type = true; - } else if (child.type == InstructionIndex::AssertionGreaterEqual) { - if (!std::get(child.value).is_integer()) { - return false; - } - has_min = true; - } else if (child.type == InstructionIndex::AssertionLessEqual) { - if (!std::get(child.value).is_integer()) { - return false; - } - has_max = true; - } - } - - return has_type && has_min && has_max; -} - -auto compiler_draft4_applicator_items_with_options( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate, - const bool track_evaluation) -> Instructions { - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - if (is_schema(schema_context.schema.at(dynamic_context.keyword))) { - if (annotate || track_evaluation) { - Instructions subchildren{compile(context, schema_context, - relative_dynamic_context(), - sourcemeta::core::empty_weak_pointer, - sourcemeta::core::empty_weak_pointer)}; - - Instructions children; - - if (!subchildren.empty()) { - children.push_back(make(sourcemeta::blaze::InstructionIndex::LoopItems, - context, schema_context, dynamic_context, - ValueNone{}, std::move(subchildren))); - } - - if (!annotate && !track_evaluation) { - return children; - } - - Instructions tail; - - if (annotate) { - tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, - context, schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{true})); - } - - if (track_evaluation) { - tail.push_back( - make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, - schema_context, relative_dynamic_context(), ValuePointer{})); - } - - children.push_back( - make(sourcemeta::blaze::InstructionIndex::LogicalWhenType, context, - schema_context, dynamic_context, - sourcemeta::core::JSON::Type::Array, std::move(tail))); - - return children; - } - - Instructions children{compile(context, schema_context, - relative_dynamic_context(), - sourcemeta::core::empty_weak_pointer, - sourcemeta::core::empty_weak_pointer)}; - if (track_evaluation) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, context, - schema_context, relative_dynamic_context(), ValuePointer{})); - } - - if (children.empty()) { - return {}; - } - - if (context.mode == Mode::FastValidation && children.size() == 3 && - is_integer_bounded_pattern(children)) { - return {make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded, - context, schema_context, dynamic_context, - extract_integer_bounds(children))}; - } - - if (context.mode == Mode::FastValidation && children.size() == 1) { - if (children.front().type == InstructionIndex::AssertionTypeStrict) { - return {make(sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrict, - context, schema_context, dynamic_context, - children.front().value)}; - } else if (children.front().type == InstructionIndex::AssertionType) { - return {make(sourcemeta::blaze::InstructionIndex::LoopItemsType, - context, schema_context, dynamic_context, - children.front().value)}; - } else if (children.front().type == - InstructionIndex::AssertionTypeStrictAny) { - return {make( - sourcemeta::blaze::InstructionIndex::LoopItemsTypeStrictAny, - context, schema_context, dynamic_context, children.front().value)}; - } else if (children.front().type == - InstructionIndex::LoopPropertiesExactlyTypeStrictHash) { - auto value_copy = children.front().value; - auto current{make(sourcemeta::blaze::InstructionIndex::LoopItems, - context, schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - if (std::get(value_copy).second.first.size() == 3) { - return {Instruction{.type = sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash3, - .relative_instance_location = - current.relative_instance_location, - .value = std::move(value_copy), - .children = {}, - .extra_index = current.extra_index}}; - } - - return {Instruction{.type = sourcemeta::blaze::InstructionIndex:: - LoopItemsPropertiesExactlyTypeStrictHash, - .relative_instance_location = - current.relative_instance_location, - .value = std::move(value_copy), - .children = {}, - .extra_index = current.extra_index}}; - } - } - - return {make(sourcemeta::blaze::InstructionIndex::LoopItems, context, - schema_context, dynamic_context, ValueNone{}, - std::move(children))}; - } - - return compiler_draft4_applicator_items_array( - context, schema_context, dynamic_context, annotate, track_evaluation); -} - -auto compiler_draft4_applicator_items(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - return compiler_draft4_applicator_items_with_options( - context, schema_context, dynamic_context, false, false); -} - -auto compiler_draft4_applicator_additionalitems_from_cursor( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const std::size_t cursor, - const bool annotate, const bool track_evaluation) -> Instructions { - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - Instructions subchildren{compile(context, schema_context, - relative_dynamic_context(), - sourcemeta::core::empty_weak_pointer, - sourcemeta::core::empty_weak_pointer)}; - - Instructions children; - - if (!subchildren.empty()) { - if (context.mode == Mode::FastValidation && cursor == 0 && !annotate && - !track_evaluation && is_integer_bounded_pattern(subchildren)) { - children.push_back( - make(sourcemeta::blaze::InstructionIndex::LoopItemsIntegerBounded, - context, schema_context, dynamic_context, - extract_integer_bounds(subchildren))); - return children; - } - - children.push_back(make(sourcemeta::blaze::InstructionIndex::LoopItemsFrom, - context, schema_context, dynamic_context, - ValueUnsignedInteger{cursor}, - std::move(subchildren))); - } - - // Avoid one extra wrapper instruction if possible - if (!annotate && !track_evaluation) { - return children; - } - - Instructions tail; - - if (annotate) { - tail.push_back(make(sourcemeta::blaze::InstructionIndex::AnnotationEmit, - context, schema_context, relative_dynamic_context(), - sourcemeta::core::JSON{true})); - } - - if (track_evaluation) { - tail.push_back(make(sourcemeta::blaze::InstructionIndex::ControlEvaluate, - context, schema_context, relative_dynamic_context(), - ValuePointer{})); - } - - assert(!tail.empty()); - children.push_back( - make(sourcemeta::blaze::InstructionIndex::LogicalWhenArraySizeGreater, - context, schema_context, dynamic_context, - ValueUnsignedInteger{cursor}, std::move(tail))); - - return children; -} - -auto compiler_draft4_applicator_additionalitems_with_options( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const bool annotate, - const bool track_evaluation) -> Instructions { - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - assert(schema_context.schema.is_object()); - - // Nothing to do here - if (!schema_context.schema.defines("items") || - schema_context.schema.at("items").is_object()) { - return {}; - } - - const auto cursor{(schema_context.schema.defines("items") && - schema_context.schema.at("items").is_array()) - ? schema_context.schema.at("items").size() - : 0}; - - return compiler_draft4_applicator_additionalitems_from_cursor( - context, schema_context, dynamic_context, cursor, annotate, - track_evaluation); -} - -auto compiler_draft4_applicator_additionalitems( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - return compiler_draft4_applicator_additionalitems_with_options( - context, schema_context, dynamic_context, false, false); -} - -auto compiler_draft4_applicator_dependencies( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - if (!schema_context.schema.at(dynamic_context.keyword).is_object()) { - return {}; - } - - Instructions children; - ValueStringMap dependencies; - - for (const auto &entry : - schema_context.schema.at(dynamic_context.keyword).as_object()) { - if (is_schema(entry.second)) { - if (!entry.second.is_boolean() || !entry.second.to_boolean()) { - children.push_back(make( - sourcemeta::blaze::InstructionIndex::LogicalWhenDefines, context, - schema_context, dynamic_context, make_property(entry.first), - compile(context, schema_context, relative_dynamic_context(), - sourcemeta::blaze::make_weak_pointer(entry.first)))); - } - } else if (entry.second.is_array()) { - std::vector properties; - for (const auto &property : entry.second.as_array()) { - assert(property.is_string()); - properties.push_back(property.to_string()); - } - - if (!properties.empty()) { - dependencies.emplace(entry.first, properties); - } - } - } - - if (!dependencies.empty()) { - children.push_back(make( - sourcemeta::blaze::InstructionIndex::AssertionPropertyDependencies, - context, schema_context, dynamic_context, std::move(dependencies))); - } - - return children; -} - -auto compiler_draft4_validation_enum(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_array()) { - return {}; - } - - if (schema_context.schema.at(dynamic_context.keyword).size() == 1) { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionEqual, context, - schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword).front()})}; - } - - std::vector> - perfect_string_hashes; - ValueSet options; - sourcemeta::core::PropertyHashJSON hasher; - for (const auto &option : - schema_context.schema.at(dynamic_context.keyword).as_array()) { - if (option.is_string()) { - const auto hash{hasher(option.to_string())}; - if (hasher.is_perfect(hash)) { - perfect_string_hashes.emplace_back(option.to_string(), hash); - } - } - - options.insert(option); - } - - // Only apply this optimisation on fast validation, as it - // can affect error messages - if (context.mode == Mode::FastValidation && - perfect_string_hashes.size() == options.size()) { - return { - make(sourcemeta::blaze::InstructionIndex::AssertionEqualsAnyStringHash, - context, schema_context, dynamic_context, - to_string_hashes(perfect_string_hashes))}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionEqualsAny, context, - schema_context, dynamic_context, std::move(options))}; -} - -auto compiler_draft4_validation_uniqueitems( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_boolean() || - !schema_context.schema.at(dynamic_context.keyword).to_boolean()) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionUnique, context, - schema_context, dynamic_context, ValueNone{})}; -} - -auto compiler_draft4_validation_maxlength(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "string") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { - return {}; - } - - return {make( - sourcemeta::blaze::InstructionIndex::AssertionStringSizeLess, context, - schema_context, dynamic_context, - ValueUnsignedInteger{ - static_cast( - schema_context.schema.at(dynamic_context.keyword).as_integer()) + - 1})}; -} - -auto compiler_draft4_validation_minlength(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "string") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "string") { - return {}; - } - - const auto value{static_cast( - schema_context.schema.at(dynamic_context.keyword).as_integer())}; - if (value <= 0) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionStringSizeGreater, - context, schema_context, dynamic_context, - ValueUnsignedInteger{value - 1})}; -} - -auto compiler_draft4_validation_maxitems(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { - return {}; - } - - return {make( - sourcemeta::blaze::InstructionIndex::AssertionArraySizeLess, context, - schema_context, dynamic_context, - ValueUnsignedInteger{ - static_cast( - schema_context.schema.at(dynamic_context.keyword).as_integer()) + - 1})}; -} - -auto compiler_draft4_validation_minitems(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "array") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "array") { - return {}; - } - - const auto value{ - schema_context.schema.at(dynamic_context.keyword).as_integer()}; - if (value <= 0) { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionArraySizeGreater, - context, schema_context, dynamic_context, - ValueUnsignedInteger{static_cast(value - 1)})}; -} - -auto compiler_draft4_validation_maxproperties( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { - return {}; - } - - return {make( - sourcemeta::blaze::InstructionIndex::AssertionObjectSizeLess, context, - schema_context, dynamic_context, - ValueUnsignedInteger{ - static_cast( - schema_context.schema.at(dynamic_context.keyword).as_integer()) + - 1})}; -} - -auto compiler_draft4_validation_minproperties( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_integral()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "object") { - return {}; - } - - // We'll handle it at the type level as an optimization - if (context.mode == Mode::FastValidation && - schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() == "object") { - return {}; - } - - const auto value{static_cast( - schema_context.schema.at(dynamic_context.keyword).as_integer())}; - if (value <= 0) { - return {}; - } + const auto value{static_cast( + schema_context.schema.at(dynamic_context.keyword).as_integer())}; + if (value <= 0) { + return {}; + } return {make(sourcemeta::blaze::InstructionIndex::AssertionObjectSizeGreater, context, schema_context, dynamic_context, ValueUnsignedInteger{value - 1})}; } -auto compiler_draft4_validation_maximum(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "integer" && - schema_context.schema.at("type").to_string() != "number") { - return {}; - } - - // TODO: As an optimization, if `minimum` is set to the same number, do - // a single equality assertion - - assert(schema_context.schema.is_object()); - if (schema_context.schema.defines("exclusiveMaximum") && - schema_context.schema.at("exclusiveMaximum").is_boolean() && - schema_context.schema.at("exclusiveMaximum").to_boolean()) { - return {make(sourcemeta::blaze::InstructionIndex::AssertionLess, context, - schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword)})}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::AssertionLessEqual, - context, schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword)})}; - } -} - -auto compiler_draft4_validation_minimum(const Context &context, - const SchemaContext &schema_context, - const DynamicContext &dynamic_context, - const Instructions &) -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { - return {}; - } - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "integer" && - schema_context.schema.at("type").to_string() != "number") { - return {}; - } - - // TODO: As an optimization, if `maximum` is set to the same number, do - // a single equality assertion - - assert(schema_context.schema.is_object()); - if (schema_context.schema.defines("exclusiveMinimum") && - schema_context.schema.at("exclusiveMinimum").is_boolean() && - schema_context.schema.at("exclusiveMinimum").to_boolean()) { - return {make(sourcemeta::blaze::InstructionIndex::AssertionGreater, context, - schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword)})}; - } else { - return {make(sourcemeta::blaze::InstructionIndex::AssertionGreaterEqual, - context, schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword)})}; - } -} - -auto compiler_draft4_validation_multipleof( - const Context &context, const SchemaContext &schema_context, - const DynamicContext &dynamic_context, const Instructions &) - -> Instructions { - if (!schema_context.schema.at(dynamic_context.keyword).is_number()) { - return {}; - } - - assert(schema_context.schema.at(dynamic_context.keyword).is_positive()); - - if (schema_context.schema.defines("type") && - schema_context.schema.at("type").is_string() && - schema_context.schema.at("type").to_string() != "integer" && - schema_context.schema.at("type").to_string() != "number") { - return {}; - } - - return {make(sourcemeta::blaze::InstructionIndex::AssertionDivisible, context, - schema_context, dynamic_context, - sourcemeta::core::JSON{ - schema_context.schema.at(dynamic_context.keyword)})}; -} - } // namespace internal #endif diff --git a/vendor/blaze/src/configuration/lock.cc b/vendor/blaze/src/configuration/lock.cc index 89ced3f08..13bee4c37 100644 --- a/vendor/blaze/src/configuration/lock.cc +++ b/vendor/blaze/src/configuration/lock.cc @@ -1,6 +1,7 @@ #include #include +#include #include #include @@ -127,7 +128,7 @@ auto Configuration::Lock::from_json(const sourcemeta::core::JSON &value, } else { try { entry.path = - std::filesystem::weakly_canonical(lock_base_path / entry_path); + sourcemeta::core::weakly_canonical(lock_base_path / entry_path); } catch (const std::filesystem::filesystem_error &) { throw ConfigurationParseError( "The lock file dependency entry path could not be resolved", diff --git a/vendor/blaze/src/configuration/parse.cc b/vendor/blaze/src/configuration/parse.cc index 9bb291390..59fa7b6e8 100644 --- a/vendor/blaze/src/configuration/parse.cc +++ b/vendor/blaze/src/configuration/parse.cc @@ -1,5 +1,6 @@ #include +#include #include #include @@ -76,15 +77,15 @@ auto Configuration::from_json(const sourcemeta::core::JSON &value, if (value.defines("path")) { const std::filesystem::path path{value.at("path").to_string()}; if (path.is_absolute()) { - result.absolute_path = std::filesystem::weakly_canonical(path); + result.absolute_path = sourcemeta::core::weakly_canonical(path); } else { result.absolute_path = - std::filesystem::weakly_canonical(base_path / path); + sourcemeta::core::weakly_canonical(base_path / path); } result.absolute_path_explicit = true; } else { - result.absolute_path = std::filesystem::weakly_canonical(base_path); + result.absolute_path = sourcemeta::core::weakly_canonical(base_path); } assert(result.absolute_path.is_absolute()); @@ -171,8 +172,8 @@ auto Configuration::from_json(const sourcemeta::core::JSON &value, const std::filesystem::path dependency_path{pair.second.to_string()}; const auto absolute_dependency_path = dependency_path.is_absolute() - ? std::filesystem::weakly_canonical(dependency_path) - : std::filesystem::weakly_canonical(base_path / dependency_path); + ? sourcemeta::core::weakly_canonical(dependency_path) + : sourcemeta::core::weakly_canonical(base_path / dependency_path); try { result.add_dependency(sourcemeta::core::URI{pair.first}, absolute_dependency_path); @@ -205,8 +206,8 @@ auto Configuration::from_json(const sourcemeta::core::JSON &value, const std::filesystem::path path{element.to_string()}; result.lint.rules.push_back( path.is_absolute() - ? std::filesystem::weakly_canonical(path) - : std::filesystem::weakly_canonical(base_path / path)); + ? sourcemeta::core::weakly_canonical(path) + : sourcemeta::core::weakly_canonical(base_path / path)); index += 1; } } @@ -226,8 +227,8 @@ auto Configuration::from_json(const sourcemeta::core::JSON &value, const std::filesystem::path path{element.to_string()}; result.ignore.push_back( path.is_absolute() - ? std::filesystem::weakly_canonical(path) - : std::filesystem::weakly_canonical(base_path / path)); + ? sourcemeta::core::weakly_canonical(path) + : sourcemeta::core::weakly_canonical(base_path / path)); index += 1; } } diff --git a/vendor/blaze/src/documentation/CMakeLists.txt b/vendor/blaze/src/documentation/CMakeLists.txt new file mode 100644 index 000000000..ca698642d --- /dev/null +++ b/vendor/blaze/src/documentation/CMakeLists.txt @@ -0,0 +1,16 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT blaze NAME documentation + FOLDER "Blaze/Documentation" + SOURCES documentation.cc documentation_html.cc) + +if(BLAZE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT blaze NAME documentation) +endif() + +target_link_libraries(sourcemeta_blaze_documentation PUBLIC + sourcemeta::core::json) +target_link_libraries(sourcemeta_blaze_documentation PUBLIC + sourcemeta::core::jsonschema) +target_link_libraries(sourcemeta_blaze_documentation PRIVATE + sourcemeta::blaze::alterschema) +target_link_libraries(sourcemeta_blaze_documentation PRIVATE + sourcemeta::core::html) diff --git a/vendor/blaze/src/documentation/documentation.cc b/vendor/blaze/src/documentation/documentation.cc new file mode 100644 index 000000000..b5d57adc1 --- /dev/null +++ b/vendor/blaze/src/documentation/documentation.cc @@ -0,0 +1,1560 @@ +#include + +#include + +#include +#include + +#include // assert +#include // std::int64_t +#include // std::map +#include // std::set +#include // std::ostringstream +#include // std::to_string +#include // std::move + +namespace sourcemeta::blaze { + +namespace { + +auto resolve_destination(const sourcemeta::core::JSON::String &raw_ref, + const sourcemeta::core::SchemaFrame &frame) + -> std::optional< + std::reference_wrapper> { + auto result{frame.traverse(raw_ref)}; + if (result.has_value()) { + return result; + } + for (const auto &[key, entry] : frame.references()) { + if (key.first == sourcemeta::core::SchemaReferenceType::Static && + entry.original == raw_ref) { + return frame.traverse(entry.destination); + } + } + return std::nullopt; +} + +struct VisitedEntry { + std::size_t identifier; + sourcemeta::core::JSON path; +}; +using VisitedSchemas = std::map; +using RefChain = std::set; + +auto type_expression_of(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + const VisitedSchemas &visited, RefChain &ref_chain) + -> sourcemeta::core::JSON { + auto result{sourcemeta::core::JSON::make_object()}; + + if (schema.is_boolean()) { + if (schema.to_boolean()) { + result.assign("kind", sourcemeta::core::JSON{"any"}); + } else { + result.assign("kind", sourcemeta::core::JSON{"never"}); + } + return result; + } + + if (!schema.is_object()) { + return result; + } + + if (schema.defines("$ref") && schema.at("$ref").is_string()) { + const auto &destination{schema.at("$ref").to_string()}; + const auto target{resolve_destination(destination, frame)}; + if (!target.has_value()) { + result.assign("kind", sourcemeta::core::JSON{"externalRef"}); + result.assign("url", sourcemeta::core::JSON{destination}); + return result; + } + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + const auto visited_entry{visited.find(&target_schema)}; + if (visited_entry != visited.end()) { + result.assign("kind", sourcemeta::core::JSON{"recursiveRef"}); + result.assign("identifier", + sourcemeta::core::JSON{static_cast( + visited_entry->second.identifier)}); + result.assign("path", visited_entry->second.path); + return result; + } + + if (ref_chain.contains(&target_schema)) { + result.assign("kind", sourcemeta::core::JSON{"any"}); + return result; + } + + ref_chain.insert(&target_schema); + auto ref_result{ + type_expression_of(target_schema, frame, root, visited, ref_chain)}; + ref_chain.erase(&target_schema); + return ref_result; + } + + if (schema.defines("$dynamicRef") && schema.at("$dynamicRef").is_string()) { + const auto &value{schema.at("$dynamicRef").to_string()}; + result.assign("kind", sourcemeta::core::JSON{"dynamicRef"}); + const auto fragment_start{value.find('#')}; + if (fragment_start != sourcemeta::core::JSON::String::npos) { + result.assign("anchor", + sourcemeta::core::JSON{value.substr(fragment_start + 1)}); + } else { + result.assign("anchor", sourcemeta::core::JSON{value}); + } + return result; + } + + if (schema.defines("enum") && schema.at("enum").is_array()) { + result.assign("kind", sourcemeta::core::JSON{"enum"}); + auto values{sourcemeta::core::JSON::make_array()}; + auto overflow{sourcemeta::core::JSON::make_array()}; + std::size_t index{0}; + for (const auto &value : schema.at("enum").as_array()) { + if (index < 10) { + values.push_back(value); + } else { + overflow.push_back(value); + } + ++index; + } + result.assign("values", std::move(values)); + if (!overflow.empty()) { + result.assign("overflow", std::move(overflow)); + } + return result; + } + + if (!schema.defines("type") || !schema.at("type").is_string()) { + result.assign("kind", sourcemeta::core::JSON{"any"}); + return result; + } + + { + const auto &type{schema.at("type").to_string()}; + if (type == "object") { + result.assign("kind", sourcemeta::core::JSON{"object"}); + } else if (type == "array") { + if (schema.defines("prefixItems") && + schema.at("prefixItems").is_array()) { + result.assign("kind", sourcemeta::core::JSON{"tuple"}); + auto items{sourcemeta::core::JSON::make_array()}; + for (const auto &item : schema.at("prefixItems").as_array()) { + items.push_back( + type_expression_of(item, frame, root, visited, ref_chain)); + } + result.assign("items", std::move(items)); + if (schema.defines("items") && schema.at("items").is_object()) { + result.assign("additional", + type_expression_of(schema.at("items"), frame, root, + visited, ref_chain)); + } else if (schema.defines("unevaluatedItems") && + schema.at("unevaluatedItems").is_object()) { + result.assign("additional", + type_expression_of(schema.at("unevaluatedItems"), frame, + root, visited, ref_chain)); + } + } else if (schema.defines("items") && schema.at("items").is_array()) { + result.assign("kind", sourcemeta::core::JSON{"tuple"}); + auto items{sourcemeta::core::JSON::make_array()}; + for (const auto &item : schema.at("items").as_array()) { + items.push_back( + type_expression_of(item, frame, root, visited, ref_chain)); + } + result.assign("items", std::move(items)); + if (schema.defines("additionalItems") && + schema.at("additionalItems").is_object()) { + result.assign("additional", + type_expression_of(schema.at("additionalItems"), frame, + root, visited, ref_chain)); + } + } else { + result.assign("kind", sourcemeta::core::JSON{"array"}); + if (schema.defines("items") && schema.at("items").is_object()) { + result.assign("items", type_expression_of(schema.at("items"), frame, + root, visited, ref_chain)); + } + } + } else if (type == "string") { + result.assign("kind", sourcemeta::core::JSON{"primitive"}); + result.assign("name", sourcemeta::core::JSON{"string"}); + } else if (type == "integer") { + result.assign("kind", sourcemeta::core::JSON{"primitive"}); + result.assign("name", sourcemeta::core::JSON{"integer"}); + } else if (type == "number") { + result.assign("kind", sourcemeta::core::JSON{"primitive"}); + result.assign("name", sourcemeta::core::JSON{"number"}); + } + } + + return result; +} + +auto type_expression_of(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + const VisitedSchemas &visited) + -> sourcemeta::core::JSON { + RefChain ref_chain; + return type_expression_of(schema, frame, root, visited, ref_chain); +} + +auto badges_of(const sourcemeta::core::JSON &schema) -> sourcemeta::core::JSON { + auto badges{sourcemeta::core::JSON::make_array()}; + if (!schema.is_object()) { + return badges; + } + if (schema.defines("format") && schema.at("format").is_string()) { + auto badge{sourcemeta::core::JSON::make_object()}; + badge.assign("kind", sourcemeta::core::JSON{"format"}); + badge.assign("value", + sourcemeta::core::JSON{schema.at("format").to_string()}); + badges.push_back(std::move(badge)); + } + if (schema.defines("contentEncoding") && + schema.at("contentEncoding").is_string()) { + auto badge{sourcemeta::core::JSON::make_object()}; + badge.assign("kind", sourcemeta::core::JSON{"encoding"}); + badge.assign("value", sourcemeta::core::JSON{ + schema.at("contentEncoding").to_string()}); + badges.push_back(std::move(badge)); + } + if (schema.defines("contentMediaType") && + schema.at("contentMediaType").is_string()) { + auto badge{sourcemeta::core::JSON::make_object()}; + badge.assign("kind", sourcemeta::core::JSON{"mime"}); + badge.assign("value", sourcemeta::core::JSON{ + schema.at("contentMediaType").to_string()}); + badges.push_back(std::move(badge)); + } + return badges; +} + +auto modifiers_of(const sourcemeta::core::JSON &schema) + -> sourcemeta::core::JSON { + auto modifiers{sourcemeta::core::JSON::make_array()}; + if (!schema.is_object()) { + return modifiers; + } + if (schema.defines("readOnly") && schema.at("readOnly").is_boolean() && + schema.at("readOnly").to_boolean()) { + modifiers.push_back(sourcemeta::core::JSON{"readOnly"}); + } + if (schema.defines("writeOnly") && schema.at("writeOnly").is_boolean() && + schema.at("writeOnly").to_boolean()) { + modifiers.push_back(sourcemeta::core::JSON{"writeOnly"}); + } + if (schema.defines("deprecated") && schema.at("deprecated").is_boolean() && + schema.at("deprecated").to_boolean()) { + modifiers.push_back(sourcemeta::core::JSON{"deprecated"}); + } + return modifiers; +} + +auto format_json_number(const sourcemeta::core::JSON &value) + -> sourcemeta::core::JSON::String { + std::ostringstream result; + sourcemeta::core::stringify(value, result); + return result.str(); +} + +auto constraints_of(const sourcemeta::core::JSON &schema) + -> sourcemeta::core::JSON { + auto constraints{sourcemeta::core::JSON::make_array()}; + if (!schema.is_object()) { + return constraints; + } + + const auto has_min_length{schema.defines("minLength") && + schema.at("minLength").is_integer()}; + const auto has_max_length{schema.defines("maxLength") && + schema.at("maxLength").is_integer()}; + if (has_min_length && has_max_length && + schema.at("minLength") == schema.at("maxLength")) { + const auto value{schema.at("minLength").to_integer()}; + if (value != 0) { + constraints.push_back(sourcemeta::core::JSON{ + "exactly " + std::to_string(value) + " chars"}); + } + } else { + if (has_min_length) { + const auto value{schema.at("minLength").to_integer()}; + if (value > 0) { + constraints.push_back( + sourcemeta::core::JSON{">= " + std::to_string(value) + " chars"}); + } + } + if (has_max_length) { + constraints.push_back(sourcemeta::core::JSON{ + "<= " + std::to_string(schema.at("maxLength").to_integer()) + + " chars"}); + } + } + + if (schema.defines("minimum") && schema.at("minimum").is_number()) { + const auto exclusive{schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_boolean() && + schema.at("exclusiveMinimum").to_boolean()}; + constraints.push_back(sourcemeta::core::JSON{ + (exclusive ? "> " : ">= ") + format_json_number(schema.at("minimum"))}); + } + if (schema.defines("maximum") && schema.at("maximum").is_number()) { + const auto exclusive{schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_boolean() && + schema.at("exclusiveMaximum").to_boolean()}; + constraints.push_back(sourcemeta::core::JSON{ + (exclusive ? "< " : "<= ") + format_json_number(schema.at("maximum"))}); + } + if (schema.defines("exclusiveMinimum") && + schema.at("exclusiveMinimum").is_number()) { + constraints.push_back(sourcemeta::core::JSON{ + "> " + format_json_number(schema.at("exclusiveMinimum"))}); + } + if (schema.defines("exclusiveMaximum") && + schema.at("exclusiveMaximum").is_number()) { + constraints.push_back(sourcemeta::core::JSON{ + "< " + format_json_number(schema.at("exclusiveMaximum"))}); + } + + if (schema.defines("multipleOf") && schema.at("multipleOf").is_number()) { + const auto &value{schema.at("multipleOf")}; + if (!value.is_integer() || value.to_integer() != 1) { + constraints.push_back( + sourcemeta::core::JSON{"multiple of " + format_json_number(value)}); + } + } + + if (schema.defines("minItems") && schema.at("minItems").is_integer()) { + const auto value{schema.at("minItems").to_integer()}; + if (value > 0) { + constraints.push_back( + sourcemeta::core::JSON{">= " + std::to_string(value) + " items"}); + } + } + if (schema.defines("maxItems") && schema.at("maxItems").is_integer()) { + constraints.push_back(sourcemeta::core::JSON{ + "<= " + std::to_string(schema.at("maxItems").to_integer()) + " items"}); + } + + if (schema.defines("uniqueItems") && schema.at("uniqueItems").is_boolean() && + schema.at("uniqueItems").to_boolean()) { + constraints.push_back(sourcemeta::core::JSON{"unique"}); + } + + if (schema.defines("minProperties") && + schema.at("minProperties").is_integer()) { + const auto value{schema.at("minProperties").to_integer()}; + if (value > 0) { + bool covered_by_required{false}; + if (schema.defines("required") && schema.at("required").is_array() && + schema.defines("properties") && schema.at("properties").is_object() && + std::cmp_equal(schema.at("required").size(), value)) { + covered_by_required = true; + for (const auto &req : schema.at("required").as_array()) { + if (!req.is_string() || + !schema.at("properties").defines(req.to_string())) { + covered_by_required = false; + break; + } + } + } + if (!covered_by_required) { + constraints.push_back(sourcemeta::core::JSON{ + ">= " + std::to_string(value) + " properties"}); + } + } + } + if (schema.defines("maxProperties") && + schema.at("maxProperties").is_integer()) { + constraints.push_back(sourcemeta::core::JSON{ + "<= " + std::to_string(schema.at("maxProperties").to_integer()) + + " properties"}); + } + + if (schema.defines("pattern") && schema.at("pattern").is_string()) { + constraints.push_back( + sourcemeta::core::JSON{"pattern: " + schema.at("pattern").to_string()}); + } + + const auto has_trivial_contains{schema.defines("contains") && + schema.at("contains").is_boolean() && + schema.at("contains").to_boolean()}; + + if (schema.defines("contains") && schema.at("contains").is_object()) { + const auto &contains_schema{schema.at("contains")}; + const auto is_flat{!contains_schema.defines("anyOf") && + !contains_schema.defines("oneOf") && + !contains_schema.defines("allOf") && + !contains_schema.defines("not") && + !contains_schema.defines("enum")}; + if (is_flat) { + if (contains_schema.defines("type") && + contains_schema.at("type").is_string()) { + constraints.push_back(sourcemeta::core::JSON{ + "contains: " + contains_schema.at("type").to_string()}); + } + + const auto inner{constraints_of(contains_schema)}; + for (const auto &constraint : inner.as_array()) { + constraints.push_back( + sourcemeta::core::JSON{"contains " + constraint.to_string()}); + } + } + } + + const auto has_min_contains{!has_trivial_contains && + schema.defines("minContains") && + schema.at("minContains").is_integer()}; + const auto has_max_contains{!has_trivial_contains && + schema.defines("maxContains") && + schema.at("maxContains").is_integer()}; + if (has_min_contains && has_max_contains && + schema.at("minContains") == schema.at("maxContains")) { + constraints.push_back(sourcemeta::core::JSON{ + "exactly " + std::to_string(schema.at("minContains").to_integer()) + + " matching items"}); + } else { + if (has_min_contains) { + const auto value{schema.at("minContains").to_integer()}; + if (value == 0) { + constraints.push_back( + sourcemeta::core::JSON{"0 or more matching items"}); + } else { + constraints.push_back(sourcemeta::core::JSON{ + ">= " + std::to_string(value) + " matching items"}); + } + } + if (has_max_contains) { + constraints.push_back(sourcemeta::core::JSON{ + "<= " + std::to_string(schema.at("maxContains").to_integer()) + + " matching items"}); + } + } + + if (schema.defines("propertyNames") && + schema.at("propertyNames").is_object()) { + const auto &names_schema{schema.at("propertyNames")}; + const auto is_branching{ + names_schema.defines("anyOf") || names_schema.defines("oneOf") || + names_schema.defines("allOf") || names_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(names_schema)}; + if (inner.empty() && names_schema.defines("type") && + names_schema.at("type").is_string()) { + constraints.push_back(sourcemeta::core::JSON{ + "keys: " + names_schema.at("type").to_string()}); + } + + for (const auto &constraint : inner.as_array()) { + constraints.push_back( + sourcemeta::core::JSON{"keys " + constraint.to_string()}); + } + } + } + + if (schema.defines("contentSchema") && + schema.at("contentSchema").is_object()) { + const auto &content_schema{schema.at("contentSchema")}; + const auto is_branching{ + content_schema.defines("anyOf") || content_schema.defines("oneOf") || + content_schema.defines("allOf") || content_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(content_schema)}; + if (inner.empty() && content_schema.defines("type") && + content_schema.at("type").is_string()) { + constraints.push_back(sourcemeta::core::JSON{ + "decoded: " + content_schema.at("type").to_string()}); + } + + for (const auto &constraint : inner.as_array()) { + constraints.push_back( + sourcemeta::core::JSON{"decoded " + constraint.to_string()}); + } + } + } + + if (schema.defines("not") && schema.at("not").is_object()) { + const auto ¬_schema{schema.at("not")}; + const auto is_branching{ + not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not")}; + if (!is_branching) { + const auto inner{constraints_of(not_schema)}; + for (const auto &constraint : inner.as_array()) { + constraints.push_back( + sourcemeta::core::JSON{"must NOT match " + constraint.to_string()}); + } + } + } + + return constraints; +} + +auto is_required_property(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON::String &property) + -> bool { + if (!schema.is_object() || !schema.defines("required") || + !schema.at("required").is_array()) { + return false; + } + for (const auto &item : schema.at("required").as_array()) { + if (item.is_string() && item.to_string() == property) { + return true; + } + } + return false; +} + +auto make_path_segment(const sourcemeta::core::JSON::String &type, + const sourcemeta::core::JSON::String &value) + -> sourcemeta::core::JSON { + auto segment{sourcemeta::core::JSON::make_object()}; + segment.assign("type", sourcemeta::core::JSON{type}); + segment.assign("value", sourcemeta::core::JSON{value}); + return segment; +} + +auto make_section(const std::string &label, sourcemeta::core::JSON tables) + -> sourcemeta::core::JSON { + auto section{sourcemeta::core::JSON::make_object()}; + section.assign("label", sourcemeta::core::JSON{label}); + section.assign("children", std::move(tables)); + return section; +} + +auto walk_schema(const sourcemeta::core::JSON &schema, bool include_root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> sourcemeta::core::JSON; + +auto is_complex_schema(const sourcemeta::core::JSON &schema) -> bool; + +auto walk_branching_subschema(const std::string &label, + const std::string &synthetic_name, + const sourcemeta::core::JSON &inner_schema, + sourcemeta::core::JSON &doc_children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier, + bool include_properties) -> void; + +auto walk_branches(const std::string &keyword, const std::string &label, + const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_all_of(const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &rows, sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_if_then_else(const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void; + +auto walk_wildcard_keyword(const sourcemeta::core::JSON &schema, + const std::string &keyword, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto walk_pattern_properties(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier) -> void; + +auto resolve_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + const VisitedSchemas &visited) + -> const sourcemeta::core::JSON & { + if (schema.is_object() && schema.defines("$ref") && + schema.at("$ref").is_string()) { + const auto target{ + resolve_destination(schema.at("$ref").to_string(), frame)}; + if (target.has_value()) { + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + if (visited.find(&target_schema) != visited.end()) { + return schema; // NOLINT(bugprone-return-const-ref-from-parameter) + } + return target_schema; + } + } + return schema; // NOLINT(bugprone-return-const-ref-from-parameter) +} + +auto emit_row(const sourcemeta::core::JSON &schema, sourcemeta::core::JSON path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier, + const bool expand_applicators = true) -> void { + auto row{sourcemeta::core::JSON::make_object()}; + row.assign("identifier", sourcemeta::core::JSON{ + static_cast(next_identifier++)}); + row.assign("path", std::move(path)); + + auto modifiers{modifiers_of(schema)}; + if (!modifiers.empty()) { + row.assign("modifiers", std::move(modifiers)); + } + + row.assign("type", type_expression_of(schema, frame, root, visited)); + + auto badges{badges_of(schema)}; + if (!badges.empty()) { + row.assign("badges", std::move(badges)); + } + + auto constraints{constraints_of(schema)}; + if (!constraints.empty()) { + row.assign("constraints", std::move(constraints)); + } + + if (schema.is_object()) { + if (schema.defines("title") && schema.at("title").is_string()) { + row.assign("title", + sourcemeta::core::JSON{schema.at("title").to_string()}); + } + if (schema.defines("description") && schema.at("description").is_string()) { + row.assign("description", + sourcemeta::core::JSON{schema.at("description").to_string()}); + } + if (schema.defines("default")) { + row.assign("default", schema.at("default")); + } + if (schema.defines("examples") && schema.at("examples").is_array()) { + auto examples{sourcemeta::core::JSON::make_array()}; + for (const auto &example : schema.at("examples").as_array()) { + examples.push_back(example); + } + row.assign("examples", std::move(examples)); + } + } + + if (expand_applicators && is_complex_schema(schema)) { + auto row_children{sourcemeta::core::JSON::make_array()}; + walk_branches("anyOf", "Any of", schema, row_children, frame, root, visited, + next_identifier); + walk_branches("oneOf", "One of", schema, row_children, frame, root, visited, + next_identifier); + walk_all_of(schema, rows, row_children, frame, root, visited, + next_identifier); + walk_if_then_else(schema, row_children, frame, root, visited, + next_identifier); + if (schema.is_object() && schema.defines("not")) { + const auto ¬_schema{schema.at("not")}; + const auto has_inline{ + not_schema.is_object() && + !(not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not")) && + !constraints_of(not_schema).empty()}; + if (!has_inline) { + walk_branching_subschema("Must NOT match", "value", not_schema, + row_children, frame, root, visited, + next_identifier, false); + } + } + if (!row_children.empty()) { + row.assign("children", std::move(row_children)); + } + } + + rows.push_back(std::move(row)); +} + +auto walk_properties(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + if (!schema.is_object() || !schema.defines("properties") || + !schema.at("properties").is_object()) { + return; + } + + for (const auto &entry : schema.at("properties").as_object()) { + const auto &resolved{resolve_ref(entry.second, frame, root, visited)}; + auto path{base_path}; + path.push_back(make_path_segment("literal", entry.first)); + + auto row{sourcemeta::core::JSON::make_object()}; + row.assign("identifier", sourcemeta::core::JSON{ + static_cast(next_identifier++)}); + row.assign("path", path); + + auto modifiers{modifiers_of(resolved)}; + if (!modifiers.empty()) { + row.assign("modifiers", std::move(modifiers)); + } + + row.assign("type", type_expression_of(resolved, frame, root, visited)); + + auto badges{badges_of(resolved)}; + if (!badges.empty()) { + row.assign("badges", std::move(badges)); + } + + row.assign("required", sourcemeta::core::JSON{ + is_required_property(schema, entry.first)}); + + auto constraints{constraints_of(resolved)}; + if (!constraints.empty()) { + row.assign("constraints", std::move(constraints)); + } + + if (resolved.is_object()) { + if (resolved.defines("title") && resolved.at("title").is_string()) { + row.assign("title", + sourcemeta::core::JSON{resolved.at("title").to_string()}); + } + if (resolved.defines("description") && + resolved.at("description").is_string()) { + row.assign("description", sourcemeta::core::JSON{ + resolved.at("description").to_string()}); + } + if (resolved.defines("default")) { + row.assign("default", resolved.at("default")); + } + if (resolved.defines("examples") && resolved.at("examples").is_array()) { + auto examples{sourcemeta::core::JSON::make_array()}; + for (const auto &example : resolved.at("examples").as_array()) { + examples.push_back(example); + } + row.assign("examples", std::move(examples)); + } + } + + const auto row_identifier{ + static_cast(row.at("identifier").to_integer())}; + + if (is_complex_schema(resolved)) { + auto prop_children{sourcemeta::core::JSON::make_array()}; + walk_branches("anyOf", "Any of", resolved, prop_children, frame, root, + visited, next_identifier); + walk_branches("oneOf", "One of", resolved, prop_children, frame, root, + visited, next_identifier); + walk_all_of(resolved, rows, prop_children, frame, root, visited, + next_identifier); + walk_if_then_else(resolved, prop_children, frame, root, visited, + next_identifier); + if (resolved.defines("not")) { + const auto ¬_schema{resolved.at("not")}; + const auto has_inline{ + not_schema.is_object() && + !(not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not")) && + !constraints_of(not_schema).empty()}; + if (!has_inline) { + walk_branching_subschema("Must NOT match", "value", not_schema, + prop_children, frame, root, visited, + next_identifier, false); + } + } + if (!prop_children.empty()) { + row.assign("children", std::move(prop_children)); + } + } + + rows.push_back(std::move(row)); + + if (resolved.is_object() && resolved.defines("type") && + resolved.at("type").is_string()) { + const auto &resolved_type{resolved.at("type").to_string()}; + if (resolved_type == "object") { + visited.emplace(&resolved, VisitedEntry{.identifier = row_identifier, + .path = path}); + walk_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(resolved, "additionalProperties", path, rows, + frame, root, visited, next_identifier); + walk_wildcard_keyword(resolved, "unevaluatedProperties", path, rows, + frame, root, visited, next_identifier); + if (!resolved.defines("additionalProperties") && + !resolved.defines("unevaluatedProperties")) { + auto open_path{path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, + frame, root, visited, next_identifier); + } + visited.erase(&resolved); + } else if (resolved_type == "array" && resolved.defines("items") && + resolved.at("items").is_object() && + !resolved.defines("prefixItems")) { + const auto &items_schema{ + resolve_ref(resolved.at("items"), frame, root, visited)}; + if (items_schema.is_object()) { + auto wildcard_path{path}; + wildcard_path.push_back(make_path_segment("wildcard", "*")); + const auto items_row_id{next_identifier}; + emit_row(items_schema, wildcard_path, rows, frame, root, visited, + next_identifier); + if (items_schema.defines("type") && + items_schema.at("type").is_string() && + items_schema.at("type").to_string() == "object") { + visited.emplace(&items_schema, + VisitedEntry{.identifier = items_row_id, + .path = wildcard_path}); + walk_properties(items_schema, wildcard_path, rows, frame, root, + visited, next_identifier); + walk_pattern_properties(items_schema, wildcard_path, rows, frame, + root, visited, next_identifier); + walk_wildcard_keyword(items_schema, "additionalProperties", + wildcard_path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(items_schema, "unevaluatedProperties", + wildcard_path, rows, frame, root, visited, + next_identifier); + if (!items_schema.defines("additionalProperties") && + !items_schema.defines("unevaluatedProperties")) { + auto open_path{wildcard_path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, + frame, root, visited, next_identifier); + } + visited.erase(&items_schema); + } + } + } + } + } +} + +auto walk_wildcard_keyword(const sourcemeta::core::JSON &schema, + const std::string &keyword, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines(keyword)) { + return; + } + + const auto &value{schema.at(keyword)}; + + if (keyword == "unevaluatedItems" && schema.defines("prefixItems")) { + return; + } + + if (value.is_boolean() && value.to_boolean()) { + auto path{base_path}; + path.push_back(make_path_segment("wildcard", "*")); + emit_row(value, std::move(path), rows, frame, root, visited, + next_identifier); + return; + } + + if (!value.is_object()) { + return; + } + + auto path{base_path}; + path.push_back(make_path_segment("wildcard", "*")); + const auto wildcard_row_id{next_identifier}; + emit_row(value, path, rows, frame, root, visited, next_identifier); + + if (value.defines("type") && value.at("type").is_string() && + value.at("type").to_string() == "object") { + visited.emplace(&value, + VisitedEntry{.identifier = wildcard_row_id, .path = path}); + walk_properties(value, path, rows, frame, root, visited, next_identifier); + walk_pattern_properties(value, path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(value, "additionalProperties", path, rows, frame, + root, visited, next_identifier); + walk_wildcard_keyword(value, "unevaluatedProperties", path, rows, frame, + root, visited, next_identifier); + if (!value.defines("additionalProperties") && + !value.defines("unevaluatedProperties")) { + auto open_path{path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, frame, + root, visited, next_identifier); + } + visited.erase(&value); + } else if (value.defines("type") && value.at("type").is_string() && + value.at("type").to_string() == "array" && + value.defines("items") && value.at("items").is_object() && + !value.defines("prefixItems")) { + const auto &items_schema{ + resolve_ref(value.at("items"), frame, root, visited)}; + if (items_schema.is_object()) { + auto items_path{path}; + items_path.push_back(make_path_segment("wildcard", "*")); + const auto items_row_id{next_identifier}; + emit_row(items_schema, items_path, rows, frame, root, visited, + next_identifier); + if (items_schema.defines("type") && items_schema.at("type").is_string() && + items_schema.at("type").to_string() == "object") { + visited.emplace(&items_schema, VisitedEntry{.identifier = items_row_id, + .path = items_path}); + walk_properties(items_schema, items_path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(items_schema, items_path, rows, frame, root, + visited, next_identifier); + walk_wildcard_keyword(items_schema, "additionalProperties", items_path, + rows, frame, root, visited, next_identifier); + walk_wildcard_keyword(items_schema, "unevaluatedProperties", items_path, + rows, frame, root, visited, next_identifier); + if (!items_schema.defines("additionalProperties") && + !items_schema.defines("unevaluatedProperties")) { + auto open_path{items_path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, + frame, root, visited, next_identifier); + } + visited.erase(&items_schema); + } + } + } +} + +auto walk_pattern_properties(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("patternProperties") || + !schema.at("patternProperties").is_object()) { + return; + } + + for (const auto &entry : schema.at("patternProperties").as_object()) { + const auto &resolved{resolve_ref(entry.second, frame, root, visited)}; + auto path{base_path}; + path.push_back(make_path_segment("pattern", entry.first)); + + const auto row_id{next_identifier}; + emit_row(resolved, path, rows, frame, root, visited, next_identifier); + + if (resolved.is_object() && resolved.defines("type") && + resolved.at("type").is_string() && + resolved.at("type").to_string() == "object") { + visited.emplace(&resolved, + VisitedEntry{.identifier = row_id, .path = path}); + walk_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(resolved, path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(resolved, "additionalProperties", path, rows, frame, + root, visited, next_identifier); + walk_wildcard_keyword(resolved, "unevaluatedProperties", path, rows, + frame, root, visited, next_identifier); + if (!resolved.defines("additionalProperties") && + !resolved.defines("unevaluatedProperties")) { + auto open_path{path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, + frame, root, visited, next_identifier); + } + visited.erase(&resolved); + } + } +} + +auto is_complex_schema(const sourcemeta::core::JSON &schema) -> bool { + if (!schema.is_object()) { + return false; + } + return schema.defines("properties") || schema.defines("anyOf") || + schema.defines("oneOf") || schema.defines("allOf") || + schema.defines("not") || schema.defines("if") || + schema.defines("prefixItems") || schema.defines("contains") || + schema.defines("patternProperties") || + schema.defines("additionalProperties") || + schema.defines("propertyNames") || schema.defines("contentSchema"); +} + +auto walk_prefix_items(const sourcemeta::core::JSON &schema, + const sourcemeta::core::JSON &base_path, + sourcemeta::core::JSON &rows, + sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + const auto has_prefix_items{schema.is_object() && + schema.defines("prefixItems") && + schema.at("prefixItems").is_array()}; + const auto has_draft4_tuple{!has_prefix_items && schema.is_object() && + schema.defines("items") && + schema.at("items").is_array()}; + if (!has_prefix_items && !has_draft4_tuple) { + return; + } + + const auto &tuple_items{has_prefix_items ? schema.at("prefixItems") + : schema.at("items")}; + + std::size_t min_items{0}; + if (schema.defines("minItems") && schema.at("minItems").is_integer() && + schema.at("minItems").to_integer() > 0) { + min_items = static_cast(schema.at("minItems").to_integer()); + } + + std::size_t index{0}; + for (const auto &item : tuple_items.as_array()) { + if (is_complex_schema(item)) { + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back( + walk_schema(item, true, frame, root, visited, next_identifier)); + children.push_back(make_section("Array item " + std::to_string(index), + std::move(section_children))); + } else { + auto path{base_path}; + path.push_back(make_path_segment("literal", std::to_string(index))); + + auto row{sourcemeta::core::JSON::make_object()}; + row.assign("identifier", sourcemeta::core::JSON{static_cast( + next_identifier++)}); + row.assign("path", std::move(path)); + + auto modifiers{modifiers_of(item)}; + if (!modifiers.empty()) { + row.assign("modifiers", std::move(modifiers)); + } + + row.assign("type", type_expression_of(item, frame, root, visited)); + + auto badges{badges_of(item)}; + if (!badges.empty()) { + row.assign("badges", std::move(badges)); + } + + row.assign("required", sourcemeta::core::JSON{index < min_items}); + + auto constraints{constraints_of(item)}; + if (!constraints.empty()) { + row.assign("constraints", std::move(constraints)); + } + + if (item.is_object()) { + if (item.defines("title") && item.at("title").is_string()) { + row.assign("title", + sourcemeta::core::JSON{item.at("title").to_string()}); + } + if (item.defines("description") && item.at("description").is_string()) { + row.assign("description", sourcemeta::core::JSON{ + item.at("description").to_string()}); + } + if (item.defines("default")) { + row.assign("default", item.at("default")); + } + if (item.defines("examples") && item.at("examples").is_array()) { + auto examples{sourcemeta::core::JSON::make_array()}; + for (const auto &example : item.at("examples").as_array()) { + examples.push_back(example); + } + row.assign("examples", std::move(examples)); + } + } + + rows.push_back(std::move(row)); + } + ++index; + } + + if (has_prefix_items && schema.defines("items") && + schema.at("items").is_object()) { + auto path{base_path}; + path.push_back(make_path_segment("wildcard", "*")); + emit_row(schema.at("items"), std::move(path), rows, frame, root, visited, + next_identifier); + } else if (has_draft4_tuple && schema.defines("additionalItems") && + schema.at("additionalItems").is_object()) { + auto path{base_path}; + path.push_back(make_path_segment("wildcard", "*")); + emit_row(schema.at("additionalItems"), std::move(path), rows, frame, root, + visited, next_identifier); + } +} + +auto walk_branches(const std::string &keyword, const std::string &label, + const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines(keyword) || + !schema.at(keyword).is_array()) { + return; + } + + auto section_children{sourcemeta::core::JSON::make_array()}; + for (const auto &branch : schema.at(keyword).as_array()) { + section_children.push_back( + walk_schema(branch, false, frame, root, visited, next_identifier)); + } + children.push_back(make_section(label, std::move(section_children))); +} + +auto has_recursive_ref_in_rows(const sourcemeta::core::JSON &rows) -> bool { + for (const auto &row : rows.as_array()) { + const auto &type{row.at("type")}; + if (type.defines("kind") && type.at("kind").to_string() == "recursiveRef") { + return true; + } + if (type.defines("kind") && type.at("kind").to_string() == "array" && + type.defines("items")) { + const auto &items{type.at("items")}; + if (items.is_object() && items.defines("kind") && + items.at("kind").to_string() == "recursiveRef") { + return true; + } + } + } + return false; +} + +auto walk_all_of(const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &rows, sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> void { + if (!schema.is_object() || !schema.defines("allOf") || + !schema.at("allOf").is_array()) { + return; + } + + const auto &all_of{schema.at("allOf").as_array()}; + + if (all_of.size() == 1) { + auto branch{walk_schema(schema.at("allOf").at(0), false, frame, root, + visited, next_identifier)}; + const auto &branch_rows{branch.at("rows")}; + const auto branch_has_recursive_ref{has_recursive_ref_in_rows(branch_rows)}; + + if (!branch_has_recursive_ref) { + // Merge the branch's root row info into the parent's root row + if (!rows.empty() && !branch_rows.empty()) { + const auto &parent_last_type{rows.at(rows.size() - 1).at("type")}; + const auto &branch_first{branch_rows.at(0)}; + const auto parent_is_any{parent_last_type.defines("kind") && + parent_last_type.at("kind").to_string() == + "any"}; + const auto &branch_first_path{branch_first.at("path")}; + const auto branch_first_is_synthetic_root{ + !branch_first_path.empty() && + branch_first_path.at(0).at("type").to_string() == "synthetic" && + branch_first_path.at(0).at("value").to_string() == "root"}; + + if (parent_is_any && branch_first_is_synthetic_root) { + // Copy fields from branch root into parent root + auto &parent_root{rows.at(rows.size() - 1)}; + parent_root.assign("type", branch_first.at("type")); + + if (branch_first.defines("constraints")) { + parent_root.assign("constraints", branch_first.at("constraints")); + } else if (parent_root.defines("constraints")) { + parent_root.erase("constraints"); + } + + if (branch_first.defines("title") && !parent_root.defines("title")) { + parent_root.assign("title", branch_first.at("title")); + } + if (branch_first.defines("description") && + !parent_root.defines("description")) { + parent_root.assign("description", branch_first.at("description")); + } + if (branch_first.defines("default") && + !parent_root.defines("default")) { + parent_root.assign("default", branch_first.at("default")); + } + + if (branch_first.defines("modifiers")) { + parent_root.assign("modifiers", branch_first.at("modifiers")); + } else if (parent_root.defines("modifiers")) { + parent_root.erase("modifiers"); + } + + if (branch_first.defines("badges")) { + parent_root.assign("badges", branch_first.at("badges")); + } else if (parent_root.defines("badges")) { + parent_root.erase("badges"); + } + + for (std::size_t index = 1; index < branch_rows.size(); ++index) { + rows.push_back(branch_rows.at(index)); + } + } else { + for (const auto &row : branch_rows.as_array()) { + rows.push_back(row); + } + } + } else { + for (const auto &row : branch_rows.as_array()) { + rows.push_back(row); + } + } + + if (branch.defines("children")) { + for (const auto &child : branch.at("children").as_array()) { + children.push_back(child); + } + } + return; + } + + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back(std::move(branch)); + children.push_back(make_section("All of", std::move(section_children))); + return; + } + + auto section_children{sourcemeta::core::JSON::make_array()}; + for (const auto &branch : all_of) { + section_children.push_back( + walk_schema(branch, false, frame, root, visited, next_identifier)); + } + children.push_back(make_section("All of", std::move(section_children))); +} + +auto walk_if_then_else(const sourcemeta::core::JSON &schema, + sourcemeta::core::JSON &children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, std::size_t &next_identifier) + -> void { + if (!schema.is_object() || !schema.defines("if") || !schema.defines("then") || + !schema.defines("else")) { + return; + } + + { + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back(walk_schema(schema.at("if"), false, frame, root, + visited, next_identifier)); + children.push_back(make_section("If", std::move(section_children))); + } + + { + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back(walk_schema(schema.at("then"), false, frame, + root, visited, next_identifier)); + children.push_back(make_section("Then", std::move(section_children))); + } + + { + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back(walk_schema(schema.at("else"), false, frame, + root, visited, next_identifier)); + children.push_back(make_section("Else", std::move(section_children))); + } +} + +auto walk_branching_subschema(const std::string &label, + const std::string &synthetic_name, + const sourcemeta::core::JSON &inner_schema, + sourcemeta::core::JSON &doc_children, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, + VisitedSchemas &visited, + std::size_t &next_identifier, + const bool include_properties) -> void { + auto table{sourcemeta::core::JSON::make_object()}; + table.assign("identifier", sourcemeta::core::JSON{ + static_cast(next_identifier++)}); + auto table_rows{sourcemeta::core::JSON::make_array()}; + auto table_children{sourcemeta::core::JSON::make_array()}; + auto synthetic_path{sourcemeta::core::JSON::make_array()}; + synthetic_path.push_back(make_path_segment("synthetic", synthetic_name)); + if (include_properties) { + walk_properties(inner_schema, synthetic_path, table_rows, frame, root, + visited, next_identifier); + } + emit_row(inner_schema, std::move(synthetic_path), table_rows, frame, root, + visited, next_identifier, false); + walk_branches("anyOf", "Any of", inner_schema, table_children, frame, root, + visited, next_identifier); + walk_branches("oneOf", "One of", inner_schema, table_children, frame, root, + visited, next_identifier); + walk_all_of(inner_schema, table_rows, table_children, frame, root, visited, + next_identifier); + table.assign("rows", std::move(table_rows)); + if (!table_children.empty()) { + table.assign("children", std::move(table_children)); + } + auto section_children{sourcemeta::core::JSON::make_array()}; + section_children.push_back(std::move(table)); + doc_children.push_back(make_section(label, std::move(section_children))); +} + +auto walk_schema(const sourcemeta::core::JSON &schema, const bool include_root, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &root, VisitedSchemas &visited, + std::size_t &next_identifier) -> sourcemeta::core::JSON { + if (schema.is_object() && schema.defines("$ref") && + schema.at("$ref").is_string()) { + const auto target{ + resolve_destination(schema.at("$ref").to_string(), frame)}; + if (target.has_value()) { + const auto &target_schema{ + sourcemeta::core::get(root, target->get().pointer)}; + const auto visited_entry{visited.find(&target_schema)}; + if (visited_entry != visited.end()) { + auto documentation{sourcemeta::core::JSON::make_object()}; + documentation.assign("identifier", + sourcemeta::core::JSON{ + static_cast(next_identifier++)}); + auto rows{sourcemeta::core::JSON::make_array()}; + auto row{sourcemeta::core::JSON::make_object()}; + row.assign("identifier", + sourcemeta::core::JSON{ + static_cast(next_identifier++)}); + auto path{sourcemeta::core::JSON::make_array()}; + path.push_back(make_path_segment("synthetic", "root")); + row.assign("path", std::move(path)); + auto type_expr{sourcemeta::core::JSON::make_object()}; + type_expr.assign("kind", sourcemeta::core::JSON{"recursiveRef"}); + type_expr.assign("identifier", + sourcemeta::core::JSON{static_cast( + visited_entry->second.identifier)}); + type_expr.assign("path", visited_entry->second.path); + row.assign("type", std::move(type_expr)); + rows.push_back(std::move(row)); + documentation.assign("rows", std::move(rows)); + return documentation; + } + auto ref_path{sourcemeta::core::JSON::make_array()}; + ref_path.push_back(make_path_segment("synthetic", "root")); + visited.emplace(&target_schema, + VisitedEntry{.identifier = next_identifier, + .path = std::move(ref_path)}); + auto result{walk_schema(target_schema, include_root, frame, root, visited, + next_identifier)}; + visited.erase(&target_schema); + return result; + } + } + + auto documentation{sourcemeta::core::JSON::make_object()}; + const auto doc_identifier{next_identifier++}; + documentation.assign( + "identifier", + sourcemeta::core::JSON{static_cast(doc_identifier)}); + + if (schema.is_object() && schema.defines("$dynamicAnchor") && + schema.at("$dynamicAnchor").is_string()) { + documentation.assign( + "dynamicAnchor", + sourcemeta::core::JSON{schema.at("$dynamicAnchor").to_string()}); + } + + auto rows{sourcemeta::core::JSON::make_array()}; + auto doc_children{sourcemeta::core::JSON::make_array()}; + + if (include_root) { + auto root_path{sourcemeta::core::JSON::make_array()}; + root_path.push_back(make_path_segment("synthetic", "root")); + emit_row(schema, std::move(root_path), rows, frame, root, visited, + next_identifier, false); + const auto root_row_identifier{static_cast( + rows.at(rows.size() - 1).at("identifier").to_integer())}; + auto visited_root_path{sourcemeta::core::JSON::make_array()}; + visited_root_path.push_back(make_path_segment("synthetic", "root")); + visited.emplace(&schema, + VisitedEntry{.identifier = root_row_identifier, + .path = std::move(visited_root_path)}); + } + + if (!schema.is_object()) { + if (!include_root) { + auto root_path{sourcemeta::core::JSON::make_array()}; + root_path.push_back(make_path_segment("synthetic", "root")); + emit_row(schema, std::move(root_path), rows, frame, root, visited, + next_identifier, false); + } + documentation.assign("rows", std::move(rows)); + return documentation; + } + + if (!include_root) { + auto root_path{sourcemeta::core::JSON::make_array()}; + root_path.push_back(make_path_segment("synthetic", "root")); + emit_row(schema, std::move(root_path), rows, frame, root, visited, + next_identifier, false); + } + + const auto empty_path{sourcemeta::core::JSON::make_array()}; + walk_properties(schema, empty_path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(schema, empty_path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(schema, "additionalProperties", empty_path, rows, frame, + root, visited, next_identifier); + if (schema.defines("type") && schema.at("type").is_string() && + schema.at("type").to_string() == "object" && + !schema.defines("additionalProperties") && + !schema.defines("unevaluatedProperties")) { + auto open_path{empty_path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, frame, + root, visited, next_identifier); + } + walk_prefix_items(schema, empty_path, rows, doc_children, frame, root, + visited, next_identifier); + + if (schema.is_object() && schema.defines("items") && + schema.at("items").is_object() && !schema.defines("prefixItems")) { + const auto &items_schema{ + resolve_ref(schema.at("items"), frame, root, visited)}; + if (items_schema.is_object()) { + auto wildcard_path{sourcemeta::core::JSON::make_array()}; + wildcard_path.push_back(make_path_segment("wildcard", "*")); + const auto items_row_id{next_identifier}; + emit_row(items_schema, wildcard_path, rows, frame, root, visited, + next_identifier); + if (items_schema.defines("type") && items_schema.at("type").is_string() && + items_schema.at("type").to_string() == "object") { + visited.emplace(&items_schema, VisitedEntry{.identifier = items_row_id, + .path = wildcard_path}); + walk_properties(items_schema, wildcard_path, rows, frame, root, visited, + next_identifier); + walk_pattern_properties(items_schema, wildcard_path, rows, frame, root, + visited, next_identifier); + walk_wildcard_keyword(items_schema, "additionalProperties", + wildcard_path, rows, frame, root, visited, + next_identifier); + walk_wildcard_keyword(items_schema, "unevaluatedProperties", + wildcard_path, rows, frame, root, visited, + next_identifier); + if (!items_schema.defines("additionalProperties") && + !items_schema.defines("unevaluatedProperties")) { + auto open_path{wildcard_path}; + open_path.push_back(make_path_segment("wildcard", "*")); + emit_row(sourcemeta::core::JSON{true}, std::move(open_path), rows, + frame, root, visited, next_identifier); + } + visited.erase(&items_schema); + } + } + } + + walk_branches("anyOf", "Any of", schema, doc_children, frame, root, visited, + next_identifier); + walk_branches("oneOf", "One of", schema, doc_children, frame, root, visited, + next_identifier); + walk_all_of(schema, rows, doc_children, frame, root, visited, + next_identifier); + walk_if_then_else(schema, doc_children, frame, root, visited, + next_identifier); + walk_wildcard_keyword(schema, "unevaluatedProperties", empty_path, rows, + frame, root, visited, next_identifier); + walk_wildcard_keyword(schema, "unevaluatedItems", empty_path, rows, frame, + root, visited, next_identifier); + + if (schema.is_object() && schema.defines("contains") && + schema.at("contains").is_object()) { + const auto &contains_schema{schema.at("contains")}; + const auto is_branching{ + contains_schema.defines("anyOf") || contains_schema.defines("oneOf") || + contains_schema.defines("allOf") || contains_schema.defines("not") || + contains_schema.defines("enum")}; + if (is_branching) { + walk_branching_subschema("Contains", "matching item", contains_schema, + doc_children, frame, root, visited, + next_identifier, false); + } + } + + if (schema.is_object() && schema.defines("contentSchema") && + schema.at("contentSchema").is_object()) { + const auto &content_schema{schema.at("contentSchema")}; + const auto is_branching{ + content_schema.defines("anyOf") || content_schema.defines("oneOf") || + content_schema.defines("allOf") || content_schema.defines("not")}; + if (is_branching) { + walk_branching_subschema("Decoded content", "decoded", content_schema, + doc_children, frame, root, visited, + next_identifier, true); + } + } + + if (schema.is_object() && schema.defines("propertyNames") && + schema.at("propertyNames").is_object()) { + const auto &names_schema{schema.at("propertyNames")}; + const auto is_branching{ + names_schema.defines("anyOf") || names_schema.defines("oneOf") || + names_schema.defines("allOf") || names_schema.defines("not")}; + if (is_branching) { + walk_branching_subschema("Property names", "key", names_schema, + doc_children, frame, root, visited, + next_identifier, false); + } + } + + if (schema.is_object() && schema.defines("not")) { + const auto ¬_schema{schema.at("not")}; + const auto is_branching{ + not_schema.is_object() && + (not_schema.defines("anyOf") || not_schema.defines("oneOf") || + not_schema.defines("allOf") || not_schema.defines("not"))}; + const auto has_inline_constraints{!is_branching && not_schema.is_object() && + !constraints_of(not_schema).empty()}; + if (!has_inline_constraints) { + walk_branching_subschema("Must NOT match", "value", not_schema, + doc_children, frame, root, visited, + next_identifier, false); + } + } + + assert(!rows.empty() || !doc_children.empty()); + + documentation.assign("rows", std::move(rows)); + if (!doc_children.empty()) { + documentation.assign("children", std::move(doc_children)); + } + + return documentation; +} + +} // namespace + +auto to_documentation(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) + -> sourcemeta::core::JSON { + sourcemeta::blaze::SchemaTransformer canonicalizer; + sourcemeta::blaze::add(canonicalizer, + sourcemeta::blaze::AlterSchemaMode::Canonicalizer); + sourcemeta::core::JSON canonical{schema}; + [[maybe_unused]] const auto canonicalized{canonicalizer.apply( + canonical, walker, resolver, + [](const auto &, const auto, const auto, const auto &, + [[maybe_unused]] const auto applied) { assert(applied); })}; + assert(canonicalized.first); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(canonical, walker, resolver); + + VisitedSchemas visited; + std::size_t next_identifier{0}; + return walk_schema(canonical, true, frame, canonical, visited, + next_identifier); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/documentation/documentation_html.cc b/vendor/blaze/src/documentation/documentation_html.cc new file mode 100644 index 000000000..91a3bbb77 --- /dev/null +++ b/vendor/blaze/src/documentation/documentation_html.cc @@ -0,0 +1,412 @@ +#include + +#include +#include + +#include // assert +#include // std::set +#include // std::ostringstream +#include // std::string, std::to_string + +namespace sourcemeta::blaze { + +namespace { + +auto json_to_string(const sourcemeta::core::JSON &value) -> std::string { + std::ostringstream stream; + sourcemeta::core::stringify(value, stream); + return stream.str(); +} + +auto is_empty_row(const sourcemeta::core::JSON &row) -> bool { + assert(row.is_object()); + assert(row.defines("type")); + return row.at("type").defines("kind") && + row.at("type").at("kind").to_string() == "any" && + !row.defines("constraints") && !row.defines("badges") && + !row.defines("modifiers") && !row.defines("title") && + !row.defines("description") && !row.defines("default") && + !row.defines("examples"); +} + +auto collect_ref_targets(const sourcemeta::core::JSON &table, + std::set &targets) -> void { + for (const auto &row : table.at("rows").as_array()) { + if (row.defines("type") && row.at("type").defines("kind") && + row.at("type").at("kind").to_string() == "recursiveRef" && + row.at("type").defines("identifier")) { + targets.insert(row.at("type").at("identifier").to_integer()); + } + if (row.defines("children")) { + for (const auto §ion : row.at("children").as_array()) { + for (const auto &child : section.at("children").as_array()) { + collect_ref_targets(child, targets); + } + } + } + } + if (table.defines("children")) { + for (const auto §ion : table.at("children").as_array()) { + for (const auto &child : section.at("children").as_array()) { + collect_ref_targets(child, targets); + } + } + } +} + +auto render_path(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &path) -> void { + assert(path.is_array()); + writer.code(); + bool first{true}; + for (const auto &segment : path.as_array()) { + assert(segment.defines("type")); + assert(segment.defines("value")); + const auto &type{segment.at("type").to_string()}; + const auto &value{segment.at("value").to_string()}; + + if (!first) { + writer.text("/"); + } + + if (type == "literal" || type == "pattern") { + writer.text(first ? "/" + value : value); + } else if (type == "wildcard") { + writer.text(first ? "/*" : "*"); + } else if (type == "synthetic") { + writer.em("(" + value + ")"); + } + + first = false; + } + + writer.close(); +} + +auto render_modifiers(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &row) -> void { + if (!row.defines("modifiers")) { + return; + } + + for (const auto &modifier : row.at("modifiers").as_array()) { + writer.span(modifier.to_string()); + } +} + +auto render_enum_values(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &values, + const bool leading_separator) -> void { + assert(values.is_array()); + bool first{true}; + for (const auto &value : values.as_array()) { + if (!first || leading_separator) { + writer.text(" | "); + } + + writer.code(json_to_string(value)); + first = false; + } +} + +auto render_type_expression(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &type) -> void { + assert(type.is_object()); + assert(type.defines("kind")); + const auto &kind{type.at("kind").to_string()}; + + if (kind == "object") { + writer.text("Object"); + } else if (kind == "primitive") { + assert(type.defines("name")); + const auto &name{type.at("name").to_string()}; + if (name == "string") { + writer.text("String"); + } else if (name == "integer") { + writer.text("Integer"); + } else if (name == "number") { + writer.text("Number"); + } + } else if (kind == "array" || kind == "tuple") { + writer.text("Array"); + } else if (kind == "enum") { + assert(type.defines("values")); + render_enum_values(writer, type.at("values"), false); + if (type.defines("overflow")) { + writer.details(); + writer.summary("+ " + std::to_string(type.at("overflow").array_size()) + + " more"); + render_enum_values(writer, type.at("overflow"), true); + writer.close(); + } + } else if (kind == "externalRef") { + assert(type.defines("url")); + const auto &url{type.at("url").to_string()}; + writer.a().attribute("href", url); + writer.text(url); + writer.close(); + } else if (kind == "recursiveRef") { + assert(type.defines("identifier")); + const auto identifier{std::to_string(type.at("identifier").to_integer())}; + writer.a().attribute("data-index", identifier); + if (type.defines("path")) { + bool first_seg{true}; + for (const auto &segment : type.at("path").as_array()) { + const auto &seg_type{segment.at("type").to_string()}; + const auto &seg_value{segment.at("value").to_string()}; + if (!first_seg) { + writer.text("/"); + } + if (seg_type == "synthetic") { + writer.text("(" + seg_value + ")"); + } else if (seg_type == "literal" || seg_type == "pattern") { + writer.text(first_seg ? "/" + seg_value : seg_value); + } else if (seg_type == "wildcard") { + writer.text(first_seg ? "/*" : "*"); + } + first_seg = false; + } + writer.text(" #" + identifier); + } else { + writer.text(identifier); + } + writer.close(); + } else if (kind == "dynamicRef") { + assert(type.defines("anchor")); + writer.text("dynamic: " + type.at("anchor").to_string()); + } else if (kind == "any") { + writer.text("Any"); + } else if (kind == "never") { + writer.text("Never"); + } +} + +auto render_badges(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &row) -> void { + if (!row.defines("badges")) { + return; + } + + for (const auto &badge : row.at("badges").as_array()) { + assert(badge.defines("kind")); + assert(badge.defines("value")); + const auto &kind{badge.at("kind").to_string()}; + const auto &value{badge.at("value").to_string()}; + if (kind == "format") { + writer.span(value); + } else if (kind == "encoding") { + writer.span("encoding: " + value); + } else if (kind == "mime") { + writer.span("mime: " + value); + } + } +} + +auto render_notes(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &row) -> void { + if (row.defines("title")) { + writer.strong(row.at("title").to_string()); + } + + if (row.defines("description")) { + writer.p(row.at("description").to_string()); + } + + if (row.defines("default")) { + writer.span(); + writer.text("default: "); + writer.code(json_to_string(row.at("default"))); + writer.close(); + } +} + +auto render_row(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &row, + const std::set &ref_targets) -> void; +auto render_section(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON §ion, + const std::set &ref_targets) -> void; +auto render_table(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &table, + const std::set &ref_targets) -> void; + +auto emit_header(sourcemeta::core::HTMLWriter &writer) -> void { + writer.thead(); + writer.tr(); + writer.th("Path"); + writer.th("Type"); + writer.th("Required"); + writer.th("Constraints"); + writer.th("Notes"); + writer.close(); + writer.close(); +} + +auto render_row(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &row, + const std::set &ref_targets) -> void { + assert(row.defines("identifier")); + assert(row.defines("path")); + assert(row.defines("type")); + + const auto identifier{row.at("identifier").to_integer()}; + writer.tr().attribute("data-index", std::to_string(identifier)); + + // Path + writer.td(); + render_path(writer, row.at("path")); + if (ref_targets.contains(identifier)) { + writer.text(" "); + writer.strong("#" + std::to_string(identifier)); + } + render_modifiers(writer, row); + writer.close(); + + // Type + writer.td(); + render_type_expression(writer, row.at("type")); + render_badges(writer, row); + writer.close(); + + // Required + writer.td(); + if (row.defines("required")) { + writer.text(row.at("required").to_boolean() ? "Yes" : "No"); + } + writer.close(); + + // Constraints + writer.td(); + if (row.defines("constraints")) { + for (const auto &constraint : row.at("constraints").as_array()) { + writer.span(constraint.to_string()); + } + } + writer.close(); + + // Notes + writer.td(); + render_notes(writer, row); + writer.close(); + + writer.close(); + + if (row.defines("children")) { + for (const auto §ion : row.at("children").as_array()) { + render_section(writer, section, ref_targets); + } + } +} + +auto render_section(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON §ion, + const std::set &ref_targets) -> void { + assert(section.defines("label")); + assert(section.defines("children")); + + writer.tr(); + writer.td().attribute("colspan", "5"); + writer.div(); + + writer.div(); + writer.text(section.at("label").to_string()); + if (section.defines("position")) { + writer.text(" "); + writer.code(section.at("position").to_string()); + } + writer.close(); + + for (const auto &child : section.at("children").as_array()) { + writer.div(); + if (child.defines("title")) { + writer.div(child.at("title").to_string()); + } + + render_table(writer, child, ref_targets); + writer.close(); + } + + writer.close(); + writer.close(); + writer.close(); +} + +auto render_table(sourcemeta::core::HTMLWriter &writer, + const sourcemeta::core::JSON &table, + const std::set &ref_targets) -> void { + assert(table.defines("identifier")); + assert(table.defines("rows")); + + writer.table().attribute("data-index", + std::to_string(table.at("identifier").to_integer())); + + const auto &rows{table.at("rows")}; + const auto has_children{table.defines("children")}; + const auto root_is_ref_target{ + !rows.empty() && rows.at(0).defines("identifier") && + ref_targets.contains(rows.at(0).at("identifier").to_integer())}; + const auto skip_root{has_children && !rows.empty() && + is_empty_row(rows.at(0)) && !root_is_ref_target}; + + if (!skip_root || rows.array_size() > 1) { + emit_header(writer); + } + + writer.tbody(); + for (std::size_t index = skip_root ? 1 : 0; index < rows.array_size(); + ++index) { + render_row(writer, rows.at(index), ref_targets); + } + + if (has_children) { + for (const auto §ion : table.at("children").as_array()) { + render_section(writer, section, ref_targets); + } + } + + writer.close(); + writer.close(); +} + +} // namespace + +auto to_html(const sourcemeta::core::JSON &documentation) -> std::string { + assert(documentation.is_object()); + assert(documentation.defines("rows")); + + std::set ref_targets; + collect_ref_targets(documentation, ref_targets); + + sourcemeta::core::HTMLWriter writer; + writer.table().attribute("class", "sourcemeta-blaze-documentation"); + + const auto &rows{documentation.at("rows")}; + const auto has_children{documentation.defines("children")}; + const auto root_is_ref_target{ + !rows.empty() && rows.at(0).defines("identifier") && + ref_targets.contains(rows.at(0).at("identifier").to_integer())}; + const auto skip_root{has_children && !rows.empty() && + is_empty_row(rows.at(0)) && !root_is_ref_target}; + + if (!skip_root || rows.array_size() > 1) { + emit_header(writer); + } + + writer.tbody(); + for (std::size_t index = skip_root ? 1 : 0; index < rows.array_size(); + ++index) { + render_row(writer, rows.at(index), ref_targets); + } + + if (has_children) { + for (const auto §ion : documentation.at("children").as_array()) { + render_section(writer, section, ref_targets); + } + } + + writer.close(); + writer.close(); + return writer.str(); +} + +} // namespace sourcemeta::blaze diff --git a/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h b/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h new file mode 100644 index 000000000..07a18b41b --- /dev/null +++ b/vendor/blaze/src/documentation/include/sourcemeta/blaze/documentation.h @@ -0,0 +1,57 @@ +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_H_ +#define SOURCEMETA_BLAZE_DOCUMENTATION_H_ + +/// @defgroup documentation Documentation +/// @brief Generate human-readable documentation from a JSON Schema +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#ifndef SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT +#include +#endif + +#include +#include + +#include // std::string + +namespace sourcemeta::blaze { + +/// @ingroup documentation +/// +/// Generate documentation JSON from an input JSON Schema. For example: +/// +/// ```cpp +/// #include +/// +/// #include +/// #include +/// +/// const sourcemeta::core::JSON schema = +/// sourcemeta::core::parse_json(R"JSON({ +/// "$schema": "https://json-schema.org/draft/2020-12/schema", +/// "type": "string" +/// })JSON"); +/// +/// const auto documentation{sourcemeta::blaze::to_documentation( +/// schema, sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver)}; +/// ``` +[[nodiscard]] SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT auto +to_documentation(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) + -> sourcemeta::core::JSON; + +/// @ingroup documentation +/// Render a documentation JSON object as an HTML string +[[nodiscard]] SOURCEMETA_BLAZE_DOCUMENTATION_EXPORT auto +to_html(const sourcemeta::core::JSON &documentation) -> std::string; + +} // namespace sourcemeta::blaze + +#endif diff --git a/vendor/blaze/src/evaluator/evaluator_describe.cc b/vendor/blaze/src/evaluator/evaluator_describe.cc index 99a2556fc..70bfcc5e3 100644 --- a/vendor/blaze/src/evaluator/evaluator_describe.cc +++ b/vendor/blaze/src/evaluator/evaluator_describe.cc @@ -99,6 +99,27 @@ auto describe_type_check(const bool valid, } } +auto describe_not_type_check(const bool valid, + const sourcemeta::core::JSON::Type current, + const sourcemeta::core::JSON::Type expected, + std::ostringstream &message) -> void { + message << "The value was expected to NOT be of type "; + message << type_name(expected); + if (!valid) { + message << " but it was of type "; + if (current == sourcemeta::core::JSON::Type::Decimal && + expected == sourcemeta::core::JSON::Type::Integer) { + message << "integer"; + } else if ((current == sourcemeta::core::JSON::Type::Integer && + expected == sourcemeta::core::JSON::Type::Real) || + current == sourcemeta::core::JSON::Type::Decimal) { + message << "number"; + } else { + message << type_name(current); + } + } +} + auto describe_types_check(const bool valid, const sourcemeta::core::JSON::Type current, const ValueTypes expected, @@ -177,6 +198,84 @@ auto describe_types_check(const bool valid, } } +auto describe_not_types_check(const bool valid, + const sourcemeta::core::JSON::Type current, + const ValueTypes expected, + std::ostringstream &message) -> void { + ValueTypes types{expected}; + const auto has_real{ + types.test(std::to_underlying(sourcemeta::core::JSON::Type::Real))}; + const auto has_integer{ + types.test(std::to_underlying(sourcemeta::core::JSON::Type::Integer))}; + const auto has_decimal{ + types.test(std::to_underlying(sourcemeta::core::JSON::Type::Decimal))}; + + if (has_real && has_integer) { + types.reset(std::to_underlying(sourcemeta::core::JSON::Type::Integer)); + } + if (has_real && has_decimal) { + types.reset(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + } + if (has_integer && has_decimal) { + types.reset(std::to_underlying(sourcemeta::core::JSON::Type::Decimal)); + } + + const auto popcount{types.count()}; + + if (popcount == 1) { + std::uint8_t type_index{0}; + for (std::uint8_t bit{0}; bit < 8; bit++) { + if (types.test(bit)) { + type_index = bit; + break; + } + } + describe_not_type_check( + valid, current, static_cast(type_index), + message); + return; + } + + message << "The value was expected to NOT be of type "; + bool first{true}; + std::uint8_t last_bit{255}; + for (std::uint8_t bit{0}; bit < 8; bit++) { + if (types.test(bit)) { + last_bit = bit; + } + } + + for (std::uint8_t bit{0}; bit < 8; bit++) { + if (types.test(bit)) { + if (!first) { + message << ", "; + } + if (bit == last_bit) { + message << "or "; + } + message << type_name(static_cast(bit)); + first = false; + } + } + + if (valid) { + message << " and it was of type "; + } else { + message << " but it was of type "; + } + + if (!valid && current == sourcemeta::core::JSON::Type::Decimal && + has_integer && !has_real) { + message << "integer"; + } else if ((!valid && current == sourcemeta::core::JSON::Type::Integer && + has_real) || + current == sourcemeta::core::JSON::Type::Decimal) { + message << "number"; + } else { + message << type_name(current); + } +} + auto describe_reference(const sourcemeta::core::JSON &target) -> std::string { std::ostringstream message; message << "The " << type_name(target.type()) @@ -216,6 +315,14 @@ auto describe(const bool valid, const Instruction &step, const sourcemeta::core::JSON &target{get(instance, instance_location)}; if (step.type == sourcemeta::blaze::InstructionIndex::AssertionFail) { + if (keyword == "enum") { + std::ostringstream message; + message << "The " << type_name(target.type()) + << " value was not expected to validate against the empty " + "enumeration"; + return message.str(); + } + if (keyword == "contains") { return "The constraints declared for this keyword were not satisfiable"; } @@ -258,7 +365,7 @@ auto describe(const bool valid, const Instruction &step, } if (step.type == sourcemeta::blaze::InstructionIndex::LogicalAnd) { - if (keyword == "allOf") { + if (keyword == "allOf" || keyword == "extends") { assert(!step.children.empty()); std::ostringstream message; message << "The " << type_name(target.type()) @@ -1325,6 +1432,14 @@ auto describe(const bool valid, const Instruction &step, return message.str(); } + if (step.type == + sourcemeta::blaze::InstructionIndex::AssertionNotTypeStrictAny) { + std::ostringstream message; + describe_not_types_check(valid, target.type(), + instruction_value(step), message); + return message.str(); + } + if (step.type == sourcemeta::blaze::InstructionIndex::AssertionTypeStringBounded) { std::ostringstream message; diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h index 87822b065..b125cd70c 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator.h @@ -44,7 +44,7 @@ struct Template { }; /// @ingroup evaluator -constexpr std::size_t JSON_VERSION{4}; +constexpr std::size_t JSON_VERSION{5}; /// @ingroup evaluator /// Parse a template from JSON diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h index d77370225..7e69da570 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_dispatch.h @@ -523,6 +523,20 @@ INSTRUCTION_HANDLER(AssertionTypeStrictAny) { EVALUATE_END(AssertionTypeStrictAny); } +INSTRUCTION_DIRECT(AssertionNotTypeStrictAny, ValueTypes) { + return !value.test(std::to_underlying(effective_type_strict_real(target))); +} + +INSTRUCTION_HANDLER(AssertionNotTypeStrictAny) { + EVALUATE_BEGIN_NO_PRECONDITION(AssertionNotTypeStrictAny); + const auto value{assume_value_copy(instruction.value)}; + assert(value.any()); + const auto &target{ + resolve_instance(instance, instruction.relative_instance_location)}; + result = DIRECT(AssertionNotTypeStrictAny, target, value); + EVALUATE_END(AssertionNotTypeStrictAny); +} + INSTRUCTION_DIRECT(AssertionTypeStringBounded, ValueRange) { const auto &[minimum, maximum, exhaustive] = value; return target.type() == JSON::Type::String && @@ -2564,7 +2578,7 @@ using DispatchHandler = bool (*)( template // Must have same order as InstructionIndex // NOLINTNEXTLINE(modernize-avoid-c-arrays) -static constexpr DispatchHandler handlers[99] = { +static constexpr DispatchHandler handlers[100] = { AssertionFail, AssertionDefines, AssertionDefinesStrict, @@ -2578,6 +2592,7 @@ static constexpr DispatchHandler handlers[99] = { AssertionTypeAny, AssertionTypeStrict, AssertionTypeStrictAny, + AssertionNotTypeStrictAny, AssertionTypeStringBounded, AssertionTypeStringUpper, AssertionTypeArrayBounded, diff --git a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h index f9c351a15..e3d39c345 100644 --- a/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h +++ b/vendor/blaze/src/evaluator/include/sourcemeta/blaze/evaluator_instruction.h @@ -33,6 +33,7 @@ enum class InstructionIndex : std::uint8_t { AssertionTypeAny, AssertionTypeStrict, AssertionTypeStrictAny, + AssertionNotTypeStrictAny, AssertionTypeStringBounded, AssertionTypeStringUpper, AssertionTypeArrayBounded, @@ -137,6 +138,7 @@ constexpr std::string_view InstructionNames[] = { "AssertionTypeAny", "AssertionTypeStrict", "AssertionTypeStrictAny", + "AssertionNotTypeStrictAny", "AssertionTypeStringBounded", "AssertionTypeStringUpper", "AssertionTypeArrayBounded", diff --git a/vendor/blaze/src/test/include/sourcemeta/blaze/test.h b/vendor/blaze/src/test/include/sourcemeta/blaze/test.h index 3662f9a20..32ae5f604 100644 --- a/vendor/blaze/src/test/include/sourcemeta/blaze/test.h +++ b/vendor/blaze/src/test/include/sourcemeta/blaze/test.h @@ -90,17 +90,17 @@ struct SOURCEMETA_BLAZE_TEST_EXPORT TestSuite { #if defined(_MSC_VER) #pragma warning(disable : 4251) #endif - /// The target schema URI or file path - sourcemeta::core::JSON::String target; + /// The target schema URIs or file paths + std::vector targets; /// The list of test cases in the suite std::vector tests; + /// The compiled schema templates for fast validation + std::vector