Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/common/jsonSchema.ts
5241 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'null' | 'array' | 'object';
7
8
export interface IJSONSchema {
9
id?: string;
10
$id?: string;
11
$schema?: string;
12
type?: JSONSchemaType | JSONSchemaType[];
13
title?: string;
14
default?: any;
15
definitions?: IJSONSchemaMap;
16
description?: string;
17
properties?: IJSONSchemaMap;
18
patternProperties?: IJSONSchemaMap;
19
additionalProperties?: boolean | IJSONSchema;
20
minProperties?: number;
21
maxProperties?: number;
22
dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
23
items?: IJSONSchema | IJSONSchema[];
24
minItems?: number;
25
maxItems?: number;
26
uniqueItems?: boolean;
27
additionalItems?: boolean | IJSONSchema;
28
pattern?: string;
29
minLength?: number;
30
maxLength?: number;
31
minimum?: number;
32
maximum?: number;
33
exclusiveMinimum?: boolean | number;
34
exclusiveMaximum?: boolean | number;
35
multipleOf?: number;
36
required?: string[];
37
$ref?: string;
38
anyOf?: IJSONSchema[];
39
allOf?: IJSONSchema[];
40
oneOf?: IJSONSchema[];
41
not?: IJSONSchema;
42
enum?: any[];
43
format?: string;
44
45
// schema draft 06
46
const?: any;
47
contains?: IJSONSchema;
48
propertyNames?: IJSONSchema;
49
examples?: any[];
50
51
// schema draft 07
52
$comment?: string;
53
if?: IJSONSchema;
54
then?: IJSONSchema;
55
else?: IJSONSchema;
56
57
// schema 2019-09
58
unevaluatedProperties?: boolean | IJSONSchema;
59
unevaluatedItems?: boolean | IJSONSchema;
60
minContains?: number;
61
maxContains?: number;
62
deprecated?: boolean;
63
dependentRequired?: { [prop: string]: string[] };
64
dependentSchemas?: IJSONSchemaMap;
65
$defs?: { [name: string]: IJSONSchema };
66
$anchor?: string;
67
$recursiveRef?: string;
68
$recursiveAnchor?: string;
69
$vocabulary?: any;
70
71
// schema 2020-12
72
prefixItems?: IJSONSchema[];
73
$dynamicRef?: string;
74
$dynamicAnchor?: string;
75
76
// VSCode extensions
77
78
defaultSnippets?: IJSONSchemaSnippet[];
79
errorMessage?: string;
80
patternErrorMessage?: string;
81
deprecationMessage?: string;
82
markdownDeprecationMessage?: string;
83
enumDescriptions?: string[];
84
markdownEnumDescriptions?: string[];
85
markdownDescription?: string;
86
doNotSuggest?: boolean;
87
suggestSortText?: string;
88
allowComments?: boolean;
89
allowTrailingCommas?: boolean;
90
secret?: boolean;
91
}
92
93
export interface IJSONSchemaMap {
94
[name: string]: IJSONSchema;
95
}
96
97
export interface IJSONSchemaSnippet {
98
label?: string;
99
description?: string;
100
body?: any; // a object that will be JSON stringified
101
bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t)
102
}
103
104
/**
105
* Converts a basic JSON schema to a TypeScript type.
106
*/
107
export type TypeFromJsonSchema<T> =
108
// enum
109
T extends { enum: infer EnumValues }
110
? UnionOf<EnumValues>
111
112
// Object with list of required properties.
113
// Values are required or optional based on `required` list.
114
: T extends { type: 'object'; properties: infer P; required: infer RequiredList }
115
? {
116
[K in keyof P]: IsRequired<K, RequiredList> extends true ? TypeFromJsonSchema<P[K]> : TypeFromJsonSchema<P[K]> | undefined;
117
} & AdditionalPropertiesType<T>
118
119
// Object with no required properties.
120
// All values are optional
121
: T extends { type: 'object'; properties: infer P }
122
? { [K in keyof P]: TypeFromJsonSchema<P[K]> | undefined } & AdditionalPropertiesType<T>
123
124
// Array
125
: T extends { type: 'array'; items: infer Items }
126
? Items extends [...infer R]
127
// If items is an array, we treat it like a tuple
128
? { [K in keyof R]: TypeFromJsonSchema<Items[K]> }
129
: Array<TypeFromJsonSchema<Items>>
130
131
// oneOf / anyof
132
// These are handled the same way as they both represent a union type.
133
// However at the validation level, they have different semantics.
134
: T extends { oneOf: infer I }
135
? MapSchemaToType<I>
136
: T extends { anyOf: infer I }
137
? MapSchemaToType<I>
138
139
// Primitive types
140
: T extends { type: infer Type }
141
// Basic type
142
? Type extends 'string' | 'number' | 'integer' | 'boolean' | 'null'
143
? SchemaPrimitiveTypeNameToType<Type>
144
// Union of primitive types
145
: Type extends [...infer R]
146
? UnionOf<{ [K in keyof R]: SchemaPrimitiveTypeNameToType<R[K]> }>
147
: never
148
149
// Fallthrough
150
: never;
151
152
type SchemaPrimitiveTypeNameToType<T> =
153
T extends 'string' ? string :
154
T extends 'number' | 'integer' ? number :
155
T extends 'boolean' ? boolean :
156
T extends 'null' ? null :
157
never;
158
159
type UnionOf<T> =
160
T extends [infer First, ...infer Rest]
161
? First | UnionOf<Rest>
162
: never;
163
164
type IsRequired<K, RequiredList> =
165
RequiredList extends []
166
? false
167
168
: RequiredList extends [K, ...infer _]
169
? true
170
171
: RequiredList extends [infer _, ...infer R]
172
? IsRequired<K, R>
173
174
: false;
175
176
type AdditionalPropertiesType<Schema> =
177
Schema extends { additionalProperties: infer AP }
178
? AP extends false ? {} : { [key: string]: TypeFromJsonSchema<Schema['additionalProperties']> }
179
: {};
180
181
type MapSchemaToType<T> = T extends [infer First, ...infer Rest]
182
? TypeFromJsonSchema<First> | MapSchemaToType<Rest>
183
: never;
184
185
interface Equals { schemas: IJSONSchema[]; id?: string }
186
187
export function getCompressedContent(schema: IJSONSchema): string {
188
let hasDups = false;
189
190
191
// visit all schema nodes and collect the ones that are equal
192
const equalsByString = new Map<string, Equals>();
193
const nodeToEquals = new Map<IJSONSchema, Equals>();
194
const visitSchemas = (next: IJSONSchema) => {
195
if (schema === next) {
196
return true;
197
}
198
const val = JSON.stringify(next);
199
if (val.length < 30) {
200
// the $ref takes around 25 chars, so we don't save anything
201
return true;
202
}
203
const eq = equalsByString.get(val);
204
if (!eq) {
205
const newEq = { schemas: [next] };
206
equalsByString.set(val, newEq);
207
nodeToEquals.set(next, newEq);
208
return true;
209
}
210
eq.schemas.push(next);
211
nodeToEquals.set(next, eq);
212
hasDups = true;
213
return false;
214
};
215
traverseNodes(schema, visitSchemas);
216
equalsByString.clear();
217
218
if (!hasDups) {
219
return JSON.stringify(schema);
220
}
221
222
let defNodeName = '$defs';
223
while (schema.hasOwnProperty(defNodeName)) {
224
defNodeName += '_';
225
}
226
227
// used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema.
228
const definitions: IJSONSchema[] = [];
229
230
function stringify(root: IJSONSchema): string {
231
return JSON.stringify(root, (_key: string, value: any) => {
232
if (value !== root) {
233
const eq = nodeToEquals.get(value);
234
if (eq && eq.schemas.length > 1) {
235
if (!eq.id) {
236
eq.id = `_${definitions.length}`;
237
definitions.push(eq.schemas[0]);
238
}
239
return { $ref: `#/${defNodeName}/${eq.id}` };
240
}
241
}
242
return value;
243
});
244
}
245
246
// stringify the schema and replace duplicate subtrees with $ref
247
// this will add new items to the definitions array
248
const str = stringify(schema);
249
250
// now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate
251
const defStrings: string[] = [];
252
for (let i = 0; i < definitions.length; i++) {
253
defStrings.push(`"_${i}":${stringify(definitions[i])}`);
254
}
255
if (defStrings.length) {
256
return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`;
257
}
258
return str;
259
}
260
261
type IJSONSchemaRef = IJSONSchema | boolean;
262
263
function isObject(thing: unknown): thing is object {
264
return typeof thing === 'object' && thing !== null;
265
}
266
267
/*
268
* Traverse a JSON schema and visit each schema node
269
*/
270
function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) {
271
if (!root || typeof root !== 'object') {
272
return;
273
}
274
const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => {
275
for (const entry of entries) {
276
if (isObject(entry)) {
277
toWalk.push(entry);
278
}
279
}
280
};
281
const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => {
282
for (const map of maps) {
283
if (isObject(map)) {
284
for (const key in map) {
285
const entry = map[key];
286
if (isObject(entry)) {
287
toWalk.push(entry);
288
}
289
}
290
}
291
}
292
};
293
const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => {
294
for (const array of arrays) {
295
if (Array.isArray(array)) {
296
for (const entry of array) {
297
if (isObject(entry)) {
298
toWalk.push(entry);
299
}
300
}
301
}
302
}
303
};
304
const collectEntryOrArrayEntries = (items: (IJSONSchemaRef[] | IJSONSchemaRef | undefined)) => {
305
if (Array.isArray(items)) {
306
for (const entry of items) {
307
if (isObject(entry)) {
308
toWalk.push(entry);
309
}
310
}
311
} else if (isObject(items)) {
312
toWalk.push(items);
313
}
314
};
315
316
const toWalk: IJSONSchema[] = [root];
317
318
let next = toWalk.pop();
319
while (next) {
320
const visitChildern = visit(next);
321
if (visitChildern) {
322
collectEntries(next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else, next.unevaluatedItems, next.unevaluatedProperties);
323
collectMapEntries(next.definitions, next.$defs, next.properties, next.patternProperties, <IJSONSchemaMap>next.dependencies, next.dependentSchemas);
324
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems);
325
collectEntryOrArrayEntries(next.items);
326
}
327
next = toWalk.pop();
328
}
329
}
330
331
332