Skip to content

Add uniqueNames option. #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
May 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ Options:
--include Further limit tsconfig to include only matching files [array] [default: []]
--ignoreErrors Generate even if the program has errors. [boolean] [default: false]
--excludePrivate Exclude private members from the schema [boolean] [default: false]
--uniqueNames Use unique names for type symbols. [boolean] [default: false]
```

### Programmatic use
Expand Down Expand Up @@ -84,6 +85,38 @@ generator.getSchemaForSymbol("MyType");
generator.getSchemaForSymbol("AnotherType");
```

```ts
// In larger projects type names may not be unique,
// while unique names may be enabled.
const settings: TJS.PartialArgs = {
uniqueNames: true
};

const generator = TJS.buildGenerator(program, settings);

// A list of all types of a given name can then be retrieved.
const symbolList = generator.getSymbols("MyType");

// Choose the appropriate type, and continue with the symbol's unique name.
generator.getSchemaForSymbol(symbolList[1].name);

// Also it is possible to get a list of all symbols.
const fullSymbolList = generator.getSymbols();
```

`getSymbols('<SymbolName>')` and `getSymbols()` return an array of `SymbolRef`, which is of the following format:

```ts
type SymbolRef = {
name: string;
typeName: string;
fullyQualifiedName: string;
symbol: ts.Symbol;
};
```

`getUserSymbols` and `getMainFileSymbols` return an array of `string`.

### Annotations

The schema generator converts annotations to JSON schema properties.
Expand Down
5 changes: 5 additions & 0 deletions test/programs/unique-names/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import "./other";

class MyObject {
is: "MyObject_1";
}
3 changes: 3 additions & 0 deletions test/programs/unique-names/other.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class MyObject {
is: "MyObject_2";
}
16 changes: 16 additions & 0 deletions test/programs/unique-names/schema.MyObject.2139669.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"is": {
"type": "string",
"enum": [
"MyObject_2"
]
}
},
"required": [
"is"
],
"$schema": "http://json-schema.org/draft-06/schema#"
}

16 changes: 16 additions & 0 deletions test/programs/unique-names/schema.MyObject.2139671.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"type": "object",
"properties": {
"is": {
"type": "string",
"enum": [
"MyObject_1"
]
}
},
"required": [
"is"
],
"$schema": "http://json-schema.org/draft-06/schema#"
}

34 changes: 34 additions & 0 deletions test/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,35 @@ export function assertSchema(group: string, type: string, settings: TJS.PartialA
});
}

export function assertSchemas(group: string, type: string, settings: TJS.PartialArgs = {}, compilerOptions?: TJS.CompilerOptions) {
it(group + " should create correct schema", () => {
if (!("required" in settings)) {
settings.required = true;
}

const generator = TJS.buildGenerator(TJS.getProgramFromFiles([resolve(BASE + group + "/main.ts")], compilerOptions), settings);
const symbols = generator!.getSymbols(type);

for (let symbol of symbols) {
const actual = generator!.getSchemaForSymbol(symbol.name);

// writeFileSync(BASE + group + `/schema.${symbol.name}.json`, JSON.stringify(actual, null, 4) + "\n\n");

const file = readFileSync(BASE + group + `/schema.${symbol.name}.json`, "utf8");
const expected = JSON.parse(file);

assert.isObject(actual);
assert.deepEqual(actual, expected, "The schema is not as expected");

// test against the meta schema
if (actual !== null) {
ajv.validateSchema(actual);
assert.equal(ajv.errors, null, "The schema is not valid");
}
}
});
}

describe("interfaces", () => {
it("should return an instance of JsonSchemaGenerator", () => {
const program = TJS.getProgramFromFiles([resolve(BASE + "comments/main.ts")]);
Expand Down Expand Up @@ -240,6 +269,11 @@ describe("schema", () => {
assertSchema("private-members", "MyObject", {
excludePrivate: true
});

assertSchemas("unique-names", "MyObject", {
uniqueNames: true
});

assertSchema("builtin-names", "Ext.Foo");
});
});
Expand Down
5 changes: 5 additions & 0 deletions typescript-json-schema-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export function run() {
.describe("out", "The output file, defaults to using stdout")
.array("validationKeywords").default("validationKeywords", defaultArgs.validationKeywords)
.describe("validationKeywords", "Provide additional validation keywords to include.")
.boolean("excludePrivate").default("excludePrivate", defaultArgs.excludePrivate)
.describe("excludePrivate", "Exclude private members from the schema.")
.boolean("uniqueNames").default("uniqueNames", defaultArgs.uniqueNames)
.describe("uniqueNames", "Use unique names for type symbols.")
.array("include").default("*", defaultArgs.include)
.describe("include", "Further limit tsconfig to include only matching files.")
.argv;
Expand All @@ -52,6 +56,7 @@ export function run() {
validationKeywords: args.validationKeywords,
include: args.include,
excludePrivate: args.excludePrivate,
uniqueNames: args.uniqueNames,
});
}

Expand Down
46 changes: 34 additions & 12 deletions typescript-json-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as glob from "glob";
import * as stringify from "json-stable-stringify";
import * as path from "path";
import * as ts from "typescript";
export { Program, CompilerOptions } from "typescript";
export { Program, CompilerOptions, Symbol } from "typescript";


const vm = require("vm");
Expand All @@ -28,6 +28,7 @@ export function getDefaultArgs(): Args {
validationKeywords: [],
include: [],
excludePrivate: false,
uniqueNames: false,
};
}

