Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/core/jsonschema/frame.cc
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema,
if (id_value) {
assert(id_value->is_string());
const std::string_view id_view{id_value->to_string()};
if (id_view.starts_with('#')) {
// A bare "#" carries no anchor name, so we treat it as no anchor at
// all.
if (id_view.starts_with('#') && id_view.size() > 1) {
// The original string is "#fragment", skip the '#'
result.emplace_back(id_view.substr(1), AnchorType::Static);
}
Expand All @@ -181,7 +183,9 @@ auto find_anchors(const sourcemeta::core::JSON &schema,
if (id_value) {
assert(id_value->is_string());
const std::string_view id_view{id_value->to_string()};
if (id_view.starts_with('#')) {
// A bare "#" carries no anchor name, so we treat it as no anchor at
// all.
if (id_view.starts_with('#') && id_view.size() > 1) {
// The original string is "#fragment", skip the '#'
result.emplace_back(id_view.substr(1), AnchorType::Static);
}
Expand Down Expand Up @@ -661,6 +665,11 @@ auto SchemaFrame::analyse(const JSON &root, const SchemaWalker &walker,

const auto maybe_fragment{maybe_relative.fragment()};

// Both 2019-09 and 2020-12 state:
//
// "$id" MUST NOT contain a non-empty fragment, and SHOULD NOT
// contain an empty fragment.
//
// See
// https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.2
// See
Expand Down
15 changes: 14 additions & 1 deletion src/core/jsonschema/jsonschema.cc
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ auto sourcemeta::core::identify(const JSON &schema,
}

const auto &identifier{schema.at(keyword)};
if (!identifier.is_string() || identifier.empty()) {
if (!identifier.is_string()) {
std::ostringstream value;
sourcemeta::core::stringify(identifier, value);
throw sourcemeta::core::SchemaKeywordError(
Expand All @@ -180,6 +180,19 @@ auto sourcemeta::core::identify(const JSON &schema,
return default_id;
}

// An empty string identifier and an identifier consisting solely of the
// empty-fragment marker "#" are both valid URI-references that resolve to
// the parent base, carrying no information. We treat them as if no
// identifier was declared at all (i.e. no new schema resource is
// introduced).
// See
// https://json-schema.org/draft/2019-09/draft-handrews-json-schema-02#rfc.section.8.2.2
// See
// https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-01#section-8.2.1-5
if (identifier.to_string().empty() || identifier.to_string() == "#") {
return default_id;
}

return identifier.to_string();
}

Expand Down
247 changes: 247 additions & 0 deletions test/jsonschema/jsonschema_frame_2019_09_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3263,3 +3263,250 @@ TEST(JSONSchema_frame_2019_09, anchor_with_valid_colon) {
EXPECT_FRAME_LOCATION_REACHABLE(
frame, Static, "https://www.sourcemeta.com/schema#foo:bar", frame.root());
}

TEST(JSONSchema_frame_2019_09, top_level_id_absolute_with_non_empty_fragment) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "https://www.sourcemeta.com/schema#foo",
"$schema": "https://json-schema.org/draft/2019-09/schema"
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};

try {
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);
FAIL();
} catch (const sourcemeta::core::SchemaFrameError &error) {
EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/schema#foo");
} catch (...) {
FAIL();
}
}

TEST(JSONSchema_frame_2019_09, nested_id_absolute_with_non_empty_fragment) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "https://www.sourcemeta.com/schema",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$defs": {
"foo": {
"$id": "https://www.sourcemeta.com/nested#foo"
}
}
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};

try {
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);
FAIL();
} catch (const sourcemeta::core::SchemaFrameError &error) {
EXPECT_EQ(error.identifier(), "https://www.sourcemeta.com/nested#foo");
} catch (...) {
FAIL();
}
}

TEST(JSONSchema_frame_2019_09, top_level_id_empty_fragment_only) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "#",
"$schema": "https://json-schema.org/draft/2019-09/schema"
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);

EXPECT_TRUE(frame.root().empty());

EXPECT_EQ(frame.locations().size(), 3);

EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA(
frame, "", "", "https://json-schema.org/draft/2019-09/schema",
JSON_Schema_2019_09, std::nullopt, false, false);
EXPECT_ANONYMOUS_FRAME_STATIC_POINTER(
frame, "#/$id", "/$id", "https://json-schema.org/draft/2019-09/schema",
JSON_Schema_2019_09, "", false, false);
EXPECT_ANONYMOUS_FRAME_STATIC_POINTER(
frame, "#/$schema", "/$schema",
"https://json-schema.org/draft/2019-09/schema", JSON_Schema_2019_09, "",
false, false);

// References

EXPECT_EQ(frame.references().size(), 1);

EXPECT_STATIC_REFERENCE(
frame, "/$schema", "https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/schema", std::nullopt,
"https://json-schema.org/draft/2019-09/schema");

// Reachability

EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root());
}

TEST(JSONSchema_frame_2019_09, nested_id_empty_fragment_only) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "https://www.sourcemeta.com/schema",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$defs": {
"foo": {
"$id": "#"
}
}
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);

EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema");

