Skip to content

Commit 777db90

Browse files
committed
Support $dynamicAnchor and $dynamicRef
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 33e1790 commit 777db90

File tree

19 files changed

+393
-28
lines changed

19 files changed

+393
-28
lines changed

README.markdown

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ to use a JSON Schema validator at runtime to enforce remaining constraints.
2323
| Core (2020-12) | `$ref` | Yes |
2424
| Core (2020-12) | `$defs` | Yes |
2525
| Core (2020-12) | `$anchor` | Yes |
26-
| Core (2020-12) | `$dynamicAnchor` | Pending |
27-
| Core (2020-12) | `$dynamicRef` | Pending |
26+
| Core (2020-12) | `$dynamicAnchor` | Yes |
27+
| Core (2020-12) | `$dynamicRef` | Yes |
2828
| Core (2020-12) | `$vocabulary` | Ignored |
2929
| Core (2020-12) | `$comment` | Ignored |
3030
| Applicator (2020-12) | `properties` | Yes |

src/ir/ir_default_compiler.h

Lines changed: 99 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
#include <sourcemeta/core/jsonschema.h>
77
#include <sourcemeta/core/regex.h>
8+
#include <sourcemeta/core/uri.h>
89

910
#include <cassert> // assert
1011
#include <string_view> // std::string_view
@@ -55,9 +56,9 @@ auto handle_string(const sourcemeta::core::JSON &schema,
5556
const sourcemeta::core::SchemaResolver &,
5657
const sourcemeta::core::JSON &subschema) -> IRScalar {
5758
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
58-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary",
59-
"type", "minLength", "maxLength", "pattern",
60-
"format"});
59+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
60+
"$defs", "$vocabulary", "type", "minLength",
61+
"maxLength", "pattern", "format"});
6162
return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer),
6263
.symbol = symbol(frame, location)},
6364
IRScalarType::String};
@@ -71,8 +72,8 @@ auto handle_object(const sourcemeta::core::JSON &schema,
7172
const sourcemeta::core::JSON &subschema) -> IRObject {
7273
ONLY_WHITELIST_KEYWORDS(
7374
schema, subschema, location.pointer,
74-
{"$defs", "$schema", "$id", "$anchor", "$vocabulary", "type",
75-
"properties", "required",
75+
{"$defs", "$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary",
76+
"type", "properties", "required",
7677
// Note that most programming languages CANNOT represent the idea
7778
// of additional properties, mainly if they differ from the types of the
7879
// other properties. Therefore, we whitelist this, but we consider it to
@@ -175,9 +176,10 @@ auto handle_integer(const sourcemeta::core::JSON &schema,
175176
const sourcemeta::core::SchemaResolver &,
176177
const sourcemeta::core::JSON &subschema) -> IRScalar {
177178
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
178-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary",
179-
"type", "minimum", "maximum", "exclusiveMinimum",
180-
"exclusiveMaximum", "multipleOf"});
179+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
180+
"$defs", "$vocabulary", "type", "minimum", "maximum",
181+
"exclusiveMinimum", "exclusiveMaximum",
182+
"multipleOf"});
181183
return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer),
182184
.symbol = symbol(frame, location)},
183185
IRScalarType::Integer};
@@ -190,9 +192,10 @@ auto handle_number(const sourcemeta::core::JSON &schema,
190192
const sourcemeta::core::SchemaResolver &,
191193
const sourcemeta::core::JSON &subschema) -> IRScalar {
192194
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
193-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary",
194-
"type", "minimum", "maximum", "exclusiveMinimum",
195-
"exclusiveMaximum", "multipleOf"});
195+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
196+
"$defs", "$vocabulary", "type", "minimum", "maximum",
197+
"exclusiveMinimum", "exclusiveMaximum",
198+
"multipleOf"});
196199
return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer),
197200
.symbol = symbol(frame, location)},
198201
IRScalarType::Number};
@@ -205,9 +208,9 @@ auto handle_array(const sourcemeta::core::JSON &schema,
205208
const sourcemeta::core::SchemaResolver &,
206209
const sourcemeta::core::JSON &subschema) -> IREntity {
207210
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
208-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary",
209-
"type", "items", "minItems", "maxItems",
210-
"uniqueItems", "contains", "minContains",
211+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
212+
"$defs", "$vocabulary", "type", "items", "minItems",
213+
"maxItems", "uniqueItems", "contains", "minContains",
211214
"maxContains", "additionalItems", "prefixItems"});
212215