Expand All @@ -51,6 +52,7 @@ export type Args = {
validationKeywords: string[];
include: string[];
excludePrivate: boolean;
uniqueNames: boolean;
};

export type PartialArgs = Partial<Args>;
Expand Down Expand Up @@ -84,6 +86,13 @@ export type Definition = {
typeof?: "function"
};

export type SymbolRef = {
name: string;
typeName: string;
fullyQualifiedName: string;
symbol: ts.Symbol;
};

function extend(target: any, ..._: any[]) {
if (target == null) { // TypeError if undefined or null
throw new TypeError("Cannot convert undefined or null to object");
Expand Down Expand Up @@ -264,6 +273,11 @@ const validationKeywords = {
export class JsonSchemaGenerator {
private tc: ts.TypeChecker;

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

constructor(
symbols: SymbolRef[],
allSymbols: { [name: string]: ts.Type },
userSymbols: { [name: string]: ts.Symbol },
inheritingTypes: { [baseName: string]: string[] },
tc: ts.TypeChecker,
private args = getDefaultArgs(),
) {
this.symbols = symbols;
this.allSymbols = allSymbols;
this.userSymbols = userSymbols;
this.inheritingTypes = inheritingTypes;
Expand Down Expand Up @@ -952,6 +968,14 @@ export class JsonSchemaGenerator {
return root;
}

public getSymbols(name?: string): SymbolRef[] {
if (name === void 0) {
return this.symbols;
}

return this.symbols.filter(symbol => symbol.typeName === name);
}

public getUserSymbols(): string[] {
return Object.keys(this.userSymbols);
}
Expand Down Expand Up @@ -1011,6 +1035,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso

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

const symbols: SymbolRef[] = [];
const allSymbols: { [name: string]: ts.Type } = {};
const userSymbols: { [name: string]: ts.Symbol } = {};
const inheritingTypes: { [baseName: string]: string[] } = {};
Expand All @@ -1024,20 +1049,17 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
|| node.kind === ts.SyntaxKind.TypeAliasDeclaration
) {
const symbol: ts.Symbol = (<any>node).symbol;

const nodeType = tc.getTypeAtLocation(node);
const fullyQualifiedName = tc.getFullyQualifiedName(symbol);
const typeName = fullyQualifiedName.replace(/".*"\./, "");
const name = !args.uniqueNames ? typeName : `${typeName}.${(<any>symbol).id}`;

// remove file name
// TODO: we probably don't want this eventually,
// as same types can occur in different files and will override eachother in allSymbols
// This means atm we can't generate all types in large programs.
const fullName = tc.getFullyQualifiedName(symbol).replace(/".*"\./, "");

allSymbols[fullName] = nodeType;
symbols.push({ name, typeName, fullyQualifiedName, symbol });
allSymbols[name] = nodeType;

// if (sourceFileIdx === 1) {
if (!sourceFile.hasNoDefaultLib) {
userSymbols[fullName] = symbol;
userSymbols[name] = symbol;
}

const baseTypes = nodeType.getBaseTypes() || [];
Expand All @@ -1047,7 +1069,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
if (!inheritingTypes[baseName]) {
inheritingTypes[baseName] = [];
}
inheritingTypes[baseName].push(fullName);
inheritingTypes[baseName].push(name);
});
} else {
ts.forEachChild(node, n => inspect(n, tc));
Expand All @@ -1056,7 +1078,7 @@ export function buildGenerator(program: ts.Program, args: PartialArgs = {}): Jso
inspect(sourceFile, typeChecker);
});

return new JsonSchemaGenerator(allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
return new JsonSchemaGenerator(symbols, allSymbols, userSymbols, inheritingTypes, typeChecker, settings);
} else {
diagnostics.forEach((diagnostic) => {
const message = ts.flattenDiagnosticMessageText(diagnostic.messageText, "\n");
Expand Down