Skip to content

Commit a5b788e

Browse files
authored
Extract $vocabulary parsing as parse_vocabularies (#2314)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 56eab6e commit a5b788e

11 files changed

Lines changed: 421 additions & 9 deletions

src/core/jsonschema/include/sourcemeta/core/jsonschema.h

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,45 @@ auto base_dialect(const JSON &schema, const SchemaResolver &resolver,
317317
std::string_view default_dialect = "")
318318
-> std::optional<SchemaBaseDialect>;
319319

320+
/// @ingroup jsonschema
321+
///
322+
/// Parse the `$vocabulary` keyword from a given schema, if set. For example:
323+
///
324+
/// ```cpp
325+
/// #include <sourcemeta/core/json.h>
326+
/// #include <sourcemeta/core/jsonschema.h>
327+
/// #include <cassert>
328+
///
329+
/// const sourcemeta::core::JSON document =
330+
/// sourcemeta::core::parse_json(R"JSON({
331+
/// "$schema": "https://json-schema.org/draft/2020-12/schema",
332+
/// "$vocabulary": {
333+
/// "https://json-schema.org/draft/2020-12/vocab/core": true,
334+
/// "https://json-schema.org/draft/2020-12/vocab/applicator": true
335+
/// }
336+
/// })JSON");
337+
///
338+
/// const auto result{
339+
/// sourcemeta::core::parse_vocabularies(
340+
/// document, sourcemeta::core::schema_resolver)};
341+
///
342+
/// assert(result.has_value());
343+
/// assert(result->size() == 2);
344+
/// ```
345+
SOURCEMETA_CORE_JSONSCHEMA_EXPORT
346+
auto parse_vocabularies(const JSON &schema, const SchemaResolver &resolver,
347+
std::string_view default_dialect = "")
348+
-> std::optional<Vocabularies>;
349+
350+
/// @ingroup jsonschema
351+
///
352+
/// A shortcut to sourcemeta::core::parse_vocabularies when the base dialect
353+
/// is already known.
354+
SOURCEMETA_CORE_JSONSCHEMA_EXPORT
355+
auto parse_vocabularies(const JSON &schema,
356+
const SchemaBaseDialect base_dialect)
357+
-> std::optional<Vocabularies>;
358+
320359
/// @ingroup jsonschema
321360
///
322361
/// List the vocabularies that a specific schema makes use of. If you set a

src/core/jsonschema/jsonschema.cc

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,55 @@ auto is_pre_vocabulary_base_dialect(
440440
}
441441
} // namespace
442442

