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
4 changes: 2 additions & 2 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
126 changes: 100 additions & 26 deletions src/ir/ir_default_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <sourcemeta/core/jsonschema.h>
#include <sourcemeta/core/regex.h>
#include <sourcemeta/core/uri.h>

#include <cassert> // assert
#include <string_view> // std::string_view
Expand Down Expand Up @@ -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};
Expand All @@ -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
Expand Down Expand Up @@ -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};
Expand All @@ -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};
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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");
Expand All @@ -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<IRType> 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,
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type NodeValue = number;

export type NodeName = string;

export type NodeAdditionalProperties = never;

export interface Node {
"name": NodeName;
"value"?: NodeValue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "Node"
}
11 changes: 11 additions & 0 deletions test/e2e/typescript/2020-12/dynamic_anchor_passthrough/schema.json
Original file line number Diff line number Diff line change
@@ -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
}
18 changes: 18 additions & 0 deletions test/e2e/typescript/2020-12/dynamic_anchor_passthrough/test.ts
Original file line number Diff line number Diff line change
@@ -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"
};
31 changes: 31 additions & 0 deletions test/e2e/typescript/2020-12/dynamic_ref_generic_list/expected.d.ts
Original file line number Diff line number Diff line change
@@ -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<string, unknown>;

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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "StringList"
}
22 changes: 22 additions & 0 deletions test/e2e/typescript/2020-12/dynamic_ref_generic_list/schema.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
23 changes: 23 additions & 0 deletions test/e2e/typescript/2020-12/dynamic_ref_generic_list/test.ts
Original file line number Diff line number Diff line change
@@ -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 ];
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "Root"
}
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
Loading
Loading