Skip to content

Commit 700dc70

Browse files
authored
Support a best approximation of if/then/else (#26)
Signed-off-by: Juan Cruz Viotti <jv@jviotti.com>
1 parent 6c8870e commit 700dc70

16 files changed

Lines changed: 360 additions & 43 deletions

File tree

README.markdown

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,9 @@ to use a JSON Schema validator at runtime to enforce remaining constraints.
3939
| Applicator (2020-12) | `allOf` | Yes |
4040
| Applicator (2020-12) | `oneOf` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
4141
| Applicator (2020-12) | `not` | **CANNOT SUPPORT** |
42-
| Applicator (2020-12) | `if` | Pending |
43-
| Applicator (2020-12) | `then` | Pending |
44-
| Applicator (2020-12) | `else` | Pending |
42+
| Applicator (2020-12) | `if` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
43+
| Applicator (2020-12) | `then` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
44+
| Applicator (2020-12) | `else` | **PARTIAL GIVEN LANGUAGE LIMITATIONS** |
4545
| Validation (2020-12) | `type` | Yes |
4646
| Validation (2020-12) | `enum` | Yes |
4747
| Validation (2020-12) | `required` | Yes |

src/generator/include/sourcemeta/codegen/generator_typescript.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class SOURCEMETA_CODEGEN_GENERATOR_EXPORT TypeScript {
3030
auto operator()(const IRTuple &entry) -> void;
3131
auto operator()(const IRUnion &entry) -> void;
3232
auto operator()(const IRIntersection &entry) -> void;
33+
auto operator()(const IRConditional &entry) -> void;
3334

3435
private:
3536
// Exporting symbols that depends on the standard C++ library is considered

src/generator/typescript.cc

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,4 +303,26 @@ auto TypeScript::operator()(const IRIntersection &entry) -> void {
303303
this->output << ";\n";
304304
}
305305

306+
auto TypeScript::operator()(const IRConditional &entry) -> void {
307+
// As a notable limitation, TypeScript cannot express the negation of an
308+
// if/then/else condition, so the else branch is wider than what JSON
309+
// Schema allows
310+
this->output << "// (if & then) | else approximation: the else branch is "
311+
"wider than what\n";
312+
this->output << "// JSON Schema allows, as TypeScript cannot express type "
313+
"negation\n";
314+
this->output << "export type "
315+
<< mangle(this->prefix, entry.pointer, entry.symbol, this->cache)
316+
<< " =\n ("
317+
<< mangle(this->prefix, entry.condition.pointer,
318+
entry.condition.symbol, this->cache)
319+
<< " & "
320+
<< mangle(this->prefix, entry.consequent.pointer,
321+
entry.consequent.symbol, this->cache)
322+
<< ") | "
323+
<< mangle(this->prefix, entry.alternative.pointer,
324+
entry.alternative.symbol, this->cache)
325+
<< ";\n";
326+
}
327+
306328
} // namespace sourcemeta::codegen

src/ir/include/sourcemeta/codegen/ir.h

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,15 +97,22 @@ struct IRImpossible : IRType {};
9797
/// @ingroup ir
9898
struct IRAny : IRType {};
9999

100+
/// @ingroup ir
101+
struct IRConditional : IRType {
102+
IRType condition;
103+
IRType consequent;
104+
IRType alternative;
105+
};
106+
100107
/// @ingroup ir
101108
struct IRReference : IRType {
102109
IRType target;
103110
};
104111

105112
/// @ingroup ir
106-
using IREntity =
107-
std::variant<IRObject, IRScalar, IREnumeration, IRUnion, IRIntersection,
108-
IRArray, IRTuple, IRImpossible, IRAny, IRReference>;
113+
using IREntity = std::variant<IRObject, IRScalar, IREnumeration, IRUnion,
114+
IRIntersection, IRConditional, IRArray, IRTuple,
115+
IRImpossible, IRAny, IRReference>;
109116

110117
/// @ingroup ir
111118
using IRResult = std::vector<IREntity>;

src/ir/ir_default_compiler.h

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,53 @@ auto handle_allof(const sourcemeta::core::JSON &schema,
612612
std::move(branches)};
613613
}
614614

