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
6 changes: 3 additions & 3 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ to use a JSON Schema validator at runtime to enforce remaining constraints.
| Applicator (2020-12) | `allOf` | Yes |
| Applicator (2020-12) | `oneOf` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
| Applicator (2020-12) | `not` | **CANNOT SUPPORT** |
| Applicator (2020-12) | `if` | Pending |
| Applicator (2020-12) | `then` | Pending |
| Applicator (2020-12) | `else` | Pending |
| Applicator (2020-12) | `if` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
| Applicator (2020-12) | `then` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
| Applicator (2020-12) | `else` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
| Validation (2020-12) | `type` | Yes |
| Validation (2020-12) | `enum` | Yes |
| Validation (2020-12) | `required` | Yes |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SOURCEMETA_CODEGEN_GENERATOR_EXPORT TypeScript {
auto operator()(const IRTuple &entry) -> void;
auto operator()(const IRUnion &entry) -> void;
auto operator()(const IRIntersection &entry) -> void;
auto operator()(const IRConditional &entry) -> void;

private:
// Exporting symbols that depends on the standard C++ library is considered
Expand Down
22 changes: 22 additions & 0 deletions src/generator/typescript.cc
Original file line number Diff line number Diff line change
Expand Up @@ -303,4 +303,26 @@ auto TypeScript::operator()(const IRIntersection &entry) -> void {
this->output << ";\n";
}

auto TypeScript::operator()(const IRConditional &entry) -> void {
// As a notable limitation, TypeScript cannot express the negation of an
// if/then/else condition, so the else branch is wider than what JSON
// Schema allows
this->output << "// (if & then) | else approximation: the else branch is "
"wider than what\n";
this->output << "// JSON Schema allows, as TypeScript cannot express type "
"negation\n";
this->output << "export type "
<< mangle(this->prefix, entry.pointer, entry.symbol, this->cache)
<< " =\n ("
<< mangle(this->prefix, entry.condition.pointer,
entry.condition.symbol, this->cache)
<< " & "
<< mangle(this->prefix, entry.consequent.pointer,
entry.consequent.symbol, this->cache)
<< ") | "
<< mangle(this->prefix, entry.alternative.pointer,
entry.alternative.symbol, this->cache)
<< ";\n";
}

} // namespace sourcemeta::codegen
13 changes: 10 additions & 3 deletions src/ir/include/sourcemeta/codegen/ir.h
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,22 @@ struct IRImpossible : IRType {};
/// @ingroup ir
struct IRAny : IRType {};

/// @ingroup ir
struct IRConditional : IRType {
IRType condition;
IRType consequent;
IRType alternative;
};

/// @ingroup ir
struct IRReference : IRType {
IRType target;
};

/// @ingroup ir
using IREntity =
std::variant<IRObject, IRScalar, IREnumeration, IRUnion, IRIntersection,
IRArray, IRTuple, IRImpossible, IRAny, IRReference>;
using IREntity = std::variant<IRObject, IRScalar, IREnumeration, IRUnion,
IRIntersection, IRConditional, IRArray, IRTuple,
IRImpossible, IRAny, IRReference>;

/// @ingroup ir
using IRResult = std::vector<IREntity>;
Expand Down
51 changes: 49 additions & 2 deletions src/ir/ir_default_compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,53 @@ auto handle_allof(const sourcemeta::core::JSON &schema,
std::move(branches)};
}

auto handle_if_then_else(
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", "if", "then", "else",
"title", "description", "default", "deprecated",
"readOnly", "writeOnly", "examples",
"unevaluatedProperties", "unevaluatedItems"});

assert(subschema.defines("if"));
assert(subschema.defines("then"));
assert(subschema.defines("else"));

auto if_pointer{sourcemeta::core::to_pointer(location.pointer)};
if_pointer.push_back("if");
const auto if_location{
frame.traverse(sourcemeta::core::to_weak_pointer(if_pointer))};
assert(if_location.has_value());

auto then_pointer{sourcemeta::core::to_pointer(location.pointer)};
then_pointer.push_back("then");
const auto then_location{
frame.traverse(sourcemeta::core::to_weak_pointer(then_pointer))};
assert(then_location.has_value());

auto else_pointer{sourcemeta::core::to_pointer(location.pointer)};
else_pointer.push_back("else");
const auto else_location{
frame.traverse(sourcemeta::core::to_weak_pointer(else_pointer))};
assert(else_location.has_value());

