Skip to content

Commit d08931c

Browse files
fabiandevdomoritz
authored andcommitted
Add uniqueNames option. (#151)
* Added uniqueNames option. * Added test for uniqueNames option. * Use single method overloading for retrieving symbols * Add function overload * Update uniqueNames test schemas * Add missing CLI options * remove function definitions
1 parent 6f05b40 commit d08931c

8 files changed

+146
-12
lines changed

Diff for: README.md

+33
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ Options:
4444
--include Further limit tsconfig to include only matching files [array] [default: []]
4545
--ignoreErrors Generate even if the program has errors. [boolean] [default: false]
4646
--excludePrivate Exclude private members from the schema [boolean] [default: false]
47+
--uniqueNames Use unique names for type symbols. [boolean] [default: false]
4748
```
4849

4950
### Programmatic use
@@ -84,6 +85,38 @@ generator.getSchemaForSymbol("MyType");
8485
generator.getSchemaForSymbol("AnotherType");
8586
```
8687

88+
```ts
89+
// In larger projects type names may not be unique,
90+
// while unique names may be enabled.
91+
const settings: TJS.PartialArgs = {
92+
uniqueNames: true
93+
};
94+
95+
const generator = TJS.buildGenerator(program, settings);
96+
97+
// A list of all types of a given name can then be retrieved.
98+
const symbolList = generator.getSymbols("MyType");
99+
100+
// Choose the appropriate type, and continue with the symbol's unique name.
101+
generator.getSchemaForSymbol(symbolList[1].name);
102+
103+
// Also it is possible to get a list of all symbols.
104+
const fullSymbolList = generator.getSymbols();
105+
```
106+
107+
`getSymbols('<SymbolName>')` and `getSymbols()` return an array of `SymbolRef`, which is of the following format:
108+
109+
```ts
110+
type SymbolRef = {
111+
name: string;
112+
typeName: string;
113+
fullyQualifiedName: string;
114+
symbol: ts.Symbol;
115+
};
116+
```
117+
118+
`getUserSymbols` and `getMainFileSymbols` return an array of `string`.
119+
87120
### Annotations
88121

89122
The schema generator converts annotations to JSON schema properties.

Diff for: test/programs/unique-names/main.ts

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import "./other";
2+
3+
class MyObject {
4+
is: "MyObject_1";
5+
}

Diff for: test/programs/unique-names/other.ts

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
class MyObject {
2+
is: "MyObject_2";
3+
}
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"is": {
5+
"type": "string",
6+
"enum": [
7+
"MyObject_2"
8+
]
9+
}
10+
},
11+
"required": [
12+
"is"
13+
],
14+
"$schema": "http://json-schema.org/draft-06/schema#"
15+
}
16+
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"is": {
5+
"type": "string",
6+
"enum": [
7+
"MyObject_1"
8+
]
9+
}
10+
},
11+
"required": [
12+
"is"
13+
],
14+
"$schema": "http://json-schema.org/draft-06/schema#"
15+
}
16+

Diff for: test/schema.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,35 @@ export function assertSchema(group: string, type: string, settings: TJS.PartialA
3838
});
3939
}
4040

41+
export function assertSchemas(group: string, type: string, settings: TJS.PartialArgs = {}, compilerOptions?: TJS.CompilerOptions) {
42+
it(group + " should create correct schema", () => {
43+
if (!("required" in settings)) {
44+
settings.required = true;
45+
}
46+
47+
const generator = TJS.buildGenerator(TJS.getProgramFromFiles([resolve(BASE + group + "/main.ts")], compilerOptions), settings);
48+
const symbols = generator!.getSymbols(type);
49+
50+
for (let symbol of symbols) {
51+
const actual = generator!.getSchemaForSymbol(symbol.name);
52+
53+
// writeFileSync(BASE + group + `/schema.${symbol.name}.json`, JSON.stringify(actual, null, 4) + "\n\n");
54+
55+
const file = readFileSync(BASE + group + `/schema.${symbol.name}.json`, "utf8");
56+
const expected = JSON.parse(file);
57+
58+
assert.isObject(actual);
59+
assert.deepEqual(actual, expected, "The schema is not as expected");
60+
61+
// test against the meta schema
62+
if (actual !== null) {
63+
ajv.validateSchema(actual);
64+
assert.equal(ajv.errors, null, "The schema is not valid");
65+
}
66+
}
67+
});
68+
}
69+
4170
describe("interfaces", () => {
4271
it("should return an instance of JsonSchemaGenerator", () => {
4372
const program = TJS.getProgramFromFiles([resolve(BASE + "comments/main.ts")]);
@@ -240,6 +269,11 @@ describe("schema", () => {
240269
assertSchema("private-members", "MyObject", {
241270
excludePrivate: true
242271
});
272+
273+
assertSchemas("unique-names", "MyObject", {
274+
uniqueNames: true
275+
});
276+
243277
assertSchema("builtin-names", "Ext.Foo");
244278
});
245279
});

Diff for: typescript-json-schema-cli.ts

+5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ export function run() {
3232
.describe("out", "The output file, defaults to using stdout")
3333
.array("validationKeywords").default("validationKeywords", defaultArgs.validationKeywords)
3434
.describe("validationKeywords", "Provide additional validation keywords to include.")
35+
.boolean("excludePrivate").default("excludePrivate", defaultArgs.excludePrivate)
36+
.describe("excludePrivate", "Exclude private members from the schema.")
37+
.boolean("uniqueNames").default("uniqueNames", defaultArgs.uniqueNames)
38+
.describe("uniqueNames", "Use unique names for type symbols.")
3539
.array("include").default("*", defaultArgs.include)
3640
.describe("include", "Further limit tsconfig to include only matching files.")
3741
.argv;
@@ -52,6 +56,7 @@ export function run() {
5256
validationKeywords: args.validationKeywords,
5357
include: args.include,
5458
excludePrivate: args.excludePrivate,
59+
uniqueNames: args.uniqueNames,
5560
});
5661
}
5762

Diff for: typescript-json-schema.ts

+34-12
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as glob from "glob";
22
import * as stringify from "json-stable-stringify";
33
import * as path from "path";
44
import * as ts from "typescript";
5-
export { Program, CompilerOptions } from "typescript";
5+
export { Program, CompilerOptions, Symbol } from "typescript";
66

77

88
const vm = require("vm");
@@ -28,6 +28,7 @@ export function getDefaultArgs(): Args {
2828
validationKeywords: [],
2929
include: [],
3030
excludePrivate: false,
31+
uniqueNames: false,
3132
};
3233
}
3334