443+
auto sourcemeta::core::parse_vocabularies(
444+
const sourcemeta::core::JSON &schema,
445+
const sourcemeta::core::SchemaBaseDialect base_dialect)
446+
-> std::optional<sourcemeta::core::Vocabularies> {
447+
if (base_dialect !=
448+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12 &&
449+
base_dialect !=
450+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12_Hyper &&
451+
base_dialect !=
452+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09 &&
453+
base_dialect !=
454+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09_Hyper) {
455+
return std::nullopt;
456+
}
457+
458+
if (!schema.is_object()) {
459+
return std::nullopt;
460+
}
461+
462+
const auto *vocabulary_entry{schema.try_at("$vocabulary")};
463+
if (!vocabulary_entry) {
464+
return std::nullopt;
465+
}
466+
467+
assert(vocabulary_entry->is_object());
468+
sourcemeta::core::Vocabularies result;
469+
for (const auto &entry : vocabulary_entry->as_object()) {
470+
assert(entry.second.is_boolean());
471+
result.insert(entry.first, entry.second.to_boolean());
472+
}
473+
474+
return result;
475+
}
476+
477+
auto sourcemeta::core::parse_vocabularies(
478+
const sourcemeta::core::JSON &schema,
479+
const sourcemeta::core::SchemaResolver &resolver,
480+
std::string_view default_dialect)
481+
-> std::optional<sourcemeta::core::Vocabularies> {
482+
const auto schema_base_dialect{
483+
sourcemeta::core::base_dialect(schema, resolver, default_dialect)};
484+
if (schema_base_dialect.has_value()) {
485+
return sourcemeta::core::parse_vocabularies(schema,
486+
schema_base_dialect.value());
487+
} else {
488+
return std::nullopt;
489+
}
490+
}
491+
443492
auto sourcemeta::core::vocabularies(
444493
const sourcemeta::core::JSON &schema,
445494
const sourcemeta::core::SchemaResolver &resolver,
@@ -545,16 +594,10 @@ auto sourcemeta::core::vocabularies(const SchemaResolver &resolver,
545594
* dialect
546595
*/
547596

548-
Vocabularies result;
549597
const auto core{core_vocabulary_known(base_dialect)};
550-
if (schema_dialect.defines("$vocabulary")) {
551-
const sourcemeta::core::JSON &vocabularies{
552-
schema_dialect.at("$vocabulary")};
553-
assert(vocabularies.is_object());
554-
for (const auto &entry : vocabularies.as_object()) {
555-
result.insert(entry.first, entry.second.to_boolean());
556-
}
557-
} else {
598+
auto result{parse_vocabularies(schema_dialect, base_dialect)
599+
.value_or(Vocabularies{})};
600+
if (result.empty()) {
558601
result.insert(core, true);
559602
}
560603

test/jsonschema/jsonschema_vocabulary_2019_09_test.cc

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,87 @@ static auto test_resolver(std::string_view identifier)
3535
}
3636
}
3737

38+
TEST(JSONSchema_vocabulary_2019_09, parse_vocabularies_with_vocabulary) {
39+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
40+
"$schema": "https://json-schema.org/draft/2019-09/schema",
41+
"$vocabulary": {
42+
"https://json-schema.org/draft/2019-09/vocab/core": true,
43+
"https://json-schema.org/draft/2019-09/vocab/applicator": true,
44+
"https://json-schema.org/draft/2019-09/vocab/validation": false
45+
}
46+
})JSON");
47+
48+
const auto result{
49+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
50+
EXPECT_TRUE(result.has_value());
51+
EXPECT_EQ(result->size(), 3);
52+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2019_09_Core);
53+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2019_09_Applicator);
54+
EXPECT_VOCABULARY_OPTIONAL(*result, JSON_Schema_2019_09_Validation);
55+
}
56+
57+
TEST(JSONSchema_vocabulary_2019_09, parse_vocabularies_without_vocabulary) {
58+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
59+
"$schema": "https://json-schema.org/draft/2019-09/schema",
60+
"type": "object"
61+
})JSON");
62+
63+
const auto result{
64+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
65+
EXPECT_FALSE(result.has_value());
66+
}
67+
68+
TEST(JSONSchema_vocabulary_2019_09, parse_vocabularies_boolean_schema) {
69+
const sourcemeta::core::JSON document{true};
70+
const auto result{sourcemeta::core::parse_vocabularies(
71+
document, test_resolver, "https://json-schema.org/draft/2019-09/schema")};
72+
EXPECT_FALSE(result.has_value());
73+
}
74+
75+
TEST(JSONSchema_vocabulary_2019_09,
76+
parse_vocabularies_with_base_dialect_with_vocabulary) {
77+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
78+
"$vocabulary": {
79+
"https://json-schema.org/draft/2019-09/vocab/core": true,
80+
"https://json-schema.org/draft/2019-09/vocab/applicator": true
81+
}
82+
})JSON");
83+
84+
const auto result{sourcemeta::core::parse_vocabularies(
85+
document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09)};
86+
EXPECT_TRUE(result.has_value());
87+
EXPECT_EQ(result->size(), 2);
88+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2019_09_Core);
89+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2019_09_Applicator);
90+
}
91+
92+
TEST(JSONSchema_vocabulary_2019_09,
93+
parse_vocabularies_with_base_dialect_without_vocabulary) {
94+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
95+
"type": "object"
96+
})JSON");
97+
98+
const auto result{sourcemeta::core::parse_vocabularies(
99+
document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09)};
100+
EXPECT_FALSE(result.has_value());
101+
}
102+
103+
TEST(JSONSchema_vocabulary_2019_09,
104+
parse_vocabularies_with_base_dialect_hyper) {
105+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
106+
"$vocabulary": {
107+
"https://json-schema.org/draft/2019-09/vocab/core": true
108+
}
109+
})JSON");
110+
111+
const auto result{sourcemeta::core::parse_vocabularies(
112+
document,
113+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2019_09_Hyper)};
114+
EXPECT_TRUE(result.has_value());
115+
EXPECT_EQ(result->size(), 1);
116+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2019_09_Core);
117+
}
118+
38119
TEST(JSONSchema_vocabulary_2019_09, no_vocabularies) {
39120
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
40121
"$schema": "https://sourcemeta.com/2019-09-no-vocabularies"

test/jsonschema/jsonschema_vocabulary_2020_12_test.cc

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,87 @@ static auto test_resolver(std::string_view identifier)
5353
}
5454
}
5555

