@typescript-eslint/rule-schema-to-typescript-types 0.0.0 → 8.45.1-alpha.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1003 -0
- package/LICENSE +21 -0
- package/README.md +9 -0
- package/dist/errors.d.ts +6 -0
- package/dist/errors.js +10 -0
- package/dist/generateArrayType.d.ts +3 -0
- package/dist/generateArrayType.js +107 -0
- package/dist/generateObjectType.d.ts +3 -0
- package/dist/generateObjectType.js +43 -0
- package/dist/generateType.d.ts +3 -0
- package/dist/generateType.js +99 -0
- package/dist/generateUnionType.d.ts +3 -0
- package/dist/generateUnionType.js +37 -0
- package/dist/getCommentLines.d.ts +2 -0
- package/dist/getCommentLines.js +7 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +59 -0
- package/dist/optimizeAST.d.ts +2 -0
- package/dist/optimizeAST.js +60 -0
- package/dist/printAST.d.ts +3 -0
- package/dist/printAST.js +130 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/dist/types.d.ts +41 -0
- package/dist/types.js +1 -0
- package/package.json +58 -1
- package/src/errors.ts +17 -0
- package/src/generateArrayType.ts +150 -0
- package/src/generateObjectType.ts +58 -0
- package/src/generateType.ts +126 -0
- package/src/generateUnionType.ts +54 -0
- package/src/getCommentLines.ts +9 -0
- package/src/index.ts +80 -0
- package/src/optimizeAST.ts +72 -0
- package/src/printAST.ts +168 -0
- package/src/types.ts +55 -0
- package/tests/index.test.ts +148 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.json +19 -0
- package/tsconfig.spec.json +14 -0
- package/vitest.config.mts +22 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema';
|
|
2
|
+
|
|
3
|
+
import { TSUtils } from '@typescript-eslint/utils';
|
|
4
|
+
|
|
5
|
+
import type { SchemaAST, RefMap } from './types.js';
|
|
6
|
+
|
|
7
|
+
import { NotSupportedError, UnexpectedError } from './errors.js';
|
|
8
|
+
import { generateArrayType } from './generateArrayType.js';
|
|
9
|
+
import { generateObjectType } from './generateObjectType.js';
|
|
10
|
+
import { generateUnionType } from './generateUnionType.js';
|
|
11
|
+
import { getCommentLines } from './getCommentLines.js';
|
|
12
|
+
|
|
13
|
+
// keywords we probably should support but currently do not support
|
|
14
|
+
const UNSUPPORTED_KEYWORDS = new Set([
|
|
15
|
+
'allOf',
|
|
16
|
+
'dependencies',
|
|
17
|
+
'extends',
|
|
18
|
+
'maxProperties',
|
|
19
|
+
'minProperties',
|
|
20
|
+
'multipleOf',
|
|
21
|
+
'not',
|
|
22
|
+
'patternProperties',
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export function generateType(schema: JSONSchema4, refMap: RefMap): SchemaAST {
|
|
26
|
+
const unsupportedProps = Object.keys(schema).filter(key =>
|
|
27
|
+
UNSUPPORTED_KEYWORDS.has(key),
|
|
28
|
+
);
|
|
29
|
+
if (unsupportedProps.length > 0) {
|
|
30
|
+
throw new NotSupportedError(unsupportedProps.join(','), schema);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const commentLines = getCommentLines(schema);
|
|
34
|
+
|
|
35
|
+
if (schema.$ref) {
|
|
36
|
+
const refName = refMap.get(schema.$ref);
|
|
37
|
+
if (refName == null) {
|
|
38
|
+
throw new UnexpectedError(
|
|
39
|
+
`Could not find definition for $ref ${
|
|
40
|
+
schema.$ref
|
|
41
|
+
}.\nAvailable refs:\n${[...refMap.keys()].join('\n')})`,
|
|
42
|
+
schema,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
commentLines,
|
|
47
|
+
type: 'type-reference',
|
|
48
|
+
typeName: refName,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
if ('enum' in schema && schema.enum) {
|
|
52
|
+
return {
|
|
53
|
+
...generateUnionType(schema.enum, refMap),
|
|
54
|
+
commentLines,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if ('anyOf' in schema && schema.anyOf) {
|
|
58
|
+
return {
|
|
59
|
+
// a union isn't *TECHNICALLY* correct - technically anyOf is actually
|
|
60
|
+
// anyOf: [T, U, V] -> T | U | V | T & U | T & V | U & V
|
|
61
|
+
// in practice though it is most used to emulate a oneOf
|
|
62
|
+
...generateUnionType(schema.anyOf, refMap),
|
|
63
|
+
commentLines,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if ('oneOf' in schema && schema.oneOf) {
|
|
67
|
+
return {
|
|
68
|
+
...generateUnionType(schema.oneOf, refMap),
|
|
69
|
+
commentLines,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!('type' in schema) || schema.type == null) {
|
|
74
|
+
throw new NotSupportedError(
|
|
75
|
+
'untyped schemas without one of [$ref, enum, oneOf]',
|
|
76
|
+
schema,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (TSUtils.isArray(schema.type)) {
|
|
80
|
+
throw new NotSupportedError('schemas with multiple types', schema);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
switch (schema.type) {
|
|
84
|
+
case 'any':
|
|
85
|
+
return {
|
|
86
|
+
commentLines,
|
|
87
|
+
type: 'type-reference',
|
|
88
|
+
typeName: 'unknown',
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
case 'null':
|
|
92
|
+
return {
|
|
93
|
+
commentLines,
|
|
94
|
+
type: 'type-reference',
|
|
95
|
+
typeName: 'null',
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
case 'number':
|
|
99
|
+
case 'string':
|
|
100
|
+
return {
|
|
101
|
+
code: schema.type,
|
|
102
|
+
commentLines,
|
|
103
|
+
type: 'literal',
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
case 'array':
|
|
107
|
+
return generateArrayType(schema, refMap);
|
|
108
|
+
|
|
109
|
+
case 'boolean':
|
|
110
|
+
return {
|
|
111
|
+
commentLines,
|
|
112
|
+
type: 'type-reference',
|
|
113
|
+
typeName: 'boolean',
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
case 'integer':
|
|
117
|
+
return {
|
|
118
|
+
commentLines,
|
|
119
|
+
type: 'type-reference',
|
|
120
|
+
typeName: 'number',
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
case 'object':
|
|
124
|
+
return generateObjectType(schema, refMap);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JSONSchema4,
|
|
3
|
+
JSONSchema4Type,
|
|
4
|
+
} from '@typescript-eslint/utils/json-schema';
|
|
5
|
+
|
|
6
|
+
import type { SchemaAST, RefMap, UnionAST } from './types.js';
|
|
7
|
+
|
|
8
|
+
import { NotSupportedError } from './errors.js';
|
|
9
|
+
import { generateType } from './generateType.js';
|
|
10
|
+
|
|
11
|
+
export function generateUnionType(
|
|
12
|
+
members: (JSONSchema4 | JSONSchema4Type)[],
|
|
13
|
+
refMap: RefMap,
|
|
14
|
+
): UnionAST {
|
|
15
|
+
const elements: SchemaAST[] = [];
|
|
16
|
+
|
|
17
|
+
for (const memberSchema of members) {
|
|
18
|
+
elements.push(
|
|
19
|
+
((): SchemaAST => {
|
|
20
|
+
switch (typeof memberSchema) {
|
|
21
|
+
case 'string':
|
|
22
|
+
return {
|
|
23
|
+
code: `'${memberSchema.replaceAll("'", "\\'")}'`,
|
|
24
|
+
commentLines: [],
|
|
25
|
+
type: 'literal',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
case 'number':
|
|
29
|
+
case 'boolean':
|
|
30
|
+
return {
|
|
31
|
+
code: `${memberSchema}`,
|
|
32
|
+
commentLines: [],
|
|
33
|
+
type: 'literal',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
case 'object':
|
|
37
|
+
if (memberSchema == null) {
|
|
38
|
+
throw new NotSupportedError('null in an enum', memberSchema);
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(memberSchema)) {
|
|
41
|
+
throw new NotSupportedError('array in an enum', memberSchema);
|
|
42
|
+
}
|
|
43
|
+
return generateType(memberSchema, refMap);
|
|
44
|
+
}
|
|
45
|
+
})(),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
commentLines: [],
|
|
51
|
+
elements,
|
|
52
|
+
type: 'union',
|
|
53
|
+
};
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { JSONSchema4 } from '@typescript-eslint/utils/json-schema';
|
|
2
|
+
|
|
3
|
+
import { TSUtils } from '@typescript-eslint/utils';
|
|
4
|
+
|
|
5
|
+
import type { SchemaAST } from './types.js';
|
|
6
|
+
|
|
7
|
+
import { generateType } from './generateType.js';
|
|
8
|
+
import { optimizeAST } from './optimizeAST.js';
|
|
9
|
+
import { printTypeAlias } from './printAST.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Converts rule options schema(s) to the equivalent TypeScript type string.
|
|
13
|
+
*
|
|
14
|
+
* @param schema Original rule schema(s) as declared in `meta.schema`.
|
|
15
|
+
* @returns Stringified TypeScript type(s) equivalent to the options schema(s).
|
|
16
|
+
*/
|
|
17
|
+
export function schemaToTypes(
|
|
18
|
+
schema: JSONSchema4 | readonly JSONSchema4[],
|
|
19
|
+
): string {
|
|
20
|
+
const [isArraySchema, schemaNormalized] = TSUtils.isArray(schema)
|
|
21
|
+
? [true, schema]
|
|
22
|
+
: [false, [schema]];
|
|
23
|
+
|
|
24
|
+
if (schemaNormalized.length === 0) {
|
|
25
|
+
return ['/** No options declared */', 'type Options = [];'].join('\n');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const refTypes: string[] = [];
|
|
29
|
+
const types: SchemaAST[] = [];
|
|
30
|
+
for (let i = 0; i < schemaNormalized.length; i += 1) {
|
|
31
|
+
const result = compileSchema(schemaNormalized[i], i);
|
|
32
|
+
refTypes.push(...result.refTypes);
|
|
33
|
+
types.push(result.type);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const optionsType = isArraySchema
|
|
37
|
+
? printTypeAlias('Options', {
|
|
38
|
+
commentLines: [],
|
|
39
|
+
elements: types,
|
|
40
|
+
spreadType: null,
|
|
41
|
+
type: 'tuple',
|
|
42
|
+
})
|
|
43
|
+
: printTypeAlias('Options', types[0]);
|
|
44
|
+
|
|
45
|
+
return [...refTypes, optionsType].join('\n\n');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function compileSchema(
|
|
49
|
+
schema: JSONSchema4,
|
|
50
|
+
index: number,
|
|
51
|
+
): { refTypes: string[]; type: SchemaAST } {
|
|
52
|
+
const refTypes: string[] = [];
|
|
53
|
+
|
|
54
|
+
const refMap = new Map<string, string>();
|
|
55
|
+
// we only support defs at the top level for simplicity
|
|
56
|
+
const defs = schema.$defs ?? schema.definitions;
|
|
57
|
+
if (defs) {
|
|
58
|
+
for (const [defKey, defSchema] of Object.entries(defs)) {
|
|
59
|
+
const typeName = toPascalCase(defKey);
|
|
60
|
+
refMap.set(`#/$defs/${defKey}`, typeName);
|
|
61
|
+
refMap.set(`#/items/${index}/$defs/${defKey}`, typeName);
|
|
62
|
+
|
|
63
|
+
const type = generateType(defSchema, refMap);
|
|
64
|
+
optimizeAST(type);
|
|
65
|
+
refTypes.push(printTypeAlias(typeName, type));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const type = generateType(schema, refMap);
|
|
70
|
+
optimizeAST(type);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
refTypes,
|
|
74
|
+
type,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function toPascalCase(key: string): string {
|
|
79
|
+
return key[0].toUpperCase() + key.substring(1);
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { SchemaAST, UnionAST } from './types.js';
|
|
2
|
+
|
|
3
|
+
export function optimizeAST(ast: SchemaAST | null): void {
|
|
4
|
+
if (ast == null) {
|
|
5
|
+
return;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
switch (ast.type) {
|
|
9
|
+
case 'array': {
|
|
10
|
+
optimizeAST(ast.elementType);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
case 'literal':
|
|
15
|
+
return;
|
|
16
|
+
|
|
17
|
+
case 'object': {
|
|
18
|
+
for (const property of ast.properties) {
|
|
19
|
+
optimizeAST(property.type);
|
|
20
|
+
}
|
|
21
|
+
optimizeAST(ast.indexSignature);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
case 'tuple': {
|
|
26
|
+
for (const element of ast.elements) {
|
|
27
|
+
optimizeAST(element);
|
|
28
|
+
}
|
|
29
|
+
optimizeAST(ast.spreadType);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
case 'type-reference':
|
|
34
|
+
return;
|
|
35
|
+
|
|
36
|
+
case 'union': {
|
|
37
|
+
const elements = unwrapUnions(ast);
|
|
38
|
+
for (const element of elements) {
|
|
39
|
+
optimizeAST(element);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// hacky way to deduplicate union members
|
|
43
|
+
const uniqueElementsMap = new Map<string, SchemaAST>();
|
|
44
|
+
for (const element of elements) {
|
|
45
|
+
uniqueElementsMap.set(JSON.stringify(element), element);
|
|
46
|
+
}
|
|
47
|
+
const uniqueElements = [...uniqueElementsMap.values()];
|
|
48
|
+
|
|
49
|
+
// @ts-expect-error -- purposely overwriting the property with a flattened list
|
|
50
|
+
ast.elements = uniqueElements;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function unwrapUnions(union: UnionAST): SchemaAST[] {
|
|
57
|
+
const elements: SchemaAST[] = [];
|
|
58
|
+
for (const element of union.elements) {
|
|
59
|
+
if (element.type === 'union') {
|
|
60
|
+
elements.push(...unwrapUnions(element));
|
|
61
|
+
} else {
|
|
62
|
+
elements.push(element);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (elements.length > 0) {
|
|
67
|
+
// preserve the union's comment lines by prepending them to the first element's lines
|
|
68
|
+
elements[0].commentLines.unshift(...union.commentLines);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return elements;
|
|
72
|
+
}
|
package/src/printAST.ts
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import naturalCompare from 'natural-compare';
|
|
2
|
+
|
|
3
|
+
import type { SchemaAST, TupleAST } from './types.js';
|
|
4
|
+
|
|
5
|
+
export function printTypeAlias(aliasName: string, ast: SchemaAST): string {
|
|
6
|
+
return `${printComment(ast)}type ${aliasName} = ${printAST(ast).code}`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function printASTWithComment(ast: SchemaAST): string {
|
|
10
|
+
const result = printAST(ast);
|
|
11
|
+
return `${printComment(result)}${result.code}`;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function printComment({
|
|
15
|
+
commentLines: commentLinesIn,
|
|
16
|
+
}: {
|
|
17
|
+
readonly commentLines?: string[] | null | undefined;
|
|
18
|
+
}): string {
|
|
19
|
+
if (commentLinesIn == null || commentLinesIn.length === 0) {
|
|
20
|
+
return '';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const commentLines: string[] = [];
|
|
24
|
+
for (const line of commentLinesIn) {
|
|
25
|
+
commentLines.push(...line.split('\n'));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (commentLines.length === 1) {
|
|
29
|
+
return `/** ${commentLines[0]} */\n`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return ['/**', ...commentLines.map(l => ` * ${l}`), ' */', ''].join('\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface CodeWithComments {
|
|
36
|
+
code: string;
|
|
37
|
+
commentLines: string[];
|
|
38
|
+
}
|
|
39
|
+
function printAST(ast: SchemaAST): CodeWithComments {
|
|
40
|
+
switch (ast.type) {
|
|
41
|
+
case 'array': {
|
|
42
|
+
const code = printAndMaybeParenthesise(ast.elementType);
|
|
43
|
+
return {
|
|
44
|
+
code: `${code.code}[]`,
|
|
45
|
+
commentLines: [...ast.commentLines, ...code.commentLines],
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
case 'literal':
|
|
50
|
+
return {
|
|
51
|
+
code: ast.code,
|
|
52
|
+
commentLines: ast.commentLines,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
case 'object': {
|
|
56
|
+
const properties = [];
|
|
57
|
+
// sort the properties so that we get consistent output regardless
|
|
58
|
+
// of import declaration order
|
|
59
|
+
const sortedPropertyDefs = ast.properties.sort((a, b) =>
|
|
60
|
+
naturalCompare(a.name, b.name),
|
|
61
|
+
);
|
|
62
|
+
for (const property of sortedPropertyDefs) {
|
|
63
|
+
const result = printAST(property.type);
|
|
64
|
+
properties.push(
|
|
65
|
+
`${printComment(result)}${property.name}${
|
|
66
|
+
property.optional ? '?:' : ':'
|
|
67
|
+
} ${result.code}`,
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (ast.indexSignature) {
|
|
72
|
+
const result = printAST(ast.indexSignature);
|
|
73
|
+
properties.push(`${printComment(result)}[k: string]: ${result.code}`);
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
// force insert a newline so prettier consistently prints all objects as multiline
|
|
77
|
+
code: `{\n${properties.join(';\n')}}`,
|
|
78
|
+
commentLines: ast.commentLines,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
case 'tuple': {
|
|
83
|
+
const elements = [];
|
|
84
|
+
for (const element of ast.elements) {
|
|
85
|
+
elements.push(printASTWithComment(element));
|
|
86
|
+
}
|
|
87
|
+
if (ast.spreadType) {
|
|
88
|
+
const result = printAndMaybeParenthesise(ast.spreadType);
|
|
89
|
+
elements.push(`${printComment(result)}...${result.code}[]`);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
code: `[${elements.join(',')}]`,
|
|
94
|
+
commentLines: ast.commentLines,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
case 'type-reference':
|
|
99
|
+
return {
|
|
100
|
+
code: ast.typeName,
|
|
101
|
+
commentLines: ast.commentLines,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
case 'union':
|
|
105
|
+
return {
|
|
106
|
+
code: ast.elements
|
|
107
|
+
.map(element => {
|
|
108
|
+
const result = printAST(element);
|
|
109
|
+
const code = `${printComment(result)} | ${result.code}`;
|
|
110
|
+
return {
|
|
111
|
+
code,
|
|
112
|
+
element,
|
|
113
|
+
};
|
|
114
|
+
})
|
|
115
|
+
// sort the union members so that we get consistent output regardless
|
|
116
|
+
// of declaration order
|
|
117
|
+
.sort((a, b) => compareElements(a, b))
|
|
118
|
+
.map(el => el.code)
|
|
119
|
+
.join('\n'),
|
|
120
|
+
commentLines: ast.commentLines,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
interface Element {
|
|
126
|
+
code: string;
|
|
127
|
+
element: SchemaAST;
|
|
128
|
+
}
|
|
129
|
+
function compareElements(a: Element, b: Element): number {
|
|
130
|
+
if (a.element.type !== b.element.type) {
|
|
131
|
+
return naturalCompare(a.code, b.code);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
switch (a.element.type) {
|
|
135
|
+
case 'array':
|
|
136
|
+
case 'literal':
|
|
137
|
+
case 'type-reference':
|
|
138
|
+
case 'object':
|
|
139
|
+
case 'union':
|
|
140
|
+
return naturalCompare(a.code, b.code);
|
|
141
|
+
|
|
142
|
+
case 'tuple': {
|
|
143
|
+
// natural compare will sort longer tuples before shorter ones
|
|
144
|
+
// which is the opposite of what we want, so we sort first by length THEN
|
|
145
|
+
// by code to ensure shorter tuples come first
|
|
146
|
+
const aElement = a.element;
|
|
147
|
+
const bElement = b.element as TupleAST;
|
|
148
|
+
if (aElement.elements.length !== bElement.elements.length) {
|
|
149
|
+
return aElement.elements.length - bElement.elements.length;
|
|
150
|
+
}
|
|
151
|
+
return naturalCompare(a.code, b.code);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function printAndMaybeParenthesise(ast: SchemaAST): CodeWithComments {
|
|
157
|
+
const printed = printAST(ast);
|
|
158
|
+
if (ast.type === 'union') {
|
|
159
|
+
return {
|
|
160
|
+
code: `(${printed.code})`,
|
|
161
|
+
commentLines: printed.commentLines,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
code: printed.code,
|
|
166
|
+
commentLines: printed.commentLines,
|
|
167
|
+
};
|
|
168
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps ref paths to generated type names.
|
|
3
|
+
*/
|
|
4
|
+
export type RefMap = ReadonlyMap<
|
|
5
|
+
// ref path
|
|
6
|
+
string,
|
|
7
|
+
// type name
|
|
8
|
+
string
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Minimal representation of the nodes in a schema being compiled to types.
|
|
13
|
+
*/
|
|
14
|
+
export type SchemaAST =
|
|
15
|
+
| ArrayAST
|
|
16
|
+
| LiteralAST
|
|
17
|
+
| ObjectAST
|
|
18
|
+
| TupleAST
|
|
19
|
+
| TypeReferenceAST
|
|
20
|
+
| UnionAST;
|
|
21
|
+
|
|
22
|
+
export interface BaseSchemaASTNode {
|
|
23
|
+
readonly commentLines: string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ArrayAST extends BaseSchemaASTNode {
|
|
27
|
+
readonly elementType: SchemaAST;
|
|
28
|
+
readonly type: 'array';
|
|
29
|
+
}
|
|
30
|
+
export interface LiteralAST extends BaseSchemaASTNode {
|
|
31
|
+
readonly code: string;
|
|
32
|
+
readonly type: 'literal';
|
|
33
|
+
}
|
|
34
|
+
export interface ObjectAST extends BaseSchemaASTNode {
|
|
35
|
+
readonly indexSignature: SchemaAST | null;
|
|
36
|
+
readonly properties: {
|
|
37
|
+
readonly name: string;
|
|
38
|
+
readonly optional: boolean;
|
|
39
|
+
readonly type: SchemaAST;
|
|
40
|
+
}[];
|
|
41
|
+
readonly type: 'object';
|
|
42
|
+
}
|
|
43
|
+
export interface TupleAST extends BaseSchemaASTNode {
|
|
44
|
+
readonly elements: SchemaAST[];
|
|
45
|
+
readonly spreadType: SchemaAST | null;
|
|
46
|
+
readonly type: 'tuple';
|
|
47
|
+
}
|
|
48
|
+
export interface TypeReferenceAST extends BaseSchemaASTNode {
|
|
49
|
+
readonly type: 'type-reference';
|
|
50
|
+
readonly typeName: string;
|
|
51
|
+
}
|
|
52
|
+
export interface UnionAST extends BaseSchemaASTNode {
|
|
53
|
+
readonly elements: SchemaAST[];
|
|
54
|
+
readonly type: 'union';
|
|
55
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { schemaToTypes } from '../src/index.js';
|
|
2
|
+
|
|
3
|
+
describe(schemaToTypes, () => {
|
|
4
|
+
it('returns a [] type when the schema is an empty array', () => {
|
|
5
|
+
const actual = schemaToTypes([]);
|
|
6
|
+
|
|
7
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
8
|
+
"/** No options declared */
|
|
9
|
+
type Options = [];"
|
|
10
|
+
`);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('returns a single Options type when the schema is not an array', () => {
|
|
14
|
+
const actual = schemaToTypes({ type: 'string' });
|
|
15
|
+
|
|
16
|
+
expect(actual).toMatchInlineSnapshot(`"type Options = string"`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('returns an array Options type when the schema is an array', () => {
|
|
20
|
+
const actual = schemaToTypes([{ type: 'string' }]);
|
|
21
|
+
|
|
22
|
+
expect(actual).toMatchInlineSnapshot(`"type Options = [string]"`);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('returns a complex Options type when the schema contains an array of items', () => {
|
|
26
|
+
const actual = schemaToTypes([
|
|
27
|
+
{
|
|
28
|
+
items: [{ type: 'string' }],
|
|
29
|
+
type: 'array',
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
34
|
+
"type Options = [ | []
|
|
35
|
+
| [string]]"
|
|
36
|
+
`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('returns a complex Options type when the schema is nested', () => {
|
|
40
|
+
const actual = schemaToTypes([
|
|
41
|
+
{
|
|
42
|
+
description: 'My schema items.',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
type: 'array',
|
|
45
|
+
},
|
|
46
|
+
]);
|
|
47
|
+
|
|
48
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
49
|
+
"type Options = [/** My schema items. */
|
|
50
|
+
string[]]"
|
|
51
|
+
`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('factors in one $ref property when a $defs property exists at the top level', () => {
|
|
55
|
+
const actual = schemaToTypes([
|
|
56
|
+
{
|
|
57
|
+
$defs: {
|
|
58
|
+
defOption: {
|
|
59
|
+
enum: ['a', 'b'],
|
|
60
|
+
type: 'string',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
additionalProperties: false,
|
|
64
|
+
properties: {
|
|
65
|
+
one: {
|
|
66
|
+
$ref: '#/items/0/$defs/defOption',
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
type: 'object',
|
|
70
|
+
},
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
74
|
+
"type DefOption = | 'a'
|
|
75
|
+
| 'b'
|
|
76
|
+
|
|
77
|
+
type Options = [{
|
|
78
|
+
one?: DefOption}]"
|
|
79
|
+
`);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('factors in one $ref property when a definitions property exists at the top level', () => {
|
|
83
|
+
const actual = schemaToTypes([
|
|
84
|
+
{
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
definitions: {
|
|
87
|
+
defOption: {
|
|
88
|
+
enum: ['a', 'b'],
|
|
89
|
+
type: 'string',
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
properties: {
|
|
93
|
+
one: {
|
|
94
|
+
$ref: '#/items/0/$defs/defOption',
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
type: 'object',
|
|
98
|
+
},
|
|
99
|
+
]);
|
|
100
|
+
|
|
101
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
102
|
+
"type DefOption = | 'a'
|
|
103
|
+
| 'b'
|
|
104
|
+
|
|
105
|
+
type Options = [{
|
|
106
|
+
one?: DefOption}]"
|
|
107
|
+
`);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('factors in two $ref properties when two $defs properties exist at the top level', () => {
|
|
111
|
+
const actual = schemaToTypes([
|
|
112
|
+
{
|
|
113
|
+
$defs: {
|
|
114
|
+
defOptionOne: {
|
|
115
|
+
enum: ['a', 'b'],
|
|
116
|
+
type: 'string',
|
|
117
|
+
},
|
|
118
|
+
defOptionTwo: {
|
|
119
|
+
enum: ['c', 'd'],
|
|
120
|
+
type: 'string',
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
additionalProperties: false,
|
|
124
|
+
properties: {
|
|
125
|
+
one: {
|
|
126
|
+
$ref: '#/items/0/$defs/defOptionOne',
|
|
127
|
+
},
|
|
128
|
+
two: {
|
|
129
|
+
$ref: '#/items/0/$defs/defOptionTwo',
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
type: 'object',
|
|
133
|
+
},
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
expect(actual).toMatchInlineSnapshot(`
|
|
137
|
+
"type DefOptionOne = | 'a'
|
|
138
|
+
| 'b'
|
|
139
|
+
|
|
140
|
+
type DefOptionTwo = | 'c'
|
|
141
|
+
| 'd'
|
|
142
|
+
|
|
143
|
+
type Options = [{
|
|
144
|
+
one?: DefOptionOne;
|
|
145
|
+
two?: DefOptionTwo}]"
|
|
146
|
+
`);
|
|
147
|
+
});
|
|
148
|
+
});
|