Skip to content

Commit ce1a670

Browse files
srsudardomoritz
andauthored
feat: add support for a types array (#2410)
* add for multiple-types, test failing * passes with all types listed * fix tests * update comment * switch to only array * a few switcheroos * throw on type/types * fix tests * code review * code review * Apply suggestion from @domoritz * doc update * add a test for mixing types --------- Co-authored-by: Dominik Moritz <[email protected]>
1 parent 710eb4e commit ce1a670

File tree

11 files changed

+234
-12
lines changed

11 files changed

+234
-12
lines changed

factory/parser.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import { SatisfiesNodeParser } from "../src/NodeParser/SatisfiesNodeParser.js";
6262
import { PromiseNodeParser } from "../src/NodeParser/PromiseNodeParser.js";
6363
import { SpreadElementNodeParser } from "../src/NodeParser/SpreadElementNodeParser.js";
6464
import { IdentifierNodeParser } from "../src/NodeParser/IdentifierNodeParser.js";
65+
import { castArray } from "../src/Utils/castArray.js";
6566

6667
export type ParserAugmentor = (parser: MutableParser) => void;
6768

@@ -73,7 +74,10 @@ export function createParser(program: ts.Program, config: CompletedConfig, augme
7374
return new ExposeNodeParser(typeChecker, nodeParser, config.expose, config.jsDoc);
7475
}
7576
function withTopRef(nodeParser: NodeParser): NodeParser {
76-
return new TopRefNodeParser(chainNodeParser, config.type, config.topRef);
77+
const typeArr = castArray(config.type);
78+
// If we have multiple types, don't set a top-level $ref.
79+
const topRefFullName = typeArr && typeArr.length === 1 ? typeArr[0] : undefined;
80+
return new TopRefNodeParser(chainNodeParser, topRefFullName, config.topRef);
7781
}
7882
function withJsDoc(nodeParser: SubNodeParser): SubNodeParser {
7983
const extraTags = new Set(config.extraTags);

src/Config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ export interface Config {
88
path?: string;
99

1010
/**
11-
* Name of the type/interface to generate schema for.
11+
* Name of the type(s)/interface(s) to generate schema for.
1212
* Use "*" to generate schemas for all exported types.
1313
*/
14-
type?: string;
14+
type?: string | string[];
1515

1616
/**
1717
* Minify the output JSON schema (no whitespace).

src/SchemaGenerator.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { TypeFormatter } from "./TypeFormatter.js";
1010
import type { StringMap } from "./Utils/StringMap.js";
1111
import { hasJsDocTag } from "./Utils/hasJsDocTag.js";
1212
import { removeUnreachable } from "./Utils/removeUnreachable.js";
13+
import { castArray } from "./Utils/castArray.js";
1314
import { symbolAtNode } from "./Utils/symbolAtNode.js";
1415

1516
export class SchemaGenerator {
@@ -20,8 +21,8 @@ export class SchemaGenerator {
2021
protected readonly config?: Config,
2122
) {}
2223

23-
public createSchema(fullName?: string): Schema {
24-
const rootNodes = this.getRootNodes(fullName);
24+
public createSchema(fullNames?: string | string[]): Schema {
25+
const rootNodes = this.getRootNodes(castArray(fullNames));
2526
return this.createSchemaFromNodes(rootNodes);
2627
}
2728

@@ -60,9 +61,15 @@ export class SchemaGenerator {
6061
};
6162
}
6263

63-
protected getRootNodes(fullName: string | undefined): ts.Node[] {
64-
if (fullName && fullName !== "*") {
65-
return [this.findNamedNode(fullName)];
64+
protected getRootNodes(fullNames: string[] | undefined): ts.Node[] {
65+
// ["*"] means generate everything.
66+
if (fullNames && fullNames.includes("*") && fullNames.length > 1) {
67+
throw new Error("Cannot mix '*' with specific type names");
68+
}
69+
70+
const generateAll = !fullNames || fullNames.length === 0 || (fullNames.length === 1 && fullNames[0] === "*");
71+
if (!generateAll) {
72+
return fullNames.map((name) => this.findNamedNode(name));
6673
}
6774

6875
const rootFileNames = this.program.getRootFileNames();

src/Utils/castArray.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export function castArray<T>(input: undefined | T | T[]): undefined | T[] {
2+
if (input === undefined) {
3+
return undefined;
4+
}
5+
6+
return Array.isArray(input) ? input : [input];
7+
}

test/config.test.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const basePath = "test/config";
2626

2727
function assertSchema(
2828
name: string,
29-
userConfig: Config & { type: string },
29+
userConfig: Config & { type: string | string[] },
3030
tsconfig?: boolean,
3131
formatterAugmentor?: FormatterAugmentor,
3232
parserAugmentor?: ParserAugmentor,
@@ -390,6 +390,20 @@ describe("config", () => {
390390
}),
391391
);
392392

393+
it(
394+
"multiple-types",
395+
assertSchema("multiple-types", {
396+
type: ["MyObject1", "MyObject2"],
397+
}),
398+
);
399+
400+
it(
401+
"multiple-types-all",
402+
assertSchema("multiple-types-all", {
403+
type: ["MyObject1", "MyObject2", "Object1Prop", "Object2Prop"],
404+
}),
405+
);
406+
393407
it(
394408
"mapped-intersection",
395409
assertSchema("mapped-intersection", {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
type NonExportedType = {
2+
misc: number;
3+
};
4+
5+
export type ExportedType = {
6+
val: string;
7+
val2: NonExportedType;
8+
};
9+
10+
export interface ExportedInterface {
11+
val: string;
12+
}
13+
14+
export type Object1Prop = {
15+
name: string;
16+
};
17+
18+
export type Object2Prop = {
19+
description: string;
20+
};
21+
22+
export type MyObject1 = {
23+
id: number;
24+
bar: Object1Prop;
25+
};
26+
27+
export type MyObject2 = {
28+
idStr: string;
29+
baz: Object2Prop;
30+
};
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"definitions": {
4+
"MyObject1": {
5+
"properties": {
6+
"id": {
7+
"type": "number"
8+
},
9+
"bar": {
10+
"$ref": "#/definitions/Object1Prop"
11+
}
12+
},
13+
"required": [
14+
"id",
15+
"bar"
16+
],
17+
"type": "object",
18+
"additionalProperties": false
19+
},
20+
"MyObject2": {
21+
"properties": {
22+
"idStr": {
23+
"type": "string"
24+
},
25+
"baz": {
26+
"$ref": "#/definitions/Object2Prop"
27+
}
28+
},
29+
"required": [
30+
"idStr",
31+
"baz"
32+
],
33+
"type": "object",
34+
"additionalProperties": false
35+
},
36+
"Object1Prop": {
37+
"properties": {
38+
"name": {
39+
"type": "string"
40+
}
41+
},
42+
"required": [
43+
"name"
44+
],
45+
"type": "object",
46+
"additionalProperties": false
47+
},
48+
"Object2Prop": {
49+
"properties": {
50+
"description": {
51+
"type": "string"
52+
}
53+
},
54+
"required": [
55+
"description"
56+
],
57+
"type": "object",
58+
"additionalProperties": false
59+
}
60+
}
61+
}

test/config/multiple-types/main.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
type NonExportedType = {
2+
misc: number;
3+
};
4+
5+
export type ExportedType = {
6+
val: string;
7+
val2: NonExportedType;
8+
};
9+
10+
export interface ExportedInterface {
11+
val: string;
12+
}
13+
14+
// Exported, so we include it as a root node
15+
export type Object1Prop = {
16+
name: string;
17+
};
18+
19+
type Object2Prop = {
20+
description: string;
21+
};
22+
23+
export type MyObject1 = {
24+
id: number;
25+
bar: Object1Prop;
26+
};
27+
28+
export type MyObject2 = {
29+
idStr: string;
30+
baz: Object2Prop;
31+
};
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"definitions": {
4+
"MyObject1": {
5+
"properties": {
6+
"id": {
7+
"type": "number"
8+
},
9+
"bar": {
10+
"$ref": "#/definitions/Object1Prop"
11+
}
12+
},
13+
"required": [
14+
"id",
15+
"bar"
16+
],
17+
"type": "object",
18+
"additionalProperties": false
19+
},
20+
"MyObject2": {
21+
"properties": {
22+
"idStr": {
23+
"type": "string"
24+
},
25+
"baz": {
26+
"properties": {
27+
"description": {
28+
"type": "string"
29+
}
30+
},
31+
"required": [
32+
"description"
33+
],
34+
"type": "object",
35+
"additionalProperties": false
36+
}
37+
},
38+
"required": [
39+
"idStr",
40+
"baz"
41+
],
42+
"type": "object",
43+
"additionalProperties": false
44+
},
45+
"Object1Prop": {
46+
"properties": {
47+
"name": {
48+
"type": "string"
49+
}
50+
},
51+
"required": [
52+
"name"
53+
],
54+
"type": "object",
55+
"additionalProperties": false
56+
}
57+
}
58+
}

test/invalid-data.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type { CompletedConfig } from "../src/Config.js";
77
import { DEFAULT_CONFIG } from "../src/Config.js";
88
import { SchemaGenerator } from "../src/SchemaGenerator.js";
99

10-
function assertSchema(name: string, type: string, message: string) {
10+
function assertSchema(name: string, type: string | string[], message: string) {
1111
return () => {
1212
const config: CompletedConfig = {
1313
...DEFAULT_CONFIG,
@@ -35,6 +35,7 @@ describe("invalid-data", () => {
3535

3636
it("script-empty", assertSchema("script-empty", "MyType", `No root type "MyType" found`));
3737
it("duplicates", assertSchema("duplicates", "MyType", `Type "A" has multiple definitions.`));
38+
it("mixing * and types", assertSchema("duplicates", ["*", "MyType"], `Cannot mix '*' with specific type names`));
3839
it(
3940
"missing-discriminator",
4041
assertSchema("missing-discriminator", "MyType", 'Cannot find discriminator keyword "type" in type B.'),

0 commit comments

Comments
 (0)