56+
TEST(JSONSchema_vocabulary_2020_12, parse_vocabularies_with_vocabulary) {
57+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
58+
"$schema": "https://json-schema.org/draft/2020-12/schema",
59+
"$vocabulary": {
60+
"https://json-schema.org/draft/2020-12/vocab/core": true,
61+
"https://json-schema.org/draft/2020-12/vocab/applicator": true,
62+
"https://json-schema.org/draft/2020-12/vocab/validation": false
63+
}
64+
})JSON");
65+
66+
const auto result{
67+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
68+
EXPECT_TRUE(result.has_value());
69+
EXPECT_EQ(result->size(), 3);
70+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2020_12_Core);
71+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2020_12_Applicator);
72+
EXPECT_VOCABULARY_OPTIONAL(*result, JSON_Schema_2020_12_Validation);
73+
}
74+
75+
TEST(JSONSchema_vocabulary_2020_12, parse_vocabularies_without_vocabulary) {
76+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
77+
"$schema": "https://json-schema.org/draft/2020-12/schema",
78+
"type": "object"
79+
})JSON");
80+
81+
const auto result{
82+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
83+
EXPECT_FALSE(result.has_value());
84+
}
85+
86+
TEST(JSONSchema_vocabulary_2020_12, parse_vocabularies_boolean_schema) {
87+
const sourcemeta::core::JSON document{true};
88+
const auto result{sourcemeta::core::parse_vocabularies(
89+
document, test_resolver, "https://json-schema.org/draft/2020-12/schema")};
90+
EXPECT_FALSE(result.has_value());
91+
}
92+
93+
TEST(JSONSchema_vocabulary_2020_12,
94+
parse_vocabularies_with_base_dialect_with_vocabulary) {
95+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
96+
"$vocabulary": {
97+
"https://json-schema.org/draft/2020-12/vocab/core": true,
98+
"https://json-schema.org/draft/2020-12/vocab/applicator": true
99+
}
100+
})JSON");
101+
102+
const auto result{sourcemeta::core::parse_vocabularies(
103+
document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12)};
104+
EXPECT_TRUE(result.has_value());
105+
EXPECT_EQ(result->size(), 2);
106+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2020_12_Core);
107+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2020_12_Applicator);
108+
}
109+
110+
TEST(JSONSchema_vocabulary_2020_12,
111+
parse_vocabularies_with_base_dialect_without_vocabulary) {
112+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
113+
"type": "object"
114+
})JSON");
115+
116+
const auto result{sourcemeta::core::parse_vocabularies(
117+
document, sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12)};
118+
EXPECT_FALSE(result.has_value());
119+
}
120+
121+
TEST(JSONSchema_vocabulary_2020_12,
122+
parse_vocabularies_with_base_dialect_hyper) {
123+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
124+
"$vocabulary": {
125+
"https://json-schema.org/draft/2020-12/vocab/core": true
126+
}
127+
})JSON");
128+
129+
const auto result{sourcemeta::core::parse_vocabularies(
130+
document,
131+
sourcemeta::core::SchemaBaseDialect::JSON_Schema_2020_12_Hyper)};
132+
EXPECT_TRUE(result.has_value());
133+
EXPECT_EQ(result->size(), 1);
134+
EXPECT_VOCABULARY_REQUIRED(*result, JSON_Schema_2020_12_Core);
135+
}
136+
56137
TEST(JSONSchema_vocabulary_2020_12, core_vocabularies_boolean_with_default) {
57138
const sourcemeta::core::JSON document{true};
58139
const sourcemeta::core::Vocabularies vocabularies{

test/jsonschema/jsonschema_vocabulary_draft0_test.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ static auto test_resolver(std::string_view identifier)
2929
}
3030
}
3131

