diff --git a/README.markdown b/README.markdown index 46949e8..f1c3f53 100644 --- a/README.markdown +++ b/README.markdown @@ -23,8 +23,8 @@ to use a JSON Schema validator at runtime to enforce remaining constraints. | Core (2020-12) | `$ref` | Yes | | Core (2020-12) | `$defs` | Yes | | Core (2020-12) | `$anchor` | Yes | -| Core (2020-12) | `$dynamicAnchor` | Pending | -| Core (2020-12) | `$dynamicRef` | Pending | +| Core (2020-12) | `$dynamicAnchor` | Yes | +| Core (2020-12) | `$dynamicRef` | Yes | | Core (2020-12) | `$vocabulary` | Ignored | | Core (2020-12) | `$comment` | Ignored | | Applicator (2020-12) | `properties` | Yes | diff --git a/src/ir/ir_default_compiler.h b/src/ir/ir_default_compiler.h index 721e22f..8d34866 100644 --- a/src/ir/ir_default_compiler.h +++ b/src/ir/ir_default_compiler.h @@ -5,6 +5,7 @@ #include #include +#include #include // assert #include // std::string_view @@ -55,9 +56,9 @@ auto handle_string(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", - "type", "minLength", "maxLength", "pattern", - "format"}); + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minLength", + "maxLength", "pattern", "format"}); return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, IRScalarType::String}; @@ -71,8 +72,8 @@ auto handle_object(const sourcemeta::core::JSON &schema, const sourcemeta::core::JSON &subschema) -> IRObject { ONLY_WHITELIST_KEYWORDS( schema, subschema, location.pointer, - {"$defs", "$schema", "$id", "$anchor", "$vocabulary", "type", - "properties", "required", + {"$defs", "$schema", "$id", "$anchor", "$dynamicAnchor", "$vocabulary", + "type", "properties", "required", // Note that most programming languages CANNOT represent the idea // of additional properties, mainly if they differ from the types of the // other properties. Therefore, we whitelist this, but we consider it to @@ -175,9 +176,10 @@ auto handle_integer(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", - "type", "minimum", "maximum", "exclusiveMinimum", - "exclusiveMaximum", "multipleOf"}); + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", + "multipleOf"}); return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, IRScalarType::Integer}; @@ -190,9 +192,10 @@ auto handle_number(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IRScalar { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", - "type", "minimum", "maximum", "exclusiveMinimum", - "exclusiveMaximum", "multipleOf"}); + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "minimum", "maximum", + "exclusiveMinimum", "exclusiveMaximum", + "multipleOf"}); return IRScalar{{.pointer = sourcemeta::core::to_pointer(location.pointer), .symbol = symbol(frame, location)}, IRScalarType::Number}; @@ -205,9 +208,9 @@ auto handle_array(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IREntity { ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", - "type", "items", "minItems", "maxItems", - "uniqueItems", "contains", "minContains", + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "type", "items", "minItems", + "maxItems", "uniqueItems", "contains", "minContains", "maxContains", "additionalItems", "prefixItems"}); using Vocabularies = sourcemeta::core::Vocabularies; @@ -321,9 +324,9 @@ auto handle_enum(const sourcemeta::core::JSON &schema, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IREntity { - ONLY_WHITELIST_KEYWORDS( - schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", "enum"}); + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "enum"}); const auto &enum_json{subschema.at("enum")}; // Boolean and null special cases @@ -357,9 +360,9 @@ auto handle_anyof(const sourcemeta::core::JSON &schema, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IREntity { - ONLY_WHITELIST_KEYWORDS( - schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", "anyOf"}); + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "anyOf"}); const auto &any_of{subschema.at("anyOf")}; assert(any_of.is_array()); @@ -391,9 +394,9 @@ auto handle_oneof(const sourcemeta::core::JSON &schema, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IREntity { - ONLY_WHITELIST_KEYWORDS( - schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", "oneOf"}); + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "oneOf"}); const auto &one_of{subschema.at("oneOf")}; assert(one_of.is_array()); @@ -425,9 +428,9 @@ auto handle_ref(const sourcemeta::core::JSON &schema, const sourcemeta::core::Vocabularies &, const sourcemeta::core::SchemaResolver &, const sourcemeta::core::JSON &subschema) -> IREntity { - ONLY_WHITELIST_KEYWORDS( - schema, subschema, location.pointer, - {"$schema", "$id", "$anchor", "$defs", "$vocabulary", "$ref"}); + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$ref"}); auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; ref_pointer.push_back("$ref"); @@ -454,6 +457,74 @@ auto handle_ref(const sourcemeta::core::JSON &schema, .symbol = symbol(frame, target_location)}}; } +auto handle_dynamic_ref(const sourcemeta::core::JSON &schema, + const sourcemeta::core::SchemaFrame &frame, + const sourcemeta::core::SchemaFrame::Location &location, + const sourcemeta::core::Vocabularies &, + const sourcemeta::core::SchemaResolver &, + const sourcemeta::core::JSON &subschema) -> IREntity { + ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer, + {"$schema", "$id", "$anchor", "$dynamicAnchor", + "$defs", "$vocabulary", "$dynamicRef"}); + + auto ref_pointer{sourcemeta::core::to_pointer(location.pointer)}; + ref_pointer.push_back("$dynamicRef"); + const auto ref_weak_pointer{sourcemeta::core::to_weak_pointer(ref_pointer)}; + + const auto &references{frame.references()}; + + // Note: The frame internally converts single-target dynamic references to + // static reference + const auto static_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Static, ref_weak_pointer})}; + if (static_reference != references.cend()) { + const auto &destination{static_reference->second.destination}; + const auto target{frame.traverse(destination)}; + if (!target.has_value()) { + throw UnexpectedSchemaError(schema, location.pointer, + "Could not resolve reference destination"); + } + + const auto &target_location{target.value().get()}; + + return IRReference{ + {.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + {.pointer = sourcemeta::core::to_pointer(target_location.pointer), + .symbol = symbol(frame, target_location)}}; + } + + // Multi-target dynamic reference: find all dynamic anchors with the matching + // fragment and emit a union of all possible targets + const auto dynamic_reference{references.find( + {sourcemeta::core::SchemaReferenceType::Dynamic, ref_weak_pointer})}; + assert(dynamic_reference != references.cend()); + assert(dynamic_reference->second.fragment.has_value()); + const auto &fragment{dynamic_reference->second.fragment.value()}; + + std::vector branches; + for (const auto &[key, entry] : frame.locations()) { + if (key.first != sourcemeta::core::SchemaReferenceType::Dynamic || + entry.type != sourcemeta::core::SchemaFrame::LocationType::Anchor) { + continue; + } + + const sourcemeta::core::URI anchor_uri{key.second}; + const auto anchor_fragment{anchor_uri.fragment()}; + if (!anchor_fragment.has_value() || anchor_fragment.value() != fragment) { + continue; + } + + branches.push_back({.pointer = sourcemeta::core::to_pointer(entry.pointer), + .symbol = symbol(frame, entry)}); + } + + assert(!branches.empty()); + return IRUnion{{.pointer = sourcemeta::core::to_pointer(location.pointer), + .symbol = symbol(frame, location)}, + std::move(branches)}; +} + auto default_compiler(const sourcemeta::core::JSON &schema, const sourcemeta::core::SchemaFrame &frame, const sourcemeta::core::SchemaFrame::Location &location, @@ -536,6 +607,9 @@ auto default_compiler(const sourcemeta::core::JSON &schema, } else if (subschema.defines("oneOf")) { return handle_oneof(schema, frame, location, vocabularies, resolver, subschema); + } else if (subschema.defines("$dynamicRef")) { + return handle_dynamic_ref(schema, frame, location, vocabularies, resolver, + subschema); } else if (subschema.defines("$ref")) { return handle_ref(schema, frame, location, vocabularies, resolver, subschema); diff --git a/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts new file mode 100644 index 0000000..7c95a10 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/expected.d.ts @@ -0,0 +1,10 @@ +export type NodeValue = number; + +export type NodeName = string; + +export type NodeAdditionalProperties = never; + +export interface Node { + "name": NodeName; + "value"?: NodeValue; +} diff --git a/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json new file mode 100644 index 0000000..75db2bf --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Node" +} diff --git a/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json new file mode 100644 index 0000000..92ac945 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "node", + "type": "object", + "required": [ "name" ], + "properties": { + "name": { "type": "string" }, + "value": { "type": "integer" } + }, + "additionalProperties": false +} diff --git a/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts new file mode 100644 index 0000000..0195268 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts @@ -0,0 +1,18 @@ +import { Node } from "./expected"; + +const valid: Node = { name: "hello", value: 42 }; + +const nameOnly: Node = { name: "hello" }; + +// @ts-expect-error +const missingName: Node = { value: 42 }; + +// @ts-expect-error +const wrongNameType: Node = { name: 42 }; + +// additionalProperties: false +const extra: Node = { + name: "hello", + // @ts-expect-error + other: "nope" +}; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts new file mode 100644 index 0000000..c86f551 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts @@ -0,0 +1,31 @@ +export type StringListStringItem = string; + +export type StringListGenericListItems = + StringListGenericListDefaultItem | + StringListStringItem; + +export type StringListGenericListDefaultItem_5 = number; + +export type StringListGenericListDefaultItem_4 = string; + +export type StringListGenericListDefaultItem_3Items = unknown; + +export type StringListGenericListDefaultItem_3 = StringListGenericListDefaultItem_3Items[]; + +export type StringListGenericListDefaultItem_2 = Record; + +export type StringListGenericListDefaultItem_1 = boolean; + +export type StringListGenericListDefaultItem_0 = null; + +export type StringListGenericListDefaultItem = + StringListGenericListDefaultItem_0 | + StringListGenericListDefaultItem_1 | + StringListGenericListDefaultItem_2 | + StringListGenericListDefaultItem_3 | + StringListGenericListDefaultItem_4 | + StringListGenericListDefaultItem_5; + +export type StringListGenericList = StringListGenericListItems[]; + +export type StringList = StringListGenericList; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json new file mode 100644 index 0000000..28068f6 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StringList" +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json new file mode 100644 index 0000000..8cdaba7 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/string-list", + "$ref": "https://example.com/generic-list", + "$defs": { + "StringItem": { + "$dynamicAnchor": "list-item", + "type": "string" + }, + "GenericList": { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/generic-list", + "type": "array", + "items": { "$dynamicRef": "#list-item" }, + "$defs": { + "DefaultItem": { + "$dynamicAnchor": "list-item" + } + } + } + } +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts new file mode 100644 index 0000000..bb66bdf --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts @@ -0,0 +1,23 @@ +import { StringList } from "./expected"; + +// At runtime, the dynamic ref resolves to the string anchor (the parent +// schema overrides the default). But the codegen emits a union of ALL possible +// targets: the unconstrained default (all JSON types) plus the string anchor. +// So the generated type allows any JSON value as items. The generated types +// are always a superset of what JSON Schema allows, never a subset. +const strings: StringList = [ "hello", "world" ]; + +const numbers: StringList = [ 1, 2, 3 ]; + +const mixed: StringList = [ "hello", 42, true, null ]; + +const objects: StringList = [ { key: "value" } ]; + +const empty: StringList = []; + +// @ts-expect-error +const notArray: StringList = "hello"; + +// undefined is not a JSON type and is not in any anchor's type union +// @ts-expect-error +const undefinedItem: StringList = [ undefined ]; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts new file mode 100644 index 0000000..ed91cca --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/expected.d.ts @@ -0,0 +1,11 @@ +export type RootStringItem = string; + +export type RootListItems = + RootListDefaultItem | + RootStringItem; + +export type RootListDefaultItem = number; + +export type RootList = RootListItems[]; + +export type Root = RootList; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json new file mode 100644 index 0000000..facad54 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "Root" +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json new file mode 100644 index 0000000..00396f1 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$ref": "list", + "$defs": { + "StringItem": { + "$dynamicAnchor": "item", + "type": "string" + }, + "List": { + "$id": "list", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "DefaultItem": { + "$dynamicAnchor": "item", + "type": "number" + } + } + } + } +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts new file mode 100644 index 0000000..2d3e070 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_multiple_anchors/test.ts @@ -0,0 +1,29 @@ +import { Root } from "./expected"; + +const strings: Root = [ "hello", "world" ]; + +// JSON Schema would reject this (the dynamic scope resolves to the root's +// string anchor at runtime, so only strings are valid). But the codegen emits +// a union of ALL possible dynamic anchor targets (string | number), because +// the actual target depends on the runtime evaluation path. The generated +// types are always a superset of what JSON Schema allows, never a subset. +const numbers: Root = [ 1, 2, 3 ]; + +const mixed: Root = [ "hello", 42 ]; + +const empty: Root = []; + +// @ts-expect-error +const notArray: Root = "hello"; + +// Boolean is not in the union (string | number) +// @ts-expect-error +const invalidItem: Root = [ true ]; + +// Null is not in the union +// @ts-expect-error +const nullItem: Root = [ null ]; + +// Object is not in the union +// @ts-expect-error +const objectItem: Root = [ { key: "value" } ]; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts new file mode 100644 index 0000000..94ba435 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/expected.d.ts @@ -0,0 +1,5 @@ +export type StringArrayItems = StringArrayItemType; + +export type StringArrayItemType = string; + +export type StringArray = StringArrayItems[]; diff --git a/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json new file mode 100644 index 0000000..368747f --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/options.json @@ -0,0 +1,3 @@ +{ + "defaultPrefix": "StringArray" +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json new file mode 100644 index 0000000..3c7fe92 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/schema.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "ItemType": { + "$dynamicAnchor": "item", + "type": "string" + } + } +} diff --git a/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts new file mode 100644 index 0000000..79a72a3 --- /dev/null +++ b/test/e2e/typescript/2020-12/dynamic_ref_single_anchor/test.ts @@ -0,0 +1,11 @@ +import { StringArray } from "./expected"; + +const valid: StringArray = [ "hello", "world" ]; + +const empty: StringArray = []; + +// @ts-expect-error +const invalidItem: StringArray = [ 42 ]; + +// @ts-expect-error +const notArray: StringArray = "hello"; diff --git a/test/ir/ir_2020_12_test.cc b/test/ir/ir_2020_12_test.cc index dbbfdfc..a7f048c 100644 --- a/test/ir/ir_2020_12_test.cc +++ b/test/ir/ir_2020_12_test.cc @@ -1307,3 +1307,88 @@ TEST(IR_2020_12, object_with_mixed_prefix_and_non_prefix_patterns) { EXPECT_EQ(object.pattern.at(0).prefix.value(), "x-"); EXPECT_FALSE(object.pattern.at(1).prefix.has_value()); } + +TEST(IR_2020_12, dynamic_ref_single_anchor) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "foo": { + "$dynamicAnchor": "item", + "type": "string" + } + } + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + ASSERT_FALSE(result.empty()); + EXPECT_IR_REFERENCE(result, 0, "/items", "/$defs/foo"); + EXPECT_IR_SCALAR(result, 1, String, "/$defs/foo"); +} + +TEST(IR_2020_12, dynamic_ref_multiple_anchors) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.com/root", + "$ref": "list", + "$defs": { + "stringItem": { + "$dynamicAnchor": "item", + "type": "string" + }, + "list": { + "$id": "list", + "type": "array", + "items": { "$dynamicRef": "#item" }, + "$defs": { + "defaultItem": { + "$dynamicAnchor": "item", + "type": "number" + } + } + } + } + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + ASSERT_EQ(result.size(), 5); + EXPECT_IR_SCALAR(result, 0, String, "/$defs/stringItem"); + EXPECT_TRUE(std::holds_alternative(result.at(1))); + EXPECT_AS_STRING(std::get(result.at(1)).pointer, + "/$defs/list/items"); + EXPECT_EQ(std::get(result.at(1)).values.size(), 2); + EXPECT_IR_SCALAR(result, 2, Number, "/$defs/list/$defs/defaultItem"); + EXPECT_IR_ARRAY(result, 3, "/$defs/list", "/$defs/list/items"); + EXPECT_IR_REFERENCE(result, 4, "", "/$defs/list"); +} + +TEST(IR_2020_12, dynamic_anchor_on_typed_schema) { + const sourcemeta::core::JSON schema{sourcemeta::core::parse_json(R"JSON({ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$dynamicAnchor": "item", + "type": "string" + })JSON")}; + + const auto result{ + sourcemeta::codegen::compile(schema, sourcemeta::core::schema_walker, + sourcemeta::core::schema_resolver, + sourcemeta::codegen::default_compiler)}; + + using namespace sourcemeta::codegen; + + EXPECT_EQ(result.size(), 1); + EXPECT_IR_SCALAR(result, 0, String, ""); +}