return IRConditional{
{.pointer = sourcemeta::core::to_pointer(location.pointer),
.symbol = symbol(frame, location)},
{.pointer = std::move(if_pointer),
.symbol = symbol(frame, if_location.value().get())},
{.pointer = std::move(then_pointer),
.symbol = symbol(frame, then_location.value().get())},
{.pointer = std::move(else_pointer),
.symbol = symbol(frame, else_location.value().get())}};
}

auto default_compiler(const sourcemeta::core::JSON &schema,
const sourcemeta::core::SchemaFrame &frame,
const sourcemeta::core::SchemaFrame::Location &location,
Expand Down Expand Up @@ -704,8 +751,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema,
return handle_ref(schema, frame, location, vocabularies, resolver,
subschema);
} else if (subschema.defines("if")) {
throw UnsupportedKeywordError(schema, location.pointer, "if",
"Unsupported keyword in subschema");
return handle_if_then_else(schema, frame, location, vocabularies, resolver,
subschema);
} else if (subschema.defines("not")) {
throw UnsupportedKeywordError(schema, location.pointer, "not",
"Unsupported keyword in subschema");
Expand Down
25 changes: 25 additions & 0 deletions test/e2e/typescript/2020-12/if_then_else_objects/expected.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export type ShapeThenRadius = number;

export interface ShapeThen {
"radius": ShapeThenRadius;
[key: string]: unknown | undefined;
}

export type ShapeIfKind = "circle";

export interface ShapeIf {
"kind": ShapeIfKind;
[key: string]: unknown | undefined;
}

export type ShapeElseSides = number;

export interface ShapeElse {
"sides": ShapeElseSides;
[key: string]: unknown | undefined;
}

// (if & then) | else approximation: the else branch is wider than what
// JSON Schema allows, as TypeScript cannot express type negation
export type Shape =
(ShapeIf & ShapeThen) | ShapeElse;
3 changes: 3 additions & 0 deletions test/e2e/typescript/2020-12/if_then_else_objects/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "Shape"
}
18 changes: 18 additions & 0 deletions test/e2e/typescript/2020-12/if_then_else_objects/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"if": {
"type": "object",
"properties": { "kind": { "const": "circle" } },
"required": [ "kind" ]
},
"then": {
"type": "object",
"properties": { "radius": { "type": "number" } },
"required": [ "radius" ]
},
"else": {
"type": "object",
"properties": { "sides": { "type": "integer" } },
"required": [ "sides" ]
}
}
31 changes: 31 additions & 0 deletions test/e2e/typescript/2020-12/if_then_else_objects/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Shape } from "./expected";

// Valid: satisfies the if condition (kind="circle") and then branch (radius)
const circle: Shape = {
kind: "circle",
radius: 5
};

// Valid: does not satisfy the if condition, satisfies else branch (sides)
const polygon: Shape = {
sides: 6
};

// Invalid: satisfies if (kind="circle") but missing then's required radius
// @ts-expect-error
const circleWithoutRadius: Shape = {
kind: "circle"
};

// Invalid: does not satisfy if, and missing else's required sides
// @ts-expect-error
const emptyObject: Shape = {};

// NOTE: This passes TypeScript but would fail JSON Schema validation.
// The if condition matches (kind is "circle"), so the then branch should
// apply (requiring radius). But our (If & Then) | Else approximation
// allows the else branch to also match when if holds.
const circleMatchingElse: Shape = {
kind: "circle",
sides: 4
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export type PatternString_1 = string;

export type PatternString_0Then = string;

export type PatternString_0If = string;

export type PatternString_0Else = string;

// (if & then) | else approximation: the else branch is wider than what
// JSON Schema allows, as TypeScript cannot express type negation
export type PatternString_0 =
(PatternString_0If & PatternString_0Then) | PatternString_0Else;

export type PatternString =
PatternString_0 &
PatternString_1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"defaultPrefix": "PatternString"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "string",
"if": { "type": "string", "maxLength": 10 },
"then": { "type": "string", "pattern": "^short" },
"else": { "type": "string", "pattern": "^long" }
}
14 changes: 14 additions & 0 deletions test/e2e/typescript/2020-12/if_then_else_validation_only/test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { PatternString } from "./expected";

// Valid: any string satisfies the type since all branches resolve to string
const short_value: PatternString = "short123";
const long_value: PatternString = "longstringvalue";
const empty_value: PatternString = "";

// Invalid: not a string
// @ts-expect-error
const number_value: PatternString = 42;

// Invalid: not a string
// @ts-expect-error
const boolean_value: PatternString = true;
Loading
Loading