Skip to content

Commit

Permalink
Add null to enums when necessary, cleaner enums with string literals. F…
Browse files Browse the repository at this point in the history
…ixes #20 #22
  • Loading branch information
domoritz committed Dec 30, 2017
1 parent b1a0c2b commit 8cdc707
Show file tree
Hide file tree
Showing 16 changed files with 167 additions and 67 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@
"build": "tsc -p .",
"watch": "tsc -p . -w",
"lint": "tslint -p .",
"test": "tsc -p . && mocha -t 10000 --require source-map-support/register dist/test",
"test": "tsc -p . && mocha -t 10000 --require source-map-support/register --recursive dist/test",
"debug": "ts-node --inspect=19248 --debug-brk ts-json-schema-generator.ts",
"run": "ts-node ts-json-schema-generator.ts"
}
Expand Down
2 changes: 1 addition & 1 deletion src/NodeParser/UnionNodeParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export class UnionNodeParser implements SubNodeParser {
const hidden = referenceHidden(this.typeChecker);
return new UnionType(
node.types
.filter((subnode: ts.Node) => !hidden(subnode))
.filter((subnode: ts.Node) => !hidden(subnode))
.map((subnode: ts.Node) => {
return this.childNodeParser.createType(subnode, context);
}),
Expand Down
2 changes: 1 addition & 1 deletion src/Schema/Definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Definition {
additionalItems?: {
anyOf: Definition[],
};
enum?: RawType[] | Definition[];
enum?: (RawType | Definition)[];
default?: RawType | Object;
additionalProperties?: false | Definition;
required?: string[];
Expand Down
9 changes: 7 additions & 2 deletions src/TypeFormatter/AnnotatedTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import { NullType } from "../Type/NullType";
import { TypeFormatter } from "../TypeFormatter";
import { uniqueArray } from "../Utils/uniqueArray";

function makeNullable(def: Definition) {
export function makeNullable(def: Definition) {
const union: Definition[] | undefined = def.oneOf || def.anyOf;
if (union && union.filter((d: Definition) => d.type === null).length > 0) {
if (union && union.filter((d: Definition) => d.type === "null").length === 0) {
union.push({ type: "null" });
} else if (def.type && def.type !== "object") {
if (isArray(def.type)) {
Expand All @@ -19,6 +19,11 @@ function makeNullable(def: Definition) {
} else if (def.type !== "null") {
def.type = [def.type, "null"];
}

// enums need null as an option
if (def.enum && def.enum.indexOf(null) === -1) {
def.enum.push(null);
}
} else {
const subdef: Definition = {};

Expand Down
15 changes: 4 additions & 11 deletions src/TypeFormatter/EnumTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,10 @@ export class EnumTypeFormatter implements SubTypeFormatter {
const values: EnumValue[] = uniqueArray(type.getValues());
const types: string[] = uniqueArray(values.map((value: EnumValue) => this.getValueType(value)));

if (types.length === 1) {
return {
type: types[0],
enum: values,
};
} else {
return {
type: types,
enum: values,
};
}
return {
type: types.length === 1 ? types[0] : types,
enum: values,
};
}
public getChildren(type: EnumType): BaseType[] {
return [];
Expand Down
17 changes: 11 additions & 6 deletions src/TypeFormatter/LiteralUnionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Definition } from "../Schema/Definition";
import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { LiteralType } from "../Type/LiteralType";
import { NullType } from "../Type/NullType";
import { UnionType } from "../Type/UnionType";
import { uniqueArray } from "../Utils/uniqueArray";

Expand All @@ -10,9 +11,10 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter {
return type instanceof UnionType && this.isLiteralUnion(type);
}
public getDefinition(type: UnionType): Definition {
const values: (string | number | boolean)[] = uniqueArray(
type.getTypes().map((item: LiteralType) => item.getValue()));
const types: string[] = uniqueArray(type.getTypes().map((item: LiteralType) => this.getLiteralType(item)));
const values: (string | number | boolean | null)[] = uniqueArray(
type.getTypes().map((item: LiteralType | NullType) => this.getLiteralValue(item)));
const types: string[] = uniqueArray(
type.getTypes().map((item: LiteralType | NullType) => this.getLiteralType(item)));

if (types.length === 1) {
return {
Expand All @@ -31,9 +33,12 @@ export class LiteralUnionTypeFormatter implements SubTypeFormatter {
}

private isLiteralUnion(type: UnionType): boolean {
return type.getTypes().every((item: BaseType) => item instanceof LiteralType);
return type.getTypes().every((item: BaseType) => item instanceof LiteralType || item instanceof NullType);
}
private getLiteralType(value: LiteralType): string {
return value.getValue() === null ? "null" : typeof value.getValue();
private getLiteralValue(value: LiteralType | NullType): string | number | boolean | null {
return value.getId() === "null" ? null : (value as LiteralType).getValue();
}
private getLiteralType(value: LiteralType | NullType): string {
return value.getId() === "null" ? "null" : typeof (value as LiteralType).getValue();
}
}
20 changes: 20 additions & 0 deletions src/TypeFormatter/UnionTypeFormatter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { SubTypeFormatter } from "../SubTypeFormatter";
import { BaseType } from "../Type/BaseType";
import { UnionType } from "../Type/UnionType";
import { TypeFormatter } from "../TypeFormatter";
import { uniqueArray } from "../Utils/uniqueArray";

export class UnionTypeFormatter implements SubTypeFormatter {
public constructor(
Expand All @@ -15,6 +16,25 @@ export class UnionTypeFormatter implements SubTypeFormatter {
}
public getDefinition(type: UnionType): Definition {
const definitions = type.getTypes().map((item: BaseType) => this.childTypeFormatter.getDefinition(item));

// special case for string literals | string -> string
let stringType = true;
let oneNotEnum = false;
for (const def of definitions) {
if (def.type !== "string") {
stringType = false;
break;
}
if (def.enum === undefined) {
oneNotEnum = true;
}
}
if (stringType && oneNotEnum) {
return {
type: "string",
};
}

return definitions.length > 1 ? {
anyOf: definitions,
} : definitions[0];
Expand Down
27 changes: 27 additions & 0 deletions test/unit/AnnotatedTypeFormatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { assert } from "chai";
import { makeNullable } from "../../src/TypeFormatter/AnnotatedTypeFormatter";

describe("makeNullable", () => {
it("makes number nullable", () => {
const n = makeNullable({type: "number"});
assert.deepEqual(n, {
type: ["number", "null"],
});
});

it("makes enum nullable", () => {
const n = makeNullable({
enum: ["foo"],
type: "string",
});
assert.deepEqual(n, {
enum: ["foo", null],
type: ["string", "null"],
});
});

it("makes anyOf nullable", () => {
const n = makeNullable({anyOf: [{type: "number"}, {type: "string"}]});
assert.deepEqual(n, {anyOf: [{type: "number"}, {type: "string"}, {type: "null"}]});
});
});
5 changes: 4 additions & 1 deletion test/valid-data.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ function assertSchema(name: string, type: string, only: boolean = false): void {

describe("valid-data", () => {
// TODO: generics recursive
// TODO: literals unions

assertSchema("simple-object", "SimpleObject");

Expand All @@ -73,6 +72,10 @@ describe("valid-data", () => {
assertSchema("enums-mixed", "Enum");
assertSchema("enums-member", "MyObject");

assertSchema("string-literals", "MyObject");
assertSchema("string-literals-inline", "MyObject");
assertSchema("string-literals-null", "MyObject");

assertSchema("namespace-deep-1", "RootNamespace.Def");
assertSchema("namespace-deep-2", "RootNamespace.SubNamespace.HelperA");
assertSchema("namespace-deep-3", "RootNamespace.SubNamespace.HelperB");
Expand Down
2 changes: 1 addition & 1 deletion test/valid-data/string-literals-inline/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class MyObject {
export interface MyObject {
foo: "ok" | "fail" | "abort";
bar: "ok" | "fail" | "abort" | string;
}
42 changes: 24 additions & 18 deletions test/valid-data/string-literals-inline/schema.json
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
{
"type": "object",
"properties": {
"foo": {
"type": "string",
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"bar": {
"type": "string"
},
"foo": {
"enum": [
"abort",
"fail",
"ok"
]
},
"bar": {
"ok",
"fail",
"abort"
],
"type": "string"
}
},
"required": [
"foo",
"bar"
],
"$schema": "http://json-schema.org/draft-04/schema#"
}
}
},
"required": [
"foo",
"bar"
],
"type": "object"
}
}
}
5 changes: 5 additions & 0 deletions test/valid-data/string-literals-null/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export interface MyObject {
enum1: "a" | "b" | null;
enum2: "a" | "b";
}

34 changes: 34 additions & 0 deletions test/valid-data/string-literals-null/schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"MyObject": {
"additionalProperties": false,
"properties": {
"enum1": {
"enum": [
"a",
"b",
null
],
"type": [
"string",
"null"
]
},
"enum2": {
"enum": [
"a",
"b"
],
"type": "string"
}
},
"required": [
"enum1",
"enum2"
],
"type": "object"
}
}
}
2 changes: 1 addition & 1 deletion test/valid-data/string-literals/main.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
type result = "ok" | "fail" | "abort";

class MyObject {
export interface MyObject {
foo: result;
bar: result | string;
}
47 changes: 24 additions & 23 deletions test/valid-data/string-literals/schema.json
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@
{
"type": "object",
"properties": {
"foo": {
"$ref": "#/definitions/result"
},
"bar": {
"type": "string"
}
},
"required": [
"foo",
"bar"
],
"$ref": "#/definitions/MyObject",
"$schema": "http://json-schema.org/draft-04/schema#",
"definitions": {
"result": {
"type": "string",
"MyObject": {
"additionalProperties": false,
"properties": {
"bar": {
"type": "string"
},
"foo": {
"enum": [
"abort",
"fail",
"ok"
]
}
},
"$schema": "http://json-schema.org/draft-04/schema#"
}
"ok",
"fail",
"abort"
],
"type": "string"
}
},
"required": [
"foo",
"bar"
],
"type": "object"
}
}
}
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"ts-json-schema-generator.ts",
"factory/**/*.ts",
"src/**/*.ts",
"test/*.ts"
"test/*.ts",
"test/unit/**/*.ts"
],
"exclude": [
"node_modules",
Expand Down

0 comments on commit 8cdc707

Please sign in to comment.