32+
TEST(JSONSchema_vocabulary_draft0, parse_vocabularies_without_vocabulary) {
33+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
34+
"$schema": "http://json-schema.org/draft-00/schema#",
35+
"type": "object"
36+
})JSON");
37+
38+
const auto result{
39+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
40+
EXPECT_FALSE(result.has_value());
41+
}
42+
43+
TEST(JSONSchema_vocabulary_draft0, parse_vocabularies_with_vocabulary) {
44+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
45+
"$schema": "http://json-schema.org/draft-00/schema#",
46+
"$vocabulary": {
47+
"https://json-schema.org/draft/2020-12/vocab/core": true
48+
}
49+
})JSON");
50+
51+
const auto result{
52+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
53+
EXPECT_FALSE(result.has_value());
54+
}
55+
3256
TEST(JSONSchema_vocabulary_draft0, schema) {
3357
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
3458
"$schema": "http://json-schema.org/draft-00/schema#",

test/jsonschema/jsonschema_vocabulary_draft1_test.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ static auto test_resolver(std::string_view identifier)
2929
}
3030
}
3131

32+
TEST(JSONSchema_vocabulary_draft1, parse_vocabularies_without_vocabulary) {
33+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
34+
"$schema": "http://json-schema.org/draft-01/schema#",
35+
"type": "object"
36+
})JSON");
37+
38+
const auto result{
39+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
40+
EXPECT_FALSE(result.has_value());
41+
}
42+
43+
TEST(JSONSchema_vocabulary_draft1, parse_vocabularies_with_vocabulary) {
44+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
45+
"$schema": "http://json-schema.org/draft-01/schema#",
46+
"$vocabulary": {
47+
"https://json-schema.org/draft/2020-12/vocab/core": true
48+
}
49+
})JSON");
50+
51+
const auto result{
52+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
53+
EXPECT_FALSE(result.has_value());
54+
}
55+
3256
TEST(JSONSchema_vocabulary_draft1, schema) {
3357
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
3458
"$schema": "http://json-schema.org/draft-01/schema#",

test/jsonschema/jsonschema_vocabulary_draft2_test.cc

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ static auto test_resolver(std::string_view identifier)
2929
}
3030
}
3131

32+
TEST(JSONSchema_vocabulary_draft2, parse_vocabularies_without_vocabulary) {
33+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
34+
"$schema": "http://json-schema.org/draft-02/schema#",
35+
"type": "object"
36+
})JSON");
37+
38+
const auto result{
39+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
40+
EXPECT_FALSE(result.has_value());
41+
}
42+
43+
TEST(JSONSchema_vocabulary_draft2, parse_vocabularies_with_vocabulary) {
44+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
45+
"$schema": "http://json-schema.org/draft-02/schema#",
46+
"$vocabulary": {
47+
"https://json-schema.org/draft/2020-12/vocab/core": true
48+
}
49+
})JSON");
50+
51+
const auto result{
52+
sourcemeta::core::parse_vocabularies(document, test_resolver)};
53+
EXPECT_FALSE(result.has_value());
54+
}
55+
3256
TEST(JSONSchema_vocabulary_draft2, schema) {
3357
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
3458
"$schema": "http://json-schema.org/draft-02/schema#",

0 commit comments

Comments
 (0)