Skip to content

Commit 393b37a

Browse files
authored
Format x- unknown keywords right after metadata keywords (#1907)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent fcecd78 commit 393b37a

File tree

2 files changed

+110
-79
lines changed

2 files changed

+110
-79
lines changed

src/core/jsonschema/jsonschema.cc

Lines changed: 91 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#include <sourcemeta/core/jsonschema.h>
22

3-
#include <cassert> // assert
4-
#include <cstdint> // std::uint64_t
5-
#include <functional> // std::less
6-
#include <limits> // std::numeric_limits
7-
#include <numeric> // std::accumulate
8-
#include <sstream> // std::ostringstream
9-
#include <type_traits> // std::remove_reference_t
10-
#include <utility> // std::move
3+
#include <cassert> // assert
4+
#include <cstdint> // std::uint64_t
5+
#include <limits> // std::numeric_limits
6+
#include <numeric> // std::accumulate
7+
#include <sstream> // std::ostringstream
8+
#include <type_traits> // std::remove_reference_t
9+
#include <unordered_map> // std::unordered_map
10+
#include <utility> // std::move
1111

1212
auto sourcemeta::core::is_schema(const sourcemeta::core::JSON &schema) -> bool {
1313
return schema.is_object() || schema.is_boolean();
@@ -476,12 +476,10 @@ auto sourcemeta::core::vocabularies(const SchemaResolver &resolver,
476476
return result;
477477
}
478478

479-
auto sourcemeta::core::schema_format_compare(
480-
const sourcemeta::core::JSON::String &left,
481-
const sourcemeta::core::JSON::String &right) -> bool {
479+
static auto keyword_rank(const sourcemeta::core::JSON::String &keyword,
480+
const std::uint64_t otherwise) -> std::uint64_t {
482481
using Rank =
483-
std::map<JSON::String, std::uint64_t, std::less<>,
484-
JSON::Allocator<std::pair<const JSON::String, std::uint64_t>>>;
482+
std::unordered_map<sourcemeta::core::JSON::String, std::uint64_t>;
485483
static Rank rank{// Most core keywords tend to come first
486484
{"$schema", 0},
487485
{"$id", 1},
@@ -501,88 +499,102 @@ auto sourcemeta::core::schema_format_compare(
501499
{"writeOnly", 14},
502500
{"default", 15},
503501

502+
// This is a placeholder for "x-"-prefixed unknown keywords,
503+
// as they are almost always metadata
504+
{"x-", 16},
505+
504506
// Then references
505-
{"$ref", 16},
506-
{"$dynamicRef", 17},
507-
{"$recursiveRef", 18},
507+
{"$ref", 17},
508+
{"$dynamicRef", 18},
509+
{"$recursiveRef", 19},
508510

509511
// Then keywords that apply to any type
510-
{"type", 19},
511-
{"disallow", 20},
512-
{"extends", 21},
513-
{"const", 22},
514-
{"enum", 23},
515-
{"optional", 0},
516-
{"requires", 0},
517-
{"allOf", 24},
518-
{"anyOf", 25},
519-
{"oneOf", 26},
520-
{"not", 27},
521-
{"if", 28},
522-
{"then", 29},
523-
{"else", 30},
512+
{"type", 20},
513+
{"disallow", 21},
514+
{"extends", 22},
515+
{"const", 23},
516+
{"enum", 24},
517+
{"optional", 25},
518+
{"requires", 26},
519+
{"allOf", 27},
520+
{"anyOf", 28},
521+
{"oneOf", 29},
522+
{"not", 30},
523+
{"if", 31},
524+
{"then", 32},
525+
{"else", 33},
524526

525527
// Then keywords about numbers
526-
{"exclusiveMaximum", 31},
527-
{"maximum", 32},
528-
{"maximumCanEqual", 33},
529-
{"exclusiveMinimum", 34},
530-
{"minimum", 35},
531-
{"minimumCanEqual", 36},
532-
{"multipleOf", 37},
533-
{"divisibleBy", 38},
534-
{"maxDecimal", 39},
528+
{"exclusiveMaximum", 34},
529+
{"maximum", 35},
530+
{"maximumCanEqual", 36},
531+
{"exclusiveMinimum", 37},
532+
{"minimum", 38},
533+
{"minimumCanEqual", 39},
534+
{"multipleOf", 40},
535+
{"divisibleBy", 41},
536+
{"maxDecimal", 42},
535537

536538
// Then keywords about strings
537-
{"pattern", 40},
538-
{"format", 41},
539-
{"maxLength", 42},
540-
{"minLength", 43},
541-
{"contentEncoding", 44},
542-
{"contentMediaType", 45},
543-
{"contentSchema", 46},
539+
{"pattern", 43},
540+
{"format", 44},
541+
{"maxLength", 45},
542+
{"minLength", 46},
543+
{"contentEncoding", 47},
544+
{"contentMediaType", 48},
545+
{"contentSchema", 49},
544546

545547
// Then keywords about arrays
546-
{"maxItems", 47},
547-
{"minItems", 48},
548-
{"uniqueItems", 49},
549-
{"maxContains", 50},
550-
{"minContains", 51},
551-
{"contains", 52},
552-
{"prefixItems", 53},
553-
{"items", 54},
554-
{"additionalItems", 55},
555-
{"unevaluatedItems", 56},
548+
{"maxItems", 50},
549+
{"minItems", 51},
550+
{"uniqueItems", 52},
551+
{"maxContains", 53},
552+
{"minContains", 54},
553+
{"contains", 55},
554+
{"prefixItems", 56},
555+
{"items", 57},
556+
{"additionalItems", 58},
557+
{"unevaluatedItems", 59},
556558

557559
// Object
558-
{"required", 57},
559-
{"maxProperties", 58},
560-
{"minProperties", 59},
561-
{"propertyNames", 60},
562-
{"properties", 61},
563-
{"patternProperties", 62},
564-
{"additionalProperties", 63},
565-
{"unevaluatedProperties", 64},
566-
{"dependentRequired", 65},
567-
{"dependencies", 66},
568-
{"dependentSchemas", 67},
560+
{"required", 60},
561+
{"maxProperties", 61},
562+
{"minProperties", 62},
563+
{"propertyNames", 63},
564+
{"properties", 64},
565+
{"patternProperties", 65},
566+
{"additionalProperties", 66},
567+
{"unevaluatedProperties", 67},
568+
{"dependentRequired", 68},
569+
{"dependencies", 69},
570+
{"dependentSchemas", 70},
569571

570572
// Reusable utilities go last
571-
{"$defs", 68},
572-
{"definitions", 69}};
573-
574-
if (rank.contains(left) || rank.contains(right)) {
575-
constexpr auto DEFAULT{std::numeric_limits<Rank::mapped_type>::max()};
576-
const auto left_rank{rank.contains(left) ? rank.at(left) : DEFAULT};
577-
const auto right_rank{rank.contains(right) ? rank.at(right) : DEFAULT};
578-
// If the ranks are equal, then either the keywords are the same or
579-
// none of them are recognized keywords.
580-
assert((left_rank != right_rank) ||
581-
(left == right || left_rank == DEFAULT));
582-
return left_rank < right_rank;
573+
{"$defs", 71},
574+
{"definitions", 72}};
575+
576+
const auto match{rank.find(keyword)};
577+
if (match != rank.cend()) {
578+
return match->second;
579+
} else if (keyword.starts_with("x-")) {
580+
assert(rank.contains("x-"));
581+
return rank["x-"];
583582
} else {
583+
return otherwise;
584+
}
585+
}
586+
587+
auto sourcemeta::core::schema_format_compare(
588+
const sourcemeta::core::JSON::String &left,
589+
const sourcemeta::core::JSON::String &right) -> bool {
590+
constexpr auto DEFAULT{std::numeric_limits<std::uint64_t>::max()};
591+
const auto left_rank{keyword_rank(left, DEFAULT)};
592+
const auto right_rank{keyword_rank(right, DEFAULT)};
593+
if (left_rank == DEFAULT && right_rank == DEFAULT) {
584594
// For unknown keywords, go alphabetically
585595
return left < right;
596+
} else {
597+
return left_rank < right_rank;
586598
}
587599
}
588600

test/jsonschema/jsonschema_format_test.cc

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,25 @@ TEST(JSONSchema_format, example_1) {
2121
})JSON");
2222
}
2323

24+
TEST(JSONSchema_format, example_2) {
25+
const sourcemeta::core::JSON document = sourcemeta::core::parse_json(R"JSON({
26+
"default": 1,
27+
"$ref": "other",
28+
"x-foo": [ "bar", "baz" ],
29+
"$schema": "https://json-schema.org/draft/2020-12/schema"
30+
})JSON");
31+
32+
std::ostringstream stream;
33+
sourcemeta::core::prettify(document, stream,
34+
sourcemeta::core::schema_format_compare);
35+
EXPECT_EQ(stream.str(), R"JSON({
36+
"$schema": "https://json-schema.org/draft/2020-12/schema",
37+
"default": 1,
38+
"x-foo": [ "bar", "baz" ],
39+
"$ref": "other"
40+
})JSON");
41+
}
42+
2443
TEST(JSONSchema_format, compare_known_vs_unknown) {
2544
EXPECT_TRUE(sourcemeta::core::schema_format_compare("$id", "foo"));
2645
EXPECT_FALSE(sourcemeta::core::schema_format_compare("foo", "$id"));

0 commit comments

Comments
 (0)