@@ -51,6 +52,7 @@ export type Args = {
5152
validationKeywords: string[];
5253
include: string[];
5354
excludePrivate: boolean;
55+
uniqueNames: boolean;
5456
};
5557

5658
export type PartialArgs = Partial<Args>;
@@ -84,6 +86,13 @@ export type Definition = {
8486
typeof?: "function"
8587
};
8688

89+
export type SymbolRef = {
90+
name: string;
91+
typeName: string;
92+
fullyQualifiedName: string;
93+
symbol: ts.Symbol;
94+
};
95+
8796
function extend(target: any, ..._: any[]) {
8897
if (target == null) { // TypeError if undefined or null
8998
throw new TypeError("Cannot convert undefined or null to object");
@@ -264,6 +273,11 @@ const validationKeywords = {
264273
export class JsonSchemaGenerator {
265274
private tc: ts.TypeChecker;
266275

276+
/**
277+
* Holds all symbols within a custom SymbolRef object, containing useful
278+
* information.
279+
*/
280+
private symbols: SymbolRef[];
267281
/**
268282
* All types for declarations of classes, interfaces, enums, and type aliases
269283
* defined in all TS files.
@@ -299,12 +313,14 @@ export class JsonSchemaGenerator {
299313
private typeNamesUsed: { [name: string]: boolean } = {};
300314

301315
constructor(
316+
symbols: SymbolRef[],
302317
allSymbols: { [name: string]: ts.Type },
303318
userSymbols: { [name: string]: ts.Symbol },
304319
inheritingTypes: { [baseName: string]: string[] },
305320
tc: ts.TypeChecker,
306321
private args = getDefaultArgs(),
307322
) {
323+
this.symbols = symbols;
308324
this.allSymbols = allSymbols;
309325
this.userSymbols = userSymbols;
310326
this.inheritingTypes = inheritingTypes;
@@ -952,6 +968,14 @@ export class JsonSchemaGenerator {
952968
return root;
953969
}
954970

971+
public getSymbols(name?: string): SymbolRef[] {
972+
if (name === void 0) {
973+
return this.symbols;
974+
}
975+
976+
return this.symbols.filter(symbol => symbol.typeName === name);
977+
}
978+
955979
public getUserSymbols(): string[] {
956980
return Object.keys(this.userSymbols);
957981
}
@@ -1011,6 +1035,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
10111035

10121036
if (diagnostics.length === 0 || args.ignoreErrors) {
10131037

1038+
const symbols: SymbolRef[] = [];
10141039
const allSymbols: { [name: string]: ts.Type } = {};
10151040
const userSymbols: { [name: string]: ts.Symbol } = {};
10161041
const inheritingTypes: { [baseName: string]: string[] } = {};
@@ -1024,20 +1049,17 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
10241049
|| node.kind === ts.SyntaxKind.TypeAliasDeclaration
10251050
) {
10261051
const symbol: ts.Symbol = (<any>node).symbol;
1027-
10281052
const nodeType = tc.getTypeAtLocation(node);
1053+
const fullyQualifiedName = tc.getFullyQualifiedName(symbol);
1054+
const typeName = fullyQualifiedName.replace(/".*"\./, "");
1055+
const name = !args.uniqueNames ? typeName : `${typeName}.${(<any>symbol).id}`;
10291056

1030-
// remove file name
1031-
// TODO: we probably don't want this eventually,
1032-
// as same types can occur in different files and will override eachother in allSymbols
1033-
// This means atm we can't generate all types in large programs.
1034-
const fullName = tc.getFullyQualifiedName(symbol).replace(/".*"\./, "");
1035-
1036-
allSymbols[fullName] = nodeType;
1057+
symbols.push({ name, typeName, fullyQualifiedName, symbol });
1058+
allSymbols[name] = nodeType;
10371059

10381060
// if (sourceFileIdx === 1) {
10391061
if (!sourceFile.hasNoDefaultLib) {
1040-
userSymbols[fullName] = symbol;
1062+
userSymbols[name] = symbol;
10411063
}
10421064

10431065
const baseTypes = nodeType.getBaseTypes() || [];
@@ -1047,7 +1069,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
10471069
if (!inheritingTypes[baseName]) {
10481070
inheritingTypes[baseName] = [];
10491071
}
1050-
inheritingTypes[baseName].push(fullName);
1072+
inheritingTypes[baseName].push(name);
10511073
});
10521074
} else {
10531075
ts.forEachChild(node, n => inspect(n, tc));
@@ -1056,7 +1078,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
10561078
inspect(sourceFile, typeChecker);
10571079
});
10581080

1059-
return new JsonSchemaGenerator(allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
1081+
return new JsonSchemaGenerator(symbols, allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
10601082
} else {
10611083
diagnostics.forEach((diagnostic) => {
10621084
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");

0 commit comments

Comments
 (0)