EXPECT_EQ(frame.locations().size(), 6);

EXPECT_FRAME_STATIC_2019_09_RESOURCE(
frame, "https://www.sourcemeta.com/schema",
"https://www.sourcemeta.com/schema", "",
"https://www.sourcemeta.com/schema", "", std::nullopt, false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$id",
"https://www.sourcemeta.com/schema", "/$id",
"https://www.sourcemeta.com/schema", "/$id", "", false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$schema",
"https://www.sourcemeta.com/schema", "/$schema",
"https://www.sourcemeta.com/schema", "/$schema", "", false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$defs",
"https://www.sourcemeta.com/schema", "/$defs",
"https://www.sourcemeta.com/schema", "/$defs", "", false, false);
EXPECT_FRAME_STATIC_2019_09_SUBSCHEMA(
frame, "https://www.sourcemeta.com/schema#/$defs/foo",
"https://www.sourcemeta.com/schema", "/$defs/foo",
"https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id",
"https://www.sourcemeta.com/schema", "/$defs/foo/$id",
"https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo",
false, true);

// References

EXPECT_EQ(frame.references().size(), 1);

EXPECT_STATIC_REFERENCE(
frame, "/$schema", "https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/schema", std::nullopt,
"https://json-schema.org/draft/2019-09/schema");

// Reachability

EXPECT_FRAME_LOCATION_REACHABLE(
frame, Static, "https://www.sourcemeta.com/schema", frame.root());
}

TEST(JSONSchema_frame_2019_09, top_level_id_empty_string) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "",
"$schema": "https://json-schema.org/draft/2019-09/schema"
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);

EXPECT_TRUE(frame.root().empty());

EXPECT_EQ(frame.locations().size(), 3);

EXPECT_ANONYMOUS_FRAME_STATIC_SUBSCHEMA(
frame, "", "", "https://json-schema.org/draft/2019-09/schema",
JSON_Schema_2019_09, std::nullopt, false, false);
EXPECT_ANONYMOUS_FRAME_STATIC_POINTER(
frame, "#/$id", "/$id", "https://json-schema.org/draft/2019-09/schema",
JSON_Schema_2019_09, "", false, false);
EXPECT_ANONYMOUS_FRAME_STATIC_POINTER(
frame, "#/$schema", "/$schema",
"https://json-schema.org/draft/2019-09/schema", JSON_Schema_2019_09, "",
false, false);

// References

EXPECT_EQ(frame.references().size(), 1);

EXPECT_STATIC_REFERENCE(
frame, "/$schema", "https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/schema", std::nullopt,
"https://json-schema.org/draft/2019-09/schema");

// Reachability

EXPECT_FRAME_LOCATION_REACHABLE(frame, Static, "", frame.root());
}

TEST(JSONSchema_frame_2019_09, nested_id_empty_string) {
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
"$id": "https://www.sourcemeta.com/schema",
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$defs": {
"foo": {
"$id": ""
}
}
})JSON");

sourcemeta::core::SchemaFrame frame{
sourcemeta::core::SchemaFrame::Mode::References};
frame.analyse(document, sourcemeta::core::schema_walker,
sourcemeta::core::schema_resolver);

EXPECT_EQ(frame.root(), "https://www.sourcemeta.com/schema");

EXPECT_EQ(frame.locations().size(), 6);

EXPECT_FRAME_STATIC_2019_09_RESOURCE(
frame, "https://www.sourcemeta.com/schema",
"https://www.sourcemeta.com/schema", "",
"https://www.sourcemeta.com/schema", "", std::nullopt, false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$id",
"https://www.sourcemeta.com/schema", "/$id",
"https://www.sourcemeta.com/schema", "/$id", "", false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$schema",
"https://www.sourcemeta.com/schema", "/$schema",
"https://www.sourcemeta.com/schema", "/$schema", "", false, false);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$defs",
"https://www.sourcemeta.com/schema", "/$defs",
"https://www.sourcemeta.com/schema", "/$defs", "", false, false);
EXPECT_FRAME_STATIC_2019_09_SUBSCHEMA(
frame, "https://www.sourcemeta.com/schema#/$defs/foo",
"https://www.sourcemeta.com/schema", "/$defs/foo",
"https://www.sourcemeta.com/schema", "/$defs/foo", "", false, true);
EXPECT_FRAME_STATIC_2019_09_POINTER(
frame, "https://www.sourcemeta.com/schema#/$defs/foo/$id",
"https://www.sourcemeta.com/schema", "/$defs/foo/$id",
"https://www.sourcemeta.com/schema", "/$defs/foo/$id", "/$defs/foo",
false, true);

// References

EXPECT_EQ(frame.references().size(), 1);

EXPECT_STATIC_REFERENCE(
frame, "/$schema", "https://json-schema.org/draft/2019-09/schema",
"https://json-schema.org/draft/2019-09/schema", std::nullopt,
"https://json-schema.org/draft/2019-09/schema");

// Reachability

EXPECT_FRAME_LOCATION_REACHABLE(
frame, Static, "https://www.sourcemeta.com/schema", frame.root());
}
Loading
Loading