From 6ff0f80271b964144b615e6d973a3c349582a0f1 Mon Sep 17 00:00:00 2001 From: Hans Date: Mon, 6 Feb 2017 19:39:31 -0800 Subject: [PATCH] fix(@ngtools/json-schema): support enums in d.ts (#4426) And add better tests. Now enums are typed on their values, not just string. Also add support for undefined if a value is truly undefined. NULL is valid JSON value. --- package.json | 2 +- packages/@angular/cli/lib/config/schema.json | 4 +- .../json-schema/src/schema-tree.spec.ts | 4 +- .../@ngtools/json-schema/src/schema-tree.ts | 24 ++++-- .../@ngtools/json-schema/src/serializer.ts | 1 + .../json-schema/src/serializers/dts.spec.ts | 49 ++++++----- .../json-schema/src/serializers/dts.ts | 17 +++- .../json-schema/src/serializers/json.ts | 3 + .../json-schema/tests/serializer/schema2.json | 6 ++ .../json-schema/tests/serializer/value1.d.ts | 26 ++++++ .../json-schema/tests/serializer/value2.d.ts | 5 ++ .../json-schema/tests/serializer/value2.json | 6 ++ .../json-schema/tests/serializer/value3.d.ts | 85 +++++++++++++++++++ 13 files changed, 196 insertions(+), 36 deletions(-) create mode 100644 packages/@ngtools/json-schema/tests/serializer/value1.d.ts create mode 100644 packages/@ngtools/json-schema/tests/serializer/value2.d.ts create mode 100644 packages/@ngtools/json-schema/tests/serializer/value3.d.ts diff --git a/package.json b/package.json index 674b150cbdd2..72d716853c86 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "test:inspect": "node --inspect --debug-brk tests/runner", "test:packages": "node scripts/run-packages-spec.js", "eslint": "eslint .", - "tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"", + "tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/tests/**\" -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"", "lint": "npm-run-all -c eslint tslint" }, "repository": { diff --git a/packages/@angular/cli/lib/config/schema.json b/packages/@angular/cli/lib/config/schema.json index a07a4fd92e14..9c1d753aa09b 100644 --- a/packages/@angular/cli/lib/config/schema.json +++ b/packages/@angular/cli/lib/config/schema.json @@ -1,6 +1,6 @@ { - "$schema": "http://json-schema.org/draft-04/schema#", - "id": "CliConfig", + "$schema": "http://json-schema.org/schema", + "id": "https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json#CliConfig", "title": "Angular CLI Config Schema", "type": "object", "properties": { diff --git a/packages/@ngtools/json-schema/src/schema-tree.spec.ts b/packages/@ngtools/json-schema/src/schema-tree.spec.ts index b220634a6fe0..25363431d4fe 100644 --- a/packages/@ngtools/json-schema/src/schema-tree.spec.ts +++ b/packages/@ngtools/json-schema/src/schema-tree.spec.ts @@ -53,12 +53,12 @@ describe('@ngtools/json-schema', () => { }); expect(proto.a instanceof Array).toBe(true); - expect(proto.a).toEqual([null, 'v1', null, 'v3']); + expect(proto.a).toEqual([undefined, 'v1', undefined, 'v3']); // Set it to a string, which is valid. proto.a[0] = 'v2'; proto.a[1] = 'INVALID'; - expect(proto.a).toEqual(['v2', null, null, 'v3']); + expect(proto.a).toEqual(['v2', undefined, undefined, 'v3']); }); it('supports default values', () => { diff --git a/packages/@ngtools/json-schema/src/schema-tree.ts b/packages/@ngtools/json-schema/src/schema-tree.ts index 539940f7e7d1..a946edb83242 100644 --- a/packages/@ngtools/json-schema/src/schema-tree.ts +++ b/packages/@ngtools/json-schema/src/schema-tree.ts @@ -273,7 +273,7 @@ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: const propertySchema = schema['properties'][name]; this._children[name] = this._createChildProperty( name, - value ? value[name] : null, + value ? value[name] : undefined, forward ? (forward as ObjectSchemaTreeNode).children[name] : null, propertySchema); } @@ -331,7 +331,7 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode> { // Keep the item's schema as a schema node. This is important to keep type information. this._itemPrototype = this._createChildProperty( - '', null, null, metaData.schema['items'], false); + '', undefined, null, metaData.schema['items'], false); } _set(value: any, init: boolean, force: boolean) { @@ -398,7 +398,7 @@ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { constructor(metaData: TreeNodeConstructorArgument) { super(metaData); - this._defined = !(metaData.value === undefined || metaData.value === null); + this._defined = metaData.value !== undefined; if ('default' in metaData.schema) { this._default = this.convert(metaData.schema['default']); } @@ -463,8 +463,8 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode { } -class EnumSchemaTreeNode extends StringSchemaTreeNode { - constructor(metaData: TreeNodeConstructorArgument) { +class EnumSchemaTreeNode extends LeafSchemaTreeNode { + constructor(metaData: TreeNodeConstructorArgument) { super(metaData); if (!Array.isArray(metaData.schema['enum'])) { @@ -480,18 +480,24 @@ class EnumSchemaTreeNode extends StringSchemaTreeNode { return this._schema['enum'].some((v: string) => v === value); } + get items() { return this._schema['enum']; } + isCompatible(v: any) { - return (typeof v == 'string' || v instanceof String) && this._isInEnum('' + v); + return this._isInEnum(v); } convert(v: any) { if (v === undefined) { return undefined; } - if (v === null || !this._isInEnum('' + v)) { - return null; + if (!this._isInEnum(v)) { + return undefined; } - return '' + v; + return v; } + + get type() { return 'any'; } + get tsType(): null { return null; } + serialize(serializer: Serializer) { serializer.outputEnum(this); } } diff --git a/packages/@ngtools/json-schema/src/serializer.ts b/packages/@ngtools/json-schema/src/serializer.ts index ddcb5d7f6c1b..00b2370bc4e7 100644 --- a/packages/@ngtools/json-schema/src/serializer.ts +++ b/packages/@ngtools/json-schema/src/serializer.ts @@ -16,6 +16,7 @@ export abstract class Serializer { abstract array(node: SchemaNode): void; abstract outputOneOf(node: SchemaNode): void; + abstract outputEnum(node: SchemaNode): void; abstract outputString(node: SchemaNode): void; abstract outputNumber(node: SchemaNode): void; diff --git a/packages/@ngtools/json-schema/src/serializers/dts.spec.ts b/packages/@ngtools/json-schema/src/serializers/dts.spec.ts index cb290690ab1b..69eb833e5242 100644 --- a/packages/@ngtools/json-schema/src/serializers/dts.spec.ts +++ b/packages/@ngtools/json-schema/src/serializers/dts.spec.ts @@ -1,5 +1,6 @@ -import * as path from 'path'; import * as fs from 'fs'; +import * as path from 'path'; +import * as ts from 'typescript'; import {DTsSerializer} from './dts'; import {SchemaClassFactory} from '../schema-class-factory'; @@ -7,24 +8,30 @@ import {RootSchemaTreeNode} from '../schema-tree'; describe('DtsSerializer', () => { - const schemaJsonFilePath = path.join(__dirname, '../../tests/schema1.json'); - const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8')); - const schemaClass = new (SchemaClassFactory(schemaJson))({}); - const schema: RootSchemaTreeNode = schemaClass.$$schema(); - - it('works', () => { - let str = ''; - function writer(s: string) { - str += s; - } - - const serializer = new DTsSerializer(writer, 'HelloWorld'); - - serializer.start(); - schema.serialize(serializer); - serializer.end(); - - // Expect optional properties to be followed by `?` - expect(str).toMatch(/stringKey\?/); - }); + for (const nb of [1, 2, 3]) { + it(`works (${nb})`, () => { + const schemaJsonFilePath = path.join(__dirname, `../../tests/serializer/schema${nb}.json`); + const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8')); + const valueDTsFilePath = path.join(__dirname, `../../tests/serializer/value${nb}.d.ts`); + const valueDTs = fs.readFileSync(valueDTsFilePath, 'utf-8'); + const valueSourceFile = ts.createSourceFile('test.d.ts', valueDTs, ts.ScriptTarget.Latest); + + const schemaClass = new (SchemaClassFactory(schemaJson))({}); + const schema: RootSchemaTreeNode = schemaClass.$$schema(); + + let str = ''; + function writer(s: string) { + str += s; + } + + const serializer = new DTsSerializer(writer); + + serializer.start(); + schema.serialize(serializer); + serializer.end(); + + const sourceFile = ts.createSourceFile('test.d.ts', str, ts.ScriptTarget.Latest); + expect(sourceFile).toEqual(valueSourceFile); + }); + } }); diff --git a/packages/@ngtools/json-schema/src/serializers/dts.ts b/packages/@ngtools/json-schema/src/serializers/dts.ts index 16029edf9ed0..15d9ae91646f 100644 --- a/packages/@ngtools/json-schema/src/serializers/dts.ts +++ b/packages/@ngtools/json-schema/src/serializers/dts.ts @@ -15,7 +15,7 @@ export class DTsSerializer implements Serializer { if (interfaceName) { _writer(`export interface ${interfaceName} `); } else { - _writer('export default interface '); + _writer('interface _ '); } } @@ -52,6 +52,9 @@ export class DTsSerializer implements Serializer { if (this._indentDelta) { this._writer('\n'); } + if (!this.interfaceName) { + this._writer('export default _;\n'); + } } object(node: SchemaNode) { @@ -126,6 +129,18 @@ export class DTsSerializer implements Serializer { this._writer(')'); } + outputEnum(node: SchemaNode) { + this._willOutputValue(); + this._writer('('); + for (let i = 0; i < node.items.length; i++) { + this._writer(JSON.stringify(node.items[i])); + if (i != node.items.length - 1) { + this._writer(' | '); + } + } + this._writer(')'); + } + outputValue(node: SchemaNode) { this._willOutputValue(); this._writer('any'); diff --git a/packages/@ngtools/json-schema/src/serializers/json.ts b/packages/@ngtools/json-schema/src/serializers/json.ts index 156778088db3..6c1e6f08ff72 100644 --- a/packages/@ngtools/json-schema/src/serializers/json.ts +++ b/packages/@ngtools/json-schema/src/serializers/json.ts @@ -126,6 +126,9 @@ export class JsonSerializer implements Serializer { outputOneOf(node: SchemaNode) { this.outputValue(node); } + outputEnum(node: SchemaNode) { + this.outputValue(node); + } outputValue(node: SchemaNode) { this._willOutputValue(); diff --git a/packages/@ngtools/json-schema/tests/serializer/schema2.json b/packages/@ngtools/json-schema/tests/serializer/schema2.json index 8afa29a93b42..989f924b8ecf 100644 --- a/packages/@ngtools/json-schema/tests/serializer/schema2.json +++ b/packages/@ngtools/json-schema/tests/serializer/schema2.json @@ -8,6 +8,12 @@ "items": { "enum": [ "v1", "v2", "v3" ] } + }, + "b": { + "type": "array", + "items": { + "enum": [ 0, 1, "string", true, null ] + } } } } diff --git a/packages/@ngtools/json-schema/tests/serializer/value1.d.ts b/packages/@ngtools/json-schema/tests/serializer/value1.d.ts new file mode 100644 index 000000000000..f4ac4a1ff47e --- /dev/null +++ b/packages/@ngtools/json-schema/tests/serializer/value1.d.ts @@ -0,0 +1,26 @@ +interface _ { + requiredKey: number; + stringKeyDefault?: string; + stringKey?: string; + booleanKey?: boolean; + numberKey?: number; + oneOfKey1?: (string | number); + oneOfKey2?: (string | string[]); + objectKey1?: { + stringKey?: string; + objectKey?: { + stringKey?: string; + }; + }; + objectKey2?: { + stringKey?: string; + [name: string]: any; + }; + arrayKey1?: { + stringKey?: string; + }[]; + arrayKey2?: { + stringKey?: string; + }[]; +} +export default _; diff --git a/packages/@ngtools/json-schema/tests/serializer/value2.d.ts b/packages/@ngtools/json-schema/tests/serializer/value2.d.ts new file mode 100644 index 000000000000..81ffef785d80 --- /dev/null +++ b/packages/@ngtools/json-schema/tests/serializer/value2.d.ts @@ -0,0 +1,5 @@ +interface _ { + a?: ("v1" | "v2" | "v3")[]; + b?: (0 | 1 | "string" | true | null)[]; +} +export default _; diff --git a/packages/@ngtools/json-schema/tests/serializer/value2.json b/packages/@ngtools/json-schema/tests/serializer/value2.json index 2cdb81d6a4fa..bb8bdf0300c6 100644 --- a/packages/@ngtools/json-schema/tests/serializer/value2.json +++ b/packages/@ngtools/json-schema/tests/serializer/value2.json @@ -4,5 +4,11 @@ "v1", "v2", "v3" + ], + "b": [ + 1, + null, + "string", + true ] } diff --git a/packages/@ngtools/json-schema/tests/serializer/value3.d.ts b/packages/@ngtools/json-schema/tests/serializer/value3.d.ts new file mode 100644 index 000000000000..150edbc203ee --- /dev/null +++ b/packages/@ngtools/json-schema/tests/serializer/value3.d.ts @@ -0,0 +1,85 @@ +interface _ { + /** + * The global configuration of the project. + */ + project?: { + version?: string; + name?: string; + }; + /** + * Properties of the different applications in this project. + */ + apps?: { + root?: string; + outDir?: string; + assets?: (string | string[]); + deployUrl?: string; + index?: string; + main?: string; + test?: string; + tsconfig?: string; + prefix?: string; + /** + * Global styles to be included in the build. + */ + styles?: (string | { + input?: string; + [name: string]: any; + })[]; + /** + * Global scripts to be included in the build. + */ + scripts?: (string | { + input: string; + [name: string]: any; + })[]; + /** + * Name and corresponding file for environment config. + */ + environments?: { + [name: string]: any; + }; + }[]; + /** + * Configuration reserved for installed third party addons. + */ + addons?: { + [name: string]: any; + }[]; + /** + * Configuration reserved for installed third party packages. + */ + packages?: { + [name: string]: any; + }[]; + e2e?: { + protractor?: { + config?: string; + }; + }; + test?: { + karma?: { + config?: string; + }; + }; + defaults?: { + styleExt?: string; + prefixInterfaces?: boolean; + poll?: number; + viewEncapsulation?: string; + changeDetection?: string; + inline?: { + style?: boolean; + template?: boolean; + }; + spec?: { + class?: boolean; + component?: boolean; + directive?: boolean; + module?: boolean; + pipe?: boolean; + service?: boolean; + }; + }; +} +export default _;