Skip to content

Commit

Permalink
fix(@ngtools/json-schema): support enums in d.ts (#4426)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
hansl authored Feb 7, 2017
1 parent 55224a1 commit 6ff0f80
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 36 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions packages/@angular/cli/lib/config/schema.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions packages/@ngtools/json-schema/src/schema-tree.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
24 changes: 15 additions & 9 deletions packages/@ngtools/json-schema/src/schema-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down Expand Up @@ -331,7 +331,7 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode<Array<any>> {

// 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) {
Expand Down Expand Up @@ -398,7 +398,7 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {

constructor(metaData: TreeNodeConstructorArgument<T>) {
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']);
}
Expand Down Expand Up @@ -463,8 +463,8 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode<string> {
}


class EnumSchemaTreeNode extends StringSchemaTreeNode {
constructor(metaData: TreeNodeConstructorArgument<string>) {
class EnumSchemaTreeNode extends LeafSchemaTreeNode<any> {
constructor(metaData: TreeNodeConstructorArgument<any>) {
super(metaData);

if (!Array.isArray(metaData.schema['enum'])) {
Expand All @@ -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); }
}


Expand Down
1 change: 1 addition & 0 deletions packages/@ngtools/json-schema/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
49 changes: 28 additions & 21 deletions packages/@ngtools/json-schema/src/serializers/dts.spec.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,37 @@
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';
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);
});
}
});
17 changes: 16 additions & 1 deletion packages/@ngtools/json-schema/src/serializers/dts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class DTsSerializer implements Serializer {
if (interfaceName) {
_writer(`export interface ${interfaceName} `);
} else {
_writer('export default interface ');
_writer('interface _ ');
}
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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');
Expand Down
3 changes: 3 additions & 0 deletions packages/@ngtools/json-schema/src/serializers/json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions packages/@ngtools/json-schema/tests/serializer/schema2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
"items": {
"enum": [ "v1", "v2", "v3" ]
}
},
"b": {
"type": "array",
"items": {
"enum": [ 0, 1, "string", true, null ]
}
}
}
}
26 changes: 26 additions & 0 deletions packages/@ngtools/json-schema/tests/serializer/value1.d.ts
Original file line number Diff line number Diff line change
@@ -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 _;
5 changes: 5 additions & 0 deletions packages/@ngtools/json-schema/tests/serializer/value2.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface _ {
a?: ("v1" | "v2" | "v3")[];
b?: (0 | 1 | "string" | true | null)[];
}
export default _;
6 changes: 6 additions & 0 deletions packages/@ngtools/json-schema/tests/serializer/value2.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,11 @@
"v1",
"v2",
"v3"
],
"b": [
1,
null,
"string",
true
]
}
85 changes: 85 additions & 0 deletions packages/@ngtools/json-schema/tests/serializer/value3.d.ts
Original file line number Diff line number Diff line change
@@ -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 _;

0 comments on commit 6ff0f80

Please sign in to comment.