213216
using Vocabularies = sourcemeta::core::Vocabularies;
@@ -321,9 +324,9 @@ auto handle_enum(const sourcemeta::core::JSON &schema,
321324
const sourcemeta::core::Vocabularies &,
322325
const sourcemeta::core::SchemaResolver &,
323326
const sourcemeta::core::JSON &subschema) -> IREntity {
324-
ONLY_WHITELIST_KEYWORDS(
325-
schema, subschema, location.pointer,
326-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary", "enum"});
327+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
328+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
329+
"$defs", "$vocabulary", "enum"});
327330
const auto &enum_json{subschema.at("enum")};
328331

329332
// Boolean and null special cases
@@ -357,9 +360,9 @@ auto handle_anyof(const sourcemeta::core::JSON &schema,
357360
const sourcemeta::core::Vocabularies &,
358361
const sourcemeta::core::SchemaResolver &,
359362
const sourcemeta::core::JSON &subschema) -> IREntity {
360-
ONLY_WHITELIST_KEYWORDS(
361-
schema, subschema, location.pointer,
362-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary", "anyOf"});
363+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
364+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
365+
"$defs", "$vocabulary", "anyOf"});
363366

364367
const auto &any_of{subschema.at("anyOf")};
365368
assert(any_of.is_array());
@@ -391,9 +394,9 @@ auto handle_oneof(const sourcemeta::core::JSON &schema,
391394
const sourcemeta::core::Vocabularies &,
392395
const sourcemeta::core::SchemaResolver &,
393396
const sourcemeta::core::JSON &subschema) -> IREntity {
394-
ONLY_WHITELIST_KEYWORDS(
395-
schema, subschema, location.pointer,
396-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary", "oneOf"});
397+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
398+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
399+
"$defs", "$vocabulary", "oneOf"});
397400

398401
const auto &one_of{subschema.at("oneOf")};
399402
assert(one_of.is_array());
@@ -425,9 +428,9 @@ auto handle_ref(const sourcemeta::core::JSON &schema,
425428
const sourcemeta::core::Vocabularies &,
426429
const sourcemeta::core::SchemaResolver &,
427430
const sourcemeta::core::JSON &subschema) -> IREntity {
428-
ONLY_WHITELIST_KEYWORDS(
429-
schema, subschema, location.pointer,
430-
{"$schema", "$id", "$anchor", "$defs", "$vocabulary", "$ref"});
431+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
432+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
433+
"$defs", "$vocabulary", "$ref"});
431434

432435
auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)};
433436
ref_pointer.push_back("$ref");
@@ -454,6 +457,73 @@ auto handle_ref(const sourcemeta::core::JSON &schema,
454457
.symbol = symbol(frame, target_location)}};
455458
}
456459

