diff --git a/DEPENDENCIES b/DEPENDENCIES index d4d6e0b..6426448 100644 --- a/DEPENDENCIES +++ b/DEPENDENCIES @@ -1,2 +1,2 @@ vendorpull https://github.com/sourcemeta/vendorpull 1dcbac42809cf87cb5b045106b863e17ad84ba02 -core https://github.com/sourcemeta/core 991a4c86b6b22b73aedabe5734cba7108a781953 +core https://github.com/sourcemeta/core 4e9d280a8a452885c7cd2bc488799a4f6410f4d8 diff --git a/src/generator/include/sourcemeta/codegen/generator_typescript.h b/src/generator/include/sourcemeta/codegen/generator_typescript.h index 96435c0..d8fce4d 100644 --- a/src/generator/include/sourcemeta/codegen/generator_typescript.h +++ b/src/generator/include/sourcemeta/codegen/generator_typescript.h @@ -24,6 +24,7 @@ class SOURCEMETA_CODEGEN_GENERATOR_EXPORT TypeScript { auto operator()(const IREnumeration &entry) -> void; auto operator()(const IRObject &entry) -> void; auto operator()(const IRImpossible &entry) -> void; + auto operator()(const IRAny &entry) -> void; auto operator()(const IRArray &entry) -> void; auto operator()(const IRReference &entry) -> void; auto operator()(const IRTuple &entry) -> void; diff --git a/src/generator/typescript.cc b/src/generator/typescript.cc index 1bbc6d4..640dce9 100644 --- a/src/generator/typescript.cc +++ b/src/generator/typescript.cc @@ -172,6 +172,12 @@ auto TypeScript::operator()(const IRImpossible &entry) -> void { << " = never;\n"; } +auto TypeScript::operator()(const IRAny &entry) -> void { + this->output << "export type " + << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) + << " = unknown;\n"; +} + auto TypeScript::operator()(const IRArray &entry) -> void { this->output << "export type " << mangle(this->prefix, entry.pointer, entry.symbol, this->cache) diff --git a/src/ir/include/sourcemeta/codegen/ir.h b/src/ir/include/sourcemeta/codegen/ir.h index ce56d67..70eedd8 100644 --- a/src/ir/include/sourcemeta/codegen/ir.h +++ b/src/ir/include/sourcemeta/codegen/ir.h @@ -83,14 +83,18 @@ struct IRTuple : IRType { /// @ingroup ir struct IRImpossible : IRType {}; +/// @ingroup ir +struct IRAny : IRType {}; + /// @ingroup ir struct IRReference : IRType { IRType target; }; /// @ingroup ir -using IREntity = std::variant; +using IREntity = + std::variant; /// @ingroup ir using IRResult = std::vector; diff --git a/src/ir/ir.cc b/src/ir/ir.cc index 86ffee3..821d6a9 100644 --- a/src/ir/ir.cc +++ b/src/ir/ir.cc @@ -46,7 +46,10 @@ auto compile(const sourcemeta::core::JSON &input, // (4) Convert every subschema into a code generation object // -------------------------------------------------------------------------- - std::unordered_set visited; + std::unordered_set + visited; IRResult result; for (const auto &[key, location] : frame.locations()) { if (location.type != diff --git a/src/ir/ir_default_compiler.h b/src/ir/ir_default_compiler.h index 745888f..83a39e6 100644 --- a/src/ir/ir_default_compiler.h +++ b/src/ir/ir_default_compiler.h @@ -37,6 +37,16 @@ auto handle_impossible(const sourcemeta::core::JSON &, .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 &) -> IRAny { + return IRAny{{.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, @@ -450,9 +460,13 @@ auto default_compiler(const sourcemeta::core::JSON &schema, // following shapes if (subschema.is_boolean()) { - assert(!subschema.to_boolean()); - return handle_impossible(schema, frame, location, vocabularies, resolver, - subschema); + 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()) { diff --git a/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts b/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts index 7480b56..06ae7df 100644 --- a/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts +++ b/test/e2e/typescript/2020-12/additional_properties_true/expected.d.ts @@ -2,35 +2,10 @@ export type FlexibleRecordName = string; export type FlexibleRecordCount = number; -export type FlexibleRecordAdditionalProperties_5 = number; - -export type FlexibleRecordAdditionalProperties_4 = string; - -export type FlexibleRecordAdditionalProperties_3 = unknown[]; - -export type FlexibleRecordAdditionalProperties_2 = Record; - -export type FlexibleRecordAdditionalProperties_1 = boolean; - -export type FlexibleRecordAdditionalProperties_0 = null; - -export type FlexibleRecordAdditionalProperties = - FlexibleRecordAdditionalProperties_0 | - FlexibleRecordAdditionalProperties_1 | - FlexibleRecordAdditionalProperties_2 | - FlexibleRecordAdditionalProperties_3 | - FlexibleRecordAdditionalProperties_4 | - FlexibleRecordAdditionalProperties_5; +export type FlexibleRecordAdditionalProperties = unknown; export interface FlexibleRecord { "name": FlexibleRecordName; "count"?: FlexibleRecordCount; - [key: string]: - // As a notable limitation, TypeScript requires index signatures - // to also include the types of all of its properties, so we must - // match a superset of what JSON Schema allows - FlexibleRecordName | - FlexibleRecordCount | - FlexibleRecordAdditionalProperties | - undefined; + [key: string]: unknown | undefined; } diff --git a/test/ir/ir_2020_12_test.cc b/test/ir/ir_2020_12_test.cc index 81c1b75..f478e3d 100644 --- a/test/ir/ir_2020_12_test.cc +++ b/test/ir/ir_2020_12_test.cc @@ -587,24 +587,29 @@ TEST(IR_2020_12, tuple_with_prefix_items) { using namespace sourcemeta::codegen; - EXPECT_EQ(result.size(), 3); + EXPECT_EQ(result.size(), 4); EXPECT_IR_SCALAR(result, 0, Integer, "/prefixItems/1"); EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); EXPECT_IR_SCALAR(result, 1, String, "/prefixItems/0"); EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); - - EXPECT_TRUE(std::holds_alternative(result.at(2))); - EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(2)).symbol); - EXPECT_EQ(std::get(result.at(2)).items.size(), 2); - EXPECT_AS_STRING(std::get(result.at(2)).items.at(0).pointer, + EXPECT_IR_ANY(result, 2, "/items"); + EXPECT_SYMBOL(std::get(result.at(2)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(3))); + EXPECT_AS_STRING(std::get(result.at(3)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(3)).symbol); + EXPECT_EQ(std::get(result.at(3)).items.size(), 2); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(0).pointer, "/prefixItems/0"); - EXPECT_SYMBOL(std::get(result.at(2)).items.at(0).symbol, "0"); - EXPECT_AS_STRING(std::get(result.at(2)).items.at(1).pointer, + EXPECT_SYMBOL(std::get(result.at(3)).items.at(0).symbol, "0"); + EXPECT_AS_STRING(std::get(result.at(3)).items.at(1).pointer, "/prefixItems/1"); - EXPECT_SYMBOL(std::get(result.at(2)).items.at(1).symbol, "1"); - EXPECT_FALSE(std::get(result.at(2)).additional.has_value()); + EXPECT_SYMBOL(std::get(result.at(3)).items.at(1).symbol, "1"); + EXPECT_TRUE(std::get(result.at(3)).additional.has_value()); + EXPECT_AS_STRING(std::get(result.at(3)).additional->pointer, + "/items"); + EXPECT_SYMBOL(std::get(result.at(3)).additional->symbol, "items"); } TEST(IR_2020_12, tuple_with_prefix_items_and_items) { @@ -736,11 +741,12 @@ TEST(IR_2020_12, oneof_two_branches) { using namespace sourcemeta::codegen; + // Note: The canonicalizer transforms oneOf to anyOf EXPECT_EQ(result.size(), 3); - EXPECT_IR_SCALAR(result, 0, Integer, "/oneOf/1"); + EXPECT_IR_SCALAR(result, 0, Integer, "/anyOf/1"); EXPECT_SYMBOL(std::get(result.at(0)).symbol, "1"); - EXPECT_IR_SCALAR(result, 1, String, "/oneOf/0"); + EXPECT_IR_SCALAR(result, 1, String, "/anyOf/0"); EXPECT_SYMBOL(std::get(result.at(1)).symbol, "0"); EXPECT_TRUE(std::holds_alternative(result.at(2))); @@ -748,10 +754,10 @@ TEST(IR_2020_12, oneof_two_branches) { EXPECT_SYMBOL(std::get(result.at(2)).symbol); EXPECT_EQ(std::get(result.at(2)).values.size(), 2); EXPECT_AS_STRING(std::get(result.at(2)).values.at(0).pointer, - "/oneOf/0"); + "/anyOf/0"); EXPECT_SYMBOL(std::get(result.at(2)).values.at(0).symbol, "0"); EXPECT_AS_STRING(std::get(result.at(2)).values.at(1).pointer, - "/oneOf/1"); + "/anyOf/1"); EXPECT_SYMBOL(std::get(result.at(2)).values.at(1).symbol, "1"); } @@ -772,13 +778,14 @@ TEST(IR_2020_12, oneof_three_branches) { using namespace sourcemeta::codegen; + // Note: The canonicalizer transforms oneOf to anyOf EXPECT_EQ(result.size(), 4); - EXPECT_IR_SCALAR(result, 0, Boolean, "/oneOf/2"); + EXPECT_IR_SCALAR(result, 0, Boolean, "/anyOf/2"); EXPECT_SYMBOL(std::get(result.at(0)).symbol, "2"); - EXPECT_IR_SCALAR(result, 1, Integer, "/oneOf/1"); + EXPECT_IR_SCALAR(result, 1, Integer, "/anyOf/1"); EXPECT_SYMBOL(std::get(result.at(1)).symbol, "1"); - EXPECT_IR_SCALAR(result, 2, String, "/oneOf/0"); + EXPECT_IR_SCALAR(result, 2, String, "/anyOf/0"); EXPECT_SYMBOL(std::get(result.at(2)).symbol, "0"); EXPECT_TRUE(std::holds_alternative(result.at(3))); @@ -786,13 +793,13 @@ TEST(IR_2020_12, oneof_three_branches) { EXPECT_SYMBOL(std::get(result.at(3)).symbol); EXPECT_EQ(std::get(result.at(3)).values.size(), 3); EXPECT_AS_STRING(std::get(result.at(3)).values.at(0).pointer, - "/oneOf/0"); + "/anyOf/0"); EXPECT_SYMBOL(std::get(result.at(3)).values.at(0).symbol, "0"); EXPECT_AS_STRING(std::get(result.at(3)).values.at(1).pointer, - "/oneOf/1"); + "/anyOf/1"); EXPECT_SYMBOL(std::get(result.at(3)).values.at(1).symbol, "1"); EXPECT_AS_STRING(std::get(result.at(3)).values.at(2).pointer, - "/oneOf/2"); + "/anyOf/2"); EXPECT_SYMBOL(std::get(result.at(3)).values.at(2).symbol, "2"); } @@ -910,12 +917,18 @@ TEST(IR_2020_12, array_without_items) { using namespace sourcemeta::codegen; - EXPECT_EQ(result.size(), 1); + // Note: The canonicalizer now adds `items: true` for arrays without items + EXPECT_EQ(result.size(), 2); - EXPECT_TRUE(std::holds_alternative(result.at(0))); - EXPECT_AS_STRING(std::get(result.at(0)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(0)).symbol); - EXPECT_FALSE(std::get(result.at(0)).items.has_value()); + EXPECT_IR_ANY(result, 0, "/items"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "items"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_TRUE(std::get(result.at(1)).items.has_value()); + EXPECT_AS_STRING(std::get(result.at(1)).items->pointer, "/items"); + EXPECT_SYMBOL(std::get(result.at(1)).items->symbol, "items"); } TEST(IR_2020_12, object_with_additional_properties_true) { @@ -935,91 +948,32 @@ TEST(IR_2020_12, object_with_additional_properties_true) { using namespace sourcemeta::codegen; - EXPECT_EQ(result.size(), 9); + // Note: The canonicalizer now keeps additionalProperties: true as IRAny + // instead of expanding it into a union of all types + EXPECT_EQ(result.size(), 3); EXPECT_IR_SCALAR(result, 0, String, "/properties/name"); EXPECT_SYMBOL(std::get(result.at(0)).symbol, "name"); - EXPECT_IR_SCALAR(result, 1, Number, "/additionalProperties/anyOf/5"); - EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties", - "5"); - EXPECT_IR_SCALAR(result, 2, String, "/additionalProperties/anyOf/4"); - EXPECT_SYMBOL(std::get(result.at(2)).symbol, "additionalProperties", - "4"); - - EXPECT_TRUE(std::holds_alternative(result.at(3))); - EXPECT_AS_STRING(std::get(result.at(3)).pointer, - "/additionalProperties/anyOf/3"); - EXPECT_SYMBOL(std::get(result.at(3)).symbol, "additionalProperties", - "3"); - EXPECT_FALSE(std::get(result.at(3)).items.has_value()); + EXPECT_IR_ANY(result, 1, "/additionalProperties"); + EXPECT_SYMBOL(std::get(result.at(1)).symbol, "additionalProperties"); - EXPECT_TRUE(std::holds_alternative(result.at(4))); - EXPECT_AS_STRING(std::get(result.at(4)).pointer, - "/additionalProperties/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(4)).symbol, "additionalProperties", - "2"); - EXPECT_EQ(std::get(result.at(4)).members.size(), 0); - EXPECT_TRUE(std::holds_alternative( - std::get(result.at(4)).additional)); - EXPECT_TRUE(std::get(std::get(result.at(4)).additional)); - - EXPECT_IR_SCALAR(result, 5, Boolean, "/additionalProperties/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(5)).symbol, "additionalProperties", - "1"); - EXPECT_IR_SCALAR(result, 6, Null, "/additionalProperties/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(6)).symbol, "additionalProperties", - "0"); - - EXPECT_TRUE(std::holds_alternative(result.at(7))); - EXPECT_AS_STRING(std::get(result.at(7)).pointer, - "/additionalProperties"); - EXPECT_SYMBOL(std::get(result.at(7)).symbol, "additionalProperties"); - EXPECT_EQ(std::get(result.at(7)).values.size(), 6); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(0).pointer, - "/additionalProperties/anyOf/0"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(0).symbol, - "additionalProperties", "0"); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(1).pointer, - "/additionalProperties/anyOf/1"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(1).symbol, - "additionalProperties", "1"); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(2).pointer, - "/additionalProperties/anyOf/2"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(2).symbol, - "additionalProperties", "2"); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(3).pointer, - "/additionalProperties/anyOf/3"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(3).symbol, - "additionalProperties", "3"); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(4).pointer, - "/additionalProperties/anyOf/4"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(4).symbol, - "additionalProperties", "4"); - EXPECT_AS_STRING(std::get(result.at(7)).values.at(5).pointer, - "/additionalProperties/anyOf/5"); - EXPECT_SYMBOL(std::get(result.at(7)).values.at(5).symbol, - "additionalProperties", "5"); - - EXPECT_TRUE(std::holds_alternative(result.at(8))); - EXPECT_AS_STRING(std::get(result.at(8)).pointer, ""); - EXPECT_SYMBOL(std::get(result.at(8)).symbol); - EXPECT_EQ(std::get(result.at(8)).members.size(), 1); - EXPECT_TRUE(std::get(result.at(8)).members.at(0).first == "name"); - EXPECT_FALSE(std::get(result.at(8)).members.at(0).second.required); - EXPECT_FALSE(std::get(result.at(8)).members.at(0).second.immutable); + EXPECT_TRUE(std::holds_alternative(result.at(2))); + EXPECT_AS_STRING(std::get(result.at(2)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(2)).symbol); + EXPECT_EQ(std::get(result.at(2)).members.size(), 1); + EXPECT_TRUE(std::get(result.at(2)).members.at(0).first == "name"); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(2)).members.at(0).second.immutable); EXPECT_AS_STRING( - std::get(result.at(8)).members.at(0).second.pointer, + std::get(result.at(2)).members.at(0).second.pointer, "/properties/name"); - EXPECT_SYMBOL(std::get(result.at(8)).members.at(0).second.symbol, + EXPECT_SYMBOL(std::get(result.at(2)).members.at(0).second.symbol, "name"); - EXPECT_TRUE(std::holds_alternative( - std::get(result.at(8)).additional)); - EXPECT_AS_STRING( - std::get(std::get(result.at(8)).additional).pointer, - "/additionalProperties"); - EXPECT_SYMBOL( - std::get(std::get(result.at(8)).additional).symbol, - "additionalProperties"); + // When additionalProperties is a boolean schema, the object stores the + // boolean value directly (while the schema itself is compiled as IRAny) + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(2)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(2)).additional)); } TEST(IR_2020_12, object_only_additional_properties) { @@ -1133,3 +1087,79 @@ TEST(IR_2020_12, embedded_resource_with_nested_id_no_duplicates) { std::get(result.at(5)).additional)); EXPECT_FALSE(std::get(std::get(result.at(5)).additional)); } + +TEST(IR_2020_12, boolean_true_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "anything": true + } + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_ANY(result, 0, "/properties/anything"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "anything"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "anything"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/anything"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "anything"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); +} + +TEST(IR_2020_12, boolean_false_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": { + "nothing": false + } + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 2); + + EXPECT_IR_IMPOSSIBLE(result, 0, "/properties/nothing"); + EXPECT_SYMBOL(std::get(result.at(0)).symbol, "nothing"); + + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, ""); + EXPECT_SYMBOL(std::get(result.at(1)).symbol); + EXPECT_EQ(std::get(result.at(1)).members.size(), 1); + EXPECT_EQ(std::get(result.at(1)).members.at(0).first, "nothing"); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.required); + EXPECT_FALSE(std::get(result.at(1)).members.at(0).second.immutable); + EXPECT_AS_STRING( + std::get(result.at(1)).members.at(0).second.pointer, + "/properties/nothing"); + EXPECT_SYMBOL(std::get(result.at(1)).members.at(0).second.symbol, + "nothing"); + EXPECT_TRUE(std::holds_alternative( + std::get(result.at(1)).additional)); + EXPECT_TRUE(std::get(std::get(result.at(1)).additional)); +} diff --git a/test/ir/ir_test_utils.h b/test/ir/ir_test_utils.h index 0292fcf..df813f0 100644 --- a/test/ir/ir_test_utils.h +++ b/test/ir/ir_test_utils.h @@ -27,6 +27,14 @@ std::get(result.at(index)).pointer, \ expected_pointer) +#define EXPECT_IR_ANY(result, index, expected_pointer) \ + EXPECT_TRUE( \ + std::holds_alternative(result.at(index))) \ + << "Expected IRAny at index " << index; \ + EXPECT_AS_STRING( \ + std::get(result.at(index)).pointer, \ + expected_pointer) + #define EXPECT_IR_ARRAY(result, index, expected_pointer, \ expected_items_pointer) \ EXPECT_TRUE( \ diff --git a/vendor/core/CMakeLists.txt b/vendor/core/CMakeLists.txt index 4edf68f..e6914a5 100644 --- a/vendor/core/CMakeLists.txt +++ b/vendor/core/CMakeLists.txt @@ -19,6 +19,7 @@ option(SOURCEMETA_CORE_JSONSCHEMA "Build the Sourcemeta Core JSON Schema library option(SOURCEMETA_CORE_JSONPOINTER "Build the Sourcemeta Core JSON Pointer library" ON) option(SOURCEMETA_CORE_JSONL "Build the Sourcemeta Core JSONL library" ON) option(SOURCEMETA_CORE_YAML "Build the Sourcemeta Core YAML library" ON) +option(SOURCEMETA_CORE_HTML "Build the Sourcemeta Core HTML library" ON) option(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA "Build the Sourcemeta Core AlterSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_EDITORSCHEMA "Build the Sourcemeta Core EditorSchema library" ON) option(SOURCEMETA_CORE_EXTENSION_SCHEMACONFIG "Build the Sourcemeta Core SchemaConfig library" ON) @@ -126,6 +127,10 @@ if(SOURCEMETA_CORE_YAML) add_subdirectory(src/core/yaml) endif() +if(SOURCEMETA_CORE_HTML) + add_subdirectory(src/core/html) +endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(src/extension/alterschema) endif() @@ -241,6 +246,10 @@ if(SOURCEMETA_CORE_TESTS) add_subdirectory(test/yaml) endif() + if(SOURCEMETA_CORE_HTML) + add_subdirectory(test/html) + endif() + if(SOURCEMETA_CORE_EXTENSION_ALTERSCHEMA) add_subdirectory(test/alterschema) endif() diff --git a/vendor/core/config.cmake.in b/vendor/core/config.cmake.in index c3d107d..2e23e90 100644 --- a/vendor/core/config.cmake.in +++ b/vendor/core/config.cmake.in @@ -20,6 +20,7 @@ if(NOT SOURCEMETA_CORE_COMPONENTS) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonpointer) list(APPEND SOURCEMETA_CORE_COMPONENTS jsonschema) list(APPEND SOURCEMETA_CORE_COMPONENTS yaml) + list(APPEND SOURCEMETA_CORE_COMPONENTS html) list(APPEND SOURCEMETA_CORE_COMPONENTS alterschema) list(APPEND SOURCEMETA_CORE_COMPONENTS editorschema) list(APPEND SOURCEMETA_CORE_COMPONENTS schemaconfig) @@ -91,6 +92,8 @@ foreach(component ${SOURCEMETA_CORE_COMPONENTS}) find_dependency(yaml CONFIG) include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_io.cmake") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_yaml.cmake") + elseif(component STREQUAL "html") + include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_html.cmake") elseif(component STREQUAL "alterschema") include("${CMAKE_CURRENT_LIST_DIR}/sourcemeta_core_uri.cmake") find_dependency(mpdecimal CONFIG) diff --git a/vendor/core/src/core/html/CMakeLists.txt b/vendor/core/src/core/html/CMakeLists.txt new file mode 100644 index 0000000..6b35797 --- /dev/null +++ b/vendor/core/src/core/html/CMakeLists.txt @@ -0,0 +1,7 @@ +sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME html + PRIVATE_HEADERS escape.h encoder.h elements.h + SOURCES escape.cc encoder.cc) + +if(SOURCEMETA_CORE_INSTALL) + sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME html) +endif() diff --git a/vendor/core/src/core/html/encoder.cc b/vendor/core/src/core/html/encoder.cc new file mode 100644 index 0000000..ffddbc5 --- /dev/null +++ b/vendor/core/src/core/html/encoder.cc @@ -0,0 +1,74 @@ +#include + +#include // std::ostream +#include // std::ostringstream +#include // std::string + +namespace sourcemeta::core { + +auto HTML::render() const -> std::string { + std::ostringstream output_stream; + output_stream << "<" << this->tag_name; + + // Render attributes + for (const auto &[attribute_name, attribute_value] : this->attributes) { + std::string escaped_value{attribute_value}; + html_escape(escaped_value); + output_stream << " " << attribute_name << "=\"" << escaped_value << "\""; + } + + if (this->self_closing) { + output_stream << " />"; + return output_stream.str(); + } + + output_stream << ">"; + + // Render children + if (this->child_elements.empty()) { + output_stream << "tag_name << ">"; + } else if (this->child_elements.size() == 1 && + std::get_if(&this->child_elements[0])) { + // Inline single text node + output_stream << this->render(this->child_elements[0]); + output_stream << "tag_name << ">"; + } else { + // Block level children + for (const auto &child_element : this->child_elements) { + output_stream << this->render(child_element); + } + output_stream << "tag_name << ">"; + } + + return output_stream.str(); +} + +auto HTML::render(const HTMLNode &child_element) const -> std::string { + if (const auto *text = std::get_if(&child_element)) { + std::string escaped_text{*text}; + html_escape(escaped_text); + return escaped_text; + } else if (const auto *raw_html = std::get_if(&child_element)) { + return raw_html->content; + } else if (const auto *html_element = std::get_if(&child_element)) { + return html_element->render(); + } + return ""; +} + +auto HTML::push_back(const HTMLNode &child) -> HTML & { + this->child_elements.push_back(child); + return *this; +} + +auto HTML::push_back(HTMLNode &&child) -> HTML & { + this->child_elements.push_back(std::move(child)); + return *this; +} + +auto operator<<(std::ostream &output_stream, const HTML &html_element) + -> std::ostream & { + return output_stream << html_element.render(); +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/html/escape.cc b/vendor/core/src/core/html/escape.cc new file mode 100644 index 0000000..6070d72 --- /dev/null +++ b/vendor/core/src/core/html/escape.cc @@ -0,0 +1,96 @@ +#include + +#include // std::string + +namespace sourcemeta::core { + +auto html_escape(std::string &text) -> void { + std::size_t write_position{0}; + std::size_t original_size{text.size()}; + + // First pass: count how much space we need + std::size_t required_size{0}; + for (char character : text) { + switch (character) { + case '&': + required_size += 5; // & + break; + case '<': + case '>': + required_size += 4; // < or > + break; + case '"': + required_size += 6; // " + break; + case '\'': + required_size += 5; // ' + break; + default: + required_size += 1; + } + } + + // If no escaping needed, return early + if (required_size == original_size) { + return; + } + + // Resize string to accommodate escaped characters + text.resize(required_size); + + // Second pass: work backwards to avoid overwriting data + std::size_t read_position{original_size}; + write_position = required_size; + + while (read_position > 0) { + --read_position; + char character = text[read_position]; + + switch (character) { + case '&': + write_position -= 5; + text[write_position] = '&'; + text[write_position + 1] = 'a'; + text[write_position + 2] = 'm'; + text[write_position + 3] = 'p'; + text[write_position + 4] = ';'; + break; + case '<': + write_position -= 4; + text[write_position] = '&'; + text[write_position + 1] = 'l'; + text[write_position + 2] = 't'; + text[write_position + 3] = ';'; + break; + case '>': + write_position -= 4; + text[write_position] = '&'; + text[write_position + 1] = 'g'; + text[write_position + 2] = 't'; + text[write_position + 3] = ';'; + break; + case '"': + write_position -= 6; + text[write_position] = '&'; + text[write_position + 1] = 'q'; + text[write_position + 2] = 'u'; + text[write_position + 3] = 'o'; + text[write_position + 4] = 't'; + text[write_position + 5] = ';'; + break; + case '\'': + write_position -= 5; + text[write_position] = '&'; + text[write_position + 1] = '#'; + text[write_position + 2] = '3'; + text[write_position + 3] = '9'; + text[write_position + 4] = ';'; + break; + default: + --write_position; + text[write_position] = character; + } + } +} + +} // namespace sourcemeta::core diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html.h b/vendor/core/src/core/html/include/sourcemeta/core/html.h new file mode 100644 index 0000000..b2a0928 --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html.h @@ -0,0 +1,17 @@ +#ifndef SOURCEMETA_CORE_HTML_H_ +#define SOURCEMETA_CORE_HTML_H_ + +/// @defgroup html HTML +/// @brief A growing implementation of HTML generation utilities per the HTML +/// Living Standard. +/// +/// This functionality is included as follows: +/// +/// ```cpp +/// #include +/// ``` + +#include +#include + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h b/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h new file mode 100644 index 0000000..a721826 --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_elements.h @@ -0,0 +1,450 @@ +#ifndef SOURCEMETA_CORE_HTML_ELEMENTS_H_ +#define SOURCEMETA_CORE_HTML_ELEMENTS_H_ + +#include + +namespace sourcemeta::core::html { + +#ifndef DOXYGEN +#define HTML_VOID_ELEMENT(name) \ + inline auto name() -> HTML { return HTML(#name, true); } \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes), true); \ + } + +#define HTML_CONTAINER_ELEMENT_NAMED(name, tag) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#tag, std::move(attributes)); \ + } \ + template \ + inline auto name(HTMLAttributes attributes, Children &&...children) \ + -> HTML { \ + return HTML(#tag, std::move(attributes), \ + std::forward(children)...); \ + } \ + template \ + inline auto name(Children &&...children) -> HTML { \ + return HTML(#tag, std::forward(children)...); \ + } + +#define HTML_CONTAINER_ELEMENT(name) HTML_CONTAINER_ELEMENT_NAMED(name, name) + +#define HTML_COMPACT_ELEMENT(name) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes)); \ + } \ + template \ + inline auto name(HTMLAttributes attributes, Children &&...children) \ + -> HTML { \ + return HTML(#name, std::move(attributes), \ + std::forward(children)...); \ + } \ + template \ + inline auto name(Children &&...children) -> HTML { \ + return HTML(#name, std::forward(children)...); \ + } + +#define HTML_VOID_ATTR_ELEMENT(name) \ + inline auto name(HTMLAttributes attributes) -> HTML { \ + return HTML(#name, std::move(attributes), true); \ + } +#endif + +/// @ingroup html +inline auto raw(std::string html_content) -> HTMLRaw { + return HTMLRaw{std::move(html_content)}; +} + +// ============================================================================= +// Document Structure Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(html) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(base) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(head) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(link) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(meta) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(style) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(title) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(body) + +// ============================================================================= +// Content Sectioning Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(address) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(article) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(aside) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(footer) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(header) + +/// @ingroup html +HTML_COMPACT_ELEMENT(h1) +/// @ingroup html +HTML_COMPACT_ELEMENT(h2) +/// @ingroup html +HTML_COMPACT_ELEMENT(h3) +/// @ingroup html +HTML_COMPACT_ELEMENT(h4) +/// @ingroup html +HTML_COMPACT_ELEMENT(h5) +/// @ingroup html +HTML_COMPACT_ELEMENT(h6) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(hgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(main) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(nav) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(section) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(search) + +// ============================================================================= +// Text Content Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(blockquote) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dd) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(div) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dl) + +/// @ingroup html +HTML_COMPACT_ELEMENT(dt) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(figcaption) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(figure) + +/// @ingroup html +HTML_VOID_ELEMENT(hr) + +/// @ingroup html +HTML_COMPACT_ELEMENT(li) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(menu) + +/// @ingroup html +HTML_COMPACT_ELEMENT(ol) + +/// @ingroup html +HTML_COMPACT_ELEMENT(p) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(pre) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ul) + +// ============================================================================= +// Inline Text Semantics Elements +// ============================================================================= + +/// @ingroup html +HTML_COMPACT_ELEMENT(a) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(abbr) + +/// @ingroup html +HTML_COMPACT_ELEMENT(b) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(bdi) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(bdo) + +/// @ingroup html +HTML_VOID_ELEMENT(br) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(cite) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(code) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(data) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(dfn) + +/// @ingroup html +HTML_COMPACT_ELEMENT(em) + +/// @ingroup html +HTML_COMPACT_ELEMENT(i) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(kbd) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(mark) + +/// @ingroup html +HTML_COMPACT_ELEMENT(q) + +/// @ingroup html +HTML_COMPACT_ELEMENT(rp) + +/// @ingroup html +HTML_COMPACT_ELEMENT(rt) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ruby) + +/// @ingroup html +HTML_COMPACT_ELEMENT(s) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(samp) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(small) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(span) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(strong) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(sub) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(sup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(time) + +/// @ingroup html +HTML_COMPACT_ELEMENT(u) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(var) + +/// @ingroup html +HTML_VOID_ELEMENT(wbr) + +// ============================================================================= +// Image and Multimedia Elements +// ============================================================================= + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(area) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(audio) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(img) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(map) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(track) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(video) + +// ============================================================================= +// Embedded Content Elements +// ============================================================================= + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(embed) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(iframe) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(object) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(picture) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(portal) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(source) + +// ============================================================================= +// Scripting Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(canvas) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(noscript) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(script) + +// ============================================================================= +// Demarcating Edits Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(del) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(ins) + +// ============================================================================= +// Table Content Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(caption) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(col) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(colgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(table) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tbody) + +/// @ingroup html +HTML_COMPACT_ELEMENT(td) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tfoot) + +/// @ingroup html +HTML_COMPACT_ELEMENT(th) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(thead) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(tr) + +// ============================================================================= +// Forms Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(button) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(datalist) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(fieldset) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(form) + +/// @ingroup html +HTML_VOID_ATTR_ELEMENT(input) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(label) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(legend) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(meter) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(optgroup) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(option) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(output) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(progress) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(select) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(textarea) + +// ============================================================================= +// Interactive Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(details) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(dialog) + +/// @ingroup html +HTML_CONTAINER_ELEMENT(summary) + +// ============================================================================= +// Web Components Elements +// ============================================================================= + +/// @ingroup html +HTML_CONTAINER_ELEMENT(slot) + +/// @ingroup html +HTML_CONTAINER_ELEMENT_NAMED(template_, template) + +#ifndef DOXYGEN +#undef HTML_VOID_ELEMENT +#undef HTML_CONTAINER_ELEMENT +#undef HTML_CONTAINER_ELEMENT_NAMED +#undef HTML_COMPACT_ELEMENT +#undef HTML_VOID_ATTR_ELEMENT +#endif + +} // namespace sourcemeta::core::html + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h b/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h new file mode 100644 index 0000000..7be0433 --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_encoder.h @@ -0,0 +1,145 @@ +#ifndef SOURCEMETA_CORE_HTML_ENCODER_H_ +#define SOURCEMETA_CORE_HTML_ENCODER_H_ + +#ifndef SOURCEMETA_CORE_HTML_EXPORT +#include +#endif + +#include + +#include // std::ostream +#include // std::string +#include // std::pair +#include // std::variant, std::holds_alternative, std::get +#include // std::vector + +namespace sourcemeta::core { + +/// @ingroup html +using HTMLAttributes = std::vector>; + +#ifndef DOXYGEN +// Forward declaration +class HTML; +#endif + +/// @ingroup html +/// Raw HTML content wrapper for unescaped content +struct SOURCEMETA_CORE_HTML_EXPORT HTMLRaw { +// 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 + std::string content; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + explicit HTMLRaw(std::string html_content) + : content{std::move(html_content)} {} +}; + +/// @ingroup html +/// A node can be either a string (text node), raw HTML content, or another HTML +/// element +using HTMLNode = std::variant; + +/// @ingroup html +/// An HTML element that can be rendered to a string. Elements can contain +/// attributes and child nodes. +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// #include +/// +/// using namespace sourcemeta::core::html; +/// +/// std::ostringstream result; +/// result << div(h1("Title"), p("Content")); +/// assert(result.str() == "

