diff --git a/src/core/jsonschema/bundle.cc b/src/core/jsonschema/bundle.cc index 2a1361042..cb6eb7239 100644 --- a/src/core/jsonschema/bundle.cc +++ b/src/core/jsonschema/bundle.cc @@ -454,10 +454,14 @@ auto bundle(JSON &schema, const SchemaWalker &walker, if (ref_overrides_adjacent_keywords(schema_base_dialect.value()) && schema.is_object() && schema.defines("$ref")) { if (schema.size() == 1) { + const auto is_draft3{schema_base_dialect.value() == + SchemaBaseDialect::JSON_Schema_Draft_3 || + schema_base_dialect.value() == + SchemaBaseDialect::JSON_Schema_Draft_3_Hyper}; auto branches{JSON::make_array()}; branches.push_back(schema); schema.at("$ref").into(std::move(branches)); - schema.rename("$ref", "allOf"); + schema.rename("$ref", is_draft3 ? "extends" : "allOf"); } else { throw SchemaError( "Cannot bundle a JSON Schema Draft 7 or older with a top-level " diff --git a/src/core/jsonschema/helpers.h b/src/core/jsonschema/helpers.h index ad2635552..b3ca84fa2 100644 --- a/src/core/jsonschema/helpers.h +++ b/src/core/jsonschema/helpers.h @@ -48,9 +48,9 @@ inline auto definitions_keyword(const SchemaBaseDialect base_dialect) case SchemaBaseDialect::JSON_Schema_Draft_6_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_4: case SchemaBaseDialect::JSON_Schema_Draft_4_Hyper: - return "definitions"; case SchemaBaseDialect::JSON_Schema_Draft_3: case SchemaBaseDialect::JSON_Schema_Draft_3_Hyper: + return "definitions"; case SchemaBaseDialect::JSON_Schema_Draft_2_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_1_Hyper: case SchemaBaseDialect::JSON_Schema_Draft_0_Hyper: diff --git a/src/core/jsonschema/known_walker.cc b/src/core/jsonschema/known_walker.cc index b31b23b53..f439eb5d4 100644 --- a/src/core/jsonschema/known_walker.cc +++ b/src/core/jsonschema/known_walker.cc @@ -154,6 +154,10 @@ auto handle_definitions(const Vocabularies &vocabularies) LocationMembers, "$ref") CHECK_VOCABULARY_WITH_DEPENDENCIES(Known::JSON_Schema_Draft_4_Hyper, {}, LocationMembers, "$ref") + CHECK_VOCABULARY_WITH_DEPENDENCIES(Known::JSON_Schema_Draft_3, {}, + LocationMembers, "$ref") + CHECK_VOCABULARY_WITH_DEPENDENCIES(Known::JSON_Schema_Draft_3_Hyper, {}, + LocationMembers, "$ref") return UNKNOWN_RESULT; } diff --git a/test/jsonschema/jsonschema_bundle_draft3_test.cc b/test/jsonschema/jsonschema_bundle_draft3_test.cc index 52a891971..d4bfacaef 100644 --- a/test/jsonschema/jsonschema_bundle_draft3_test.cc +++ b/test/jsonschema/jsonschema_bundle_draft3_test.cc @@ -14,6 +14,32 @@ static auto test_resolver(std::string_view identifier) "id": "https://www.sourcemeta.com/test-1", "type": "string" })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-2") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-2", + "extends": { "$ref": "test-3" } + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-3") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-3", + "extends": { "$ref": "test-1" } + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/test-4") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-4", + "type": "boolean" + })JSON"); + } else if (identifier == "https://www.sourcemeta.com/recursive") { + return sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/recursive", + "properties": { + "foo": { "$ref": "#" } + } + })JSON"); } else { return sourcemeta::core::schema_resolver(identifier); } @@ -58,7 +84,272 @@ TEST(JSONSchema_bundle_draft3, simple_bundling) { } })JSON"); + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-1" } + }, + "definitions": { + "https://www.sourcemeta.com/test-1": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, simple_with_id) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/test-1" }, + "bar": { + "id": "https://www.sourcemeta.com", + "extends": { "$ref": "test-2" } + } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/test-1" }, + "bar": { + "id": "https://www.sourcemeta.com", + "extends": { "$ref": "test-2" } + } + }, + "definitions": { + "https://www.sourcemeta.com/test-1": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + }, + "https://www.sourcemeta.com/test-2": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-2", + "extends": { "$ref": "test-3" } + }, + "https://www.sourcemeta.com/test-3": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-3", + "extends": { "$ref": "test-1" } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, schema_not_found) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "foo": { "$ref": "https://www.sourcemeta.com/xxx" } + } + })JSON"); + EXPECT_THROW(sourcemeta::core::bundle( document, sourcemeta::core::schema_walker, test_resolver), - sourcemeta::core::SchemaError); + sourcemeta::core::SchemaResolutionError); +} + +TEST(JSONSchema_bundle_draft3, idempotency) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-2" } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-2" } + }, + "definitions": { + "https://www.sourcemeta.com/test-1": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + }, + "https://www.sourcemeta.com/test-2": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-2", + "extends": { "$ref": "test-3" } + }, + "https://www.sourcemeta.com/test-3": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-3", + "extends": { "$ref": "test-1" } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, pre_embedded) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-2" } + }, + "definitions": { + "already-embedded": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-2" } + }, + "definitions": { + "already-embedded": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + }, + "https://www.sourcemeta.com/test-2": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-2", + "extends": { "$ref": "test-3" } + }, + "https://www.sourcemeta.com/test-3": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-3", + "extends": { "$ref": "test-1" } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, taken_definitions_entry) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-1" }, + "extra": { "$ref": "https://www.sourcemeta.com/test-4" } + }, + "definitions": { + "https://www.sourcemeta.com/test-1": { "type": "object" }, + "https://www.sourcemeta.com/test-4": { "type": "object" }, + "https://www.sourcemeta.com/test-4/x": { "type": "object" }, + "https://www.sourcemeta.com/test-4/x/x": { "type": "object" } + } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "id": "https://example.com", + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "test": { "$ref": "https://www.sourcemeta.com/test-1" }, + "extra": { "$ref": "https://www.sourcemeta.com/test-4" } + }, + "definitions": { + "https://www.sourcemeta.com/test-1": { "type": "object" }, + "https://www.sourcemeta.com/test-1/x": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + }, + "https://www.sourcemeta.com/test-4": { "type": "object" }, + "https://www.sourcemeta.com/test-4/x": { "type": "object" }, + "https://www.sourcemeta.com/test-4/x/x": { "type": "object" }, + "https://www.sourcemeta.com/test-4/x/x/x": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-4", + "type": "boolean" + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, recursive) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "extends": { "$ref": "https://www.sourcemeta.com/recursive" } + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "extends": { "$ref": "https://www.sourcemeta.com/recursive" }, + "definitions": { + "https://www.sourcemeta.com/recursive": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/recursive", + "properties": { + "foo": { "$ref": "#" } + } + } + } + })JSON"); + + EXPECT_EQ(document, expected); +} + +TEST(JSONSchema_bundle_draft3, standalone_ref_with_default_dialect) { + sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$ref": "https://www.sourcemeta.com/test-1" + })JSON"); + + sourcemeta::core::bundle(document, sourcemeta::core::schema_walker, + test_resolver, + "http://json-schema.org/draft-03/schema#"); + + const sourcemeta::core::JSON expected = sourcemeta::core::parse_json(R"JSON({ + "extends": [ { "$ref": "https://www.sourcemeta.com/test-1" } ], + "definitions": { + "https://www.sourcemeta.com/test-1": { + "$schema": "http://json-schema.org/draft-03/schema#", + "id": "https://www.sourcemeta.com/test-1", + "type": "string" + } + } + })JSON"); + + EXPECT_EQ(document, expected); } diff --git a/test/jsonschema/jsonschema_frame_draft3_test.cc b/test/jsonschema/jsonschema_frame_draft3_test.cc index 182048cb9..08d8c8fec 100644 --- a/test/jsonschema/jsonschema_frame_draft3_test.cc +++ b/test/jsonschema/jsonschema_frame_draft3_test.cc @@ -942,3 +942,116 @@ TEST(JSONSchema_frame_draft3, id_fragment_invalid_whitespace) { FAIL(); } } + +TEST(JSONSchema_frame_draft3, definitions_subschemas) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "definitions": { + "string": { "type": "string" } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.locations().size(), 5); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/definitions", "/definitions", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + false); + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "#/definitions/string", "/definitions/string", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + true); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/definitions/string/type", "/definitions/string/type", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, + "/definitions/string", false, true); + + EXPECT_EQ(frame.references().size(), 1); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-03/schema", + "http://json-schema.org/draft-03/schema", std::nullopt, + "http://json-schema.org/draft-03/schema#"); + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); + EXPECT_FRAME_LOCATION_NON_REACHABLE(frame, Static, "#/definitions/string", + frame.root()); + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "#/definitions/string", + "#/definitions/string"); +} + +TEST(JSONSchema_frame_draft3, ref_into_definitions) { + const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({ + "$schema": "http://json-schema.org/draft-03/schema#", + "properties": { + "foo": { "$ref": "#/definitions/string" } + }, + "definitions": { + "string": { "type": "string" } + } + })JSON"); + + sourcemeta::core::SchemaFrame frame{ + sourcemeta::core::SchemaFrame::Mode::References}; + frame.analyse(document, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver); + + EXPECT_EQ(frame.locations().size(), 8); + + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "", "", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, std::nullopt, false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/$schema", "/$schema", "http://json-schema.org/draft-03/schema#", + JSON_Schema_Draft_3, "", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/properties", "/properties", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + false); + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "#/properties/foo", "/properties/foo", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/properties/foo/$ref", "/properties/foo/$ref", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, + "/properties/foo", false, false); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/definitions", "/definitions", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + false); + EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA( + frame, "#/definitions/string", "/definitions/string", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, "", false, + true); + EXPECT_ANONYMOUS_FRAME_STATIC_POINTER( + frame, "#/definitions/string/type", "/definitions/string/type", + "http://json-schema.org/draft-03/schema#", JSON_Schema_Draft_3, + "/definitions/string", false, true); + + EXPECT_EQ(frame.references().size(), 2); + + EXPECT_STATIC_REFERENCE( + frame, "/$schema", "http://json-schema.org/draft-03/schema", + "http://json-schema.org/draft-03/schema", std::nullopt, + "http://json-schema.org/draft-03/schema#"); + EXPECT_STATIC_REFERENCE(frame, "/properties/foo/$ref", "#/definitions/string", + "", "/definitions/string", "#/definitions/string"); + + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root()); + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "#/properties/foo", + frame.root()); + EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "#/definitions/string", + frame.root()); +} diff --git a/test/jsonschema/jsonschema_walker_draft3_test.cc b/test/jsonschema/jsonschema_walker_draft3_test.cc index db399714d..2b981cdb7 100644 --- a/test/jsonschema/jsonschema_walker_draft3_test.cc +++ b/test/jsonschema/jsonschema_walker_draft3_test.cc @@ -48,6 +48,31 @@ TEST(JSONSchema_walker_draft3, ref) { EXPECT_TRUE(result.instances.none()); } +TEST(JSONSchema_walker_draft3, definitions) { + using namespace sourcemeta::core; + const auto &result{schema_walker("definitions", VOCABULARIES_DRAFT3)}; + EXPECT_EQ(result.type, SchemaKeywordType::LocationMembers); + EXPECT_TRUE(result.vocabulary.has_value()); + EXPECT_VOCABULARY_KNOWN(result.vocabulary.value(), JSON_Schema_Draft_3); + const std::unordered_set expected{"$ref"}; + EXPECT_EQ(result.dependencies, expected); + EXPECT_TRUE(result.order_dependencies.empty()); + EXPECT_TRUE(result.instances.none()); +} + +TEST(JSONSchema_walker_draft3_hyperschema, definitions) { + using namespace sourcemeta::core; + const auto &result{ + schema_walker("definitions", VOCABULARIES_DRAFT3_HYPERSCHEMA)}; + EXPECT_EQ(result.type, SchemaKeywordType::LocationMembers); + EXPECT_TRUE(result.vocabulary.has_value()); + EXPECT_VOCABULARY_KNOWN(result.vocabulary.value(), JSON_Schema_Draft_3_Hyper); + const std::unordered_set expected{"$ref"}; + EXPECT_EQ(result.dependencies, expected); + EXPECT_TRUE(result.order_dependencies.empty()); + EXPECT_TRUE(result.instances.none()); +} + TEST(JSONSchema_walker_draft3, items) { using namespace sourcemeta::core; const auto &result{schema_walker("items", VOCABULARIES_DRAFT3)};