273 lines
7.2 KiB
TypeScript
273 lines
7.2 KiB
TypeScript
export type JSONSchemaType = 'string' | 'number' | 'boolean' | 'null' | 'array' | 'object';
|
|
|
|
/**
|
|
* fork from vscode
|
|
*/
|
|
export interface IJSONSchema {
|
|
id?: string;
|
|
$id?: string;
|
|
$schema?: string;
|
|
type?: JSONSchemaType | JSONSchemaType[];
|
|
title?: string;
|
|
default?: any;
|
|
definitions?: IJSONSchemaMap;
|
|
description?: string;
|
|
properties?: IJSONSchemaMap;
|
|
patternProperties?: IJSONSchemaMap;
|
|
additionalProperties?: boolean | IJSONSchema;
|
|
minProperties?: number;
|
|
maxProperties?: number;
|
|
dependencies?: IJSONSchemaMap | { [prop: string]: string[] };
|
|
items?: IJSONSchema | IJSONSchema[];
|
|
minItems?: number;
|
|
maxItems?: number;
|
|
uniqueItems?: boolean;
|
|
additionalItems?: boolean | IJSONSchema;
|
|
pattern?: string;
|
|
minLength?: number;
|
|
maxLength?: number;
|
|
minimum?: number;
|
|
maximum?: number;
|
|
exclusiveMinimum?: boolean | number;
|
|
exclusiveMaximum?: boolean | number;
|
|
multipleOf?: number;
|
|
required?: string[];
|
|
$ref?: string;
|
|
anyOf?: IJSONSchema[];
|
|
allOf?: IJSONSchema[];
|
|
oneOf?: IJSONSchema[];
|
|
not?: IJSONSchema;
|
|
enum?: any[];
|
|
format?: string;
|
|
|
|
// schema draft 06
|
|
const?: any;
|
|
contains?: IJSONSchema;
|
|
propertyNames?: IJSONSchema;
|
|
examples?: any[];
|
|
|
|
// schema draft 07
|
|
$comment?: string;
|
|
if?: IJSONSchema;
|
|
then?: IJSONSchema;
|
|
else?: IJSONSchema;
|
|
|
|
// schema 2019-09
|
|
unevaluatedProperties?: boolean | IJSONSchema;
|
|
unevaluatedItems?: boolean | IJSONSchema;
|
|
minContains?: number;
|
|
maxContains?: number;
|
|
deprecated?: boolean;
|
|
dependentRequired?: { [prop: string]: string[] };
|
|
dependentSchemas?: IJSONSchemaMap;
|
|
$defs?: { [name: string]: IJSONSchema };
|
|
$anchor?: string;
|
|
$recursiveRef?: string;
|
|
$recursiveAnchor?: string;
|
|
$vocabulary?: any;
|
|
|
|
// schema 2020-12
|
|
prefixItems?: IJSONSchema[];
|
|
$dynamicRef?: string;
|
|
$dynamicAnchor?: string;
|
|
|
|
// internal extensions
|
|
errorMessage?: string;
|
|
}
|
|
|
|
export interface IJSONSchemaMap {
|
|
[name: string]: IJSONSchema;
|
|
}
|
|
|
|
export interface IJSONSchemaSnippet {
|
|
label?: string;
|
|
description?: string;
|
|
body?: any; // a object that will be JSON stringified
|
|
bodyText?: string; // an already stringified JSON object that can contain new lines (\n) and tabs (\t)
|
|
}
|
|
|
|
/**
|
|
* Converts a basic JSON schema to a TypeScript type.
|
|
*
|
|
* TODO: only supports basic schemas. Doesn't support all JSON schema features.
|
|
*/
|
|
export type SchemaToType<T> = T extends { type: 'string' }
|
|
? string
|
|
: T extends { type: 'number' }
|
|
? number
|
|
: T extends { type: 'boolean' }
|
|
? boolean
|
|
: T extends { type: 'null' }
|
|
? null
|
|
: T extends { type: 'object'; properties: infer P }
|
|
? { [K in keyof P]: SchemaToType<P[K]> }
|
|
: T extends { type: 'array'; items: infer I }
|
|
? Array<SchemaToType<I>>
|
|
: never;
|
|
|
|
interface Equals {
|
|
schemas: IJSONSchema[];
|
|
id?: string;
|
|
}
|
|
|
|
export function getCompressedContent(schema: IJSONSchema): string {
|
|
let hasDups = false;
|
|
|
|
// visit all schema nodes and collect the ones that are equal
|
|
const equalsByString = new Map<string, Equals>();
|
|
const nodeToEquals = new Map<IJSONSchema, Equals>();
|
|
const visitSchemas = (next: IJSONSchema) => {
|
|
if (schema === next) {
|
|
return true;
|
|
}
|
|
const val = JSON.stringify(next);
|
|
if (val.length < 30) {
|
|
// the $ref takes around 25 chars, so we don't save anything
|
|
return true;
|
|
}
|
|
const eq = equalsByString.get(val);
|
|
if (!eq) {
|
|
const newEq = { schemas: [next] };
|
|
equalsByString.set(val, newEq);
|
|
nodeToEquals.set(next, newEq);
|
|
return true;
|
|
}
|
|
eq.schemas.push(next);
|
|
nodeToEquals.set(next, eq);
|
|
hasDups = true;
|
|
return false;
|
|
};
|
|
traverseNodes(schema, visitSchemas);
|
|
equalsByString.clear();
|
|
|
|
if (!hasDups) {
|
|
return JSON.stringify(schema);
|
|
}
|
|
|
|
let defNodeName = '$defs';
|
|
while (Reflect.has(schema, defNodeName)) {
|
|
defNodeName += '_';
|
|
}
|
|
|
|
// used to collect all schemas that are later put in `$defs`. The index in the array is the id of the schema.
|
|
const definitions: IJSONSchema[] = [];
|
|
|
|
function stringify(root: IJSONSchema): string {
|
|
return JSON.stringify(root, (_key: string, value: any) => {
|
|
if (value !== root) {
|
|
const eq = nodeToEquals.get(value);
|
|
if (eq && eq.schemas.length > 1) {
|
|
if (!eq.id) {
|
|
eq.id = `_${definitions.length}`;
|
|
definitions.push(eq.schemas[0]);
|
|
}
|
|
return { $ref: `#/${defNodeName}/${eq.id}` };
|
|
}
|
|
}
|
|
return value;
|
|
});
|
|
}
|
|
|
|
// stringify the schema and replace duplicate subtrees with $ref
|
|
// this will add new items to the definitions array
|
|
const str = stringify(schema);
|
|
|
|
// now stringify the definitions. Each invication of stringify cann add new items to the definitions array, so the length can grow while we iterate
|
|
const defStrings: string[] = [];
|
|
for (let i = 0; i < definitions.length; i++) {
|
|
defStrings.push(`"_${i}":${stringify(definitions[i])}`);
|
|
}
|
|
if (defStrings.length) {
|
|
return `${str.substring(0, str.length - 1)},"${defNodeName}":{${defStrings.join(',')}}}`;
|
|
}
|
|
return str;
|
|
}
|
|
|
|
type IJSONSchemaRef = IJSONSchema | boolean;
|
|
|
|
function isObject(thing: any): thing is object {
|
|
return typeof thing === 'object' && thing !== null;
|
|
}
|
|
|
|
/*
|
|
* Traverse a JSON schema and visit each schema node
|
|
*/
|
|
function traverseNodes(root: IJSONSchema, visit: (schema: IJSONSchema) => boolean) {
|
|
if (!root || typeof root !== 'object') {
|
|
return;
|
|
}
|
|
const collectEntries = (...entries: (IJSONSchemaRef | undefined)[]) => {
|
|
for (const entry of entries) {
|
|
if (isObject(entry)) {
|
|
toWalk.push(entry);
|
|
}
|
|
}
|
|
};
|
|
const collectMapEntries = (...maps: (IJSONSchemaMap | undefined)[]) => {
|
|
for (const map of maps) {
|
|
if (isObject(map)) {
|
|
for (const key in map) {
|
|
const entry = map[key];
|
|
if (isObject(entry)) {
|
|
toWalk.push(entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const collectArrayEntries = (...arrays: (IJSONSchemaRef[] | undefined)[]) => {
|
|
for (const array of arrays) {
|
|
if (Array.isArray(array)) {
|
|
for (const entry of array) {
|
|
if (isObject(entry)) {
|
|
toWalk.push(entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
const collectEntryOrArrayEntries = (items: IJSONSchemaRef[] | IJSONSchemaRef | undefined) => {
|
|
if (Array.isArray(items)) {
|
|
for (const entry of items) {
|
|
if (isObject(entry)) {
|
|
toWalk.push(entry);
|
|
}
|
|
}
|
|
} else if (isObject(items)) {
|
|
toWalk.push(items);
|
|
}
|
|
};
|
|
|
|
const toWalk: IJSONSchema[] = [root];
|
|
|
|
let next = toWalk.pop();
|
|
while (next) {
|
|
const visitChildern = visit(next);
|
|
if (visitChildern) {
|
|
collectEntries(
|
|
next.additionalItems,
|
|
next.additionalProperties,
|
|
next.not,
|
|
next.contains,
|
|
next.propertyNames,
|
|
next.if,
|
|
next.then,
|
|
next.else,
|
|
next.unevaluatedItems,
|
|
next.unevaluatedProperties,
|
|
);
|
|
collectMapEntries(
|
|
next.definitions,
|
|
next.$defs,
|
|
next.properties,
|
|
next.patternProperties,
|
|
<IJSONSchemaMap>next.dependencies,
|
|
next.dependentSchemas,
|
|
);
|
|
collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.prefixItems);
|
|
collectEntryOrArrayEntries(next.items);
|
|
}
|
|
next = toWalk.pop();
|
|
}
|
|
}
|