460+
auto handle_dynamic_ref(const sourcemeta::core::JSON &schema,
461+
const sourcemeta::core::SchemaFrame &frame,
462+
const sourcemeta::core::SchemaFrame::Location &location,
463+
const sourcemeta::core::Vocabularies &,
464+
const sourcemeta::core::SchemaResolver &,
465+
const sourcemeta::core::JSON &subschema) -> IREntity {
466+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
467+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
468+
"$defs", "$vocabulary", "$dynamicRef"});
469+
470+
auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)};
471+
ref_pointer.push_back("$dynamicRef");
472+
const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)};
473+
474+
const auto &references{frame.references()};
475+
476+
// The frame converts single-target dynamic references to static references
477+
const auto static_reference{references.find(
478+
{sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})};
479+
if (static_reference != references.cend()) {
480+
const auto &destination{static_reference->second.destination};
481+
const auto target{frame.traverse(destination)};
482+
if (!target.has_value()) {
483+
throw UnexpectedSchemaError(schema, location.pointer,
484+
"Could not resolve reference destination");
485+
}
486+
487+
const auto &target_location{target.value().get()};
488+
489+
return IRReference{
490+
{.pointer = sourcemeta::core::to_pointer(location.pointer),
491+
.symbol = symbol(frame, location)},
492+
{.pointer = sourcemeta::core::to_pointer(target_location.pointer),
493+
.symbol = symbol(frame, target_location)}};
494+
}
495+
496+
// Multi-target dynamic reference: find all dynamic anchors with the matching
497+
// fragment and emit a union of all possible targets
498+
const auto dynamic_reference{references.find(
499+
{sourcemeta::core::SchemaReferenceType::Dynamic, ref_weak_pointer})};
500+
assert(dynamic_reference != references.cend());
501+
assert(dynamic_reference->second.fragment.has_value());
502+
const auto &fragment{dynamic_reference->second.fragment.value()};
503+
504+
std::vector<IRType> branches;
505+
for (const auto &[key, entry] : frame.locations()) {
506+
if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic ||
507+
entry.type != sourcemeta::core::SchemaFrame::LocationType::Anchor) {
508+
continue;
509+
}
510+
511+
const sourcemeta::core::URI anchor_uri{key.second};
512+
const auto anchor_fragment{anchor_uri.fragment()};
513+
if (!anchor_fragment.has_value() || anchor_fragment.value() != fragment) {
514+
continue;
515+
}
516+
517+
branches.push_back({.pointer = sourcemeta::core::to_pointer(entry.pointer),
518+
.symbol = symbol(frame, entry)});
519+
}
520+
521+
assert(!branches.empty());
522+
return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer),
523+
.symbol = symbol(frame, location)},
524+
std::move(branches)};
525+
}
526+
457527
auto default_compiler(const sourcemeta::core::JSON &schema,
458528
const sourcemeta::core::SchemaFrame &frame,
459529
const sourcemeta::core::SchemaFrame::Location &location,
@@ -536,6 +606,9 @@ auto default_compiler(const sourcemeta::core::JSON &schema,
536606
} else if (subschema.defines("oneOf")) {
537607
return handle_oneof(schema, frame, location, vocabularies, resolver,
538608
subschema);
609+
} else if (subschema.defines("$dynamicRef")) {
610+
return handle_dynamic_ref(schema, frame, location, vocabularies, resolver,
611+
subschema);
539612
} else if (subschema.defines("$ref")) {
540613
return handle_ref(schema, frame, location, vocabularies, resolver,
541614
subschema);
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export type NodeValue = number;
2+
3+
export type NodeName = string;
4+
5+
export type NodeAdditionalProperties = never;
6+
7+
export interface Node {
8+
"name": NodeName;
9+
"value"?: NodeValue;
10+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "Node"
3+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$dynamicAnchor": "node",
4+
"type": "object",
5+
"required": [ "name" ],
6+
"properties": {
7+
"name": { "type": "string" },
8+
"value": { "type": "integer" }
9+
},
10+
"additionalProperties": false
11+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Node } from "./expected";
2+
3+
const valid: Node = { name: "hello", value: 42 };
4+
5+
const nameOnly: Node = { name: "hello" };
6+
7+
// @ts-expect-error
8+
const missingName: Node = { value: 42 };
9+
10+
// @ts-expect-error
11+
const wrongNameType: Node = { name: 42 };
12+
13+
// additionalProperties: false
14+
const extra: Node = {
15+
name: "hello",
16+
// @ts-expect-error
17+
other: "nope"
18+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export type StringListStringItem = string;
2+
3+
export type StringListGenericListItems =
4+
StringListGenericListDefaultItem |
5+
StringListStringItem;
6+
7+
export type StringListGenericListDefaultItem_5 = number;
8+
9+
export type StringListGenericListDefaultItem_4 = string;
10+
11+
export type StringListGenericListDefaultItem_3Items = unknown;
12+
13+
export type StringListGenericListDefaultItem_3 = StringListGenericListDefaultItem_3Items[];
14+
15+
export type StringListGenericListDefaultItem_2 = Record<string, unknown>;
16+
17+
export type StringListGenericListDefaultItem_1 = boolean;
18+
19+
export type StringListGenericListDefaultItem_0 = null;
20+
21+
export type StringListGenericListDefaultItem =
22+
StringListGenericListDefaultItem_0 |
23+
StringListGenericListDefaultItem_1 |
24+
StringListGenericListDefaultItem_2 |
25+
StringListGenericListDefaultItem_3 |
26+
StringListGenericListDefaultItem_4 |
27+
StringListGenericListDefaultItem_5;
28+
29+
export type StringListGenericList = StringListGenericListItems[];
30+
31+
export type StringList = StringListGenericList;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "StringList"
3+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"$id": "https://example.com/string-list",
4+
"$ref": "https://example.com/generic-list",
5+
"$defs": {
6+
"StringItem": {
7+
"$dynamicAnchor": "list-item",
8+
"type": "string"
9+
},
10+
"GenericList": {
11+
"$schema": "https://json-schema.org/draft/2020-12/schema",
12+
"$id": "https://example.com/generic-list",
13+
"type": "array",
14+
"items": { "$dynamicRef": "#list-item" },
15+
"$defs": {
16+
"DefaultItem": {
17+
"$dynamicAnchor": "list-item"
18+
}
19+
}
20+
}
21+
}
22+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { StringList } from "./expected";
2+
3+
// The generic list has an unconstrained default anchor (accepts anything)
4+
// plus a string anchor. The union includes all types, so anything goes.
5+
const strings: StringList = [ "hello", "world" ];
6+
7+
const numbers: StringList = [ 1, 2, 3 ];
8+
9+
const mixed: StringList = [ "hello", 42, true, null ];
10+
11+
const empty: StringList = [];
12+
13+
// @ts-expect-error
14+
const notArray: StringList = "hello";

0 commit comments

Comments
 (0)