Skip to content

Commit

Permalink
Add uniqueNames option. (#151)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fabiandev authored and domoritz committed May 10, 2018
1 parent 6f05b40 commit d08931c
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 12 deletions.
33 changes: 33 additions & 0 deletions README.md
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
@@ -0,0 +1,5 @@
import "./other";

class MyObject {
is: "MyObject_1";
}
3 changes: 3 additions & 0 deletions test/programs/unique-names/other.ts
@@ -0,0 +1,3 @@
class MyObject {
is: "MyObject_2";
}
16 changes: 16 additions & 0 deletions test/programs/unique-names/schema.MyObject.2139669.json
@@ -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
@@ -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
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
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
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

0 comments on commit d08931c

Please sign in to comment.