From 28bbe6e9e33cb213b1ce0a53992f49aa6aa334e3 Mon Sep 17 00:00:00 2001 From: Klaus Reimer Date: Wed, 5 Jun 2019 08:09:55 +0200 Subject: [PATCH] Class support (#112) * Implement class support * Ignore property if type can not be read * Merge interface and class node parser and ignore private/protected/static properties * Update schema URLs to draft-07 --- factory/parser.ts | 4 +- index.ts | 2 +- ...rser.ts => InterfaceAndClassNodeParser.ts} | 38 ++++++++----- src/SchemaGenerator.ts | 1 + test/valid-data.test.ts | 7 +++ test/valid-data/class-extra-props/main.ts | 5 ++ test/valid-data/class-extra-props/schema.json | 26 +++++++++ test/valid-data/class-generics/main.ts | 9 ++++ test/valid-data/class-generics/schema.json | 54 +++++++++++++++++++ test/valid-data/class-inheritance/main.ts | 9 ++++ test/valid-data/class-inheritance/schema.json | 26 +++++++++ test/valid-data/class-multi/main.ts | 8 +++ test/valid-data/class-multi/schema.json | 38 +++++++++++++ test/valid-data/class-recursion/main.ts | 4 ++ test/valid-data/class-recursion/schema.json | 22 ++++++++ test/valid-data/class-single/main.ts | 36 +++++++++++++ test/valid-data/class-single/schema.json | 22 ++++++++ 17 files changed, 295 insertions(+), 16 deletions(-) rename src/NodeParser/{InterfaceNodeParser.ts => InterfaceAndClassNodeParser.ts} (57%) create mode 100644 test/valid-data/class-extra-props/main.ts create mode 100644 test/valid-data/class-extra-props/schema.json create mode 100644 test/valid-data/class-generics/main.ts create mode 100644 test/valid-data/class-generics/schema.json create mode 100644 test/valid-data/class-inheritance/main.ts create mode 100644 test/valid-data/class-inheritance/schema.json create mode 100644 test/valid-data/class-multi/main.ts create mode 100644 test/valid-data/class-multi/schema.json create mode 100644 test/valid-data/class-recursion/main.ts create mode 100644 test/valid-data/class-recursion/schema.json create mode 100644 test/valid-data/class-single/main.ts create mode 100644 test/valid-data/class-single/schema.json diff --git a/factory/parser.ts b/factory/parser.ts index 44cc7a58b..017b02f71 100644 --- a/factory/parser.ts +++ b/factory/parser.ts @@ -15,7 +15,7 @@ import { CallExpressionParser } from "../src/NodeParser/CallExpressionParser"; import { EnumNodeParser } from "../src/NodeParser/EnumNodeParser"; import { ExpressionWithTypeArgumentsNodeParser } from "../src/NodeParser/ExpressionWithTypeArgumentsNodeParser"; import { IndexedAccessTypeNodeParser } from "../src/NodeParser/IndexedAccessTypeNodeParser"; -import { InterfaceNodeParser } from "../src/NodeParser/InterfaceNodeParser"; +import { InterfaceAndClassNodeParser } from "../src/NodeParser/InterfaceAndClassNodeParser"; import { IntersectionNodeParser } from "../src/NodeParser/IntersectionNodeParser"; import { LiteralNodeParser } from "../src/NodeParser/LiteralNodeParser"; import { MappedTypeNodeParser } from "../src/NodeParser/MappedTypeNodeParser"; @@ -101,7 +101,7 @@ export function createParser(program: ts.Program, config: Config): NodeParser { new TypeAliasNodeParser(typeChecker, chainNodeParser))))) .addNodeParser(withExpose(withJsDoc(new EnumNodeParser(typeChecker)))) .addNodeParser(withCircular(withExpose(withJsDoc( - new InterfaceNodeParser(typeChecker, withJsDoc(chainNodeParser)), + new InterfaceAndClassNodeParser(typeChecker, withJsDoc(chainNodeParser)), )))) .addNodeParser(withCircular(withExpose(withJsDoc( new TypeLiteralNodeParser(withJsDoc(chainNodeParser)), diff --git a/index.ts b/index.ts index 6d45b0400..1bf693ece 100644 --- a/index.ts +++ b/index.ts @@ -82,7 +82,7 @@ export * from "./src/NodeParser/NumberTypeNodeParser"; export * from "./src/NodeParser/StringTypeNodeParser"; export * from "./src/NodeParser/EnumNodeParser"; export * from "./src/NodeParser/ExpressionWithTypeArgumentsNodeParser"; -export * from "./src/NodeParser/InterfaceNodeParser"; +export * from "./src/NodeParser/InterfaceAndClassNodeParser"; export * from "./src/NodeParser/ParenthesizedNodeParser"; export * from "./src/NodeParser/TypeAliasNodeParser"; export * from "./src/NodeParser/TypeLiteralNodeParser"; diff --git a/src/NodeParser/InterfaceNodeParser.ts b/src/NodeParser/InterfaceAndClassNodeParser.ts similarity index 57% rename from src/NodeParser/InterfaceNodeParser.ts rename to src/NodeParser/InterfaceAndClassNodeParser.ts index de277e8f7..7e277b49e 100644 --- a/src/NodeParser/InterfaceNodeParser.ts +++ b/src/NodeParser/InterfaceAndClassNodeParser.ts @@ -6,17 +6,18 @@ import { ObjectProperty, ObjectType } from "../Type/ObjectType"; import { isHidden } from "../Utils/isHidden"; import { getKey } from "../Utils/nodeKey"; -export class InterfaceNodeParser implements SubNodeParser { +export class InterfaceAndClassNodeParser implements SubNodeParser { public constructor( private typeChecker: ts.TypeChecker, private childNodeParser: NodeParser, ) { } - public supportsNode(node: ts.InterfaceDeclaration): boolean { - return node.kind === ts.SyntaxKind.InterfaceDeclaration; + public supportsNode(node: ts.InterfaceDeclaration | ts.ClassDeclaration): boolean { + return node.kind === ts.SyntaxKind.InterfaceDeclaration || node.kind === ts.SyntaxKind.ClassDeclaration; } - public createType(node: ts.InterfaceDeclaration, context: Context): BaseType { + + public createType(node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context): BaseType { if (node.typeParameters && node.typeParameters.length) { node.typeParameters.forEach((typeParam) => { const nameSymbol = this.typeChecker.getSymbolAtLocation(typeParam.name)!; @@ -37,7 +38,7 @@ export class InterfaceNodeParser implements SubNodeParser { ); } - private getBaseTypes(node: ts.InterfaceDeclaration, context: Context): BaseType[] { + private getBaseTypes(node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context): BaseType[] { if (!node.heritageClauses) { return []; } @@ -48,17 +49,25 @@ export class InterfaceNodeParser implements SubNodeParser { ], []); } - private getProperties(node: ts.InterfaceDeclaration, context: Context): ObjectProperty[] { - return node.members - .filter(ts.isPropertySignature) + private getProperties(node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context): ObjectProperty[] { + function isProperty(member: ts.Node): member is (ts.PropertyDeclaration | ts.PropertySignature) { + return ts.isPropertySignature(member) || ts.isPropertyDeclaration(member); + } + return (>node.members) + .filter(isProperty) + .filter(prop => !prop.modifiers || !prop.modifiers.some(modifier => + modifier.kind === ts.SyntaxKind.PrivateKeyword || + modifier.kind === ts.SyntaxKind.ProtectedKeyword || + modifier.kind === ts.SyntaxKind.StaticKeyword)) .reduce((result: ObjectProperty[], propertyNode) => { const propertySymbol: ts.Symbol = (propertyNode as any).symbol; - if (isHidden(propertySymbol)) { + const propertyType = propertyNode.type; + if (!propertyType || isHidden(propertySymbol)) { return result; } const objectProperty: ObjectProperty = new ObjectProperty( propertySymbol.getName(), - this.childNodeParser.createType(propertyNode.type!, context), + this.childNodeParser.createType(propertyType, context), !propertyNode.questionToken, ); @@ -66,8 +75,10 @@ export class InterfaceNodeParser implements SubNodeParser { return result; }, []); } - private getAdditionalProperties(node: ts.InterfaceDeclaration, context: Context): BaseType | false { - const indexSignature = node.members.find(ts.isIndexSignatureDeclaration); + + private getAdditionalProperties(node: ts.InterfaceDeclaration | ts.ClassDeclaration, context: Context): + BaseType | false { + const indexSignature = (>node.members).find(ts.isIndexSignatureDeclaration); if (!indexSignature) { return false; } @@ -76,6 +87,7 @@ export class InterfaceNodeParser implements SubNodeParser { } private getTypeId(node: ts.Node, context: Context): string { - return `interface-${getKey(node, context)}`; + const nodeType = ts.isInterfaceDeclaration(node) ? "interface" : "class"; + return `${nodeType}-${getKey(node, context)}`; } } diff --git a/src/SchemaGenerator.ts b/src/SchemaGenerator.ts index 71d290133..c8d1e65a8 100644 --- a/src/SchemaGenerator.ts +++ b/src/SchemaGenerator.ts @@ -74,6 +74,7 @@ export class SchemaGenerator { private inspectNode(node: ts.Node, typeChecker: ts.TypeChecker, allTypes: Map): void { if ( node.kind === ts.SyntaxKind.InterfaceDeclaration || + node.kind === ts.SyntaxKind.ClassDeclaration || node.kind === ts.SyntaxKind.EnumDeclaration || node.kind === ts.SyntaxKind.TypeAliasDeclaration ) { diff --git a/test/valid-data.test.ts b/test/valid-data.test.ts index c00958650..b3967a994 100644 --- a/test/valid-data.test.ts +++ b/test/valid-data.test.ts @@ -56,6 +56,13 @@ describe("valid-data", () => { it("interface-recursion", assertSchema("interface-recursion", "MyObject")); it("interface-extra-props", assertSchema("interface-extra-props", "MyObject")); + it("class-single", assertSchema("class-single", "MyObject")); + it("class-multi", assertSchema("class-multi", "MyObject")); + it("class-recursion", assertSchema("class-recursion", "MyObject")); + it("class-extra-props", assertSchema("class-extra-props", "MyObject")); + it("class-inheritance", assertSchema("class-inheritance", "MyObject")); + it("class-generics", assertSchema("class-generics", "MyObject")); + it("structure-private", assertSchema("structure-private", "MyObject")); it("structure-anonymous", assertSchema("structure-anonymous", "MyObject")); it("structure-recursion", assertSchema("structure-recursion", "MyObject")); diff --git a/test/valid-data/class-extra-props/main.ts b/test/valid-data/class-extra-props/main.ts new file mode 100644 index 000000000..37be7fc45 --- /dev/null +++ b/test/valid-data/class-extra-props/main.ts @@ -0,0 +1,5 @@ +export class MyObject { + public required: string; + public optional?: number; + [name: string]: string|number; +} diff --git a/test/valid-data/class-extra-props/schema.json b/test/valid-data/class-extra-props/schema.json new file mode 100644 index 000000000..4dfbcb5a3 --- /dev/null +++ b/test/valid-data/class-extra-props/schema.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "type": "object", + "properties": { + "required": { + "type": "string" + }, + "optional": { + "type": "number" + } + }, + "required": [ + "required" + ], + "additionalProperties": { + "type": [ + "string", + "number" + ] + } + } + }, + "$ref": "#/definitions/MyObject" +} diff --git a/test/valid-data/class-generics/main.ts b/test/valid-data/class-generics/main.ts new file mode 100644 index 000000000..911ef095c --- /dev/null +++ b/test/valid-data/class-generics/main.ts @@ -0,0 +1,9 @@ +export class Base { + public a: T; +} + +export class MyObject extends Base { + public b: string; + public c: Base; + public d: Base; +} diff --git a/test/valid-data/class-generics/schema.json b/test/valid-data/class-generics/schema.json new file mode 100644 index 000000000..9541a9dc8 --- /dev/null +++ b/test/valid-data/class-generics/schema.json @@ -0,0 +1,54 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "Base": { + "additionalProperties": false, + "properties": { + "a": { + "type": "boolean" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + "Base": { + "additionalProperties": false, + "properties": { + "a": { + "type": "string" + } + }, + "required": [ + "a" + ], + "type": "object" + }, + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "string" + }, + "c": { + "$ref": "#/definitions/Base" + }, + "d": { + "$ref": "#/definitions/Base" + } + }, + "required": [ + "a", + "b", + "c", + "d" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/class-inheritance/main.ts b/test/valid-data/class-inheritance/main.ts new file mode 100644 index 000000000..00bc4d897 --- /dev/null +++ b/test/valid-data/class-inheritance/main.ts @@ -0,0 +1,9 @@ +export class Base { + public a: number; + public b: string | string; +} + +export class MyObject extends Base { + public c: boolean; + public b: string; +} diff --git a/test/valid-data/class-inheritance/schema.json b/test/valid-data/class-inheritance/schema.json new file mode 100644 index 000000000..cebc75839 --- /dev/null +++ b/test/valid-data/class-inheritance/schema.json @@ -0,0 +1,26 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "a": { + "type": "number" + }, + "b": { + "type": "string" + }, + "c": { + "type": "boolean" + } + }, + "required": [ + "a", + "b", + "c" + ], + "type": "object" + } + } +} diff --git a/test/valid-data/class-multi/main.ts b/test/valid-data/class-multi/main.ts new file mode 100644 index 000000000..42ee5914d --- /dev/null +++ b/test/valid-data/class-multi/main.ts @@ -0,0 +1,8 @@ +export class MyObject { + public subA: MySubObject; + public subB: MySubObject; +} +export class MySubObject { + public propA: number; + public propB: number; +} diff --git a/test/valid-data/class-multi/schema.json b/test/valid-data/class-multi/schema.json new file mode 100644 index 000000000..6d583bebb --- /dev/null +++ b/test/valid-data/class-multi/schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "type": "object", + "properties": { + "subA": { + "$ref": "#/definitions/MySubObject" + }, + "subB": { + "$ref": "#/definitions/MySubObject" + } + }, + "required": [ + "subA", + "subB" + ], + "additionalProperties": false + }, + "MySubObject": { + "type": "object", + "properties": { + "propA": { + "type": "number" + }, + "propB": { + "type": "number" + } + }, + "required": [ + "propA", + "propB" + ], + "additionalProperties": false + } + }, + "$ref": "#/definitions/MyObject" +} diff --git a/test/valid-data/class-recursion/main.ts b/test/valid-data/class-recursion/main.ts new file mode 100644 index 000000000..439dd9eab --- /dev/null +++ b/test/valid-data/class-recursion/main.ts @@ -0,0 +1,4 @@ +export class MyObject { + public propA: number; + public propB: MyObject; +} diff --git a/test/valid-data/class-recursion/schema.json b/test/valid-data/class-recursion/schema.json new file mode 100644 index 000000000..ac19cdf1a --- /dev/null +++ b/test/valid-data/class-recursion/schema.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "type": "object", + "properties": { + "propA": { + "type": "number" + }, + "propB": { + "$ref": "#/definitions/MyObject" + } + }, + "required": [ + "propA", + "propB" + ], + "additionalProperties": false + } + }, + "$ref": "#/definitions/MyObject" +} diff --git a/test/valid-data/class-single/main.ts b/test/valid-data/class-single/main.ts new file mode 100644 index 000000000..74675ea26 --- /dev/null +++ b/test/valid-data/class-single/main.ts @@ -0,0 +1,36 @@ +export class MyObject { + // Static properties must be ignored + public static staticProp: number; + + public propA: number; + public propB: number; + + // Properties without type must be ignored + public noType; + + // Protected properties must be ignored + protected protectedProp: string; + + // Protected properties must be ignored + private privateProp: boolean; + + // Constructors must be ignored + public constructor() { + this.privateProp = false; + } + + // Normal method must be ignored + public getPrivateProp() { + return this.privateProp; + } + + // Getter methods must be ignored + public get getterSetter(): number { + return this.propA; + } + + // Setter methods must be ignored + public set getterSetter(value: number) { + this.propA = value; + } +} diff --git a/test/valid-data/class-single/schema.json b/test/valid-data/class-single/schema.json new file mode 100644 index 000000000..beec5b2e5 --- /dev/null +++ b/test/valid-data/class-single/schema.json @@ -0,0 +1,22 @@ +{ + "$ref": "#/definitions/MyObject", + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "MyObject": { + "additionalProperties": false, + "properties": { + "propA": { + "type": "number" + }, + "propB": { + "type": "number" + } + }, + "required": [ + "propA", + "propB" + ], + "type": "object" + } + } +}