Title

Content

"); +/// ``` +class SOURCEMETA_CORE_HTML_EXPORT HTML { +public: + HTML(std::string tag, bool self_closing_tag = false) + : tag_name(std::move(tag)), self_closing(self_closing_tag) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + bool self_closing_tag = false) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(self_closing_tag) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + std::vector children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + child_elements(std::move(children)), self_closing(false) {} + + HTML(std::string tag, HTMLAttributes tag_attributes, + std::vector children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(false) { + this->child_elements.reserve(children.size()); + for (auto &child_element : children) { + this->child_elements.emplace_back(std::move(child_element)); + } + } + + HTML(std::string tag, std::vector children) + : tag_name(std::move(tag)), child_elements(std::move(children)), + self_closing(false) {} + + HTML(std::string tag, std::vector children) + : tag_name(std::move(tag)), self_closing(false) { + this->child_elements.reserve(children.size()); + for (auto &child_element : children) { + this->child_elements.emplace_back(std::move(child_element)); + } + } + + template + HTML(std::string tag, HTMLAttributes tag_attributes, Children &&...children) + : tag_name(std::move(tag)), attributes(std::move(tag_attributes)), + self_closing(false) { + (this->child_elements.push_back(std::forward(children)), ...); + } + + template + HTML(std::string tag, Children &&...children) + : tag_name(std::move(tag)), self_closing(false) { + (this->child_elements.push_back(std::forward(children)), ...); + } + + [[nodiscard]] auto render() const -> std::string; + + auto push_back(const HTMLNode &child) -> HTML &; + auto push_back(HTMLNode &&child) -> HTML &; + + // Stream operator declaration + friend SOURCEMETA_CORE_HTML_EXPORT auto + operator<<(std::ostream &output_stream, const HTML &html_element) + -> std::ostream &; + +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 + std::string tag_name; + HTMLAttributes attributes; + std::vector child_elements; +#if defined(_MSC_VER) +#pragma warning(default : 4251) +#endif + bool self_closing; + + [[nodiscard]] auto render(const HTMLNode &child_element) const -> std::string; +}; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h new file mode 100644 index 0000000..2d5d11e --- /dev/null +++ b/vendor/core/src/core/html/include/sourcemeta/core/html_escape.h @@ -0,0 +1,38 @@ +#ifndef SOURCEMETA_CORE_HTML_ESCAPE_H_ +#define SOURCEMETA_CORE_HTML_ESCAPE_H_ + +#ifndef SOURCEMETA_CORE_HTML_EXPORT +#include +#endif + +#include // std::string + +namespace sourcemeta::core { + +/// @ingroup html +/// HTML character escaping implementation per HTML Living Standard. +/// See: https://html.spec.whatwg.org/multipage/parsing.html#escapingString +/// +/// This function escapes the five HTML special characters in-place: +/// - `&` becomes `&` +/// - `<` becomes `<` +/// - `>` becomes `>` +/// - `"` becomes `"` +/// - `'` becomes `'` +/// +/// For example: +/// +/// ```cpp +/// #include +/// #include +/// +/// std::string text{""}; +/// sourcemeta::core::html_escape(text); +/// assert(text == "<script>alert('xss')</script>"); +/// ``` +SOURCEMETA_CORE_HTML_EXPORT +auto html_escape(std::string &text) -> void; + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h index 9af4a88..92689ae 100644 --- a/vendor/core/src/core/json/include/sourcemeta/core/json_value.h +++ b/vendor/core/src/core/json/include/sourcemeta/core/json_value.h @@ -43,6 +43,8 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { template using Allocator = std::allocator; /// The string type used by the JSON document. using String = std::basic_string>; + /// The string view type used by the JSON document. + using StringView = std::basic_string_view; /// The array type used by the JSON document. using Array = JSONArray; /// The object type used by the JSON document. @@ -66,16 +68,16 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// A set of types using TypeSet = std::bitset<8>; + /// The context type for parse callbacks + enum class ParseContext : std::uint8_t { Root, Property, Index }; + /// An optional callback that can be passed to parsing functions to obtain - /// metadata during the parsing process. Each subdocument will emit 2 events: - /// a "pre" and a "post". When parsing object and arrays, during the "pre" - /// event, the value corresponds to the property name or index, respectively. - using ParseCallback = - std::function; + /// metadata during the parsing process + using ParseCallback = std::function; + /// A comparison function between object property keys. /// See https://en.cppreference.com/w/cpp/named_req/Compare using KeyComparison = std::function; @@ -1174,7 +1176,7 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { [[nodiscard]] auto defines_any(std::initializer_list keys) const -> bool; - /// This method checks if an JSON array contains a given JSON instance. For + /// This method checks if a JSON array contains a given JSON instance. For /// example: /// /// ```cpp @@ -1188,20 +1190,34 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// ``` [[nodiscard]] auto contains(const JSON &element) const -> bool; - /// This method checks if an JSON string contains a given string. For + /// This method checks if a JSON array contains a given string. For /// example: /// /// ```cpp /// #include /// #include /// - /// const sourcemeta::core::JSON document{"foo bar baz"}; + /// const sourcemeta::core::JSON document = + /// sourcemeta::core::parse_json(R"JSON([ "foo", "bar", "baz" ])JSON"); /// assert(document.contains("bar")); - /// assert(!document.contains("baz")); + /// assert(!document.contains("qux")); + /// ``` + [[nodiscard]] auto contains(const StringView element) const -> bool; + + /// This method checks if a JSON string includes a given substring. For + /// example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::JSON document{"foo bar baz"}; + /// assert(document.includes("bar")); + /// assert(!document.includes("qux")); /// ``` - [[nodiscard]] auto contains(const String &input) const -> bool; + [[nodiscard]] auto includes(const String &input) const -> bool; - /// This method checks if an JSON string contains a given character. For + /// This method checks if a JSON string includes a given character. For /// example: /// /// ```cpp @@ -1209,10 +1225,10 @@ class SOURCEMETA_CORE_JSON_EXPORT JSON { /// #include /// /// const sourcemeta::core::JSON document{"foo"}; - /// assert(document.contains('f')); - /// assert(!document.contains('b')); + /// assert(document.includes('f')); + /// assert(!document.includes('b')); /// ``` - [[nodiscard]] auto contains(const String::value_type input) const -> bool; + [[nodiscard]] auto includes(const String::value_type input) const -> bool; /// This method checks if an JSON array does not contain duplicated items. For /// example: diff --git a/vendor/core/src/core/json/json_value.cc b/vendor/core/src/core/json/json_value.cc index 11f90f9..6140dc0 100644 --- a/vendor/core/src/core/json/json_value.cc +++ b/vendor/core/src/core/json/json_value.cc @@ -847,12 +847,24 @@ JSON::defines_any(std::initializer_list keys) const -> bool { element) != this->as_array().cend(); } -[[nodiscard]] auto JSON::contains(const JSON::String &input) const -> bool { +[[nodiscard]] auto JSON::contains(const JSON::StringView element) const + -> bool { + assert(this->is_array()); + for (const auto &item : this->as_array()) { + if (item.is_string() && item.to_string() == element) { + return true; + } + } + + return false; +} + +[[nodiscard]] auto JSON::includes(const JSON::String &input) const -> bool { assert(this->is_string()); return this->to_string().find(input) != JSON::String::npos; } -[[nodiscard]] auto JSON::contains(const JSON::String::value_type input) const +[[nodiscard]] auto JSON::includes(const JSON::String::value_type input) const -> bool { assert(this->is_string()); return this->to_string().find(input) != JSON::String::npos; diff --git a/vendor/core/src/core/json/parser.h b/vendor/core/src/core/json/parser.h index 44523a1..7341549 100644 --- a/vendor/core/src/core/json/parser.h +++ b/vendor/core/src/core/json/parser.h @@ -730,25 +730,23 @@ auto parse_number( // We use "goto" to avoid recursion // NOLINTBEGIN(cppcoreguidelines-avoid-goto) -#define CALLBACK_PRE(value_type, value) \ +#define CALLBACK_PRE(value_type, context, index, property) \ if (callback) { \ - assert((value).is_null() || (value).is_string() || (value).is_integer()); \ callback(JSON::ParsePhase::Pre, JSON::Type::value_type, line, column, \ - value); \ + context, index, property); \ } -#define CALLBACK_PRE_WITH_POSITION(value_type, line, column, value) \ +#define CALLBACK_PRE_WITH_POSITION(value_type, line, column, context, index, \ + property) \ if (callback) { \ - assert((value).is_null() || (value).is_string() || (value).is_integer()); \ callback(JSON::ParsePhase::Pre, JSON::Type::value_type, line, column, \ - value); \ + context, index, property); \ } -#define CALLBACK_POST(value_type, value) \ +#define CALLBACK_POST(value_type) \ if (callback) { \ - assert((value).type() == JSON::Type::value_type); \ callback(JSON::ParsePhase::Post, JSON::Type::value_type, line, column, \ - value); \ + JSON::ParseContext::Root, 0, JSON::StringView{}); \ } namespace sourcemeta::core { @@ -781,30 +779,27 @@ auto internal_parse_json( switch (character) { case internal::constant_true.front(): if (callback) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Boolean, JSON{nullptr}); + CALLBACK_PRE(Boolean, JSON::ParseContext::Root, 0, JSON::StringView{}); const auto value{internal::parse_boolean_true(line, column, stream)}; - CALLBACK_POST(Boolean, value); + CALLBACK_POST(Boolean); return value; } else { return internal::parse_boolean_true(line, column, stream); } case internal::constant_false.front(): if (callback) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Boolean, JSON{nullptr}); + CALLBACK_PRE(Boolean, JSON::ParseContext::Root, 0, JSON::StringView{}); const auto value{internal::parse_boolean_false(line, column, stream)}; - CALLBACK_POST(Boolean, value); + CALLBACK_POST(Boolean); return value; } else { return internal::parse_boolean_false(line, column, stream); } case internal::constant_null.front(): if (callback) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Null, JSON{nullptr}); + CALLBACK_PRE(Null, JSON::ParseContext::Root, 0, JSON::StringView{}); const auto value{internal::parse_null(line, column, stream)}; - CALLBACK_POST(Null, value); + CALLBACK_POST(Null); return value; } else { return internal::parse_null(line, column, stream); @@ -815,21 +810,18 @@ auto internal_parse_json( // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf case internal::token_string_quote: if (callback) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(String, JSON{nullptr}); + CALLBACK_PRE(String, JSON::ParseContext::Root, 0, JSON::StringView{}); const Result value{internal::parse_string(line, column, stream)}; - CALLBACK_POST(String, value); + CALLBACK_POST(String); return value; } else { return Result{internal::parse_string(line, column, stream)}; } case internal::token_array_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Array, JSON{nullptr}); + CALLBACK_PRE(Array, JSON::ParseContext::Root, 0, JSON::StringView{}); goto do_parse_array; case internal::token_object_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Object, JSON{nullptr}); + CALLBACK_PRE(Object, JSON::ParseContext::Root, 0, JSON::StringView{}); goto do_parse_object; case internal::token_number_minus: @@ -849,20 +841,20 @@ auto internal_parse_json( const auto value{ internal::parse_number(line, column, stream, character)}; if (value.is_integer()) { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Integer, current_line, current_column, - JSON{nullptr}); - CALLBACK_POST(Integer, value); + JSON::ParseContext::Root, 0, + JSON::StringView{}); + CALLBACK_POST(Integer); } else if (value.is_decimal()) { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Decimal, current_line, current_column, - JSON{nullptr}); - CALLBACK_POST(Decimal, value); + JSON::ParseContext::Root, 0, + JSON::StringView{}); + CALLBACK_POST(Decimal); } else { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Real, current_line, current_column, - JSON{nullptr}); - CALLBACK_POST(Real, value); + JSON::ParseContext::Root, 0, + JSON::StringView{}); + CALLBACK_POST(Real); } return value; @@ -924,7 +916,7 @@ auto internal_parse_json( // Positional case internal::token_array_end: if (frames.top().get().empty()) { - CALLBACK_POST(Array, frames.top().get()); + CALLBACK_POST(Array); goto do_parse_container_end; } else { throw JSONParseError(line, column); @@ -932,43 +924,43 @@ auto internal_parse_json( // Values case internal::token_array_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Array, JSON{frames.top().get().size()}); + CALLBACK_PRE(Array, JSON::ParseContext::Index, frames.top().get().size(), + JSON::StringView{}); goto do_parse_array; case internal::token_object_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Object, JSON{frames.top().get().size()}); + CALLBACK_PRE(Object, JSON::ParseContext::Index, frames.top().get().size(), + JSON::StringView{}); goto do_parse_object; case internal::constant_true.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Boolean, JSON{frames.top().get().size()}); + CALLBACK_PRE(Boolean, JSON::ParseContext::Index, + frames.top().get().size(), JSON::StringView{}); frames.top().get().push_back( internal::parse_boolean_true(line, column, stream)); - CALLBACK_POST(Boolean, frames.top().get().back()); + CALLBACK_POST(Boolean); goto do_parse_array_item_separator; case internal::constant_false.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Boolean, JSON{frames.top().get().size()}); + CALLBACK_PRE(Boolean, JSON::ParseContext::Index, + frames.top().get().size(), JSON::StringView{}); frames.top().get().push_back( internal::parse_boolean_false(line, column, stream)); - CALLBACK_POST(Boolean, frames.top().get().back()); + CALLBACK_POST(Boolean); goto do_parse_array_item_separator; case internal::constant_null.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(Null, JSON{frames.top().get().size()}); + CALLBACK_PRE(Null, JSON::ParseContext::Index, frames.top().get().size(), + JSON::StringView{}); frames.top().get().push_back(internal::parse_null(line, column, stream)); - CALLBACK_POST(Null, frames.top().get().back()); + CALLBACK_POST(Null); goto do_parse_array_item_separator; // A string is a sequence of Unicode code points wrapped with quotation // marks (U+0022). See // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf case internal::token_string_quote: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE(String, JSON{frames.top().get().size()}); + CALLBACK_PRE(String, JSON::ParseContext::Index, frames.top().get().size(), + JSON::StringView{}); frames.top().get().push_back( Result{internal::parse_string(line, column, stream)}); - CALLBACK_POST(String, frames.top().get().back()); + CALLBACK_POST(String); goto do_parse_array_item_separator; case internal::token_number_minus: @@ -985,30 +977,31 @@ auto internal_parse_json( if (callback) { const auto current_line{line}; const auto current_column{column}; + const auto current_index{frames.top().get().size()}; const auto value{ internal::parse_number(line, column, stream, character)}; if (value.is_integer()) { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Integer, current_line, current_column, - JSON{frames.top().get().size()}); + JSON::ParseContext::Index, current_index, + JSON::StringView{}); } else if (value.is_decimal()) { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Decimal, current_line, current_column, - JSON{frames.top().get().size()}); + JSON::ParseContext::Index, current_index, + JSON::StringView{}); } else { - // TODO: Don't create expensive JSON values on the spot CALLBACK_PRE_WITH_POSITION(Real, current_line, current_column, - JSON{frames.top().get().size()}); + JSON::ParseContext::Index, current_index, + JSON::StringView{}); } frames.top().get().push_back(value); if (value.is_integer()) { - CALLBACK_POST(Integer, frames.top().get().back()); + CALLBACK_POST(Integer); } else if (value.is_decimal()) { - CALLBACK_POST(Decimal, frames.top().get().back()); + CALLBACK_POST(Decimal); } else { - CALLBACK_POST(Real, frames.top().get().back()); + CALLBACK_POST(Real); } } else { frames.top().get().push_back( @@ -1041,7 +1034,7 @@ auto internal_parse_json( case internal::token_array_delimiter: goto do_parse_array_item; case internal::token_array_end: - CALLBACK_POST(Array, frames.top().get()); + CALLBACK_POST(Array); goto do_parse_container_end; // Insignificant whitespace is allowed before or after any token. @@ -1099,7 +1092,7 @@ auto internal_parse_json( switch (character) { case internal::token_object_end: if (frames.top().get().empty()) { - CALLBACK_POST(Object, frames.top().get()); + CALLBACK_POST(Object); goto do_parse_container_end; } else { goto error; @@ -1159,44 +1152,44 @@ auto internal_parse_json( switch (character) { // Values case internal::token_array_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Array, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Array, key_line, key_column, + JSON::ParseContext::Property, 0, key); goto do_parse_array; case internal::token_object_begin: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Object, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Object, key_line, key_column, + JSON::ParseContext::Property, 0, key); goto do_parse_object; case internal::constant_true.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Boolean, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Boolean, key_line, key_column, + JSON::ParseContext::Property, 0, key); frames.top().get().assign( key, internal::parse_boolean_true(line, column, stream)); - CALLBACK_POST(Boolean, frames.top().get().at(key)); + CALLBACK_POST(Boolean); goto do_parse_object_property_end; case internal::constant_false.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Boolean, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Boolean, key_line, key_column, + JSON::ParseContext::Property, 0, key); frames.top().get().assign( key, internal::parse_boolean_false(line, column, stream)); - CALLBACK_POST(Boolean, frames.top().get().at(key)); + CALLBACK_POST(Boolean); goto do_parse_object_property_end; case internal::constant_null.front(): - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Null, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Null, key_line, key_column, + JSON::ParseContext::Property, 0, key); frames.top().get().assign(key, internal::parse_null(line, column, stream)); - CALLBACK_POST(Null, frames.top().get().at(key)); + CALLBACK_POST(Null); goto do_parse_object_property_end; // A string is a sequence of Unicode code points wrapped with quotation // marks (U+0022). See // https://www.ecma-international.org/wp-content/uploads/ECMA-404_2nd_edition_december_2017.pdf case internal::token_string_quote: - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(String, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(String, key_line, key_column, + JSON::ParseContext::Property, 0, key); frames.top().get().assign( key, Result{internal::parse_string(line, column, stream)}); - CALLBACK_POST(String, frames.top().get().at(key)); + CALLBACK_POST(String); goto do_parse_object_property_end; case internal::token_number_minus: @@ -1214,24 +1207,24 @@ auto internal_parse_json( const auto value{ internal::parse_number(line, column, stream, character)}; if (value.is_integer()) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Integer, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Integer, key_line, key_column, + JSON::ParseContext::Property, 0, key); } else if (value.is_decimal()) { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Decimal, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Decimal, key_line, key_column, + JSON::ParseContext::Property, 0, key); } else { - // TODO: Don't create expensive JSON values on the spot - CALLBACK_PRE_WITH_POSITION(Real, key_line, key_column, JSON{key}); + CALLBACK_PRE_WITH_POSITION(Real, key_line, key_column, + JSON::ParseContext::Property, 0, key); } frames.top().get().assign(key, value); if (value.is_integer()) { - CALLBACK_POST(Integer, frames.top().get().at(key)); + CALLBACK_POST(Integer); } else if (value.is_decimal()) { - CALLBACK_POST(Decimal, frames.top().get().at(key)); + CALLBACK_POST(Decimal); } else { - CALLBACK_POST(Real, frames.top().get().at(key)); + CALLBACK_POST(Real); } } else { frames.top().get().assign( @@ -1263,7 +1256,7 @@ auto internal_parse_json( case internal::token_object_delimiter: goto do_parse_object_property_key; case internal::token_object_end: - CALLBACK_POST(Object, frames.top().get()); + CALLBACK_POST(Object); goto do_parse_container_end; // Insignificant whitespace is allowed before or after any token. diff --git a/vendor/core/src/core/jsonpointer/CMakeLists.txt b/vendor/core/src/core/jsonpointer/CMakeLists.txt index 2030055..0f89f3d 100644 --- a/vendor/core/src/core/jsonpointer/CMakeLists.txt +++ b/vendor/core/src/core/jsonpointer/CMakeLists.txt @@ -1,7 +1,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonpointer PRIVATE_HEADERS pointer.h position.h error.h token.h walker.h - SOURCES jsonpointer.cc stringify.h parser.h grammar.h position.cc mangle.cc) + SOURCES jsonpointer.cc stringify.h parser.h grammar.h position.cc) if(SOURCEMETA_CORE_INSTALL) sourcemeta_library_install(NAMESPACE sourcemeta PROJECT core NAME jsonpointer) diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h index 67f7ea1..7682ae5 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer.h @@ -522,41 +522,6 @@ auto to_string(const WeakPointer &pointer) -> std::basic_string>; -/// @ingroup jsonpointer -/// -/// Mangle a JSON Pointer template and prefix into a collision-free identifier. -/// -/// The encoding rules for ASCII characters (0x00-0x7F) are: -/// -/// - Lowercase at segment start (except x, u, z): capitalize (no marker) -/// - Lowercase x, u, z at segment start: hex escape (reserved characters) -/// - Uppercase at segment start (except X, U, Z): U + letter -/// - Uppercase X, U, Z at segment start: hex escape (reserved characters) -/// - Non-segment-start lowercase: as-is -/// - Non-segment-start uppercase (except X, U): as-is -/// - Non-segment-start X: X58, Non-segment-start U: X55 -/// - ASCII digits (0-9): as-is -/// - Other ASCII (space, punctuation, control): hex escape, starts new segment -/// - Z/z reserved for special token prefixes -/// -/// For non-ASCII bytes (0x80-0xFF, e.g. UTF-8 sequences): -/// -/// - Always hex escaped -/// - Do NOT start a new segment (preserves UTF-8 multi-byte sequences) -/// -/// For example: -/// -/// ```cpp -/// #include -/// #include -/// -/// const sourcemeta::core::Pointer pointer{"foo", "bar"}; -/// const auto result{sourcemeta::core::mangle(pointer, "schema")}; -/// assert(result == "Schema_Foo_Bar"); -/// ``` -SOURCEMETA_CORE_JSONPOINTER_EXPORT -auto mangle(const Pointer &pointer, std::string_view prefix) -> std::string; - /// @ingroup jsonpointer /// /// Stringify the input JSON Pointer into a properly escaped URI fragment. For @@ -673,39 +638,4 @@ auto from_json(const JSON &value) -> std::optional { } // namespace sourcemeta::core -// This hash specialisation is intentationally constant with a decent tolerance -// to collisions -namespace std { -template -struct hash>> { - auto - operator()(const sourcemeta::core::GenericPointer< - PropertyT, - sourcemeta::core::PropertyHashJSON> - &pointer) const noexcept -> std::size_t { - const auto size{pointer.size()}; - if (size == 0) { - return size; - } - - const auto &first{pointer.at(0)}; - const auto &middle{pointer.at(size / 2)}; - const auto &last{pointer.at(size - 1)}; - - return size + - (first.is_property() - ? static_cast(first.property_hash().a) - : first.to_index()) + - (middle.is_property() - ? static_cast(middle.property_hash().a) - : middle.to_index()) + - (last.is_property() - ? static_cast(last.property_hash().a) - : last.to_index()); - } -}; -} // namespace std - #endif diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h index 0e5931e..1f968fc 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_pointer.h @@ -460,6 +460,37 @@ template class GenericPointer { return result; } + /// Get a copy of the JSON Pointer starting from a given token index up to + /// (but not including) a given end index. This method is undefined if the + /// start index is greater than the end index or if the end index is greater + /// than the pointer size. For example: + /// + /// ```cpp + /// #include + /// #include + /// + /// const sourcemeta::core::Pointer pointer{"foo", "bar", "baz", "qux"}; + /// const sourcemeta::core::Pointer result{pointer.slice(1, 3)}; + /// assert(result.size() == 2); + /// assert(result.at(0).is_property()); + /// assert(result.at(0).to_property() == "bar"); + /// assert(result.at(1).is_property()); + /// assert(result.at(1).to_property() == "baz"); + /// ``` + [[nodiscard]] auto slice(const std::size_t start, const std::size_t end) const + -> GenericPointer { + assert(start <= end); + assert(end <= this->size()); + auto new_begin{this->data.cbegin()}; + std::advance(new_begin, start); + auto new_end{this->data.cbegin()}; + std::advance(new_end, end); + GenericPointer result; + result.reserve(end - start); + std::copy(new_begin, new_end, std::back_inserter(result.data)); + return result; + } + /// Concatenate a JSON Pointer with another JSON Pointer, getting a new /// pointer as a result. For example: /// @@ -674,6 +705,13 @@ template class GenericPointer { return this->data == other.data; } + /// Compare with a reference wrapper + [[nodiscard]] auto + operator==(const std::reference_wrapper> + &other) const noexcept -> bool { + return this->data == other.get().data; + } + /// Overload to support ordering of JSON Pointers. Typically for sorting /// reasons. [[nodiscard]] auto @@ -682,6 +720,81 @@ template class GenericPointer { return this->data < other.data; } + /// Compare with a reference wrapper for ordering + [[nodiscard]] auto + operator<(const std::reference_wrapper> + &other) const noexcept -> bool { + return this->data < other.get().data; + } + + /// Hash functor for use with containers + struct Hasher { + using is_transparent = void; + + auto + operator()(const GenericPointer &pointer) const noexcept + -> std::size_t { + const auto size{pointer.size()}; + if (size == 0) { + return size; + } + + const auto &first{pointer.at(0)}; + const auto &middle{pointer.at(size / 2)}; + const auto &last{pointer.at(size - 1)}; + + return size + + (first.is_property() + ? static_cast(first.property_hash().a) + : first.to_index()) + + (middle.is_property() + ? static_cast(middle.property_hash().a) + : middle.to_index()) + + (last.is_property() + ? static_cast(last.property_hash().a) + : last.to_index()); + } + + auto operator()( + const std::reference_wrapper> + &reference) const noexcept -> std::size_t { + return (*this)(reference.get()); + } + }; + + /// Comparator for use with containers + struct Comparator { + using is_transparent = void; + + auto operator()(const GenericPointer &left, + const GenericPointer &right) const noexcept + -> bool { + return left == right; + } + + auto operator()( + const std::reference_wrapper> + &left, + const std::reference_wrapper> + &right) const noexcept -> bool { + return left.get() == right.get(); + } + + auto operator()( + const std::reference_wrapper> + &left, + const GenericPointer &right) const noexcept -> bool { + return left.get() == right; + } + + auto operator()( + const GenericPointer &left, + const std::reference_wrapper> + &right) const noexcept -> bool { + return left == right.get(); + } + }; + private: Container data; }; diff --git a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h index f02d834..b19f8f3 100644 --- a/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h +++ b/vendor/core/src/core/jsonpointer/include/sourcemeta/core/jsonpointer_position.h @@ -50,7 +50,8 @@ class SOURCEMETA_CORE_JSONPOINTER_EXPORT PointerPositionTracker { std::tuple; auto operator()(const JSON::ParsePhase phase, const JSON::Type, const std::uint64_t line, const std::uint64_t column, - const JSON &value) -> void; + const JSON::ParseContext context, const std::size_t index, + const JSON::StringView property) -> void; [[nodiscard]] auto get(const Pointer &pointer) const -> std::optional; [[nodiscard]] auto size() const -> std::size_t; diff --git a/vendor/core/src/core/jsonpointer/mangle.cc b/vendor/core/src/core/jsonpointer/mangle.cc deleted file mode 100644 index 64cfb31..0000000 --- a/vendor/core/src/core/jsonpointer/mangle.cc +++ /dev/null @@ -1,169 +0,0 @@ -#include - -#include // assert -#include // std::setfill, std::setw -#include // std::ostringstream -#include // std::string_view - -namespace { - -// Special characters -constexpr auto ESCAPE_PREFIX = 'X'; -constexpr auto UPPERCASE_PREFIX = 'U'; -constexpr auto SEPARATOR = '_'; -constexpr auto HYPHEN = '-'; - -// Reserved characters that need escaping -constexpr auto RESERVED_X_UPPER = 'X'; -constexpr auto RESERVED_X_LOWER = 'x'; -constexpr auto RESERVED_U_UPPER = 'U'; -constexpr auto RESERVED_U_LOWER = 'u'; -constexpr auto RESERVED_Z_UPPER = 'Z'; -constexpr auto RESERVED_Z_LOWER = 'z'; - -// Special token markers -constexpr std::string_view TOKEN_EMPTY = "ZEmpty"; -constexpr std::string_view TOKEN_INDEX = "ZIndex"; - -constexpr auto ASCII_MAX = static_cast(0x80); - -// Locale-independent ASCII character classification -inline auto is_ascii_alpha(unsigned char character) noexcept -> bool { - return (character >= 'A' && character <= 'Z') || - (character >= 'a' && character <= 'z'); -} - -inline auto is_ascii_digit(unsigned char character) noexcept -> bool { - return character >= '0' && character <= '9'; -} - -inline auto is_ascii_lower(unsigned char character) noexcept -> bool { - return character >= 'a' && character <= 'z'; -} - -inline auto to_ascii_upper(unsigned char character) noexcept -> char { - if (character >= 'a' && character <= 'z') { - return static_cast(character - 'a' + 'A'); - } - return static_cast(character); -} - -inline auto hex_escape(std::ostringstream &output, char character) noexcept - -> void { - output << ESCAPE_PREFIX << std::uppercase << std::hex << std::setfill('0') - << std::setw(2) - << static_cast(static_cast(character)); -} - -inline auto is_reserved_at_start(char character) noexcept -> bool { - switch (character) { - case RESERVED_X_UPPER: - case RESERVED_X_LOWER: - case RESERVED_U_UPPER: - case RESERVED_U_LOWER: - case RESERVED_Z_UPPER: - case RESERVED_Z_LOWER: - return true; - default: - return false; - } -} - -inline auto encode_prefix(std::ostringstream &output, - std::string_view input) noexcept -> void { - bool capitalize_next{true}; - bool first{true}; - - for (const auto character : input) { - const auto unsigned_character{static_cast(character)}; - - if (is_ascii_alpha(unsigned_character)) { - if (capitalize_next && is_ascii_lower(unsigned_character)) { - output << to_ascii_upper(unsigned_character); - } else { - output << character; - } - capitalize_next = false; - } else if (is_ascii_digit(unsigned_character)) { - if (first) { - output << SEPARATOR; - } - output << character; - capitalize_next = false; - } else if (character == SEPARATOR || character == HYPHEN) { - capitalize_next = true; - } else { - hex_escape(output, character); - capitalize_next = true; - } - - first = false; - } -} - -inline auto encode_string(std::ostringstream &output, - const std::string &input) noexcept -> void { - bool segment_start{true}; - - for (const auto character : input) { - const auto unsigned_character{static_cast(character)}; - - if (is_ascii_alpha(unsigned_character)) { - const bool is_lower{is_ascii_lower(unsigned_character)}; - if (segment_start) { - if (is_reserved_at_start(character)) { - hex_escape(output, character); - } else if (is_lower) { - output << to_ascii_upper(unsigned_character); - } else { - output << UPPERCASE_PREFIX << character; - } - } else if (character == RESERVED_X_UPPER || - character == RESERVED_U_UPPER) { - hex_escape(output, character); - } else { - output << character; - } - segment_start = false; - } else if (is_ascii_digit(unsigned_character)) { - output << character; - segment_start = false; - } else { - hex_escape(output, character); - // Only ASCII non-alphanumeric starts a new segment - // Non-ASCII bytes (>= 0x80) do not start new segments (UTF-8 handling) - segment_start = (unsigned_character < ASCII_MAX); - } - } -} - -inline auto encode_string_or_empty(std::ostringstream &output, - const std::string &input) noexcept -> void { - if (input.empty()) { - output << TOKEN_EMPTY; - } else { - encode_string(output, input); - } -} - -} // namespace - -namespace sourcemeta::core { - -auto mangle(const Pointer &pointer, const std::string_view prefix) - -> std::string { - assert(!prefix.empty()); - std::ostringstream output; - encode_prefix(output, prefix); - for (const auto &token : pointer) { - output << SEPARATOR; - if (token.is_property()) { - encode_string_or_empty(output, token.to_property()); - } else { - output << TOKEN_INDEX << token.to_index(); - } - } - return output.str(); -} - -} // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonpointer/position.cc b/vendor/core/src/core/jsonpointer/position.cc index 661d8dc..d43dfae 100644 --- a/vendor/core/src/core/jsonpointer/position.cc +++ b/vendor/core/src/core/jsonpointer/position.cc @@ -8,18 +8,21 @@ namespace sourcemeta::core { -auto PointerPositionTracker::operator()(const JSON::ParsePhase phase, - const JSON::Type, - const std::uint64_t line, - const std::uint64_t column, - const JSON &value) -> void { +auto PointerPositionTracker::operator()( + const JSON::ParsePhase phase, const JSON::Type, const std::uint64_t line, + const std::uint64_t column, const JSON::ParseContext context, + const std::size_t index, const JSON::StringView property) -> void { if (phase == JSON::ParsePhase::Pre) { this->stack.emplace(line, column); - if (value.is_string()) { - this->current.push_back(value.to_string()); - } else if (value.is_integer()) { - this->current.push_back( - static_cast(value.to_integer())); + switch (context) { + case JSON::ParseContext::Property: + this->current.push_back(JSON::String{property}); + break; + case JSON::ParseContext::Index: + this->current.push_back(index); + break; + case JSON::ParseContext::Root: + break; } } else if (phase == JSON::ParsePhase::Post) { assert(!this->stack.empty()); @@ -35,16 +38,22 @@ auto PointerPositionTracker::operator()(const JSON::ParsePhase phase, auto PointerPositionTracker::get(const Pointer &pointer) const -> std::optional { + assert(this->stack.empty()); + assert(this->current.empty()); const auto result{this->data.find(pointer)}; return result == this->data.cend() ? std::nullopt : std::optional{result->second}; } auto PointerPositionTracker::size() const -> std::size_t { + assert(this->stack.empty()); + assert(this->current.empty()); return this->data.size(); } auto PointerPositionTracker::to_json() const -> JSON { + assert(this->stack.empty()); + assert(this->current.empty()); auto result{JSON::make_object()}; for (const auto &entry : this->data) { result.assign_assume_new(to_string(entry.first), diff --git a/vendor/core/src/core/jsonschema/CMakeLists.txt b/vendor/core/src/core/jsonschema/CMakeLists.txt index e0e4a66..24b051d 100644 --- a/vendor/core/src/core/jsonschema/CMakeLists.txt +++ b/vendor/core/src/core/jsonschema/CMakeLists.txt @@ -6,7 +6,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME jsonschema PRIVATE_HEADERS bundle.h walker.h frame.h error.h types.h transform.h vocabularies.h SOURCES jsonschema.cc vocabularies.cc known_walker.cc - frame.cc walker.cc bundle.cc transformer.cc format.cc + frame.cc walker.cc bundle.cc transformer.cc format.cc helpers.h "${CMAKE_CURRENT_BINARY_DIR}/known_resolver.cc") if(SOURCEMETA_CORE_INSTALL) diff --git a/vendor/core/src/core/jsonschema/frame.cc b/vendor/core/src/core/jsonschema/frame.cc index 96cedbd..63fac3e 100644 --- a/vendor/core/src/core/jsonschema/frame.cc +++ b/vendor/core/src/core/jsonschema/frame.cc @@ -1,5 +1,7 @@ #include +#include "helpers.h" + #include // std::sort, std::all_of, std::any_of #include // assert #include // std::less @@ -62,10 +64,16 @@ auto find_anchors(const sourcemeta::core::JSON &schema, sourcemeta::core::Vocabularies::Known::JSON_Schema_2019_09_Core)) { if (schema.defines("$recursiveAnchor")) { const auto &anchor{schema.at("$recursiveAnchor")}; - assert(anchor.is_boolean()); - if (anchor.to_boolean()) { - // We store a 2019-09 recursive anchor as an empty anchor - result.emplace_back(std::string_view{}, AnchorType::Dynamic); + if (anchor.is_boolean()) { + if (anchor.to_boolean()) { + // We store a 2019-09 recursive anchor as an empty anchor + result.emplace_back(std::string_view{}, AnchorType::Dynamic); + } + } else { + std::ostringstream value; + sourcemeta::core::stringify(anchor, value); + throw sourcemeta::core::SchemaKeywordError( + "$recursiveAnchor", value.str(), "Invalid recursive anchor value"); } } @@ -125,11 +133,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema, return result; } -template -auto find_nearest_bases_ref( - const std::unordered_map> &bases, - const sourcemeta::core::WeakPointer &pointer) +template +auto find_nearest_bases_ref(const MapType &bases, + const sourcemeta::core::WeakPointer &pointer) -> std::optional< std::pair>, sourcemeta::core::WeakPointer>> { @@ -150,14 +156,12 @@ auto find_nearest_bases_ref( return std::nullopt; } -template -auto find_nearest_bases( - const std::unordered_map> &bases, - const sourcemeta::core::WeakPointer &pointer, - const std::optional &default_base) +template +auto find_nearest_bases(const MapType &bases, + const sourcemeta::core::WeakPointer &pointer, + const std::optional &default_base) -> std::pair, sourcemeta::core::WeakPointer> { - const auto result{find_nearest_bases_ref(bases, pointer)}; + const auto result{find_nearest_bases_ref(bases, pointer)}; if (result.has_value()) { return {result->first.get(), result->second}; } @@ -170,21 +174,37 @@ auto find_nearest_bases( return {{}, sourcemeta::core::empty_weak_pointer}; } -auto find_every_base( - const std::unordered_map> - &bases, - const sourcemeta::core::WeakPointer &pointer) - -> std::vector> { +template struct CombinedWalkResult { + std::optional< + std::pair>, + sourcemeta::core::WeakPointer>> + dialect_match; std::vector> - result; + every_base; +}; + +template +auto find_dialect_and_all_bases(const DialectMapType &base_dialects, + const BaseMapType &base_uris, + const sourcemeta::core::WeakPointer &pointer) + -> CombinedWalkResult { + CombinedWalkResult result; auto current_pointer{pointer}; while (true) { - const auto match{bases.find(current_pointer)}; - if (match != bases.cend()) { - for (const auto &base : match->second) { - result.emplace_back(std::string_view{base}, current_pointer); + if (!result.dialect_match.has_value()) { + const auto dialect_it{base_dialects.find(current_pointer)}; + if (dialect_it != base_dialects.cend()) { + result.dialect_match = + std::make_pair(std::cref(dialect_it->second), current_pointer); + } + } + + const auto base_it{base_uris.find(current_pointer)}; + if (base_it != base_uris.cend()) { + for (const auto &base : base_it->second) { + result.every_base.emplace_back(std::string_view{base}, current_pointer); } } @@ -195,38 +215,15 @@ auto find_every_base( current_pointer = current_pointer.initial(); } - if (result.empty() || - result.back().second != sourcemeta::core::empty_weak_pointer) { - result.emplace_back(std::string_view{}, - sourcemeta::core::empty_weak_pointer); + if (result.every_base.empty() || + result.every_base.back().second != sourcemeta::core::empty_weak_pointer) { + result.every_base.emplace_back(std::string_view{}, + sourcemeta::core::empty_weak_pointer); } return result; } -// TODO: Why do we have this function both here and on `walker.cc`? -auto ref_overrides_adjacent_keywords( - const sourcemeta::core::SchemaBaseDialect base_dialect) -> bool { - using sourcemeta::core::SchemaBaseDialect; - // In older drafts, the presence of `$ref` would override any sibling - // keywords - // See - // https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3 - switch (base_dialect) { - case SchemaBaseDialect::JSON_Schema_Draft_7: - case SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_6: - case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_4: - case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_3: - case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: - return true; - default: - return false; - } -} - auto supports_id_anchors(const sourcemeta::core::SchemaBaseDialect base_dialect) -> bool { using sourcemeta::core::SchemaBaseDialect; @@ -284,6 +281,7 @@ auto store(sourcemeta::core::SchemaFrame::Locations &frame, const std::string_view dialect, const sourcemeta::core::SchemaBaseDialect base_dialect, const std::optional &parent, + const bool property_name, const bool orphan, const bool ignore_if_present = false, const bool already_canonical = false) -> void { auto canonical{already_canonical ? std::move(uri) @@ -296,7 +294,9 @@ auto store(sourcemeta::core::SchemaFrame::Locations &frame, .pointer = pointer_from_root, .relative_pointer = relative_pointer_offset, .dialect = dialect, - .base_dialect = base_dialect}}); + .base_dialect = base_dialect, + .property_name = property_name, + .orphan = orphan}}); if (!ignore_if_present && !inserted) { throw_already_exists(iterator->first.second); } @@ -317,6 +317,7 @@ struct InternalEntry { // NOLINTNEXTLINE(bugprone-exception-escape) struct CacheSubschema { bool orphan{}; + bool property_name{}; std::optional parent{}; }; @@ -379,6 +380,9 @@ auto SchemaFrame::to_json( entry.assign_assume_new( "baseDialect", JSON{JSON::String{to_string(location.second.base_dialect)}}); + entry.assign_assume_new("propertyName", + JSON{location.second.property_name}); + entry.assign_assume_new("orphan", JSON{location.second.orphan}); switch (location.first.first) { case SchemaReferenceType::Static: @@ -436,12 +440,13 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, std::string_view default_id, const SchemaFrame::Paths &paths) -> void { this->reset(); - assert(std::unordered_set(paths.cbegin(), paths.cend()).size() == - paths.size()); + assert((std::unordered_set(paths.cbegin(), + paths.cend()) + .size() == paths.size())); std::vector subschema_entries; - std::unordered_map subschemas; - std::unordered_map> base_uris; - std::unordered_map> base_dialects; + std::map subschemas; + std::map> base_uris; + std::map> base_dialects; for (const auto &path : paths) { // Passing paths that overlap is undefined behavior. No path should @@ -465,7 +470,14 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, const auto maybe_id{sourcemeta::core::identify( schema, root_base_dialect.value(), default_id)}; if (!maybe_id.empty()) { - root_id = URI::canonicalize(maybe_id); + try { + root_id = URI::canonicalize(maybe_id); + } catch (const URIParseError &) { + throw SchemaKeywordError( + sourcemeta::core::id_keyword(root_base_dialect.value()), + std::string{maybe_id}, "The identifier is not a valid URI"); + } + this->root_ = root_id.value(); } } @@ -486,7 +498,7 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, store(this->locations_, SchemaReferenceType::Static, SchemaFrame::LocationType::Resource, default_id_canonical, this->root_, path, path.size(), root_dialect, - root_base_dialect.value(), std::nullopt); + root_base_dialect.value(), std::nullopt, false, false); base_uris.insert({path, {root_id.value(), default_id_canonical}}); } @@ -523,8 +535,10 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, : std::nullopt}; // Store information - subschemas.emplace(entry.pointer, CacheSubschema{.orphan = entry.orphan, - .parent = entry.parent}); + subschemas.emplace(entry.pointer, + CacheSubschema{.orphan = entry.orphan, + .property_name = entry.property_name, + .parent = entry.parent}); subschema_entries.emplace_back( InternalEntry{.common = std::move(entry), .id = std::move(id)}); current_subschema_entries.emplace_back(subschema_entries.size() - 1); @@ -537,7 +551,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, if (entry.id.has_value()) { assert(entry.common.base_dialect.has_value()); const bool ref_overrides = - ref_overrides_adjacent_keywords(entry.common.base_dialect.value()); + sourcemeta::core::ref_overrides_adjacent_keywords( + entry.common.base_dialect.value()); const bool is_pre_2019_09_location_independent_identifier = supports_id_anchors(entry.common.base_dialect.value()) && entry.id.value().starts_with('#'); @@ -547,7 +562,7 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, // identifier, we ignore it as a traditional identifier and take // care of it as an anchor !is_pre_2019_09_location_independent_identifier) { - const auto bases{find_nearest_bases( + const auto bases{find_nearest_bases( base_uris, common_pointer_weak, entry.id ? std::optional{*entry.id} : std::nullopt)}; @@ -558,8 +573,18 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, continue; } - const sourcemeta::core::URI base{base_string}; - sourcemeta::core::URI maybe_relative{entry.id.value()}; + sourcemeta::core::URI base; + sourcemeta::core::URI maybe_relative; + try { + base = sourcemeta::core::URI{base_string}; + maybe_relative = sourcemeta::core::URI{entry.id.value()}; + } catch (const sourcemeta::core::URIParseError &) { + throw sourcemeta::core::SchemaKeywordError( + sourcemeta::core::id_keyword( + entry.common.base_dialect.value()), + entry.id.value(), "The identifier is not a valid URI"); + } + const auto maybe_fragment{maybe_relative.fragment()}; // See @@ -567,7 +592,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, // See // https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-8.2.1-5 if (maybe_fragment.has_value() && !maybe_fragment.value().empty()) { - throw SchemaError( + throw SchemaFrameError( + entry.id.value(), "Identifiers must not contain non-empty fragments"); } @@ -590,7 +616,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Resource, new_id, new_id, common_pointer_weak, common_pointer_weak.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent); + common_parent, entry.common.property_name, + entry.common.orphan); } auto base_uri_match{base_uris.find(common_pointer_weak)}; @@ -612,8 +639,15 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, const auto maybe_metaschema{ sourcemeta::core::dialect(entry.common.subschema.get())}; if (!maybe_metaschema.empty()) { - sourcemeta::core::URI metaschema{maybe_metaschema}; - const auto nearest_bases{find_nearest_bases( + sourcemeta::core::URI metaschema; + try { + metaschema = sourcemeta::core::URI{maybe_metaschema}; + } catch (const URIParseError &) { + throw SchemaKeywordError("$schema", std::string{maybe_metaschema}, + "The dialect is not a valid URI"); + } + + const auto nearest_bases{find_nearest_bases( base_uris, common_pointer_weak, entry.id ? std::optional{*entry.id} : std::nullopt)}; @@ -639,7 +673,7 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, // Handle schema anchors for (const auto &[name, type] : find_anchors(entry.common.subschema.get(), entry.common.vocabularies)) { - const auto bases{find_nearest_bases( + const auto bases{find_nearest_bases( base_uris, common_pointer_weak, entry.id ? std::optional{*entry.id} : std::nullopt)}; @@ -653,7 +687,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, relative_anchor_uri, "", common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent); + common_parent, entry.common.property_name, + entry.common.orphan); } if (type == AnchorType::Dynamic || type == AnchorType::All) { @@ -661,7 +696,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, relative_anchor_uri, "", common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent); + common_parent, entry.common.property_name, + entry.common.orphan); // Register a dynamic anchor as a static anchor if possible too if (entry.common.vocabularies.contains( @@ -670,7 +706,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, relative_anchor_uri, "", common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent, true); + common_parent, entry.common.property_name, + entry.common.orphan, true); } } } else { @@ -700,7 +737,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, anchor_uri, base_view, common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent); + common_parent, entry.common.property_name, + entry.common.orphan); } if (type == AnchorType::Dynamic || type == AnchorType::All) { @@ -709,9 +747,9 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, anchor_uri, base_view, common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent); + common_parent, entry.common.property_name, + entry.common.orphan); - // Register a dynamic anchor as a static anchor if possible too if (entry.common.vocabularies.contains( Vocabularies::Known::JSON_Schema_2020_12_Core)) { store(this->locations_, @@ -719,7 +757,8 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, SchemaFrame::LocationType::Anchor, anchor_uri, base_view, common_pointer_weak, bases.second.size(), entry.common.dialect, entry.common.base_dialect.value(), - common_parent, true); + common_parent, entry.common.property_name, + entry.common.orphan, true); } } @@ -742,13 +781,41 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, for (const auto &relative_pointer : pointers) { const auto pointer_weak{path.concat(relative_pointer)}; - const auto dialect_match{ - find_nearest_bases_ref(base_dialects, pointer_weak)}; - const auto &dialect_for_pointer{dialect_match.has_value() - ? dialect_match->first.get().front() - : root_dialect}; + const auto combined{find_dialect_and_all_bases( + base_dialects, base_uris, pointer_weak)}; + const auto &dialect_for_pointer{ + combined.dialect_match.has_value() + ? combined.dialect_match->first.get().front() + : root_dialect}; + const auto &every_base_result{combined.every_base}; + + std::optional> nearest_base_info; + for (const auto &entry : every_base_result) { + if (!entry.first.empty()) { + nearest_base_info = entry; + break; + } + } - auto every_base_result = find_every_base(base_uris, pointer_weak); + const auto subschema_it{subschemas.find(pointer_weak)}; + const bool is_subschema{subschema_it != subschemas.cend()}; + const auto nearest_base_depth = + nearest_base_info.has_value() ? nearest_base_info->second.size() : 0; + + std::string_view hoisted_base_view{}; + sourcemeta::core::SchemaBaseDialect hoisted_base_dialect{}; + if (nearest_base_info.has_value()) { + const JSON::String nearest_base_str{nearest_base_info->first}; + const auto base_entry{this->locations_.find( + {SchemaReferenceType::Static, nearest_base_str})}; + if (base_entry != this->locations_.cend()) { + hoisted_base_view = base_entry->first.second; + hoisted_base_dialect = base_entry->second.base_dialect; + } else { + hoisted_base_view = nearest_base_info->first; + hoisted_base_dialect = root_base_dialect.value(); + } + } WeakPointer cached_base{}; for (const auto &base : every_base_result) { @@ -769,40 +836,49 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, this->locations_.contains({SchemaReferenceType::Static, result}); if (!contains) { - const auto nearest_bases{ - find_nearest_bases(base_uris, pointer_weak, - std::optional{base.first})}; - assert(!nearest_bases.first.empty()); - const auto ¤t_base{nearest_bases.first.front()}; - - const auto base_entry{this->locations_.find( - {SchemaReferenceType::Static, current_base})}; - - const std::string_view base_view{ - base_entry != this->locations_.cend() - ? std::string_view{base_entry->first.second} - : std::string_view{current_base}}; - - const sourcemeta::core::SchemaBaseDialect current_base_dialect{ - base_entry != this->locations_.cend() - ? base_entry->second.base_dialect - : root_base_dialect.value()}; - - const auto subschema{subschemas.find(pointer_weak)}; - if (subschema != subschemas.cend()) { + std::string_view base_view; + sourcemeta::core::SchemaBaseDialect current_base_dialect; + + if (nearest_base_info.has_value()) { + base_view = hoisted_base_view; + current_base_dialect = hoisted_base_dialect; + } else { + const JSON::String current_base{base.first}; + const auto base_entry{this->locations_.find( + {SchemaReferenceType::Static, current_base})}; + if (base_entry != this->locations_.cend()) { + base_view = base_entry->first.second; + current_base_dialect = base_entry->second.base_dialect; + } else { + base_view = base.first; + current_base_dialect = root_base_dialect.value(); + } + } + + if (is_subschema) { store(this->locations_, SchemaReferenceType::Static, SchemaFrame::LocationType::Subschema, std::move(result), - base_view, pointer_weak, nearest_bases.second.size(), + base_view, pointer_weak, nearest_base_depth, dialect_for_pointer, current_base_dialect, - subschema->second.parent, false, true); + subschema_it->second.parent, + subschema_it->second.property_name, + subschema_it->second.orphan, false, true); } else { + const auto &parent_pointer{combined.dialect_match.has_value() + ? combined.dialect_match->second + : empty_weak_pointer}; + const auto parent_subschema_it{subschemas.find(parent_pointer)}; + const bool parent_property_name{ + parent_subschema_it != subschemas.cend() && + parent_subschema_it->second.property_name}; + const bool parent_orphan{parent_subschema_it != subschemas.cend() && + parent_subschema_it->second.orphan}; + store(this->locations_, SchemaReferenceType::Static, SchemaFrame::LocationType::Pointer, std::move(result), - base_view, pointer_weak, nearest_bases.second.size(), - dialect_for_pointer, current_base_dialect, - dialect_match.has_value() ? dialect_match->second - : empty_weak_pointer, - false, true); + base_view, pointer_weak, nearest_base_depth, + dialect_for_pointer, current_base_dialect, parent_pointer, + parent_property_name, parent_orphan, false, true); } } } @@ -817,36 +893,57 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, for (const auto &entry : subschema_entries) { const auto &common_pointer_weak{entry.common.pointer}; if (entry.common.subschema.get().is_object()) { - const auto nearest_bases{find_nearest_bases( + const auto nearest_bases{find_nearest_bases( base_uris, common_pointer_weak, entry.id ? std::optional{*entry.id} : std::nullopt)}; if (entry.common.subschema.get().defines("$ref")) { - if (entry.common.subschema.get().at("$ref").is_string()) { - const auto &original{ - entry.common.subschema.get().at("$ref").to_string()}; - sourcemeta::core::URI ref{original}; - if (!nearest_bases.first.empty()) { - ref.resolve_from(nearest_bases.first.front()); - } + if (!entry.common.subschema.get().at("$ref").is_string()) { + std::ostringstream value; + sourcemeta::core::stringify(entry.common.subschema.get().at("$ref"), + value); + throw sourcemeta::core::SchemaKeywordError("$ref", value.str(), + "Invalid reference value"); + } - ref.canonicalize(); - auto ref_pointer{common_pointer_weak}; - ref_pointer.push_back(std::cref(KEYWORD_REF)); - const auto [it, inserted] = this->references_.insert_or_assign( - {SchemaReferenceType::Static, std::move(ref_pointer)}, - SchemaFrame::ReferencesEntry{.original = original, - .destination = ref.recompose(), - .base = std::string_view{}, - .fragment = std::nullopt}); - set_base_and_fragment(it->second); + const auto &original{ + entry.common.subschema.get().at("$ref").to_string()}; + sourcemeta::core::URI ref; + try { + ref = sourcemeta::core::URI{original}; + } catch (const URIParseError &) { + throw sourcemeta::core::SchemaKeywordError( + "$ref", original, "The reference is not a valid URI"); + } + + if (!nearest_bases.first.empty()) { + ref.resolve_from(nearest_bases.first.front()); } + + ref.canonicalize(); + auto ref_pointer{common_pointer_weak}; + ref_pointer.push_back(std::cref(KEYWORD_REF)); + const auto [it, inserted] = this->references_.insert_or_assign( + {SchemaReferenceType::Static, std::move(ref_pointer)}, + SchemaFrame::ReferencesEntry{.original = original, + .destination = ref.recompose(), + .base = std::string_view{}, + .fragment = std::nullopt}); + set_base_and_fragment(it->second); } if (entry.common.vocabularies.contains( Vocabularies::Known::JSON_Schema_2019_09_Core) && entry.common.subschema.get().defines("$recursiveRef")) { - assert(entry.common.subschema.get().at("$recursiveRef").is_string()); + if (!entry.common.subschema.get().at("$recursiveRef").is_string()) { + std::ostringstream value; + sourcemeta::core::stringify( + entry.common.subschema.get().at("$recursiveRef"), value); + throw sourcemeta::core::SchemaKeywordError( + "$recursiveRef", value.str(), + "Invalid recursive reference value"); + } + const auto &ref{ entry.common.subschema.get().at("$recursiveRef").to_string()}; @@ -883,42 +980,56 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker, if (entry.common.vocabularies.contains( Vocabularies::Known::JSON_Schema_2020_12_Core) && entry.common.subschema.get().defines("$dynamicRef")) { - if (entry.common.subschema.get().at("$dynamicRef").is_string()) { - const auto &original{ - entry.common.subschema.get().at("$dynamicRef").to_string()}; - sourcemeta::core::URI ref{original}; - if (!nearest_bases.first.empty()) { - ref.resolve_from(nearest_bases.first.front()); - } + if (!entry.common.subschema.get().at("$dynamicRef").is_string()) { + std::ostringstream value; + sourcemeta::core::stringify( + entry.common.subschema.get().at("$dynamicRef"), value); + throw sourcemeta::core::SchemaKeywordError( + "$dynamicRef", value.str(), "Invalid dynamic reference value"); + } - ref.canonicalize(); - auto ref_string{ref.recompose()}; - - // Note that here we cannot enforce the bookending requirement, - // as the dynamic reference may point to a schema resource that - // is not part of or bundled within the schema we are analyzing here. - - const auto has_fragment{ref.fragment().has_value()}; - const auto maybe_static_frame{ - this->locations_.find({SchemaReferenceType::Static, ref_string})}; - const auto maybe_dynamic_frame{this->locations_.find( - {SchemaReferenceType::Dynamic, ref_string})}; - const auto behaves_as_static{ - !has_fragment || - (has_fragment && maybe_static_frame != this->locations_.end() && - maybe_dynamic_frame == this->locations_.end())}; - auto dynamic_ref_pointer{common_pointer_weak}; - dynamic_ref_pointer.push_back(std::cref(KEYWORD_DYNAMIC_REF)); - const auto [it, inserted] = this->references_.insert_or_assign( - {behaves_as_static ? SchemaReferenceType::Static - : SchemaReferenceType::Dynamic, - std::move(dynamic_ref_pointer)}, - SchemaFrame::ReferencesEntry{.original = original, - .destination = std::move(ref_string), - .base = std::string_view{}, - .fragment = std::nullopt}); - set_base_and_fragment(it->second); + const auto &original{ + entry.common.subschema.get().at("$dynamicRef").to_string()}; + sourcemeta::core::URI ref; + try { + ref = sourcemeta::core::URI{original}; + } catch (const URIParseError &) { + throw sourcemeta::core::SchemaKeywordError( + "$dynamicRef", original, + "The dynamic reference is not a valid URI"); + } + + if (!nearest_bases.first.empty()) { + ref.resolve_from(nearest_bases.first.front()); } + + ref.canonicalize(); + auto ref_string{ref.recompose()}; + + // Note that here we cannot enforce the bookending requirement, + // as the dynamic reference may point to a schema resource that + // is not part of or bundled within the schema we are analyzing here. + + const auto has_fragment{ref.fragment().has_value()}; + const auto maybe_static_frame{ + this->locations_.find({SchemaReferenceType::Static, ref_string})}; + const auto maybe_dynamic_frame{ + this->locations_.find({SchemaReferenceType::Dynamic, ref_string})}; + const auto behaves_as_static{ + !has_fragment || + (has_fragment && maybe_static_frame != this->locations_.end() && + maybe_dynamic_frame == this->locations_.end())}; + auto dynamic_ref_pointer{common_pointer_weak}; + dynamic_ref_pointer.push_back(std::cref(KEYWORD_DYNAMIC_REF)); + const auto [it, inserted] = this->references_.insert_or_assign( + {behaves_as_static ? SchemaReferenceType::Static + : SchemaReferenceType::Dynamic, + std::move(dynamic_ref_pointer)}, + SchemaFrame::ReferencesEntry{.original = original, + .destination = std::move(ref_string), + .base = std::string_view{}, + .fragment = std::nullopt}); + set_base_and_fragment(it->second); } } } @@ -1072,11 +1183,28 @@ auto SchemaFrame::traverse(const std::string_view uri) const auto SchemaFrame::traverse(const WeakPointer &pointer) const -> std::optional> { - // TODO: This is slow. Consider adding a pointer-indexed secondary - // lookup structure to SchemaFrame - for (const auto &entry : this->locations_) { - if (entry.second.pointer == pointer) { - return entry.second; + this->populate_pointer_to_location(); + const auto iterator{this->pointer_to_location_.find(std::cref(pointer))}; + if (iterator == this->pointer_to_location_.cend() || + iterator->second.empty()) { + return std::nullopt; + } + + return *(iterator->second.front()); +} + +auto SchemaFrame::traverse(const WeakPointer &pointer, + const LocationType type) const + -> std::optional> { + this->populate_pointer_to_location(); + const auto iterator{this->pointer_to_location_.find(std::cref(pointer))}; + if (iterator == this->pointer_to_location_.cend()) { + return std::nullopt; + } + + for (const auto *location : iterator->second) { + if (location->type == type) { + return *location; } } @@ -1085,9 +1213,24 @@ auto SchemaFrame::traverse(const WeakPointer &pointer) const auto SchemaFrame::uri(const WeakPointer &pointer) const -> std::optional> { - for (const auto &entry : this->locations_) { - if (entry.second.pointer == pointer) { - return entry.first.second; + this->populate_pointer_to_location(); + const auto iterator{this->pointer_to_location_.find(std::cref(pointer))}; + if (iterator == this->pointer_to_location_.cend()) { + return std::nullopt; + } + + const Location *best{nullptr}; + for (const auto *location : iterator->second) { + if (best == nullptr || location->type < best->type) { + best = location; + } + } + + if (best != nullptr) { + for (const auto &entry : this->locations_) { + if (&entry.second == best) { + return entry.first.second; + } } } @@ -1243,9 +1386,274 @@ auto SchemaFrame::empty() const noexcept -> bool { } auto SchemaFrame::reset() -> void { + // Note that order of removal is important to avoid undefined behaviour + this->pointer_to_location_.clear(); + this->reachability_.clear(); this->root_.clear(); this->locations_.clear(); this->references_.clear(); } +auto SchemaFrame::populate_pointer_to_location() const -> void { + if (!this->pointer_to_location_.empty()) { + return; + } + + this->pointer_to_location_.reserve(this->locations_.size()); + for (const auto &entry : this->locations_) { + this->pointer_to_location_[std::cref(entry.second.pointer)].push_back( + &entry.second); + } +} + +// TODO: Find a way to split or simplify this monster while preserving +// its performance? +auto SchemaFrame::populate_reachability(const SchemaWalker &walker, + const SchemaResolver &resolver) const + -> void { + if (!this->reachability_.empty()) { + return; + } + + // --------------------------------------------------------------------------- + // (1) Find all unreachable pointers + // --------------------------------------------------------------------------- + + std::vector> unreachable_pointers; + + if (this->pointer_to_location_.empty()) { + std::unordered_set, + WeakPointer::Hasher, WeakPointer::Comparator> + has_non_pointer_location; + std::unordered_set, + WeakPointer::Hasher, WeakPointer::Comparator> + has_non_orphan; + + for (const auto &entry : this->locations_) { + auto [iterator, inserted] = this->pointer_to_location_.try_emplace( + std::cref(entry.second.pointer), std::vector{}); + iterator->second.push_back(&entry.second); + if (entry.second.type != LocationType::Pointer) { + has_non_pointer_location.insert(iterator->first); + if (!entry.second.orphan) { + has_non_orphan.insert(iterator->first); + } + } + } + + for (const auto &pointer_reference : has_non_pointer_location) { + const bool is_reachable = has_non_orphan.contains(pointer_reference); + this->reachability_.emplace(pointer_reference, is_reachable); + if (!is_reachable) { + unreachable_pointers.push_back(pointer_reference); + } + } + } else { + for (const auto &[pointer_reference, locations] : + this->pointer_to_location_) { + const auto has_non_pointer{ + std::ranges::any_of(locations, [](const Location *location) { + return location->type != LocationType::Pointer; + })}; + if (!has_non_pointer) { + continue; + } + + const auto any_non_orphan{ + std::ranges::any_of(locations, [](const Location *location) { + return location->type != LocationType::Pointer && !location->orphan; + })}; + this->reachability_.emplace(pointer_reference, any_non_orphan); + if (!any_non_orphan) { + unreachable_pointers.push_back(pointer_reference); + } + } + } + + // --------------------------------------------------------------------------- + // (2) Build a reverse mapping from reference destinations to their sources + // --------------------------------------------------------------------------- + + std::vector> + reference_destinations; + reference_destinations.reserve(this->references_.size()); + + for (const auto &reference : this->references_) { + const auto &source_pointer{reference.first.second}; + if (source_pointer.empty()) { + continue; + } + + const WeakPointer *destination_pointer{nullptr}; + const auto destination_location{this->locations_.find( + {SchemaReferenceType::Static, reference.second.destination})}; + if (destination_location != this->locations_.cend()) { + destination_pointer = &destination_location->second.pointer; + } else { + const auto dynamic_destination{this->locations_.find( + {SchemaReferenceType::Dynamic, reference.second.destination})}; + if (dynamic_destination != this->locations_.cend()) { + destination_pointer = &dynamic_destination->second.pointer; + } + } + + if (destination_pointer != nullptr) { + reference_destinations.emplace_back(&source_pointer, destination_pointer); + } + } + + std::unordered_map, + std::vector, WeakPointer::Hasher, + WeakPointer::Comparator> + references_by_destination; + for (const auto &[source, destination] : reference_destinations) { + references_by_destination[std::cref(*destination)].push_back(source); + } + + // --------------------------------------------------------------------------- + // (3) Precompute which references could make each orphan reachable + // --------------------------------------------------------------------------- + + struct PotentialSource { + const WeakPointer *source_pointer; + bool crosses; + }; + struct PotentialReach { + std::reference_wrapper pointer; + std::vector potential_sources; + }; + std::vector unreachable_with_sources; + unreachable_with_sources.reserve(unreachable_pointers.size()); + + std::unordered_map vocabularies_cache; + + for (const auto &pointer_reference : unreachable_pointers) { + const auto &pointer{pointer_reference.get()}; + PotentialReach entry{.pointer = pointer_reference, .potential_sources = {}}; + + WeakPointer ancestor = pointer; + while (!ancestor.empty()) { + auto destination_iterator = + references_by_destination.find(std::cref(ancestor)); + if (destination_iterator != references_by_destination.end()) { + bool crosses{false}; + if (ancestor != pointer) { + auto check_location{this->traverse(pointer)}; + while (check_location.has_value()) { + const auto &location{check_location->get()}; + if (location.pointer == ancestor) { + break; + } + + if (!location.parent.has_value()) { + break; + } + + const auto parent_location{this->traverse(location.parent.value())}; + if (!parent_location.has_value()) { + break; + } + + const auto relative{ + location.pointer.slice(location.parent.value().size())}; + if (!relative.empty() && relative.at(0).is_property()) { + const auto &parent_loc{parent_location->get()}; + auto vocab_iterator = + vocabularies_cache.find(parent_loc.base_dialect); + if (vocab_iterator == vocabularies_cache.end()) { + auto [inserted_iterator, inserted] = vocabularies_cache.emplace( + parent_loc.base_dialect, + this->vocabularies(parent_loc, resolver)); + vocab_iterator = inserted_iterator; + } + + const auto &keyword_result{ + walker(relative.at(0).to_property(), vocab_iterator->second)}; + if (keyword_result.type == SchemaKeywordType::LocationMembers) { + crosses = true; + break; + } + } + + check_location = parent_location; + } + } + + for (const auto *source_pointer : destination_iterator->second) { + entry.potential_sources.push_back(PotentialSource{ + .source_pointer = source_pointer, .crosses = crosses}); + } + } + ancestor = ancestor.initial(); + } + + if (!entry.potential_sources.empty()) { + unreachable_with_sources.push_back(std::move(entry)); + } + } + + std::ranges::sort(unreachable_with_sources, [](const PotentialReach &left, + const PotentialReach &right) { + return left.pointer.get().size() < right.pointer.get().size(); + }); + + // --------------------------------------------------------------------------- + // (4) Propagate reachability through references using fixpoint iteration + // --------------------------------------------------------------------------- + + bool changed{true}; + while (changed) { + changed = false; + + auto write_iterator = unreachable_with_sources.begin(); + for (auto read_iterator = unreachable_with_sources.begin(); + read_iterator != unreachable_with_sources.end(); ++read_iterator) { + bool became_reachable = false; + + for (const auto &potential_source : read_iterator->potential_sources) { + if (potential_source.crosses) { + continue; + } + + const auto &source_parent{potential_source.source_pointer->initial()}; + bool source_parent_reachable{source_parent.empty()}; + if (!source_parent_reachable) { + const auto reachability_iterator{ + this->reachability_.find(std::cref(source_parent))}; + source_parent_reachable = + reachability_iterator != this->reachability_.end() && + reachability_iterator->second; + } + + if (source_parent_reachable) { + became_reachable = true; + break; + } + } + + if (became_reachable) { + this->reachability_[read_iterator->pointer] = true; + changed = true; + } else { + if (write_iterator != read_iterator) { + *write_iterator = std::move(*read_iterator); + } + ++write_iterator; + } + } + unreachable_with_sources.erase(write_iterator, + unreachable_with_sources.end()); + } +} + +auto SchemaFrame::is_reachable(const Location &location, + const SchemaWalker &walker, + const SchemaResolver &resolver) const -> bool { + assert(location.type != LocationType::Pointer); + this->populate_reachability(walker, resolver); + const auto iterator{this->reachability_.find(std::cref(location.pointer))}; + assert(iterator != this->reachability_.end()); + return iterator->second; +} + } // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonschema/helpers.h b/vendor/core/src/core/jsonschema/helpers.h new file mode 100644 index 0000000..8136371 --- /dev/null +++ b/vendor/core/src/core/jsonschema/helpers.h @@ -0,0 +1,59 @@ +#ifndef SOURCEMETA_CORE_JSONSCHEMA_HELPERS_H +#define SOURCEMETA_CORE_JSONSCHEMA_HELPERS_H + +#include + +#include // assert +#include // std::string_view + +namespace sourcemeta::core { + +inline auto id_keyword(const SchemaBaseDialect base_dialect) + -> std::string_view { + switch (base_dialect) { + case SchemaBaseDialect::JSON_Schema_2020_12: + case SchemaBaseDialect::JSON_Schema_2020_12_Hyper: + case SchemaBaseDialect::JSON_Schema_2019_09: + case SchemaBaseDialect::JSON_Schema_2019_09_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_7: + case SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_6: + case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: + return "$id"; + case SchemaBaseDialect::JSON_Schema_Draft_4: + case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_3: + case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_2_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_1_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_0_Hyper: + return "id"; + } + + assert(false); + return "$id"; +} + +// In older drafts, the presence of `$ref` would override any sibling keywords +// See +// https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3 +inline auto +ref_overrides_adjacent_keywords(const SchemaBaseDialect base_dialect) -> bool { + switch (base_dialect) { + case SchemaBaseDialect::JSON_Schema_Draft_7: + case SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_6: + case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_4: + case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: + case SchemaBaseDialect::JSON_Schema_Draft_3: + case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: + return true; + default: + return false; + } +} + +} // namespace sourcemeta::core + +#endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h index 56eac65..4b5dc49 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_error.h @@ -241,6 +241,33 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaBaseDialectError std::string base_dialect_; }; +/// @ingroup jsonschema +/// An error that represents a schema keyword error +class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaKeywordError + : public std::exception { +public: + SchemaKeywordError(const std::string_view keyword, + const std::string_view value, const char *message) + : keyword_{keyword}, value_{value}, message_{message} {} + + [[nodiscard]] auto what() const noexcept -> const char * override { + return this->message_; + } + + [[nodiscard]] auto value() const noexcept -> std::string_view { + return this->value_; + } + + [[nodiscard]] auto keyword() const noexcept -> std::string_view { + return this->keyword_; + } + +private: + std::string keyword_; + std::string value_; + const char *message_; +}; + /// @ingroup jsonschema /// An error that represents a schema frame error class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrameError diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h index 9f95dbe..1061f87 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_frame.h @@ -17,6 +17,7 @@ #include // std::optional #include // std::set #include // std::tuple +#include // std::unordered_map #include // std::unordered_set #include // std::pair #include // std::vector @@ -61,6 +62,12 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrame { SchemaFrame(const Mode mode) : mode_{mode} {} + // We rely on internal caches that would be dangling otherwise + SchemaFrame(const SchemaFrame &) = delete; + auto operator=(const SchemaFrame &) -> SchemaFrame & = delete; + SchemaFrame(SchemaFrame &&) = delete; + auto operator=(SchemaFrame &&) -> SchemaFrame & = delete; + // Query the current mode that the schema frame was configured with [[nodiscard]] auto mode() const noexcept -> Mode { return this->mode_; } @@ -113,6 +120,8 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrame { std::size_t relative_pointer; std::string_view dialect; SchemaBaseDialect base_dialect; + bool property_name; + bool orphan; }; /// A JSON Schema reference frame is a mapping of URIs to schema identifiers, @@ -183,6 +192,11 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrame { [[nodiscard]] auto traverse(const WeakPointer &pointer) const -> std::optional>; + /// Get the location of a specific type associated with a given pointer + [[nodiscard]] auto traverse(const WeakPointer &pointer, + const LocationType type) const + -> std::optional>; + /// Turn an absolute pointer into a location URI [[nodiscard]] auto uri(const WeakPointer &pointer) const -> std::optional>; @@ -227,7 +241,16 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrame { /// Reset the frame, clearing all analysed data auto reset() -> void; + /// Determines if a location could be evaluated during validation + [[nodiscard]] auto is_reachable(const Location &location, + const SchemaWalker &walker, + const SchemaResolver &resolver) const -> bool; + private: + auto populate_pointer_to_location() const -> void; + auto populate_reachability(const SchemaWalker &walker, + const SchemaResolver &resolver) const -> void; + Mode mode_; // Exporting symbols that depends on the standard C++ library is considered // safe. @@ -238,6 +261,13 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaFrame { JSON::String root_; Locations locations_; References references_; + mutable std::unordered_map, + std::vector, WeakPointer::Hasher, + WeakPointer::Comparator> + pointer_to_location_; + mutable std::unordered_map, bool, + WeakPointer::Hasher, WeakPointer::Comparator> + reachability_; #if defined(_MSC_VER) #pragma warning(default : 4251 4275) #endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h index a377087..55ff68d 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_transform.h @@ -18,7 +18,9 @@ #include // std::set #include // std::string #include // std::string_view -#include // std::move, std::forward, std::pair +#include // std::tuple +#include // std::is_same_v, std::true_type +#include // std::move, std::forward #include // std::vector namespace sourcemeta::core { @@ -113,8 +115,8 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformRule { [[nodiscard]] auto check(const JSON &schema, const JSON &root, const Vocabularies &vocabularies, const SchemaWalker &walker, const SchemaResolver &resolver, - const SchemaFrame &frame, const SchemaFrame::Location &location) const - -> Result; + const SchemaFrame &frame, const SchemaFrame::Location &location, + const JSON::String &exclude_keyword) const -> Result; /// A method to optionally fix any reference location that was affected by the /// transformation. @@ -130,7 +132,7 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformRule { const SchemaResolver &resolver) const -> Result = 0; /// The rule transformation. If this virtual method is not overriden, - /// then the rule condition is considered to not be fixable. + /// then the rule is considered to not mutate the schema. virtual auto transform(JSON &schema, const Result &result) const -> void; private: @@ -163,6 +165,9 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformRule { /// public: /// MyRule() : sourcemeta::core::SchemaTransformRule("my_rule") {}; /// +/// using mutates = std::true_type; +/// using reframe_after_transform = std::true_type; +/// /// [[nodiscard]] auto condition(const sourcemeta::core::JSON &schema, /// const sourcemeta::core::Vocabularies /// &vocabularies, @@ -227,7 +232,15 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer { /// It is the caller's responsibility to not add duplicate rules. template T, typename... Args> auto add(Args &&...args) -> void { - this->rules.push_back(std::make_unique(std::forward(args)...)); + static_assert(requires { typename T::mutates; }); + static_assert(requires { typename T::reframe_after_transform; }); + static_assert( + std::is_same_v || + std::is_same_v); + this->rules.emplace_back( + std::make_unique(std::forward(args)...), + std::is_same_v, + std::is_same_v); } /// Remove a rule from the bundle @@ -245,19 +258,19 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer { const SchemaTransformRule::Result &)>; /// Apply the bundle of rules to a schema - [[nodiscard]] auto apply(JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, - const Callback &callback, - std::string_view default_dialect = "", - std::string_view default_id = "") const + [[nodiscard]] auto + apply(JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, const Callback &callback, + std::string_view default_dialect = "", std::string_view default_id = "", + const JSON::String &exclude_keyword = "") const -> std::pair; /// Report back the rules from the bundle that need to be applied to a schema - [[nodiscard]] auto check(const JSON &schema, const SchemaWalker &walker, - const SchemaResolver &resolver, - const Callback &callback, - std::string_view default_dialect = "", - std::string_view default_id = "") const + [[nodiscard]] auto + check(const JSON &schema, const SchemaWalker &walker, + const SchemaResolver &resolver, const Callback &callback, + std::string_view default_dialect = "", std::string_view default_id = "", + const JSON::String &exclude_keyword = "") const -> std::pair; [[nodiscard]] auto begin() const -> auto { return this->rules.cbegin(); } @@ -270,7 +283,8 @@ class SOURCEMETA_CORE_JSONSCHEMA_EXPORT SchemaTransformer { #if defined(_MSC_VER) #pragma warning(disable : 4251) #endif - std::vector> rules; + std::vector, bool, bool>> + rules; #if defined(_MSC_VER) #pragma warning(default : 4251) #endif diff --git a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_types.h b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_types.h index 1413d96..30f0ddf 100644 --- a/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_types.h +++ b/vendor/core/src/core/jsonschema/include/sourcemeta/core/jsonschema_types.h @@ -226,6 +226,7 @@ struct SchemaIteratorEntry { std::optional base_dialect; std::reference_wrapper subschema; bool orphan; + bool property_name; }; } // namespace sourcemeta::core diff --git a/vendor/core/src/core/jsonschema/jsonschema.cc b/vendor/core/src/core/jsonschema/jsonschema.cc index 937ee43..aad3309 100644 --- a/vendor/core/src/core/jsonschema/jsonschema.cc +++ b/vendor/core/src/core/jsonschema/jsonschema.cc @@ -1,9 +1,12 @@ #include +#include "helpers.h" + #include // assert #include // std::uint64_t #include // std::numeric_limits #include // std::accumulate +#include // std::ostringstream #include // std::remove_reference_t #include // std::unordered_map #include // std::move @@ -98,37 +101,6 @@ auto sourcemeta::core::to_base_dialect(const std::string_view base_dialect) return std::nullopt; } -namespace { - -static auto id_keyword(const sourcemeta::core::SchemaBaseDialect base_dialect) - -> std::string_view { - using sourcemeta::core::SchemaBaseDialect; - switch (base_dialect) { - case SchemaBaseDialect::JSON_Schema_2020_12: - case SchemaBaseDialect::JSON_Schema_2020_12_Hyper: - case SchemaBaseDialect::JSON_Schema_2019_09: - case SchemaBaseDialect::JSON_Schema_2019_09_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_7: - case SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_6: - case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: - return "$id"; - case SchemaBaseDialect::JSON_Schema_Draft_4: - case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_3: - case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_2_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_1_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_0_Hyper: - return "id"; - } - - assert(false); - return {}; -} - -} // namespace - auto sourcemeta::core::identify(const sourcemeta::core::JSON &schema, const SchemaResolver &resolver, std::string_view default_dialect, @@ -157,7 +129,7 @@ auto sourcemeta::core::identify(const JSON &schema, return default_id; } - const std::string keyword{id_keyword(base_dialect)}; + const std::string keyword{sourcemeta::core::id_keyword(base_dialect)}; if (!schema.defines(keyword)) { return default_id; @@ -165,8 +137,10 @@ auto sourcemeta::core::identify(const JSON &schema, const auto &identifier{schema.at(keyword)}; if (!identifier.is_string() || identifier.empty()) { - throw sourcemeta::core::SchemaError( - "The schema identifier property is invalid"); + std::ostringstream value; + sourcemeta::core::stringify(identifier, value); + throw sourcemeta::core::SchemaKeywordError( + keyword, value.str(), "The schema identifier is invalid"); } // In older drafts, the presence of `$ref` would override any sibling @@ -192,7 +166,7 @@ auto sourcemeta::core::identify(const JSON &schema, auto sourcemeta::core::anonymize(JSON &schema, const SchemaBaseDialect base_dialect) -> void { if (schema.is_object()) { - schema.erase(std::string{id_keyword(base_dialect)}); + schema.erase(std::string{sourcemeta::core::id_keyword(base_dialect)}); } } @@ -213,7 +187,8 @@ auto sourcemeta::core::reidentify(JSON &schema, std::string_view new_identifier, -> void { assert(is_schema(schema)); assert(schema.is_object()); - schema.assign(std::string{id_keyword(base_dialect)}, JSON{new_identifier}); + schema.assign(std::string{sourcemeta::core::id_keyword(base_dialect)}, + JSON{new_identifier}); // If we reidentify, and the identifier is still not retrievable, then // we are facing the Draft 7 `$ref` sibling edge case, and we cannot @@ -231,7 +206,15 @@ auto sourcemeta::core::dialect(const sourcemeta::core::JSON &schema, return default_dialect; } - return schema.at("$schema").to_string(); + const auto &dialect_value{schema.at("$schema")}; + if (!dialect_value.is_string()) { + std::ostringstream value; + sourcemeta::core::stringify(dialect_value, value); + throw sourcemeta::core::SchemaKeywordError("$schema", value.str(), + "The dialect value is invalid"); + } + + return dialect_value.to_string(); } auto sourcemeta::core::metaschema( @@ -285,9 +268,17 @@ auto sourcemeta::core::base_dialect( const std::optional metaschema{ resolver(effective_dialect)}; if (!metaschema.has_value()) { + URI effective_dialect_uri; + try { + effective_dialect_uri = URI{effective_dialect}; + } catch (const URIParseError &) { + throw sourcemeta::core::SchemaKeywordError( + "$schema", std::string{effective_dialect}, + "The dialect is not a valid URI"); + } + // Relative meta-schema references are invalid according to the // JSON Schema specifications. They must be absolute ones - const URI effective_dialect_uri{effective_dialect}; if (effective_dialect_uri.is_relative()) { throw sourcemeta::core::SchemaRelativeMetaschemaResolutionError( std::string{effective_dialect}); diff --git a/vendor/core/src/core/jsonschema/transformer.cc b/vendor/core/src/core/jsonschema/transformer.cc index d044eb5..2ff1010 100644 --- a/vendor/core/src/core/jsonschema/transformer.cc +++ b/vendor/core/src/core/jsonschema/transformer.cc @@ -36,6 +36,87 @@ auto calculate_health_percentage(const std::size_t subschemas, return static_cast(result); } +auto check_rules( + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const std::vector, bool, bool>> + &rules, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const sourcemeta::core::SchemaTransformer::Callback &callback, + const sourcemeta::core::JSON::String &exclude_keyword, + const bool non_mutating_only) -> std::pair { + std::unordered_set + visited; + bool result{true}; + std::size_t subschema_count{0}; + std::size_t subschema_failures{0}; + + 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 [visited_iterator, inserted] = + visited.insert(sourcemeta::core::to_pointer(entry.second.pointer)); + if (!inserted) { + continue; + } + const auto &entry_pointer{*visited_iterator}; + + subschema_count += 1; + + const auto ¤t{sourcemeta::core::get(schema, entry_pointer)}; + const auto current_vocabularies{frame.vocabularies(entry.second, resolver)}; + + bool subschema_failed{false}; + for (const auto &[rule, mutates, _] : rules) { + // TODO: In this case, can we avoid framing and the entire subschema loop + // if there will be no rules to execute that match this criteria? + if (non_mutating_only && mutates) { + continue; + } + + const auto outcome{rule->check(current, schema, current_vocabularies, + walker, resolver, frame, entry.second, + exclude_keyword)}; + if (outcome.applies) { + subschema_failed = true; + callback(entry_pointer, rule->name(), rule->message(), outcome); + } + } + + if (subschema_failed) { + subschema_failures += 1; + result = false; + } + } + + return {result, + calculate_health_percentage(subschema_count, subschema_failures)}; +} + +auto analyse_frame(sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const std::string_view default_dialect, + const std::string_view default_id) -> void { + + // If we use the default id when there is already one, framing will duplicate + // the locations leading to duplicate check reports + if (!sourcemeta::core::identify(schema, resolver, default_dialect).empty()) { + frame.analyse(schema, walker, resolver, default_dialect); + } else { + frame.analyse(schema, walker, resolver, default_dialect, default_id); + } +} + } // namespace namespace sourcemeta::core { @@ -68,91 +149,54 @@ auto SchemaTransformRule::rereference(const std::string_view reference, "The reference broke after transformation"); } -auto SchemaTransformRule::check(const JSON &schema, const JSON &root, - const Vocabularies &vocabularies, - const SchemaWalker &walker, - const SchemaResolver &resolver, - const SchemaFrame &frame, - const SchemaFrame::Location &location) const - -> SchemaTransformRule::Result { - return this->condition(schema, root, vocabularies, frame, location, walker, - resolver); +auto SchemaTransformRule::check( + const JSON &schema, const JSON &root, const Vocabularies &vocabularies, + const SchemaWalker &walker, const SchemaResolver &resolver, + const SchemaFrame &frame, const SchemaFrame::Location &location, + const JSON::String &exclude_keyword) const -> SchemaTransformRule::Result { + auto result{this->condition(schema, root, vocabularies, frame, location, + walker, resolver)}; + + // Support rule exclusion + if (result.applies && !exclude_keyword.empty() && schema.is_object()) { + const auto *exclude_value{schema.try_at(exclude_keyword)}; + if (exclude_value != nullptr && + ((exclude_value->is_string() && + exclude_value->to_string() == this->name()) || + (exclude_value->is_array() && + exclude_value->contains(this->name())))) { + return false; + } + } + + return result; } auto SchemaTransformer::check(const JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, const SchemaTransformer::Callback &callback, std::string_view default_dialect, - std::string_view default_id) const + std::string_view default_id, + const JSON::String &exclude_keyword) const -> std::pair { SchemaFrame frame{SchemaFrame::Mode::References}; - - // If we use the default id when there is already one, framing will duplicate - // the locations leading to duplicate check reports - if (!sourcemeta::core::identify(schema, resolver, default_dialect).empty()) { - frame.analyse(schema, walker, resolver, default_dialect); - } else { - frame.analyse(schema, walker, resolver, default_dialect, default_id); - } - - std::unordered_set visited; - bool result{true}; - std::size_t subschema_count{0}; - std::size_t subschema_failures{0}; - for (const auto &entry : frame.locations()) { - if (entry.second.type != SchemaFrame::LocationType::Resource && - entry.second.type != SchemaFrame::LocationType::Subschema) { - continue; - } - - // Framing may report resource twice or more given default identifiers and - // nested resources, risking reporting the same errors twice - const auto [visited_iterator, inserted] = - visited.insert(to_pointer(entry.second.pointer)); - if (!inserted) { - continue; - } - const auto &entry_pointer{*visited_iterator}; - - subschema_count += 1; - - const auto ¤t{get(schema, entry_pointer)}; - const auto current_vocabularies{frame.vocabularies(entry.second, resolver)}; - bool subresult{true}; - for (const auto &rule : this->rules) { - const auto outcome{rule->check(current, schema, current_vocabularies, - walker, resolver, frame, entry.second)}; - if (outcome.applies) { - subresult = false; - callback(entry_pointer, rule->name(), rule->message(), outcome); - } - } - - if (!subresult) { - subschema_failures += 1; - result = false; - } - } - - return {result, - calculate_health_percentage(subschema_count, subschema_failures)}; + analyse_frame(frame, schema, walker, resolver, default_dialect, default_id); + return check_rules(schema, frame, this->rules, walker, resolver, callback, + exclude_keyword, false); } auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, const SchemaTransformer::Callback &callback, std::string_view default_dialect, - std::string_view default_id) const + std::string_view default_id, + const JSON::String &exclude_keyword) const -> std::pair { assert(!this->rules.empty()); std::unordered_set, ProcessedRuleHasher> processed_rules; - bool result{true}; - std::size_t subschema_count{0}; - std::size_t subschema_failures{0}; - SchemaFrame frame{SchemaFrame::Mode::References}; struct PotentiallyBrokenReference { @@ -167,13 +211,12 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, while (true) { if (frame.empty()) { - frame.analyse(schema, walker, resolver, default_dialect, default_id); + analyse_frame(frame, schema, walker, resolver, default_dialect, + default_id); } - std::unordered_set visited; + std::unordered_set visited; bool applied{false}; - subschema_count = 0; - subschema_failures = 0; for (const auto &entry : frame.locations()) { if (entry.second.type != SchemaFrame::LocationType::Resource && @@ -189,17 +232,20 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, continue; } const auto &entry_pointer{*visited_iterator}; - - subschema_count += 1; - auto ¤t{get(schema, entry_pointer)}; const auto current_vocabularies{ frame.vocabularies(entry.second, resolver)}; - bool subschema_failed{false}; - for (const auto &rule : this->rules) { - auto outcome{rule->condition(current, schema, current_vocabularies, - frame, entry.second, walker, resolver)}; + for (const auto &[rule, mutates, reframe_after_transform] : this->rules) { + // Only process mutating rules in the main loop. + // Non-mutating rules will be processed once at the end. + if (!mutates) { + continue; + } + + auto outcome{rule->check(current, schema, current_vocabularies, walker, + resolver, frame, entry.second, + exclude_keyword)}; if (!outcome.applies) { continue; @@ -226,18 +272,14 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, target.relative_pointer}); } - try { - rule->transform(current, outcome); - } catch (const SchemaAbortError &) { - result = false; - subschema_failed = true; - callback(entry_pointer, rule->name(), rule->message(), outcome); - continue; - } + rule->transform(current, outcome); applied = true; - frame.analyse(schema, walker, resolver, default_dialect, default_id); + if (reframe_after_transform) { + analyse_frame(frame, schema, walker, resolver, default_dialect, + default_id); + } const auto new_location{frame.traverse(to_weak_pointer(entry_pointer))}; // The location should still exist after transform @@ -249,8 +291,8 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, // The condition must always be false after applying the // transformation in order to avoid infinite loops - if (rule->condition(current, schema, new_vocabularies, frame, - new_location.value().get(), walker, 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(); @@ -306,11 +348,9 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, frame.reset(); } - goto core_transformer_start_again; - } - - if (subschema_failed) { - subschema_failures += 1; + if (references_fixed || reframe_after_transform) { + goto core_transformer_start_again; + } } } @@ -320,13 +360,17 @@ auto SchemaTransformer::apply(JSON &schema, const SchemaWalker &walker, } } - return {result, - calculate_health_percentage(subschema_count, subschema_failures)}; + if (frame.empty()) { + analyse_frame(frame, schema, walker, resolver, default_dialect, default_id); + } + + return check_rules(schema, frame, this->rules, walker, resolver, callback, + exclude_keyword, true); } auto SchemaTransformer::remove(const std::string_view name) -> bool { - return std::erase_if(this->rules, [&name](const auto &rule) { - return rule->name() == name; + return std::erase_if(this->rules, [&name](const auto &entry) { + return std::get<0>(entry)->name() == name; }) > 0; } diff --git a/vendor/core/src/core/jsonschema/walker.cc b/vendor/core/src/core/jsonschema/walker.cc index 1ae0b37..c5711c8 100644 --- a/vendor/core/src/core/jsonschema/walker.cc +++ b/vendor/core/src/core/jsonschema/walker.cc @@ -1,33 +1,13 @@ #include +#include "helpers.h" + #include // std::max, std::sort #include // assert namespace { enum class SchemaWalkerType_t : std::uint8_t { Deep, Flat }; -auto ref_overrides_adjacent_keywords( - const sourcemeta::core::SchemaBaseDialect base_dialect) -> bool { - using sourcemeta::core::SchemaBaseDialect; - // In older drafts, the presence of `$ref` would override any sibling - // keywords - // See - // https://json-schema.org/draft-07/draft-handrews-json-schema-01#rfc.section.8.3 - switch (base_dialect) { - case SchemaBaseDialect::JSON_Schema_Draft_7: - case SchemaBaseDialect::JSON_Schema_Draft_7_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_6: - case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_4: - case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: - case SchemaBaseDialect::JSON_Schema_Draft_3: - case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: - return true; - default: - return false; - } -} - auto walk(const std::optional &parent, const sourcemeta::core::WeakPointer &pointer, std::vector &subschemas, @@ -37,7 +17,7 @@ auto walk(const std::optional &parent, const std::string_view dialect, const sourcemeta::core::SchemaBaseDialect base_dialect, const SchemaWalkerType_t type, const std::size_t level, - const bool orphan) -> void { + const bool orphan, const bool property_name) -> void { if (!is_schema(subschema)) { return; } @@ -92,7 +72,8 @@ auto walk(const std::optional &parent, .base_dialect = current_base_dialect, .subschema = subschema, - .orphan = orphan}; + .orphan = orphan, + .property_name = property_name}; subschemas.push_back(std::move(entry)); } @@ -104,7 +85,7 @@ auto walk(const std::optional &parent, const auto has_overriding_ref{ subschema.defines("$ref") && - ref_overrides_adjacent_keywords(current_base_dialect)}; + sourcemeta::core::ref_overrides_adjacent_keywords(current_base_dialect)}; for (auto &pair : subschema.as_object()) { const auto &keyword_info{walker(pair.first, vocabularies)}; @@ -123,7 +104,8 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + false); } break; case sourcemeta::core::SchemaKeywordType:: @@ -131,7 +113,8 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + true); } break; case sourcemeta::core::SchemaKeywordType:: @@ -139,7 +122,8 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + false); } break; case sourcemeta::core::SchemaKeywordType:: @@ -147,35 +131,40 @@ auto walk(const std::optional &parent, sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + false); } break; case sourcemeta::core::SchemaKeywordType::ApplicatorValueTraverseParent: { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + false); } break; case sourcemeta::core::SchemaKeywordType::ApplicatorValueInPlaceOther: { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + property_name); } break; case sourcemeta::core::SchemaKeywordType::ApplicatorValueInPlaceNegate: { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + property_name); } break; case sourcemeta::core::SchemaKeywordType::ApplicatorValueInPlaceMaybe: { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + property_name); } break; case sourcemeta::core::SchemaKeywordType::ApplicatorElementsTraverseItem: @@ -186,7 +175,7 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, false); } } @@ -200,7 +189,7 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, property_name); } } @@ -214,7 +203,7 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, property_name); } } @@ -229,7 +218,7 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, property_name); } } @@ -244,7 +233,7 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, false); } } @@ -259,7 +248,7 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, false); } } @@ -273,7 +262,7 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, property_name); } } @@ -287,7 +276,7 @@ auto walk(const std::optional &parent, new_pointer.push_back(std::cref(subpair.first)); walk(pointer, new_pointer, subschemas, subpair.second, walker, resolver, current_dialect, current_base_dialect, type, - level + 1, true); + level + 1, true, false); } } @@ -302,13 +291,14 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, false); } } else { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + false); } break; @@ -322,13 +312,14 @@ auto walk(const std::optional &parent, new_pointer.emplace_back(index); walk(pointer, new_pointer, subschemas, pair.second.at(index), walker, resolver, current_dialect, current_base_dialect, type, - level + 1, orphan); + level + 1, orphan, property_name); } } else { sourcemeta::core::WeakPointer new_pointer{pointer}; new_pointer.push_back(std::cref(pair.first)); walk(pointer, new_pointer, subschemas, pair.second, walker, resolver, - current_dialect, current_base_dialect, type, level + 1, orphan); + current_dialect, current_base_dialect, type, level + 1, orphan, + property_name); } break; @@ -366,7 +357,8 @@ sourcemeta::core::SchemaIterator::SchemaIterator( .vocabularies = {}, .base_dialect = std::nullopt, .subschema = schema, - .orphan = false}; + .orphan = false, + .property_name = false}; this->subschemas.push_back(std::move(entry)); } else { const auto resolved_base_dialect{ @@ -374,7 +366,7 @@ sourcemeta::core::SchemaIterator::SchemaIterator( assert(resolved_base_dialect.has_value()); walk(std::nullopt, pointer, this->subschemas, schema, walker, resolver, resolved_dialect, resolved_base_dialect.value(), - SchemaWalkerType_t::Deep, 0, false); + SchemaWalkerType_t::Deep, 0, false, false); } } @@ -392,7 +384,7 @@ sourcemeta::core::SchemaIteratorFlat::SchemaIteratorFlat( assert(resolved_base_dialect.has_value()); walk(std::nullopt, pointer, this->subschemas, schema, walker, resolver, resolved_dialect, resolved_base_dialect.value(), - SchemaWalkerType_t::Flat, 0, false); + SchemaWalkerType_t::Flat, 0, false, false); } } @@ -427,7 +419,8 @@ sourcemeta::core::SchemaKeywordIterator::SchemaKeywordIterator( .vocabularies = vocabularies, .base_dialect = maybe_base_dialect, .subschema = entry.second, - .orphan = false}; + .orphan = false, + .property_name = false}; this->entries.push_back(std::move(subschema_entry)); } diff --git a/vendor/core/src/core/yaml/yaml.cc b/vendor/core/src/core/yaml/yaml.cc index d8a1fcc..162a5cb 100644 --- a/vendor/core/src/core/yaml/yaml.cc +++ b/vendor/core/src/core/yaml/yaml.cc @@ -59,7 +59,8 @@ struct AnchoredValue { sourcemeta::core::JSON value; std::vector> + std::uint64_t, sourcemeta::core::JSON::ParseContext, + std::size_t, std::string>> callbacks; }; @@ -67,14 +68,18 @@ using AnchorMap = std::unordered_map; auto consume_value_event(yaml_parser_t *parser, yaml_event_t *event, const sourcemeta::core::JSON::ParseCallback &callback, - const sourcemeta::core::JSON &context, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::StringView property, AnchorMap &anchors, const yaml_mark_t *override_start_mark = nullptr) -> sourcemeta::core::JSON; auto consume_scalar_event(yaml_event_t *event, const sourcemeta::core::JSON::ParseCallback &callback, - const sourcemeta::core::JSON &context, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::StringView property, AnchorMap &anchors, const yaml_mark_t *override_start_mark = nullptr) -> sourcemeta::core::JSON { @@ -117,18 +122,22 @@ auto consume_scalar_event(yaml_event_t *event, std::vector> + std::uint64_t, sourcemeta::core::JSON::ParseContext, + std::size_t, std::string>> recorded_callbacks; if (callback) { callback(sourcemeta::core::JSON::ParsePhase::Pre, type, start_line, - start_column, context); + start_column, context, index, property); recorded_callbacks.emplace_back(sourcemeta::core::JSON::ParsePhase::Pre, - type, start_line, start_column, context); + type, start_line, start_column, context, + index, std::string{property}); callback(sourcemeta::core::JSON::ParsePhase::Post, type, end_line, - end_column, result); - recorded_callbacks.emplace_back(sourcemeta::core::JSON::ParsePhase::Post, - type, end_line, end_column, result); + end_column, sourcemeta::core::JSON::ParseContext::Root, 0, + sourcemeta::core::JSON::StringView{}); + recorded_callbacks.emplace_back( + sourcemeta::core::JSON::ParsePhase::Post, type, end_line, end_column, + sourcemeta::core::JSON::ParseContext::Root, 0, ""); } if (event->data.scalar.anchor) { @@ -145,8 +154,10 @@ auto consume_scalar_event(yaml_event_t *event, auto consume_sequence_events( yaml_parser_t *parser, yaml_event_t *start_event, const sourcemeta::core::JSON::ParseCallback &callback, - const sourcemeta::core::JSON &context, AnchorMap &anchors, - const yaml_mark_t *override_start_mark = nullptr) + const sourcemeta::core::JSON::ParseContext context, + const std::size_t context_index, + const sourcemeta::core::JSON::StringView context_property, + AnchorMap &anchors, const yaml_mark_t *override_start_mark = nullptr) -> sourcemeta::core::JSON { const yaml_mark_t &start_mark{override_start_mark ? *override_start_mark : start_event->start_mark}; @@ -156,18 +167,23 @@ auto consume_sequence_events( const bool has_anchor{start_event->data.sequence_start.anchor != nullptr}; std::vector> + std::uint64_t, sourcemeta::core::JSON::ParseContext, + std::size_t, std::string>> recorded_callbacks; sourcemeta::core::JSON::ParseCallback effective_callback; if (has_anchor && callback) { - effective_callback = - [&](const sourcemeta::core::JSON::ParsePhase phase, - const sourcemeta::core::JSON::Type type, const std::uint64_t line, - const std::uint64_t column, const sourcemeta::core::JSON &value) { - recorded_callbacks.emplace_back(phase, type, line, column, value); - callback(phase, type, line, column, value); - }; + effective_callback = [&](const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, + const std::uint64_t line, + const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext ctx, + const std::size_t idx, + const sourcemeta::core::JSON::StringView prop) { + recorded_callbacks.emplace_back(phase, type, line, column, ctx, idx, + std::string{prop}); + callback(phase, type, line, column, ctx, idx, prop); + }; } else { effective_callback = callback; } @@ -175,11 +191,11 @@ auto consume_sequence_events( if (effective_callback) { effective_callback(sourcemeta::core::JSON::ParsePhase::Pre, sourcemeta::core::JSON::Type::Array, start_line, - start_column, context); + start_column, context, context_index, context_property); } auto result{sourcemeta::core::JSON::make_array()}; - std::uint64_t index{0}; + std::size_t index{0}; while (true) { yaml_event_t event; @@ -195,7 +211,9 @@ auto consume_sequence_events( if (effective_callback) { effective_callback(sourcemeta::core::JSON::ParsePhase::Post, sourcemeta::core::JSON::Type::Array, end_line, - end_column, result); + end_column, + sourcemeta::core::JSON::ParseContext::Root, 0, + sourcemeta::core::JSON::StringView{}); } if (has_anchor) { @@ -210,9 +228,10 @@ auto consume_sequence_events( return result; } - auto value{consume_value_event( - parser, &event, effective_callback, - sourcemeta::core::JSON{static_cast(index)}, anchors)}; + auto value{consume_value_event(parser, &event, effective_callback, + sourcemeta::core::JSON::ParseContext::Index, + index, sourcemeta::core::JSON::StringView{}, + anchors)}; result.push_back(std::move(value)); index++; } @@ -221,8 +240,10 @@ auto consume_sequence_events( auto consume_mapping_events( yaml_parser_t *parser, yaml_event_t *start_event, const sourcemeta::core::JSON::ParseCallback &callback, - const sourcemeta::core::JSON &context, AnchorMap &anchors, - const yaml_mark_t *override_start_mark = nullptr) + const sourcemeta::core::JSON::ParseContext context, + const std::size_t context_index, + const sourcemeta::core::JSON::StringView context_property, + AnchorMap &anchors, const yaml_mark_t *override_start_mark = nullptr) -> sourcemeta::core::JSON { const yaml_mark_t &start_mark{override_start_mark ? *override_start_mark : start_event->start_mark}; @@ -232,18 +253,23 @@ auto consume_mapping_events( const bool has_anchor{start_event->data.mapping_start.anchor != nullptr}; std::vector> + std::uint64_t, sourcemeta::core::JSON::ParseContext, + std::size_t, std::string>> recorded_callbacks; sourcemeta::core::JSON::ParseCallback effective_callback; if (has_anchor && callback) { - effective_callback = - [&](const sourcemeta::core::JSON::ParsePhase phase, - const sourcemeta::core::JSON::Type type, const std::uint64_t line, - const std::uint64_t column, const sourcemeta::core::JSON &value) { - recorded_callbacks.emplace_back(phase, type, line, column, value); - callback(phase, type, line, column, value); - }; + effective_callback = [&](const sourcemeta::core::JSON::ParsePhase phase, + const sourcemeta::core::JSON::Type type, + const std::uint64_t line, + const std::uint64_t column, + const sourcemeta::core::JSON::ParseContext ctx, + const std::size_t idx, + const sourcemeta::core::JSON::StringView prop) { + recorded_callbacks.emplace_back(phase, type, line, column, ctx, idx, + std::string{prop}); + callback(phase, type, line, column, ctx, idx, prop); + }; } else { effective_callback = callback; } @@ -251,7 +277,7 @@ auto consume_mapping_events( if (effective_callback) { effective_callback(sourcemeta::core::JSON::ParsePhase::Pre, sourcemeta::core::JSON::Type::Object, start_line, - start_column, context); + start_column, context, context_index, context_property); } auto result{sourcemeta::core::JSON::make_object()}; @@ -270,7 +296,9 @@ auto consume_mapping_events( if (effective_callback) { effective_callback(sourcemeta::core::JSON::ParsePhase::Post, sourcemeta::core::JSON::Type::Object, end_line, - end_column, result); + end_column, + sourcemeta::core::JSON::ParseContext::Root, 0, + sourcemeta::core::JSON::StringView{}); } if (has_anchor) { @@ -301,16 +329,19 @@ auto consume_mapping_events( throw sourcemeta::core::YAMLParseError("Failed to parse YAML event"); } - auto value{consume_value_event(parser, &value_event, effective_callback, - sourcemeta::core::JSON{key}, anchors, - &key_start_mark)}; + auto value{ + consume_value_event(parser, &value_event, effective_callback, + sourcemeta::core::JSON::ParseContext::Property, 0, + key, anchors, &key_start_mark)}; result.assign(key, std::move(value)); } } auto consume_value_event(yaml_parser_t *parser, yaml_event_t *event, const sourcemeta::core::JSON::ParseCallback &callback, - const sourcemeta::core::JSON &context, + const sourcemeta::core::JSON::ParseContext context, + const std::size_t index, + const sourcemeta::core::JSON::StringView property, AnchorMap &anchors, const yaml_mark_t *override_start_mark) -> sourcemeta::core::JSON { @@ -318,26 +349,36 @@ auto consume_value_event(yaml_parser_t *parser, yaml_event_t *event, switch (event->type) { case YAML_SCALAR_EVENT: - result = consume_scalar_event(event, callback, context, anchors, - override_start_mark); + result = consume_scalar_event(event, callback, context, index, property, + anchors, override_start_mark); yaml_event_delete(event); break; case YAML_SEQUENCE_START_EVENT: - result = consume_sequence_events(parser, event, callback, context, - anchors, override_start_mark); + result = consume_sequence_events(parser, event, callback, context, index, + property, anchors, override_start_mark); yaml_event_delete(event); break; case YAML_MAPPING_START_EVENT: - result = consume_mapping_events(parser, event, callback, context, anchors, - override_start_mark); + result = consume_mapping_events(parser, event, callback, context, index, + property, anchors, override_start_mark); yaml_event_delete(event); break; case YAML_ALIAS_EVENT: { const std::string anchor_name{ reinterpret_cast(event->data.alias.anchor)}; + const std::uint64_t alias_start_line{(override_start_mark + ? override_start_mark->line + : event->start_mark.line) + + 1}; + const std::uint64_t alias_start_column{(override_start_mark + ? override_start_mark->column + : event->start_mark.column) + + 1}; + const std::uint64_t alias_end_line{event->end_mark.line + 1}; + const std::uint64_t alias_end_column{event->end_mark.column}; yaml_event_delete(event); const auto anchor_it{anchors.find(anchor_name)}; @@ -349,20 +390,29 @@ auto consume_value_event(yaml_parser_t *parser, yaml_event_t *event, if (callback) { const auto &callbacks{anchor_it->second.callbacks}; - if (result.is_object() || result.is_array()) { - for (std::size_t index = 1; index < callbacks.size() - 1; index++) { - callback( - std::get<0>(callbacks[index]), std::get<1>(callbacks[index]), - std::get<2>(callbacks[index]), std::get<3>(callbacks[index]), - std::get<4>(callbacks[index])); - } - } else { - if (!callbacks.empty()) { - const auto &post_trace = callbacks.back(); - callback(std::get<0>(post_trace), std::get<1>(post_trace), - std::get<2>(post_trace), std::get<3>(post_trace), - std::get<4>(post_trace)); + if (!callbacks.empty()) { + const auto value_type{std::get<1>(callbacks.front())}; + callback(sourcemeta::core::JSON::ParsePhase::Pre, value_type, + alias_start_line, alias_start_column, context, index, + property); + + if (result.is_object() || result.is_array()) { + for (std::size_t callback_index = 1; + callback_index < callbacks.size() - 1; callback_index++) { + callback(std::get<0>(callbacks[callback_index]), + std::get<1>(callbacks[callback_index]), + std::get<2>(callbacks[callback_index]), + std::get<3>(callbacks[callback_index]), + std::get<4>(callbacks[callback_index]), + std::get<5>(callbacks[callback_index]), + std::get<6>(callbacks[callback_index])); + } } + + callback(sourcemeta::core::JSON::ParsePhase::Post, value_type, + alias_end_line, alias_end_column, + sourcemeta::core::JSON::ParseContext::Root, 0, + sourcemeta::core::JSON::StringView{}); } } @@ -406,8 +456,9 @@ auto parse_yaml_from_events( throw sourcemeta::core::YAMLParseError("Failed to parse YAML value"); } - auto result{consume_value_event(parser, &event, callback, - sourcemeta::core::JSON{nullptr}, anchors)}; + auto result{consume_value_event( + parser, &event, callback, sourcemeta::core::JSON::ParseContext::Root, 0, + sourcemeta::core::JSON::StringView{}, anchors)}; if (!yaml_parser_parse(parser, &event)) { throw sourcemeta::core::YAMLParseError("Failed to parse YAML document end"); diff --git a/vendor/core/src/extension/alterschema/CMakeLists.txt b/vendor/core/src/extension/alterschema/CMakeLists.txt index 506936b..ee43cbb 100644 --- a/vendor/core/src/extension/alterschema/CMakeLists.txt +++ b/vendor/core/src/extension/alterschema/CMakeLists.txt @@ -1,10 +1,10 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema SOURCES alterschema.cc # Canonicalizer - canonicalizer/boolean_true.h canonicalizer/const_as_enum.h canonicalizer/exclusive_maximum_integer_to_maximum.h canonicalizer/exclusive_minimum_integer_to_minimum.h + canonicalizer/items_implicit.h canonicalizer/max_contains_covered_by_max_items.h canonicalizer/min_items_given_min_contains.h canonicalizer/min_items_implicit.h @@ -20,6 +20,10 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema canonicalizer/type_union_implicit.h # Common + common/allof_false_simplify.h + common/anyof_false_simplify.h + common/anyof_remove_false_schemas.h + common/anyof_true_simplify.h common/const_with_type.h common/orphan_definitions.h common/content_media_type_without_encoding.h @@ -33,6 +37,7 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema common/duplicate_anyof_branches.h common/duplicate_enum_values.h common/duplicate_required_values.h + common/empty_object_as_true.h common/else_empty.h common/else_without_if.h common/enum_with_type.h @@ -50,19 +55,21 @@ sourcemeta_library(NAMESPACE sourcemeta PROJECT core NAME alterschema common/non_applicable_enum_validation_keywords.h common/non_applicable_type_specific_keywords.h common/not_false.h + common/oneof_false_simplify.h + common/oneof_to_anyof_disjoint_types.h common/required_properties_in_properties.h common/single_type_array.h common/then_empty.h common/then_without_if.h common/unknown_keywords_prefix.h common/unknown_local_ref.h + common/unsatisfiable_drop_validation.h common/unnecessary_allof_ref_wrapper_draft.h common/unnecessary_allof_ref_wrapper_modern.h common/unnecessary_allof_wrapper.h common/unsatisfiable_in_place_applicator_type.h # Linter - linter/additional_properties_default.h linter/comment_trim.h linter/content_schema_default.h linter/definitions_to_defs.h diff --git a/vendor/core/src/extension/alterschema/alterschema.cc b/vendor/core/src/extension/alterschema/alterschema.cc index bd2af60..131b20c 100644 --- a/vendor/core/src/extension/alterschema/alterschema.cc +++ b/vendor/core/src/extension/alterschema/alterschema.cc @@ -29,10 +29,10 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) } // Canonicalizer -#include "canonicalizer/boolean_true.h" #include "canonicalizer/const_as_enum.h" #include "canonicalizer/exclusive_maximum_integer_to_maximum.h" #include "canonicalizer/exclusive_minimum_integer_to_minimum.h" +#include "canonicalizer/items_implicit.h" #include "canonicalizer/max_contains_covered_by_max_items.h" #include "canonicalizer/min_items_given_min_contains.h" #include "canonicalizer/min_items_implicit.h" @@ -48,6 +48,10 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "canonicalizer/type_union_implicit.h" // Common +#include "common/allof_false_simplify.h" +#include "common/anyof_false_simplify.h" +#include "common/anyof_remove_false_schemas.h" +#include "common/anyof_true_simplify.h" #include "common/const_with_type.h" #include "common/content_media_type_without_encoding.h" #include "common/content_schema_without_media_type.h" @@ -62,6 +66,7 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "common/duplicate_required_values.h" #include "common/else_empty.h" #include "common/else_without_if.h" +#include "common/empty_object_as_true.h" #include "common/enum_with_type.h" #include "common/equal_numeric_bounds_to_enum.h" #include "common/exclusive_maximum_number_and_maximum.h" @@ -77,6 +82,8 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "common/non_applicable_enum_validation_keywords.h" #include "common/non_applicable_type_specific_keywords.h" #include "common/not_false.h" +#include "common/oneof_false_simplify.h" +#include "common/oneof_to_anyof_disjoint_types.h" #include "common/orphan_definitions.h" #include "common/required_properties_in_properties.h" #include "common/single_type_array.h" @@ -87,10 +94,10 @@ inline auto APPLIES_TO_POINTERS(std::vector &&keywords) #include "common/unnecessary_allof_ref_wrapper_draft.h" #include "common/unnecessary_allof_ref_wrapper_modern.h" #include "common/unnecessary_allof_wrapper.h" +#include "common/unsatisfiable_drop_validation.h" #include "common/unsatisfiable_in_place_applicator_type.h" // Linter -#include "linter/additional_properties_default.h" #include "linter/comment_trim.h" #include "linter/content_schema_default.h" #include "linter/definitions_to_defs.h" @@ -139,9 +146,16 @@ 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(); + bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -173,7 +187,6 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); if (mode == AlterSchemaMode::Canonicalizer) { - bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -189,11 +202,11 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); } if (mode == AlterSchemaMode::Linter) { bundle.add(); - bundle.add(); bundle.add(); bundle.add(); bundle.add(); @@ -226,6 +239,7 @@ auto add(SchemaTransformer &bundle, const AlterSchemaMode mode) -> void { bundle.add(); bundle.add(); bundle.add(); + bundle.add(); } } // namespace sourcemeta::core diff --git a/vendor/core/src/extension/alterschema/canonicalizer/boolean_true.h b/vendor/core/src/extension/alterschema/canonicalizer/boolean_true.h deleted file mode 100644 index 18cdb03..0000000 --- a/vendor/core/src/extension/alterschema/canonicalizer/boolean_true.h +++ /dev/null @@ -1,23 +0,0 @@ -class BooleanTrue final : public SchemaTransformRule { -public: - BooleanTrue() - : SchemaTransformRule{ - "boolean_true", - "The boolean schema `true` is syntax sugar for the empty schema"} { - }; - - [[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 &, - const sourcemeta::core::SchemaWalker &, - const sourcemeta::core::SchemaResolver &) const - -> sourcemeta::core::SchemaTransformRule::Result override { - return schema.is_boolean() && schema.to_boolean(); - } - - auto transform(JSON &schema, const Result &) const -> void override { - schema.into(sourcemeta::core::JSON::make_object()); - } -}; diff --git a/vendor/core/src/extension/alterschema/canonicalizer/const_as_enum.h b/vendor/core/src/extension/alterschema/canonicalizer/const_as_enum.h index bc9dde0..138f718 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/const_as_enum.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/const_as_enum.h @@ -1,5 +1,7 @@ class ConstAsEnum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ConstAsEnum() : SchemaTransformRule{"const_as_enum", "Setting `const` is syntax sugar for an " diff --git a/vendor/core/src/extension/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h b/vendor/core/src/extension/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h index 5534bcb..1624687 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/exclusive_maximum_integer_to_maximum.h @@ -1,5 +1,7 @@ class ExclusiveMaximumIntegerToMaximum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ExclusiveMaximumIntegerToMaximum() : SchemaTransformRule{ "exclusive_maximum_integer_to_maximum", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h b/vendor/core/src/extension/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h index 9fd09f6..87479ab 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/exclusive_minimum_integer_to_minimum.h @@ -1,5 +1,7 @@ class ExclusiveMinimumIntegerToMinimum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ExclusiveMinimumIntegerToMinimum() : SchemaTransformRule{ "exclusive_minimum_integer_to_minimum", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/items_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/items_implicit.h new file mode 100644 index 0000000..50a361e --- /dev/null +++ b/vendor/core/src/extension/alterschema/canonicalizer/items_implicit.h @@ -0,0 +1,40 @@ +class ItemsImplicit final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + ItemsImplicit() + : SchemaTransformRule{"items_implicit", + "Every array has an implicit `items` " + "that consists of the boolean schema `true`"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + ONLY_CONTINUE_IF( + ((vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2020_12_Applicator)) || + (vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Validation) && + vocabularies.contains( + Vocabularies::Known::JSON_Schema_2019_09_Applicator)) || + 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")); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.assign("items", JSON{true}); + } +}; diff --git a/vendor/core/src/extension/alterschema/canonicalizer/max_contains_covered_by_max_items.h b/vendor/core/src/extension/alterschema/canonicalizer/max_contains_covered_by_max_items.h index 3946bcc..225a289 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/max_contains_covered_by_max_items.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/max_contains_covered_by_max_items.h @@ -1,5 +1,7 @@ class MaxContainsCoveredByMaxItems final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; MaxContainsCoveredByMaxItems() : SchemaTransformRule{ "max_contains_covered_by_max_items", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/min_items_given_min_contains.h b/vendor/core/src/extension/alterschema/canonicalizer/min_items_given_min_contains.h index c3337f0..56a2335 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/min_items_given_min_contains.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/min_items_given_min_contains.h @@ -1,5 +1,7 @@ class MinItemsGivenMinContains final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MinItemsGivenMinContains() : SchemaTransformRule{ "min_items_given_min_contains", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/min_items_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/min_items_implicit.h index ca0d00c..dfdeae0 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/min_items_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/min_items_implicit.h @@ -1,5 +1,7 @@ class MinItemsImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MinItemsImplicit() : SchemaTransformRule{"min_items_implicit", "Every array has a minimum size of zero items"} {}; diff --git a/vendor/core/src/extension/alterschema/canonicalizer/min_length_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/min_length_implicit.h index 1eba9e3..7414758 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/min_length_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/min_length_implicit.h @@ -1,5 +1,7 @@ class MinLengthImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MinLengthImplicit() : SchemaTransformRule{ "min_length_implicit", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/min_properties_covered_by_required.h b/vendor/core/src/extension/alterschema/canonicalizer/min_properties_covered_by_required.h index 40f8a61..60a3191 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/min_properties_covered_by_required.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/min_properties_covered_by_required.h @@ -1,5 +1,7 @@ class MinPropertiesCoveredByRequired final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; MinPropertiesCoveredByRequired() : SchemaTransformRule{ "min_properties_covered_by_required", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/min_properties_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/min_properties_implicit.h index b1cb98e..9a4dca7 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/min_properties_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/min_properties_implicit.h @@ -1,5 +1,7 @@ class MinPropertiesImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MinPropertiesImplicit() : SchemaTransformRule{ "min_properties_implicit", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/multiple_of_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/multiple_of_implicit.h index 32a2c86..b04a47f 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/multiple_of_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/multiple_of_implicit.h @@ -1,5 +1,7 @@ class MultipleOfImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MultipleOfImplicit() : SchemaTransformRule{"multiple_of_implicit", "The unit of `multipleOf` is the integer 1"} {}; diff --git a/vendor/core/src/extension/alterschema/canonicalizer/no_metadata.h b/vendor/core/src/extension/alterschema/canonicalizer/no_metadata.h index 80b53c2..3a2ff33 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/no_metadata.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/no_metadata.h @@ -1,5 +1,7 @@ class NoMetadata final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; NoMetadata() : SchemaTransformRule{"no_metadata", "Annotations, comments, and unknown keywords have " diff --git a/vendor/core/src/extension/alterschema/canonicalizer/properties_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/properties_implicit.h index 6b36eb8..780f095 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/properties_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/properties_implicit.h @@ -1,5 +1,7 @@ class PropertiesImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; PropertiesImplicit() : SchemaTransformRule{"properties_implicit", "Every object has an implicit `properties` " diff --git a/vendor/core/src/extension/alterschema/canonicalizer/type_array_to_any_of.h b/vendor/core/src/extension/alterschema/canonicalizer/type_array_to_any_of.h index 7c55c17..1e9c74b 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/type_array_to_any_of.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/type_array_to_any_of.h @@ -1,5 +1,7 @@ class TypeArrayToAnyOf final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; TypeArrayToAnyOf() : SchemaTransformRule{ "type_array_to_any_of", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/type_boolean_as_enum.h b/vendor/core/src/extension/alterschema/canonicalizer/type_boolean_as_enum.h index 565a65b..bf689d9 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/type_boolean_as_enum.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/type_boolean_as_enum.h @@ -1,5 +1,7 @@ class TypeBooleanAsEnum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; TypeBooleanAsEnum() : SchemaTransformRule{ "type_boolean_as_enum", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/type_null_as_enum.h b/vendor/core/src/extension/alterschema/canonicalizer/type_null_as_enum.h index 92eb429..81a01c2 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/type_null_as_enum.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/type_null_as_enum.h @@ -1,5 +1,7 @@ class TypeNullAsEnum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; TypeNullAsEnum() : SchemaTransformRule{ "type_null_as_enum", diff --git a/vendor/core/src/extension/alterschema/canonicalizer/type_union_implicit.h b/vendor/core/src/extension/alterschema/canonicalizer/type_union_implicit.h index 11fb779..aff2a46 100644 --- a/vendor/core/src/extension/alterschema/canonicalizer/type_union_implicit.h +++ b/vendor/core/src/extension/alterschema/canonicalizer/type_union_implicit.h @@ -1,5 +1,7 @@ class TypeUnionImplicit final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; TypeUnionImplicit() : SchemaTransformRule{ "type_union_implicit", diff --git a/vendor/core/src/extension/alterschema/common/allof_false_simplify.h b/vendor/core/src/extension/alterschema/common/allof_false_simplify.h new file mode 100644 index 0000000..f8a59b1 --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/allof_false_simplify.h @@ -0,0 +1,45 @@ +class AllOfFalseSimplify final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + AllOfFalseSimplify() + : SchemaTransformRule{"allof_false_simplify", + "When `allOf` contains a `false` branch, the " + "schema is unsatisfiable"} {}; + + [[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 + -> sourcemeta::core::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}) && + schema.is_object() && schema.defines(KEYWORD) && + !schema.defines("not") && schema.at(KEYWORD).is_array()); + + const auto &allof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < allof.size(); ++index) { + const auto &entry{allof.at(index)}; + if (entry.is_boolean() && !entry.to_boolean()) { + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return APPLIES_TO_POINTERS({Pointer{KEYWORD, index}}); + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.at("allOf").into(JSON{true}); + schema.rename("allOf", "not"); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/anyof_false_simplify.h b/vendor/core/src/extension/alterschema/common/anyof_false_simplify.h new file mode 100644 index 0000000..562ba54 --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/anyof_false_simplify.h @@ -0,0 +1,40 @@ +class AnyOfFalseSimplify final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + AnyOfFalseSimplify() + : SchemaTransformRule{"anyof_false_simplify", + "An `anyOf` of a single `false` branch is " + "unsatisfiable"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"anyOf"}; + 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(KEYWORD) && + !schema.defines("not") && schema.at(KEYWORD).is_array() && + schema.at(KEYWORD).size() == 1); + + const auto &entry{schema.at(KEYWORD).front()}; + ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return APPLIES_TO_POINTERS({Pointer{KEYWORD, 0}}); + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.at("anyOf").into(JSON{true}); + schema.rename("anyOf", "not"); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/anyof_remove_false_schemas.h b/vendor/core/src/extension/alterschema/common/anyof_remove_false_schemas.h new file mode 100644 index 0000000..d8bee7d --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/anyof_remove_false_schemas.h @@ -0,0 +1,66 @@ +class AnyOfRemoveFalseSchemas final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + AnyOfRemoveFalseSchemas() + : SchemaTransformRule{ + "anyof_remove_false_schemas", + "The boolean schema `false` is guaranteed to never match in " + "`anyOf`, as it is sufficient for any other branch to match"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"anyOf"}; + 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(KEYWORD) && + schema.at(KEYWORD).is_array() && + schema.at(KEYWORD).contains(JSON{false})); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + + std::vector false_locations; + bool has_non_false{false}; + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + const auto &entry{anyof.at(index)}; + if (entry.is_boolean() && !entry.to_boolean()) { + false_locations.push_back(Pointer{KEYWORD, index}); + } else { + has_non_false = true; + } + } + + ONLY_CONTINUE_IF(has_non_false); + return APPLIES_TO_POINTERS(std::move(false_locations)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + static const JSON::String KEYWORD{"anyOf"}; + + std::unordered_set indices_to_remove; + for (const auto &location : result.locations) { + indices_to_remove.insert(location.at(1).to_index()); + } + + auto new_anyof{JSON::make_array()}; + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + if (!indices_to_remove.contains(index)) { + new_anyof.push_back(anyof.at(index)); + } + } + + schema.assign(KEYWORD, std::move(new_anyof)); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/anyof_true_simplify.h b/vendor/core/src/extension/alterschema/common/anyof_true_simplify.h new file mode 100644 index 0000000..8cdef08 --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/anyof_true_simplify.h @@ -0,0 +1,46 @@ +class AnyOfTrueSimplify final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + AnyOfTrueSimplify() + : SchemaTransformRule{ + "anyof_true_simplify", + "An `anyOf` with a `true` or `{}` branch always succeeds"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"anyOf"}; + 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() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array()); + + const auto &anyof{schema.at(KEYWORD)}; + for (std::size_t index = 0; index < anyof.size(); ++index) { + const auto &entry{anyof.at(index)}; + if ((entry.is_boolean() && entry.to_boolean()) || + (entry.is_object() && entry.empty())) { + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return APPLIES_TO_POINTERS({Pointer{KEYWORD, index}}); + } + } + + return false; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.erase("anyOf"); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/const_with_type.h b/vendor/core/src/extension/alterschema/common/const_with_type.h index 261f528..820f5ae 100644 --- a/vendor/core/src/extension/alterschema/common/const_with_type.h +++ b/vendor/core/src/extension/alterschema/common/const_with_type.h @@ -1,5 +1,7 @@ class ConstWithType final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ConstWithType() : SchemaTransformRule{ "const_with_type", diff --git a/vendor/core/src/extension/alterschema/common/content_media_type_without_encoding.h b/vendor/core/src/extension/alterschema/common/content_media_type_without_encoding.h index 745f3a9..7c8e3e0 100644 --- a/vendor/core/src/extension/alterschema/common/content_media_type_without_encoding.h +++ b/vendor/core/src/extension/alterschema/common/content_media_type_without_encoding.h @@ -1,5 +1,7 @@ class ContentMediaTypeWithoutEncoding final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ContentMediaTypeWithoutEncoding() : SchemaTransformRule{ "content_media_type_without_encoding", diff --git a/vendor/core/src/extension/alterschema/common/content_schema_without_media_type.h b/vendor/core/src/extension/alterschema/common/content_schema_without_media_type.h index fce5a85..bfa89ef 100644 --- a/vendor/core/src/extension/alterschema/common/content_schema_without_media_type.h +++ b/vendor/core/src/extension/alterschema/common/content_schema_without_media_type.h @@ -3,6 +3,8 @@ class ContentSchemaWithoutMediaType final : public SchemaTransformRule { static inline const std::string KEYWORD{"contentSchema"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ContentSchemaWithoutMediaType() : SchemaTransformRule{ "content_schema_without_media_type", diff --git a/vendor/core/src/extension/alterschema/common/dependencies_property_tautology.h b/vendor/core/src/extension/alterschema/common/dependencies_property_tautology.h index 125a20f..e7eec43 100644 --- a/vendor/core/src/extension/alterschema/common/dependencies_property_tautology.h +++ b/vendor/core/src/extension/alterschema/common/dependencies_property_tautology.h @@ -1,5 +1,7 @@ class DependenciesPropertyTautology final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DependenciesPropertyTautology() : SchemaTransformRule{ "dependencies_property_tautology", diff --git a/vendor/core/src/extension/alterschema/common/dependent_required_tautology.h b/vendor/core/src/extension/alterschema/common/dependent_required_tautology.h index 30f174e..2cebac0 100644 --- a/vendor/core/src/extension/alterschema/common/dependent_required_tautology.h +++ b/vendor/core/src/extension/alterschema/common/dependent_required_tautology.h @@ -1,5 +1,7 @@ class DependentRequiredTautology final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DependentRequiredTautology() : SchemaTransformRule{ "dependent_required_tautology", diff --git a/vendor/core/src/extension/alterschema/common/draft_official_dialect_without_empty_fragment.h b/vendor/core/src/extension/alterschema/common/draft_official_dialect_without_empty_fragment.h index 060752e..ff47fa6 100644 --- a/vendor/core/src/extension/alterschema/common/draft_official_dialect_without_empty_fragment.h +++ b/vendor/core/src/extension/alterschema/common/draft_official_dialect_without_empty_fragment.h @@ -1,6 +1,8 @@ class DraftOfficialDialectWithoutEmptyFragment final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DraftOfficialDialectWithoutEmptyFragment() : SchemaTransformRule{"draft_official_dialect_without_empty_fragment", "The official dialect URI of Draft 7 and older " diff --git a/vendor/core/src/extension/alterschema/common/draft_ref_siblings.h b/vendor/core/src/extension/alterschema/common/draft_ref_siblings.h index d720363..2b4e02d 100644 --- a/vendor/core/src/extension/alterschema/common/draft_ref_siblings.h +++ b/vendor/core/src/extension/alterschema/common/draft_ref_siblings.h @@ -1,5 +1,7 @@ class DraftRefSiblings final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DraftRefSiblings() : SchemaTransformRule{"draft_ref_siblings", "In Draft 7 and older dialects, keywords sibling " diff --git a/vendor/core/src/extension/alterschema/common/drop_allof_empty_schemas.h b/vendor/core/src/extension/alterschema/common/drop_allof_empty_schemas.h index 31abc14..346844f 100644 --- a/vendor/core/src/extension/alterschema/common/drop_allof_empty_schemas.h +++ b/vendor/core/src/extension/alterschema/common/drop_allof_empty_schemas.h @@ -1,5 +1,7 @@ class DropAllOfEmptySchemas final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DropAllOfEmptySchemas() : SchemaTransformRule{"drop_allof_empty_schemas", "Empty schemas in `allOf` are redundant and can be " diff --git a/vendor/core/src/extension/alterschema/common/duplicate_allof_branches.h b/vendor/core/src/extension/alterschema/common/duplicate_allof_branches.h index cf39f14..2b84e54 100644 --- a/vendor/core/src/extension/alterschema/common/duplicate_allof_branches.h +++ b/vendor/core/src/extension/alterschema/common/duplicate_allof_branches.h @@ -1,5 +1,7 @@ class DuplicateAllOfBranches final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DuplicateAllOfBranches() : SchemaTransformRule{ "duplicate_allof_branches", diff --git a/vendor/core/src/extension/alterschema/common/duplicate_anyof_branches.h b/vendor/core/src/extension/alterschema/common/duplicate_anyof_branches.h index 1465628..50719fe 100644 --- a/vendor/core/src/extension/alterschema/common/duplicate_anyof_branches.h +++ b/vendor/core/src/extension/alterschema/common/duplicate_anyof_branches.h @@ -1,5 +1,7 @@ class DuplicateAnyOfBranches final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DuplicateAnyOfBranches() : SchemaTransformRule{ "duplicate_anyof_branches", diff --git a/vendor/core/src/extension/alterschema/common/duplicate_enum_values.h b/vendor/core/src/extension/alterschema/common/duplicate_enum_values.h index c3fec36..8b422cf 100644 --- a/vendor/core/src/extension/alterschema/common/duplicate_enum_values.h +++ b/vendor/core/src/extension/alterschema/common/duplicate_enum_values.h @@ -1,5 +1,7 @@ class DuplicateEnumValues final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DuplicateEnumValues() : SchemaTransformRule{"duplicate_enum_values", "Setting duplicate values in `enum` is " diff --git a/vendor/core/src/extension/alterschema/common/duplicate_required_values.h b/vendor/core/src/extension/alterschema/common/duplicate_required_values.h index d1fd6ad..9e3f3cb 100644 --- a/vendor/core/src/extension/alterschema/common/duplicate_required_values.h +++ b/vendor/core/src/extension/alterschema/common/duplicate_required_values.h @@ -1,5 +1,7 @@ class DuplicateRequiredValues final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DuplicateRequiredValues() : SchemaTransformRule{ "duplicate_required_values", diff --git a/vendor/core/src/extension/alterschema/common/else_empty.h b/vendor/core/src/extension/alterschema/common/else_empty.h index fe883ad..c4f64a0 100644 --- a/vendor/core/src/extension/alterschema/common/else_empty.h +++ b/vendor/core/src/extension/alterschema/common/else_empty.h @@ -3,6 +3,8 @@ class ElseEmpty final : public SchemaTransformRule { static inline const std::string KEYWORD{"else"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ElseEmpty() : SchemaTransformRule{"else_empty", "Setting the `else` keyword to the empty schema " diff --git a/vendor/core/src/extension/alterschema/common/else_without_if.h b/vendor/core/src/extension/alterschema/common/else_without_if.h index 7b871dd..475c622 100644 --- a/vendor/core/src/extension/alterschema/common/else_without_if.h +++ b/vendor/core/src/extension/alterschema/common/else_without_if.h @@ -3,6 +3,8 @@ class ElseWithoutIf final : public SchemaTransformRule { static inline const std::string KEYWORD{"else"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ElseWithoutIf() : SchemaTransformRule{"else_without_if", "The `else` keyword is meaningless " diff --git a/vendor/core/src/extension/alterschema/common/empty_object_as_true.h b/vendor/core/src/extension/alterschema/common/empty_object_as_true.h new file mode 100644 index 0000000..288cab7 --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/empty_object_as_true.h @@ -0,0 +1,32 @@ +class EmptyObjectAsTrue final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; + EmptyObjectAsTrue() + : SchemaTransformRule{ + "empty_object_as_true", + "The empty schema `{}` accepts all values and is equivalent to the " + "boolean schema `true`"} {}; + + [[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 + -> sourcemeta::core::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}) && + schema.is_object() && schema.empty()); + return true; + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.into(JSON{true}); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/enum_with_type.h b/vendor/core/src/extension/alterschema/common/enum_with_type.h index 350f73d..c2a8949 100644 --- a/vendor/core/src/extension/alterschema/common/enum_with_type.h +++ b/vendor/core/src/extension/alterschema/common/enum_with_type.h @@ -1,5 +1,7 @@ class EnumWithType final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; EnumWithType() : SchemaTransformRule{ "enum_with_type", diff --git a/vendor/core/src/extension/alterschema/common/equal_numeric_bounds_to_enum.h b/vendor/core/src/extension/alterschema/common/equal_numeric_bounds_to_enum.h index 698fa93..c7cf7de 100644 --- a/vendor/core/src/extension/alterschema/common/equal_numeric_bounds_to_enum.h +++ b/vendor/core/src/extension/alterschema/common/equal_numeric_bounds_to_enum.h @@ -1,5 +1,7 @@ class EqualNumericBoundsToEnum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; EqualNumericBoundsToEnum() : SchemaTransformRule{ "equal_numeric_bounds_to_enum", diff --git a/vendor/core/src/extension/alterschema/common/exclusive_maximum_number_and_maximum.h b/vendor/core/src/extension/alterschema/common/exclusive_maximum_number_and_maximum.h index 6a1426b..dd096d0 100644 --- a/vendor/core/src/extension/alterschema/common/exclusive_maximum_number_and_maximum.h +++ b/vendor/core/src/extension/alterschema/common/exclusive_maximum_number_and_maximum.h @@ -1,5 +1,7 @@ class ExclusiveMaximumNumberAndMaximum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ExclusiveMaximumNumberAndMaximum() : SchemaTransformRule{ "exclusive_maximum_number_and_maximum", diff --git a/vendor/core/src/extension/alterschema/common/exclusive_minimum_number_and_minimum.h b/vendor/core/src/extension/alterschema/common/exclusive_minimum_number_and_minimum.h index 53c0e99..3fb192f 100644 --- a/vendor/core/src/extension/alterschema/common/exclusive_minimum_number_and_minimum.h +++ b/vendor/core/src/extension/alterschema/common/exclusive_minimum_number_and_minimum.h @@ -1,5 +1,7 @@ class ExclusiveMinimumNumberAndMinimum final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ExclusiveMinimumNumberAndMinimum() : SchemaTransformRule{ "exclusive_minimum_number_and_minimum", diff --git a/vendor/core/src/extension/alterschema/common/if_without_then_else.h b/vendor/core/src/extension/alterschema/common/if_without_then_else.h index 3f19961..93bafb3 100644 --- a/vendor/core/src/extension/alterschema/common/if_without_then_else.h +++ b/vendor/core/src/extension/alterschema/common/if_without_then_else.h @@ -3,6 +3,8 @@ class IfWithoutThenElse final : public SchemaTransformRule { static inline const std::string KEYWORD{"if"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; IfWithoutThenElse() : SchemaTransformRule{ "if_without_then_else", diff --git a/vendor/core/src/extension/alterschema/common/ignored_metaschema.h b/vendor/core/src/extension/alterschema/common/ignored_metaschema.h index 4fa07a1..3faed03 100644 --- a/vendor/core/src/extension/alterschema/common/ignored_metaschema.h +++ b/vendor/core/src/extension/alterschema/common/ignored_metaschema.h @@ -1,5 +1,7 @@ class IgnoredMetaschema final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; IgnoredMetaschema() : SchemaTransformRule{ "ignored_metaschema", diff --git a/vendor/core/src/extension/alterschema/common/max_contains_without_contains.h b/vendor/core/src/extension/alterschema/common/max_contains_without_contains.h index 00944a4..4cad64f 100644 --- a/vendor/core/src/extension/alterschema/common/max_contains_without_contains.h +++ b/vendor/core/src/extension/alterschema/common/max_contains_without_contains.h @@ -1,5 +1,7 @@ class MaxContainsWithoutContains final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MaxContainsWithoutContains() : SchemaTransformRule{"max_contains_without_contains", "The `maxContains` keyword is meaningless " diff --git a/vendor/core/src/extension/alterschema/common/maximum_real_for_integer.h b/vendor/core/src/extension/alterschema/common/maximum_real_for_integer.h index 6494672..65b77b9 100644 --- a/vendor/core/src/extension/alterschema/common/maximum_real_for_integer.h +++ b/vendor/core/src/extension/alterschema/common/maximum_real_for_integer.h @@ -1,5 +1,7 @@ class MaximumRealForInteger final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; MaximumRealForInteger() : SchemaTransformRule{ "maximum_real_for_integer", diff --git a/vendor/core/src/extension/alterschema/common/min_contains_without_contains.h b/vendor/core/src/extension/alterschema/common/min_contains_without_contains.h index 5f14de3..1bd56a7 100644 --- a/vendor/core/src/extension/alterschema/common/min_contains_without_contains.h +++ b/vendor/core/src/extension/alterschema/common/min_contains_without_contains.h @@ -1,5 +1,7 @@ class MinContainsWithoutContains final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MinContainsWithoutContains() : SchemaTransformRule{"min_contains_without_contains", "The `minContains` keyword is meaningless " diff --git a/vendor/core/src/extension/alterschema/common/minimum_real_for_integer.h b/vendor/core/src/extension/alterschema/common/minimum_real_for_integer.h index 65db909..ba46ddb 100644 --- a/vendor/core/src/extension/alterschema/common/minimum_real_for_integer.h +++ b/vendor/core/src/extension/alterschema/common/minimum_real_for_integer.h @@ -1,5 +1,7 @@ class MinimumRealForInteger final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; MinimumRealForInteger() : SchemaTransformRule{ "minimum_real_for_integer", diff --git a/vendor/core/src/extension/alterschema/common/modern_official_dialect_with_empty_fragment.h b/vendor/core/src/extension/alterschema/common/modern_official_dialect_with_empty_fragment.h index caa12e0..555dc68 100644 --- a/vendor/core/src/extension/alterschema/common/modern_official_dialect_with_empty_fragment.h +++ b/vendor/core/src/extension/alterschema/common/modern_official_dialect_with_empty_fragment.h @@ -1,6 +1,8 @@ class ModernOfficialDialectWithEmptyFragment final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ModernOfficialDialectWithEmptyFragment() : SchemaTransformRule{ "modern_official_dialect_with_empty_fragment", diff --git a/vendor/core/src/extension/alterschema/common/non_applicable_additional_items.h b/vendor/core/src/extension/alterschema/common/non_applicable_additional_items.h index 6b7f39e..4ac055c 100644 --- a/vendor/core/src/extension/alterschema/common/non_applicable_additional_items.h +++ b/vendor/core/src/extension/alterschema/common/non_applicable_additional_items.h @@ -3,6 +3,8 @@ class NonApplicableAdditionalItems final : public SchemaTransformRule { static inline const std::string KEYWORD{"additionalItems"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; NonApplicableAdditionalItems() : SchemaTransformRule{ "non_applicable_additional_items", diff --git a/vendor/core/src/extension/alterschema/common/non_applicable_enum_validation_keywords.h b/vendor/core/src/extension/alterschema/common/non_applicable_enum_validation_keywords.h index 6de098f..5930370 100644 --- a/vendor/core/src/extension/alterschema/common/non_applicable_enum_validation_keywords.h +++ b/vendor/core/src/extension/alterschema/common/non_applicable_enum_validation_keywords.h @@ -1,5 +1,7 @@ class NonApplicableEnumValidationKeywords final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; NonApplicableEnumValidationKeywords() : SchemaTransformRule{ "non_applicable_enum_validation_keywords", diff --git a/vendor/core/src/extension/alterschema/common/non_applicable_type_specific_keywords.h b/vendor/core/src/extension/alterschema/common/non_applicable_type_specific_keywords.h index 98bebe1..db188c2 100644 --- a/vendor/core/src/extension/alterschema/common/non_applicable_type_specific_keywords.h +++ b/vendor/core/src/extension/alterschema/common/non_applicable_type_specific_keywords.h @@ -1,5 +1,7 @@ class NonApplicableTypeSpecificKeywords final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; NonApplicableTypeSpecificKeywords() : SchemaTransformRule{"non_applicable_type_specific_keywords", "Avoid keywords that don't apply to the type or " diff --git a/vendor/core/src/extension/alterschema/common/not_false.h b/vendor/core/src/extension/alterschema/common/not_false.h index f901689..65de31f 100644 --- a/vendor/core/src/extension/alterschema/common/not_false.h +++ b/vendor/core/src/extension/alterschema/common/not_false.h @@ -3,6 +3,8 @@ class NotFalse final : public SchemaTransformRule { static inline const std::string KEYWORD{"not"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; NotFalse() : SchemaTransformRule{"not_false", "Setting the `not` keyword to `false` imposes no " diff --git a/vendor/core/src/extension/alterschema/common/oneof_false_simplify.h b/vendor/core/src/extension/alterschema/common/oneof_false_simplify.h new file mode 100644 index 0000000..64c4adf --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/oneof_false_simplify.h @@ -0,0 +1,40 @@ +class OneOfFalseSimplify final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + OneOfFalseSimplify() + : SchemaTransformRule{"oneof_false_simplify", + "A `oneOf` of a single `false` branch is " + "unsatisfiable"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"oneOf"}; + 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(KEYWORD) && + !schema.defines("not") && schema.at(KEYWORD).is_array() && + schema.at(KEYWORD).size() == 1); + + const auto &entry{schema.at(KEYWORD).front()}; + ONLY_CONTINUE_IF(entry.is_boolean() && !entry.to_boolean()); + ONLY_CONTINUE_IF(!frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(KEYWORD)})); + return APPLIES_TO_POINTERS({Pointer{KEYWORD, 0}}); + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.at("oneOf").into(JSON{true}); + schema.rename("oneOf", "not"); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/oneof_to_anyof_disjoint_types.h b/vendor/core/src/extension/alterschema/common/oneof_to_anyof_disjoint_types.h new file mode 100644 index 0000000..d9a16bc --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/oneof_to_anyof_disjoint_types.h @@ -0,0 +1,97 @@ +class OneOfToAnyOfDisjointTypes final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + OneOfToAnyOfDisjointTypes() + : SchemaTransformRule{ + "oneof_to_anyof_disjoint_types", + "A `oneOf` where all branches have disjoint types can be safely " + "converted to `anyOf`"} {}; + + [[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 + -> sourcemeta::core::SchemaTransformRule::Result override { + static const JSON::String KEYWORD{"oneOf"}; + 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() && schema.defines(KEYWORD) && + schema.at(KEYWORD).is_array() && + schema.at(KEYWORD).size() > 1); + + const auto has_validation_vocabulary{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, + Vocabularies::Known::JSON_Schema_Draft_3, + Vocabularies::Known::JSON_Schema_Draft_2, + Vocabularies::Known::JSON_Schema_Draft_1})}; + + const auto has_const_vocabulary{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})}; + + const auto &oneof{schema.at(KEYWORD)}; + std::vector type_sets; + type_sets.reserve(oneof.size()); + + for (const auto &branch : oneof.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()}; + + if (has_type) { + type_sets.push_back(parse_schema_type(branch.at("type"))); + } else if (has_const && !has_enum) { + JSON::TypeSet branch_types; + branch_types.set(static_cast(branch.at("const").type())); + type_sets.push_back(branch_types); + } else if (has_enum && !has_const) { + JSON::TypeSet branch_types; + for (const auto &item : branch.at("enum").as_array()) { + branch_types.set(static_cast(item.type())); + } + type_sets.push_back(branch_types); + } else { + return false; + } + } + + for (std::size_t index = 0; index < type_sets.size(); ++index) { + for (std::size_t other = index + 1; other < type_sets.size(); ++other) { + ONLY_CONTINUE_IF((type_sets[index] & type_sets[other]).none()); + } + } + + return APPLIES_TO_KEYWORDS(KEYWORD); + } + + auto transform(JSON &schema, const Result &) const -> void override { + schema.rename("oneOf", "anyOf"); + } + + [[nodiscard]] auto + rereference(const std::string_view, const Pointer &origin [[maybe_unused]], + const Pointer &target, const Pointer ¤t) const + -> Pointer override { + const Pointer oneof_prefix{current.concat({"oneOf"})}; + const Pointer anyof_prefix{current.concat({"anyOf"})}; + return target.rebase(oneof_prefix, anyof_prefix); + } +}; diff --git a/vendor/core/src/extension/alterschema/common/orphan_definitions.h b/vendor/core/src/extension/alterschema/common/orphan_definitions.h index 426cd56..047156f 100644 --- a/vendor/core/src/extension/alterschema/common/orphan_definitions.h +++ b/vendor/core/src/extension/alterschema/common/orphan_definitions.h @@ -1,5 +1,7 @@ class OrphanDefinitions final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; OrphanDefinitions() : SchemaTransformRule{ "orphan_definitions", @@ -12,8 +14,8 @@ class OrphanDefinitions final : public SchemaTransformRule { 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 + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver) const -> sourcemeta::core::SchemaTransformRule::Result override { ONLY_CONTINUE_IF(schema.is_object()); const bool has_modern_core{ @@ -28,35 +30,11 @@ class OrphanDefinitions final : public SchemaTransformRule { schema.defines("definitions")}; ONLY_CONTINUE_IF(has_defs || has_definitions); - bool has_external_to_defs{false}; - bool has_external_to_definitions{false}; - std::unordered_set outside_referenced_defs; - std::unordered_set outside_referenced_definitions; - - for (const auto &[key, reference] : frame.references()) { - const auto destination_location{frame.traverse(reference.destination)}; - if (destination_location.has_value()) { - const auto &destination_pointer{destination_location->get().pointer}; - if (has_defs) { - process_reference(key.second, destination_pointer, location.pointer, - "$defs", has_external_to_defs, - outside_referenced_defs); - } - - if (has_definitions) { - process_reference(key.second, destination_pointer, location.pointer, - "definitions", has_external_to_definitions, - outside_referenced_definitions); - } - } - } - std::vector orphans; - collect_orphans(schema, "$defs", has_defs, has_external_to_defs, - outside_referenced_defs, orphans); - collect_orphans(schema, "definitions", has_definitions, - has_external_to_definitions, outside_referenced_definitions, - orphans); + collect_orphans(frame, walker, resolver, location.pointer, schema, "$defs", + has_defs, orphans); + collect_orphans(frame, walker, resolver, location.pointer, schema, + "definitions", has_definitions, orphans); ONLY_CONTINUE_IF(!orphans.empty()); return APPLIES_TO_POINTERS(std::move(orphans)); @@ -71,55 +49,72 @@ class OrphanDefinitions final : public SchemaTransformRule { schema.at(container).erase(pointer.at(1).to_property()); } - remove_empty_container(schema, "$defs"); - remove_empty_container(schema, "definitions"); + if (schema.defines("$defs") && schema.at("$defs").empty()) { + schema.erase("$defs"); + } + + if (schema.defines("definitions") && schema.at("definitions").empty()) { + schema.erase("definitions"); + } } private: - static auto process_reference( - const WeakPointer &source_pointer, const WeakPointer &destination_pointer, - const WeakPointer &prefix, std::string_view container, bool &has_external, - std::unordered_set &referenced) -> void { - if (!destination_pointer.starts_with(prefix, container) || - destination_pointer.size() <= prefix.size() + 1) { - return; - } + static auto has_reachable_reference_through( + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const WeakPointer &pointer) -> bool { + for (const auto &reference : frame.references()) { + const auto destination{frame.traverse(reference.second.destination)}; + if (!destination.has_value()) { + continue; + } - const auto &entry_token{destination_pointer.at(prefix.size() + 1)}; - if (entry_token.is_property()) { - const auto &entry_name{entry_token.to_property()}; - if (!source_pointer.starts_with(prefix, container)) { - has_external = true; - referenced.insert(entry_name); - } else if (!source_pointer.starts_with(prefix, container, entry_name)) { - referenced.insert(entry_name); + if (!destination->get().pointer.starts_with(pointer)) { + continue; + } + + const auto &source_pointer{reference.first.second}; + if (source_pointer.empty()) { + return true; } - } - } - static auto - collect_orphans(const JSON &schema, const JSON::String &container, - const bool has_container, const bool has_external_reference, - const std::unordered_set &referenced, - std::vector &orphans) -> void { - if (has_container) { - const auto &maybe_object{schema.at(container)}; - if (maybe_object.is_object()) { - // If no external references to container, all definitions are orphans - // Otherwise, only unreferenced definitions are orphans - for (const auto &entry : maybe_object.as_object()) { - if (!has_external_reference || !referenced.contains(entry.first)) { - orphans.push_back(Pointer{container, entry.first}); - } - } + const auto source_location{frame.traverse( + source_pointer.initial(), + sourcemeta::core::SchemaFrame::LocationType::Subschema)}; + if (source_location.has_value() && + frame.is_reachable(source_location->get(), walker, resolver)) { + return true; } } + + return false; } - static auto remove_empty_container(JSON &schema, const JSON::String &name) - -> void { - if (schema.defines(name) && schema.at(name).empty()) { - schema.erase(name); + static auto collect_orphans(const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaWalker &walker, + const sourcemeta::core::SchemaResolver &resolver, + const WeakPointer &prefix, const JSON &schema, + const JSON::String &container, + const bool has_container, + std::vector &orphans) -> void { + if (!has_container || !schema.at(container).is_object()) { + return; + } + + for (const auto &entry : schema.at(container).as_object()) { + const WeakPointer entry_pointer{std::cref(container), + std::cref(entry.first)}; + const auto absolute_entry_pointer{prefix.concat(entry_pointer)}; + const auto entry_location{frame.traverse( + absolute_entry_pointer, + sourcemeta::core::SchemaFrame::LocationType::Subschema)}; + if (entry_location.has_value() && + !frame.is_reachable(entry_location->get(), walker, resolver) && + !has_reachable_reference_through(frame, walker, resolver, + absolute_entry_pointer)) { + orphans.push_back(Pointer{container, entry.first}); + } } } }; diff --git a/vendor/core/src/extension/alterschema/common/required_properties_in_properties.h b/vendor/core/src/extension/alterschema/common/required_properties_in_properties.h index 8cddcb4..26739fa 100644 --- a/vendor/core/src/extension/alterschema/common/required_properties_in_properties.h +++ b/vendor/core/src/extension/alterschema/common/required_properties_in_properties.h @@ -1,5 +1,7 @@ class RequiredPropertiesInProperties final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; RequiredPropertiesInProperties() : SchemaTransformRule{ "required_properties_in_properties", diff --git a/vendor/core/src/extension/alterschema/common/single_type_array.h b/vendor/core/src/extension/alterschema/common/single_type_array.h index 9aea9a3..742ea10 100644 --- a/vendor/core/src/extension/alterschema/common/single_type_array.h +++ b/vendor/core/src/extension/alterschema/common/single_type_array.h @@ -1,5 +1,7 @@ class SingleTypeArray final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; SingleTypeArray() : SchemaTransformRule{"single_type_array", "Setting `type` to an array of a single type is " diff --git a/vendor/core/src/extension/alterschema/common/then_empty.h b/vendor/core/src/extension/alterschema/common/then_empty.h index 36c1d22..eacf299 100644 --- a/vendor/core/src/extension/alterschema/common/then_empty.h +++ b/vendor/core/src/extension/alterschema/common/then_empty.h @@ -3,6 +3,8 @@ class ThenEmpty final : public SchemaTransformRule { static inline const std::string KEYWORD{"then"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ThenEmpty() : SchemaTransformRule{"then_empty", "Setting the `then` keyword to the empty schema " diff --git a/vendor/core/src/extension/alterschema/common/then_without_if.h b/vendor/core/src/extension/alterschema/common/then_without_if.h index ed55756..75d526d 100644 --- a/vendor/core/src/extension/alterschema/common/then_without_if.h +++ b/vendor/core/src/extension/alterschema/common/then_without_if.h @@ -3,6 +3,8 @@ class ThenWithoutIf final : public SchemaTransformRule { static inline const std::string KEYWORD{"then"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ThenWithoutIf() : SchemaTransformRule{"then_without_if", "The `then` keyword is meaningless " diff --git a/vendor/core/src/extension/alterschema/common/unknown_keywords_prefix.h b/vendor/core/src/extension/alterschema/common/unknown_keywords_prefix.h index 19c418a..37691be 100644 --- a/vendor/core/src/extension/alterschema/common/unknown_keywords_prefix.h +++ b/vendor/core/src/extension/alterschema/common/unknown_keywords_prefix.h @@ -1,5 +1,7 @@ class UnknownKeywordsPrefix final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnknownKeywordsPrefix() : SchemaTransformRule{ "unknown_keywords_prefix", diff --git a/vendor/core/src/extension/alterschema/common/unknown_local_ref.h b/vendor/core/src/extension/alterschema/common/unknown_local_ref.h index c204a31..1901e91 100644 --- a/vendor/core/src/extension/alterschema/common/unknown_local_ref.h +++ b/vendor/core/src/extension/alterschema/common/unknown_local_ref.h @@ -3,6 +3,8 @@ class UnknownLocalRef final : public SchemaTransformRule { static inline const std::string KEYWORD{"$ref"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnknownLocalRef() : SchemaTransformRule{ "unknown_local_ref", diff --git a/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_draft.h b/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_draft.h index e599230..f5f8d38 100644 --- a/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_draft.h +++ b/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_draft.h @@ -1,5 +1,7 @@ class UnnecessaryAllOfRefWrapperDraft final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnnecessaryAllOfRefWrapperDraft() : SchemaTransformRule{"unnecessary_allof_ref_wrapper_draft", "Wrapping `$ref` in `allOf` is only necessary if " diff --git a/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_modern.h b/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_modern.h index 4f83e63..ade6bd7 100644 --- a/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_modern.h +++ b/vendor/core/src/extension/alterschema/common/unnecessary_allof_ref_wrapper_modern.h @@ -1,5 +1,7 @@ class UnnecessaryAllOfRefWrapperModern final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnnecessaryAllOfRefWrapperModern() : SchemaTransformRule{"unnecessary_allof_ref_wrapper_modern", "Wrapping `$ref` in `allOf` was only necessary in " diff --git a/vendor/core/src/extension/alterschema/common/unnecessary_allof_wrapper.h b/vendor/core/src/extension/alterschema/common/unnecessary_allof_wrapper.h index 0ab990b..33c78e0 100644 --- a/vendor/core/src/extension/alterschema/common/unnecessary_allof_wrapper.h +++ b/vendor/core/src/extension/alterschema/common/unnecessary_allof_wrapper.h @@ -3,6 +3,8 @@ class UnnecessaryAllOfWrapper final : public SchemaTransformRule { static inline const std::string KEYWORD{"allOf"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnnecessaryAllOfWrapper() : SchemaTransformRule{"unnecessary_allof_wrapper", "Keywords inside `allOf` that do not conflict with " diff --git a/vendor/core/src/extension/alterschema/common/unsatisfiable_drop_validation.h b/vendor/core/src/extension/alterschema/common/unsatisfiable_drop_validation.h new file mode 100644 index 0000000..4e5b44c --- /dev/null +++ b/vendor/core/src/extension/alterschema/common/unsatisfiable_drop_validation.h @@ -0,0 +1,85 @@ +class UnsatisfiableDropValidation final : public SchemaTransformRule { +public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; + UnsatisfiableDropValidation() + : SchemaTransformRule{"unsatisfiable_drop_validation", + "Do not place assertions or applicators next to an " + "unsatisfiable negation"} {}; + + [[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 + -> sourcemeta::core::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() && schema.defines("not") && + schema.at("not").is_boolean() && + schema.at("not").to_boolean()); + + std::vector positions; + for (const auto &entry : schema.as_object()) { + if (entry.first == "not") { + continue; + } + + const auto &metadata{walker(entry.first, vocabularies)}; + if (!is_removable_keyword_type(metadata.type)) { + continue; + } + + if (frame.has_references_through( + location.pointer, WeakPointer::Token{std::cref(entry.first)})) { + continue; + } + + positions.push_back(Pointer{entry.first}); + } + + ONLY_CONTINUE_IF(!positions.empty()); + return APPLIES_TO_POINTERS(std::move(positions)); + } + + auto transform(JSON &schema, const Result &result) const -> void override { + for (const auto &location : result.locations) { + schema.erase(location.at(0).to_property()); + } + } + +private: + static auto is_removable_keyword_type(const SchemaKeywordType type) -> bool { + switch (type) { + case SchemaKeywordType::Assertion: + case SchemaKeywordType::Reference: + case SchemaKeywordType::LocationMembers: + case SchemaKeywordType::ApplicatorMembersTraversePropertyStatic: + case SchemaKeywordType::ApplicatorMembersTraversePropertyRegex: + case SchemaKeywordType::ApplicatorValueTraverseSomeProperty: + case SchemaKeywordType::ApplicatorValueTraverseAnyPropertyKey: + case SchemaKeywordType::ApplicatorValueTraverseAnyItem: + case SchemaKeywordType::ApplicatorValueTraverseSomeItem: + case SchemaKeywordType::ApplicatorValueTraverseParent: + case SchemaKeywordType::ApplicatorElementsTraverseItem: + case SchemaKeywordType::ApplicatorValueOrElementsTraverseAnyItemOrItem: + case SchemaKeywordType::ApplicatorValueOrElementsInPlace: + case SchemaKeywordType::ApplicatorMembersInPlaceSome: + case SchemaKeywordType::ApplicatorElementsInPlace: + case SchemaKeywordType::ApplicatorElementsInPlaceSome: + case SchemaKeywordType::ApplicatorElementsInPlaceSomeNegate: + case SchemaKeywordType::ApplicatorValueInPlaceMaybe: + case SchemaKeywordType::ApplicatorValueInPlaceOther: + case SchemaKeywordType::ApplicatorValueInPlaceNegate: + return true; + default: + return false; + } + } +}; diff --git a/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h b/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h index 7c677d3..08152fa 100644 --- a/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h +++ b/vendor/core/src/extension/alterschema/common/unsatisfiable_in_place_applicator_type.h @@ -1,5 +1,7 @@ class UnsatisfiableInPlaceApplicatorType final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnsatisfiableInPlaceApplicatorType() : SchemaTransformRule{ "unsatisfiable_in_place_applicator_type", diff --git a/vendor/core/src/extension/alterschema/linter/additional_properties_default.h b/vendor/core/src/extension/alterschema/linter/additional_properties_default.h deleted file mode 100644 index 0e1eed0..0000000 --- a/vendor/core/src/extension/alterschema/linter/additional_properties_default.h +++ /dev/null @@ -1,44 +0,0 @@ -class AdditionalPropertiesDefault final : public SchemaTransformRule { -private: - static inline const std::string KEYWORD{"additionalProperties"}; - -public: - AdditionalPropertiesDefault() - : SchemaTransformRule{ - "additional_properties_default", - "Setting the `additionalProperties` keyword to the true schema " - "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 &frame, - const sourcemeta::core::SchemaFrame::Location &location, - const sourcemeta::core::SchemaWalker &, - const sourcemeta::core::SchemaResolver &) const - -> sourcemeta::core::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, - Vocabularies::Known::JSON_Schema_Draft_4, - Vocabularies::Known::JSON_Schema_Draft_3, - 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(KEYWORD) && - ((schema.at(KEYWORD).is_boolean() && schema.at(KEYWORD).to_boolean()) || - (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); - } - - auto transform(JSON &schema, const Result &) const -> void override { - schema.erase(KEYWORD); - } -}; diff --git a/vendor/core/src/extension/alterschema/linter/comment_trim.h b/vendor/core/src/extension/alterschema/linter/comment_trim.h index eb9e57e..c296908 100644 --- a/vendor/core/src/extension/alterschema/linter/comment_trim.h +++ b/vendor/core/src/extension/alterschema/linter/comment_trim.h @@ -1,5 +1,7 @@ class CommentTrim final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; CommentTrim() : SchemaTransformRule{ "comment_trim", diff --git a/vendor/core/src/extension/alterschema/linter/content_schema_default.h b/vendor/core/src/extension/alterschema/linter/content_schema_default.h index b482723..411d35a 100644 --- a/vendor/core/src/extension/alterschema/linter/content_schema_default.h +++ b/vendor/core/src/extension/alterschema/linter/content_schema_default.h @@ -3,6 +3,8 @@ class ContentSchemaDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"contentSchema"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ContentSchemaDefault() : SchemaTransformRule{ "content_schema_default", diff --git a/vendor/core/src/extension/alterschema/linter/definitions_to_defs.h b/vendor/core/src/extension/alterschema/linter/definitions_to_defs.h index 9efe13e..016bec7 100644 --- a/vendor/core/src/extension/alterschema/linter/definitions_to_defs.h +++ b/vendor/core/src/extension/alterschema/linter/definitions_to_defs.h @@ -1,5 +1,7 @@ class DefinitionsToDefs final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DefinitionsToDefs() : SchemaTransformRule{"definitions_to_defs", "`definitions` was superseded by `$defs` in " diff --git a/vendor/core/src/extension/alterschema/linter/dependencies_default.h b/vendor/core/src/extension/alterschema/linter/dependencies_default.h index cb84cfe..29dde6b 100644 --- a/vendor/core/src/extension/alterschema/linter/dependencies_default.h +++ b/vendor/core/src/extension/alterschema/linter/dependencies_default.h @@ -3,6 +3,8 @@ class DependenciesDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"dependencies"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DependenciesDefault() : SchemaTransformRule{ "dependencies_default", diff --git a/vendor/core/src/extension/alterschema/linter/dependent_required_default.h b/vendor/core/src/extension/alterschema/linter/dependent_required_default.h index 9e2a460..f108dc3 100644 --- a/vendor/core/src/extension/alterschema/linter/dependent_required_default.h +++ b/vendor/core/src/extension/alterschema/linter/dependent_required_default.h @@ -1,5 +1,7 @@ class DependentRequiredDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; DependentRequiredDefault() : SchemaTransformRule{ "dependent_required_default", diff --git a/vendor/core/src/extension/alterschema/linter/description_trailing_period.h b/vendor/core/src/extension/alterschema/linter/description_trailing_period.h index 125cc33..31e6bc4 100644 --- a/vendor/core/src/extension/alterschema/linter/description_trailing_period.h +++ b/vendor/core/src/extension/alterschema/linter/description_trailing_period.h @@ -1,5 +1,7 @@ class DescriptionTrailingPeriod final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DescriptionTrailingPeriod() : SchemaTransformRule{ "description_trailing_period", diff --git a/vendor/core/src/extension/alterschema/linter/description_trim.h b/vendor/core/src/extension/alterschema/linter/description_trim.h index b7ff154..7b17442 100644 --- a/vendor/core/src/extension/alterschema/linter/description_trim.h +++ b/vendor/core/src/extension/alterschema/linter/description_trim.h @@ -1,5 +1,7 @@ class DescriptionTrim final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DescriptionTrim() : SchemaTransformRule{ "description_trim", diff --git a/vendor/core/src/extension/alterschema/linter/duplicate_examples.h b/vendor/core/src/extension/alterschema/linter/duplicate_examples.h index 8cba86b..8471bd3 100644 --- a/vendor/core/src/extension/alterschema/linter/duplicate_examples.h +++ b/vendor/core/src/extension/alterschema/linter/duplicate_examples.h @@ -1,5 +1,7 @@ class DuplicateExamples final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; DuplicateExamples() : SchemaTransformRule{ "duplicate_examples", diff --git a/vendor/core/src/extension/alterschema/linter/enum_to_const.h b/vendor/core/src/extension/alterschema/linter/enum_to_const.h index 930ec6f..7e1ffb2 100644 --- a/vendor/core/src/extension/alterschema/linter/enum_to_const.h +++ b/vendor/core/src/extension/alterschema/linter/enum_to_const.h @@ -1,5 +1,7 @@ class EnumToConst final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; EnumToConst() : SchemaTransformRule{ "enum_to_const", diff --git a/vendor/core/src/extension/alterschema/linter/equal_numeric_bounds_to_const.h b/vendor/core/src/extension/alterschema/linter/equal_numeric_bounds_to_const.h index 041f9ed..603d569 100644 --- a/vendor/core/src/extension/alterschema/linter/equal_numeric_bounds_to_const.h +++ b/vendor/core/src/extension/alterschema/linter/equal_numeric_bounds_to_const.h @@ -1,5 +1,7 @@ class EqualNumericBoundsToConst final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; EqualNumericBoundsToConst() : SchemaTransformRule{ "equal_numeric_bounds_to_const", diff --git a/vendor/core/src/extension/alterschema/linter/items_array_default.h b/vendor/core/src/extension/alterschema/linter/items_array_default.h index a21bd02..0afbc43 100644 --- a/vendor/core/src/extension/alterschema/linter/items_array_default.h +++ b/vendor/core/src/extension/alterschema/linter/items_array_default.h @@ -1,5 +1,7 @@ class ItemsArrayDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ItemsArrayDefault() : SchemaTransformRule{"items_array_default", "Setting the `items` keyword to the empty array " diff --git a/vendor/core/src/extension/alterschema/linter/items_schema_default.h b/vendor/core/src/extension/alterschema/linter/items_schema_default.h index 8d77ee8..aee24af 100644 --- a/vendor/core/src/extension/alterschema/linter/items_schema_default.h +++ b/vendor/core/src/extension/alterschema/linter/items_schema_default.h @@ -3,6 +3,8 @@ class ItemsSchemaDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"items"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; ItemsSchemaDefault() : SchemaTransformRule{"items_schema_default", "Setting the `items` keyword to the true schema " diff --git a/vendor/core/src/extension/alterschema/linter/multiple_of_default.h b/vendor/core/src/extension/alterschema/linter/multiple_of_default.h index 3f9c477..6915657 100644 --- a/vendor/core/src/extension/alterschema/linter/multiple_of_default.h +++ b/vendor/core/src/extension/alterschema/linter/multiple_of_default.h @@ -1,5 +1,7 @@ class MultipleOfDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; MultipleOfDefault() : SchemaTransformRule{ "multiple_of_default", diff --git a/vendor/core/src/extension/alterschema/linter/pattern_properties_default.h b/vendor/core/src/extension/alterschema/linter/pattern_properties_default.h index d3e437b..6decf0e 100644 --- a/vendor/core/src/extension/alterschema/linter/pattern_properties_default.h +++ b/vendor/core/src/extension/alterschema/linter/pattern_properties_default.h @@ -1,5 +1,7 @@ class PatternPropertiesDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; PatternPropertiesDefault() : SchemaTransformRule{ "pattern_properties_default", diff --git a/vendor/core/src/extension/alterschema/linter/properties_default.h b/vendor/core/src/extension/alterschema/linter/properties_default.h index 5c82418..dd43363 100644 --- a/vendor/core/src/extension/alterschema/linter/properties_default.h +++ b/vendor/core/src/extension/alterschema/linter/properties_default.h @@ -1,5 +1,7 @@ class PropertiesDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; PropertiesDefault() : SchemaTransformRule{ "properties_default", diff --git a/vendor/core/src/extension/alterschema/linter/property_names_default.h b/vendor/core/src/extension/alterschema/linter/property_names_default.h index 49cd48f..479ca09 100644 --- a/vendor/core/src/extension/alterschema/linter/property_names_default.h +++ b/vendor/core/src/extension/alterschema/linter/property_names_default.h @@ -3,6 +3,8 @@ class PropertyNamesDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"propertyNames"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; PropertyNamesDefault() : SchemaTransformRule{ "property_names_default", diff --git a/vendor/core/src/extension/alterschema/linter/property_names_type_default.h b/vendor/core/src/extension/alterschema/linter/property_names_type_default.h index 7bb20dd..4ced15c 100644 --- a/vendor/core/src/extension/alterschema/linter/property_names_type_default.h +++ b/vendor/core/src/extension/alterschema/linter/property_names_type_default.h @@ -1,5 +1,7 @@ class PropertyNamesTypeDefault final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; PropertyNamesTypeDefault() : SchemaTransformRule{ "property_names_type_default", diff --git a/vendor/core/src/extension/alterschema/linter/simple_properties_identifiers.h b/vendor/core/src/extension/alterschema/linter/simple_properties_identifiers.h index 07cf73e..7f4f30d 100644 --- a/vendor/core/src/extension/alterschema/linter/simple_properties_identifiers.h +++ b/vendor/core/src/extension/alterschema/linter/simple_properties_identifiers.h @@ -1,5 +1,7 @@ class SimplePropertiesIdentifiers final : public SchemaTransformRule { public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; SimplePropertiesIdentifiers() // Inspired by // https://json-structure.github.io/core/draft-vasters-json-structure-core.html#section-3.6 diff --git a/vendor/core/src/extension/alterschema/linter/title_description_equal.h b/vendor/core/src/extension/alterschema/linter/title_description_equal.h index 9c6cc52..01320ba 100644 --- a/vendor/core/src/extension/alterschema/linter/title_description_equal.h +++ b/vendor/core/src/extension/alterschema/linter/title_description_equal.h @@ -1,5 +1,7 @@ class TitleDescriptionEqual final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; TitleDescriptionEqual() : SchemaTransformRule{ "title_description_equal", diff --git a/vendor/core/src/extension/alterschema/linter/title_trailing_period.h b/vendor/core/src/extension/alterschema/linter/title_trailing_period.h index b8c8158..bf7b9ad 100644 --- a/vendor/core/src/extension/alterschema/linter/title_trailing_period.h +++ b/vendor/core/src/extension/alterschema/linter/title_trailing_period.h @@ -1,5 +1,7 @@ class TitleTrailingPeriod final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; TitleTrailingPeriod() : SchemaTransformRule{ "title_trailing_period", diff --git a/vendor/core/src/extension/alterschema/linter/title_trim.h b/vendor/core/src/extension/alterschema/linter/title_trim.h index e523fc8..3846fc3 100644 --- a/vendor/core/src/extension/alterschema/linter/title_trim.h +++ b/vendor/core/src/extension/alterschema/linter/title_trim.h @@ -1,5 +1,7 @@ class TitleTrim final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::false_type; TitleTrim() : SchemaTransformRule{ "title_trim", diff --git a/vendor/core/src/extension/alterschema/linter/top_level_description.h b/vendor/core/src/extension/alterschema/linter/top_level_description.h index b9034e2..d1da356 100644 --- a/vendor/core/src/extension/alterschema/linter/top_level_description.h +++ b/vendor/core/src/extension/alterschema/linter/top_level_description.h @@ -1,5 +1,7 @@ class TopLevelDescription final : public SchemaTransformRule { public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; TopLevelDescription() : SchemaTransformRule{ "top_level_description", diff --git a/vendor/core/src/extension/alterschema/linter/top_level_examples.h b/vendor/core/src/extension/alterschema/linter/top_level_examples.h index 2a453fa..c92c407 100644 --- a/vendor/core/src/extension/alterschema/linter/top_level_examples.h +++ b/vendor/core/src/extension/alterschema/linter/top_level_examples.h @@ -1,5 +1,7 @@ class TopLevelExamples final : public SchemaTransformRule { public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; TopLevelExamples() : SchemaTransformRule{ "top_level_examples", diff --git a/vendor/core/src/extension/alterschema/linter/top_level_title.h b/vendor/core/src/extension/alterschema/linter/top_level_title.h index 1f56d08..cc7fa2c 100644 --- a/vendor/core/src/extension/alterschema/linter/top_level_title.h +++ b/vendor/core/src/extension/alterschema/linter/top_level_title.h @@ -1,5 +1,7 @@ class TopLevelTitle final : public SchemaTransformRule { public: + using mutates = std::false_type; + using reframe_after_transform = std::false_type; TopLevelTitle() : SchemaTransformRule{ "top_level_title", diff --git a/vendor/core/src/extension/alterschema/linter/unevaluated_items_default.h b/vendor/core/src/extension/alterschema/linter/unevaluated_items_default.h index 7139b48..6ce4570 100644 --- a/vendor/core/src/extension/alterschema/linter/unevaluated_items_default.h +++ b/vendor/core/src/extension/alterschema/linter/unevaluated_items_default.h @@ -3,6 +3,8 @@ class UnevaluatedItemsDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"unevaluatedItems"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnevaluatedItemsDefault() : SchemaTransformRule{ "unevaluated_items_default", diff --git a/vendor/core/src/extension/alterschema/linter/unevaluated_properties_default.h b/vendor/core/src/extension/alterschema/linter/unevaluated_properties_default.h index f241c66..b7fff5b 100644 --- a/vendor/core/src/extension/alterschema/linter/unevaluated_properties_default.h +++ b/vendor/core/src/extension/alterschema/linter/unevaluated_properties_default.h @@ -3,6 +3,8 @@ class UnevaluatedPropertiesDefault final : public SchemaTransformRule { static inline const std::string KEYWORD{"unevaluatedProperties"}; public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnevaluatedPropertiesDefault() : SchemaTransformRule{ "unevaluated_properties_default", diff --git a/vendor/core/src/extension/alterschema/linter/unsatisfiable_max_contains.h b/vendor/core/src/extension/alterschema/linter/unsatisfiable_max_contains.h index e206f2e..6ff7c0f 100644 --- a/vendor/core/src/extension/alterschema/linter/unsatisfiable_max_contains.h +++ b/vendor/core/src/extension/alterschema/linter/unsatisfiable_max_contains.h @@ -1,5 +1,7 @@ class UnsatisfiableMaxContains final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnsatisfiableMaxContains() : SchemaTransformRule{ "unsatisfiable_max_contains", diff --git a/vendor/core/src/extension/alterschema/linter/unsatisfiable_min_properties.h b/vendor/core/src/extension/alterschema/linter/unsatisfiable_min_properties.h index fb63ac1..30c72c7 100644 --- a/vendor/core/src/extension/alterschema/linter/unsatisfiable_min_properties.h +++ b/vendor/core/src/extension/alterschema/linter/unsatisfiable_min_properties.h @@ -1,5 +1,7 @@ class UnsatisfiableMinProperties final : public SchemaTransformRule { public: + using mutates = std::true_type; + using reframe_after_transform = std::true_type; UnsatisfiableMinProperties() : SchemaTransformRule{ "unsatisfiable_min_properties", diff --git a/vendor/core/src/extension/editorschema/editorschema.cc b/vendor/core/src/extension/editorschema/editorschema.cc index 4080aba..d595efd 100644 --- a/vendor/core/src/extension/editorschema/editorschema.cc +++ b/vendor/core/src/extension/editorschema/editorschema.cc @@ -1,6 +1,7 @@ #include #include // assert +#include // std::map namespace { @@ -74,10 +75,7 @@ struct SubschemaChange { auto for_editor(JSON &schema, const SchemaWalker &walker, const SchemaResolver &resolver, std::string_view default_dialect) -> void { - // (1) Bring in all of the references - bundle(schema, walker, resolver, default_dialect); - - // (2) Frame the schema and collect all changes we need to make + // (1) Frame the schema and collect all changes we need to make std::vector reference_changes; std::vector subschema_changes; @@ -85,6 +83,18 @@ auto for_editor(JSON &schema, const SchemaWalker &walker, SchemaFrame frame{SchemaFrame::Mode::References}; frame.analyse(schema, walker, resolver, default_dialect); + // Otherwise the input is not bundled + assert(frame.standalone()); + + // Note that `std::unordered_map` is slower here due to high collision rates + // from the simple pointer hashes + std::map> + pointer_to_uri; + for (const auto &entry : frame.locations()) { + pointer_to_uri.emplace(entry.second.pointer, + std::cref(entry.first.second)); + } + // Collect reference changes for (const auto &[key, reference] : frame.references()) { assert(!key.second.empty()); @@ -109,9 +119,10 @@ auto for_editor(JSON &schema, const SchemaWalker &walker, } } else { if (keyword == "$schema") { - const auto uri{frame.uri(key.second)}; - assert(uri.has_value()); - const auto origin{frame.traverse(uri.value().get())}; + // Use pre-built index instead of O(n) frame.uri() scan + const auto uri_it{pointer_to_uri.find(key.second)}; + assert(uri_it != pointer_to_uri.end()); + const auto origin{frame.traverse(uri_it->second.get())}; assert(origin.has_value()); reference_changes.push_back( {to_pointer(key.second), @@ -160,7 +171,7 @@ auto for_editor(JSON &schema, const SchemaWalker &walker, } } - // (3) Apply reference changes + // (2) Apply reference changes for (const auto &change : reference_changes) { if (!change.new_value.empty()) { set(schema, change.pointer, JSON{change.new_value}); @@ -170,7 +181,7 @@ auto for_editor(JSON &schema, const SchemaWalker &walker, } } - // (4) Apply subschema changes + // (3) Apply subschema changes for (const auto &change : subschema_changes) { auto &subschema{get(schema, change.pointer)}; diff --git a/vendor/core/src/extension/editorschema/include/sourcemeta/core/editorschema.h b/vendor/core/src/extension/editorschema/include/sourcemeta/core/editorschema.h index 716c17a..8d871ba 100644 --- a/vendor/core/src/extension/editorschema/include/sourcemeta/core/editorschema.h +++ b/vendor/core/src/extension/editorschema/include/sourcemeta/core/editorschema.h @@ -31,6 +31,8 @@ namespace sourcemeta::core { /// might be edge cases that we cannot cover. The real solution is for popular /// editors to fix their JSON Schema language support. /// +/// Note that the input schema is expected to be already bundled. +/// /// ```cpp /// #include /// #include @@ -41,6 +43,9 @@ namespace sourcemeta::core { /// "$ref": "another" /// })JSON"); /// +/// sourcemeta::core::bundle(schema, +/// sourcemeta::core::schema_walker, +/// sourcemeta::core::schema_resolver); /// sourcemeta::core::for_editor(schema, /// sourcemeta::core::schema_walker, /// sourcemeta::core::schema_resolver);