615+
auto handle_if_then_else(
616+
const sourcemeta::core::JSON &schema,
617+
const sourcemeta::core::SchemaFrame &frame,
618+
const sourcemeta::core::SchemaFrame::Location &location,
619+
const sourcemeta::core::Vocabularies &,
620+
const sourcemeta::core::SchemaResolver &,
621+
const sourcemeta::core::JSON &subschema) -> IREntity {
622+
ONLY_WHITELIST_KEYWORDS(schema, subschema, location.pointer,
623+
{"$schema", "$id", "$anchor", "$dynamicAnchor",
624+
"$defs", "$vocabulary", "if", "then", "else",
625+
"title", "description", "default", "deprecated",
626+
"readOnly", "writeOnly", "examples",
627+
"unevaluatedProperties", "unevaluatedItems"});
628+
629+
assert(subschema.defines("if"));
630+
assert(subschema.defines("then"));
631+
assert(subschema.defines("else"));
632+
633+
auto if_pointer{sourcemeta::core::to_pointer(location.pointer)};
634+
if_pointer.push_back("if");
635+
const auto if_location{
636+
frame.traverse(sourcemeta::core::to_weak_pointer(if_pointer))};
637+
assert(if_location.has_value());
638+
639+
auto then_pointer{sourcemeta::core::to_pointer(location.pointer)};
640+
then_pointer.push_back("then");
641+
const auto then_location{
642+
frame.traverse(sourcemeta::core::to_weak_pointer(then_pointer))};
643+
assert(then_location.has_value());
644+
645+
auto else_pointer{sourcemeta::core::to_pointer(location.pointer)};
646+
else_pointer.push_back("else");
647+
const auto else_location{
648+
frame.traverse(sourcemeta::core::to_weak_pointer(else_pointer))};
649+
assert(else_location.has_value());
650+
651+
return IRConditional{
652+
{.pointer = sourcemeta::core::to_pointer(location.pointer),
653+
.symbol = symbol(frame, location)},
654+
{.pointer = std::move(if_pointer),
655+
.symbol = symbol(frame, if_location.value().get())},
656+
{.pointer = std::move(then_pointer),
657+
.symbol = symbol(frame, then_location.value().get())},
658+
{.pointer = std::move(else_pointer),
659+
.symbol = symbol(frame, else_location.value().get())}};
660+
}
661+
615662
auto default_compiler(const sourcemeta::core::JSON &schema,
616663
const sourcemeta::core::SchemaFrame &frame,
617664
const sourcemeta::core::SchemaFrame::Location &location,
@@ -704,8 +751,8 @@ auto default_compiler(const sourcemeta::core::JSON &schema,
704751
return handle_ref(schema, frame, location, vocabularies, resolver,
705752
subschema);
706753
} else if (subschema.defines("if")) {
707-
throw UnsupportedKeywordError(schema, location.pointer, "if",
708-
"Unsupported keyword in subschema");
754+
return handle_if_then_else(schema, frame, location, vocabularies, resolver,
755+
subschema);
709756
} else if (subschema.defines("not")) {
710757
throw UnsupportedKeywordError(schema, location.pointer, "not",
711758
"Unsupported keyword in subschema");
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
export type ShapeThenRadius = number;
2+
3+
export interface ShapeThen {
4+
"radius": ShapeThenRadius;
5+
[key: string]: unknown | undefined;
6+
}
7+
8+
export type ShapeIfKind = "circle";
9+
10+
export interface ShapeIf {
11+
"kind": ShapeIfKind;
12+
[key: string]: unknown | undefined;
13+
}
14+
15+
export type ShapeElseSides = number;
16+
17+
export interface ShapeElse {
18+
"sides": ShapeElseSides;
19+
[key: string]: unknown | undefined;
20+
}
21+
22+
// (if & then) | else approximation: the else branch is wider than what
23+
// JSON Schema allows, as TypeScript cannot express type negation
24+
export type Shape =
25+
(ShapeIf & ShapeThen) | ShapeElse;
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"defaultPrefix": "Shape"
3+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"if": {
4+
"type": "object",
5+
"properties": { "kind": { "const": "circle" } },
6+
"required": [ "kind" ]
7+
},
8+
"then": {
9+
"type": "object",
10+
"properties": { "radius": { "type": "number" } },
11+
"required": [ "radius" ]
12+
},
13+
"else": {
14+
"type": "object",
15+
"properties": { "sides": { "type": "integer" } },
16+
"required": [ "sides" ]
17+
}
18+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Shape } from "./expected";
2+
3+
// Valid: satisfies the if condition (kind="circle") and then branch (radius)
4+
const circle: Shape = {
5+
kind: "circle",
6+
radius: 5
7+
};
8+
9+
// Valid: does not satisfy the if condition, satisfies else branch (sides)
10+
const polygon: Shape = {
11+
sides: 6
12+
};
13+
14+
// Invalid: satisfies if (kind="circle") but missing then's required radius
15+
// @ts-expect-error
16+
const circleWithoutRadius: Shape = {
17+
kind: "circle"
18+
};
19+
20+
// Invalid: does not satisfy if, and missing else's required sides
21+
// @ts-expect-error
22+
const emptyObject: Shape = {};
23+
24+
// NOTE: This passes TypeScript but would fail JSON Schema validation.
25+
// The if condition matches (kind is "circle"), so the then branch should
26+
// apply (requiring radius). But our (If & Then) | Else approximation
27+
// allows the else branch to also match when if holds.
28+
const circleMatchingElse: Shape = {
29+
kind: "circle",
30+
sides: 4
31+
};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
export type PatternString_1 = string;
2+
3+
export type PatternString_0Then = string;
4+
5+
export type PatternString_0If = string;
6+
7+
export type PatternString_0Else = string;
8+
9+
// (if & then) | else approximation: the else branch is wider than what
10+
// JSON Schema allows, as TypeScript cannot express type negation
11+
export type PatternString_0 =
12+
(PatternString_0If & PatternString_0Then) | PatternString_0Else;
13+
14+
export type PatternString =
15+
PatternString_0 &
16+
PatternString_1;

0 commit comments

Comments
 (0)