-
Notifications
You must be signed in to change notification settings - Fork 188
/
UnionTypeFormatter.ts
156 lines (138 loc) · 5.61 KB
/
UnionTypeFormatter.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
import { JSONSchema7 } from "json-schema";
import { Definition } from "../Schema/Definition";
import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { NeverType } from "../Type/NeverType";
import { UnionType } from "../Type/UnionType";
import { TypeFormatter } from "../TypeFormatter";
import { derefType } from "../Utils/derefType";
import { getTypeByKey } from "../Utils/typeKeys";
import { uniqueArray } from "../Utils/uniqueArray";
type DiscriminatorType = "json-schema" | "open-api";
export class UnionTypeFormatter implements SubTypeFormatter {
public constructor(protected childTypeFormatter: TypeFormatter, private discriminatorType?: DiscriminatorType) {}
public supportsType(type: UnionType): boolean {
return type instanceof UnionType;
}
private getTypeDefinitions(type: UnionType) {
return type
.getTypes()
.filter((item) => !(derefType(item) instanceof NeverType))
.map((item) => this.childTypeFormatter.getDefinition(item));
}
private getJsonSchemaDiscriminatorDefinition(type: UnionType): Definition {
const definitions = this.getTypeDefinitions(type);
const discriminator = type.getDiscriminator();
if (!discriminator) throw new Error("discriminator is undefined");
const kindTypes = type
.getTypes()
.filter((item) => !(derefType(item) instanceof NeverType))
.map((item) => getTypeByKey(item, new LiteralType(discriminator)));
const undefinedIndex = kindTypes.findIndex((item) => item === undefined);
if (undefinedIndex != -1) {
throw new Error(
`Cannot find discriminator keyword "${discriminator}" in type ${JSON.stringify(
type.getTypes()[undefinedIndex]
)}.`
);
}
const kindDefinitions = kindTypes.map((item) => this.childTypeFormatter.getDefinition(item as BaseType));
const allOf = [];
for (let i = 0; i < definitions.length; i++) {
allOf.push({
if: {
properties: { [discriminator]: kindDefinitions[i] },
},
then: definitions[i],
});
}
const kindValues = kindDefinitions
.map((item) => item.const)
.filter((item): item is string | number | boolean | null => item !== undefined);
const duplicates = kindValues.filter((item, index) => kindValues.indexOf(item) !== index);
if (duplicates.length > 0) {
throw new Error(
`Duplicate discriminator values: ${duplicates.join(", ")} in type ${JSON.stringify(type.getName())}.`
);
}
const properties = {
[discriminator]: {
enum: kindValues,
},
};
return { type: "object", properties, required: [discriminator], allOf };
}
private getOpenApiDiscriminatorDefinition(type: UnionType): Definition {
const oneOf = this.getTypeDefinitions(type);
const discriminator = type.getDiscriminator();
if (!discriminator) throw new Error("discriminator is undefined");
return {
type: "object",
discriminator: { propertyName: discriminator },
required: [discriminator],
oneOf,
} as JSONSchema7;
}
public getDefinition(type: UnionType): Definition {
const discriminator = type.getDiscriminator();
if (discriminator !== undefined) {
if (this.discriminatorType === "open-api") return this.getOpenApiDiscriminatorDefinition(type);
return this.getJsonSchemaDiscriminatorDefinition(type);
}
const definitions = this.getTypeDefinitions(type);
// TODO: why is this not covered by LiteralUnionTypeFormatter?
// special case for string literals | string -> string
let stringType = true;
let oneNotEnum = false;
for (const def of definitions) {
if (def.type !== "string") {
stringType = false;
break;
}
if (def.enum === undefined) {
oneNotEnum = true;
}
}
if (stringType && oneNotEnum) {
const values = [];
for (const def of definitions) {
if (def.enum) {
values.push(...def.enum);
} else if (def.const) {
values.push(def.const);
} else {
return {
type: "string",
};
}
}
return {
type: "string",
enum: values,
};
}
const flattenedDefinitions: JSONSchema7[] = [];
// Flatten anyOf inside anyOf unless the anyOf has an annotation
for (const def of definitions) {
const keys = Object.keys(def);
if (keys.length === 1 && keys[0] === "anyOf") {
flattenedDefinitions.push(...(def.anyOf as any));
} else {
flattenedDefinitions.push(def);
}
}
return flattenedDefinitions.length > 1
? {
anyOf: flattenedDefinitions,
}
: flattenedDefinitions[0];
}
public getChildren(type: UnionType): BaseType[] {
return uniqueArray(
type
.getTypes()
.reduce((result: BaseType[], item) => [...result, ...this.childTypeFormatter.getChildren(item)], [])
);
}
}