Skip to content

Commit 6ff0f80

Browse files
authored
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.
1 parent 55224a1 commit 6ff0f80

File tree

13 files changed

+196
-36
lines changed

13 files changed

+196
-36
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"test:inspect": "node --inspect --debug-brk tests/runner",
2222
"test:packages": "node scripts/run-packages-spec.js",
2323
"eslint": "eslint .",
24-
"tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"",
24+
"tslint": "tslint \"**/*.ts\" -c tslint.json -e \"**/tests/**\" -e \"**/blueprints/*/files/**/*.ts\" -e \"node_modules/**\" -e \"tmp/**\" -e \"dist/**\"",
2525
"lint": "npm-run-all -c eslint tslint"
2626
},
2727
"repository": {

packages/@angular/cli/lib/config/schema.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
2-
"$schema": "http://json-schema.org/draft-04/schema#",
3-
"id": "CliConfig",
2+
"$schema": "http://json-schema.org/schema",
3+
"id": "https://github.com/angular/angular-cli/blob/master/packages/@angular/cli/lib/config/schema.json#CliConfig",
44
"title": "Angular CLI Config Schema",
55
"type": "object",
66
"properties": {

packages/@ngtools/json-schema/src/schema-tree.spec.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,12 @@ describe('@ngtools/json-schema', () => {
5353
});
5454

5555
expect(proto.a instanceof Array).toBe(true);
56-
expect(proto.a).toEqual([null, 'v1', null, 'v3']);
56+
expect(proto.a).toEqual([undefined, 'v1', undefined, 'v3']);
5757

5858
// Set it to a string, which is valid.
5959
proto.a[0] = 'v2';
6060
proto.a[1] = 'INVALID';
61-
expect(proto.a).toEqual(['v2', null, null, 'v3']);
61+
expect(proto.a).toEqual(['v2', undefined, undefined, 'v3']);
6262
});
6363

6464
it('supports default values', () => {

packages/@ngtools/json-schema/src/schema-tree.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,7 @@ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]:
273273
const propertySchema = schema['properties'][name];
274274
this._children[name] = this._createChildProperty(
275275
name,
276-
value ? value[name] : null,
276+
value ? value[name] : undefined,
277277
forward ? (forward as ObjectSchemaTreeNode).children[name] : null,
278278
propertySchema);
279279
}
@@ -331,7 +331,7 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode<Array<any>> {
331331

332332
// Keep the item's schema as a schema node. This is important to keep type information.
333333
this._itemPrototype = this._createChildProperty(
334-
'', null, null, metaData.schema['items'], false);
334+
'', undefined, null, metaData.schema['items'], false);
335335
}
336336

337337
_set(value: any, init: boolean, force: boolean) {
@@ -398,7 +398,7 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
398398

399399
constructor(metaData: TreeNodeConstructorArgument<T>) {
400400
super(metaData);
401-
this._defined = !(metaData.value === undefined || metaData.value === null);
401+
this._defined = metaData.value !== undefined;
402402
if ('default' in metaData.schema) {
403403
this._default = this.convert(metaData.schema['default']);
404404
}
@@ -463,8 +463,8 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode<string> {
463463
}
464464

465465

466-
class EnumSchemaTreeNode extends StringSchemaTreeNode {
467-
constructor(metaData: TreeNodeConstructorArgument<string>) {
466+
class EnumSchemaTreeNode extends LeafSchemaTreeNode<any> {
467+
constructor(metaData: TreeNodeConstructorArgument<any>) {
468468
super(metaData);
469469

470470
if (!Array.isArray(metaData.schema['enum'])) {
@@ -480,18 +480,24 @@ class EnumSchemaTreeNode extends StringSchemaTreeNode {
480480
return this._schema['enum'].some((v: string) => v === value);
481481
}
482482

483+
get items() { return this._schema['enum']; }
484+
483485
isCompatible(v: any) {
484-
return (typeof v == 'string' || v instanceof String) && this._isInEnum('' + v);
486+
return this._isInEnum(v);
485487
}
486488
convert(v: any) {
487489
if (v === undefined) {
488490
return undefined;
489491
}
490-
if (v === null || !this._isInEnum('' + v)) {
491-
return null;
492+
if (!this._isInEnum(v)) {
493+
return undefined;
492494
}
493-
return '' + v;
495+
return v;
494496
}
497+
498+
get type() { return 'any'; }
499+
get tsType(): null { return null; }
500+
serialize(serializer: Serializer) { serializer.outputEnum(this); }
495501
}
496502

497503

packages/@ngtools/json-schema/src/serializer.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export abstract class Serializer {
1616
abstract array(node: SchemaNode): void;
1717

1818
abstract outputOneOf(node: SchemaNode): void;
19+
abstract outputEnum(node: SchemaNode): void;
1920

2021
abstract outputString(node: SchemaNode): void;
2122
abstract outputNumber(node: SchemaNode): void;
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,37 @@
1-
import * as path from 'path';
21
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as ts from 'typescript';
34

45
import {DTsSerializer} from './dts';
56
import {SchemaClassFactory} from '../schema-class-factory';
67
import {RootSchemaTreeNode} from '../schema-tree';
78

89

910
describe('DtsSerializer', () => {
10-
const schemaJsonFilePath = path.join(__dirname, '../../tests/schema1.json');
11-
const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8'));
12-
const schemaClass = new (SchemaClassFactory(schemaJson))({});
13-
const schema: RootSchemaTreeNode = schemaClass.$$schema();
14-
15-
it('works', () => {
16-
let str = '';
17-
function writer(s: string) {
18-
str += s;
19-
}
20-
21-
const serializer = new DTsSerializer(writer, 'HelloWorld');
22-
23-
serializer.start();
24-
schema.serialize(serializer);
25-
serializer.end();
26-
27-
// Expect optional properties to be followed by `?`
28-
expect(str).toMatch(/stringKey\?/);
29-
});
11+
for (const nb of [1, 2, 3]) {
12+
it(`works (${nb})`, () => {
13+
const schemaJsonFilePath = path.join(__dirname, `../../tests/serializer/schema${nb}.json`);
14+
const schemaJson = JSON.parse(fs.readFileSync(schemaJsonFilePath, 'utf-8'));
15+
const valueDTsFilePath = path.join(__dirname, `../../tests/serializer/value${nb}.d.ts`);
16+
const valueDTs = fs.readFileSync(valueDTsFilePath, 'utf-8');
17+
const valueSourceFile = ts.createSourceFile('test.d.ts', valueDTs, ts.ScriptTarget.Latest);
18+
19+
const schemaClass = new (SchemaClassFactory(schemaJson))({});
20+
const schema: RootSchemaTreeNode = schemaClass.$$schema();
21+
22+
let str = '';
23+
function writer(s: string) {
24+
str += s;
25+
}
26+
27+
const serializer = new DTsSerializer(writer);
28+
29+
serializer.start();
30+
schema.serialize(serializer);
31+
serializer.end();
32+
33+
const sourceFile = ts.createSourceFile('test.d.ts', str, ts.ScriptTarget.Latest);
34+
expect(sourceFile).toEqual(valueSourceFile);
35+
});
36+
}
3037
});

packages/@ngtools/json-schema/src/serializers/dts.ts

+16-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export class DTsSerializer implements Serializer {
1515
if (interfaceName) {
1616
_writer(`export interface ${interfaceName} `);
1717
} else {
18-
_writer('export default interface ');
18+
_writer('interface _ ');
1919
}
2020
}
2121

@@ -52,6 +52,9 @@ export class DTsSerializer implements Serializer {
5252
if (this._indentDelta) {
5353
this._writer('\n');
5454
}
55+
if (!this.interfaceName) {
56+
this._writer('export default _;\n');
57+
}
5558
}
5659

5760
object(node: SchemaNode) {
@@ -126,6 +129,18 @@ export class DTsSerializer implements Serializer {
126129
this._writer(')');
127130
}
128131

132+
outputEnum(node: SchemaNode) {
133+
this._willOutputValue();
134+
this._writer('(');
135+
for (let i = 0; i < node.items.length; i++) {
136+
this._writer(JSON.stringify(node.items[i]));
137+
if (i != node.items.length - 1) {
138+
this._writer(' | ');
139+
}
140+
}
141+
this._writer(')');
142+
}
143+
129144
outputValue(node: SchemaNode) {
130145
this._willOutputValue();
131146
this._writer('any');

packages/@ngtools/json-schema/src/serializers/json.ts

+3
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ export class JsonSerializer implements Serializer {
126126
outputOneOf(node: SchemaNode) {
127127
this.outputValue(node);
128128
}
129+
outputEnum(node: SchemaNode) {
130+
this.outputValue(node);
131+
}
129132

130133
outputValue(node: SchemaNode) {
131134
this._willOutputValue();

packages/@ngtools/json-schema/tests/serializer/schema2.json

+6
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"items": {
99
"enum": [ "v1", "v2", "v3" ]
1010
}
11+
},
12+
"b": {
13+
"type": "array",
14+
"items": {
15+
"enum": [ 0, 1, "string", true, null ]
16+
}
1117
}
1218
}
1319
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
interface _ {
2+
requiredKey: number;
3+
stringKeyDefault?: string;
4+
stringKey?: string;
5+
booleanKey?: boolean;
6+
numberKey?: number;
7+
oneOfKey1?: (string | number);
8+
oneOfKey2?: (string | string[]);
9+
objectKey1?: {
10+
stringKey?: string;
11+
objectKey?: {
12+
stringKey?: string;
13+
};
14+
};
15+
objectKey2?: {
16+
stringKey?: string;
17+
[name: string]: any;
18+
};
19+
arrayKey1?: {
20+
stringKey?: string;
21+
}[];
22+
arrayKey2?: {
23+
stringKey?: string;
24+
}[];
25+
}
26+
export default _;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
interface _ {
2+
a?: ("v1" | "v2" | "v3")[];
3+
b?: (0 | 1 | "string" | true | null)[];
4+
}
5+
export default _;

packages/@ngtools/json-schema/tests/serializer/value2.json

+6
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"v1",
55
"v2",
66
"v3"
7+
],
8+
"b": [
9+
1,
10+
null,
11+
"string",
12+
true
713
]
814
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
interface _ {
2+
/**
3+
* The global configuration of the project.
4+
*/
5+
project?: {
6+
version?: string;
7+
name?: string;
8+
};
9+
/**
10+
* Properties of the different applications in this project.
11+
*/
12+
apps?: {
13+
root?: string;
14+
outDir?: string;
15+
assets?: (string | string[]);
16+
deployUrl?: string;
17+
index?: string;
18+
main?: string;
19+
test?: string;
20+
tsconfig?: string;
21+
prefix?: string;
22+
/**
23+
* Global styles to be included in the build.
24+
*/
25+
styles?: (string | {
26+
input?: string;
27+
[name: string]: any;
28+
})[];
29+
/**
30+
* Global scripts to be included in the build.
31+
*/
32+
scripts?: (string | {
33+
input: string;
34+
[name: string]: any;
35+
})[];
36+
/**
37+
* Name and corresponding file for environment config.
38+
*/
39+
environments?: {
40+
[name: string]: any;
41+
};
42+
}[];
43+
/**
44+
* Configuration reserved for installed third party addons.
45+
*/
46+
addons?: {
47+
[name: string]: any;
48+
}[];
49+
/**
50+
* Configuration reserved for installed third party packages.
51+
*/
52+
packages?: {
53+
[name: string]: any;
54+
}[];
55+
e2e?: {
56+
protractor?: {
57+
config?: string;
58+
};
59+
};
60+
test?: {
61+
karma?: {
62+
config?: string;
63+
};
64+
};
65+
defaults?: {
66+
styleExt?: string;
67+
prefixInterfaces?: boolean;
68+
poll?: number;
69+
viewEncapsulation?: string;
70+
changeDetection?: string;
71+
inline?: {
72+
style?: boolean;
73+
template?: boolean;
74+
};
75+
spec?: {
76+
class?: boolean;
77+
component?: boolean;
78+
directive?: boolean;
79+
module?: boolean;
80+
pipe?: boolean;
81+
service?: boolean;
82+
};
83+
};
84+
}
85+
export default _;

0 commit comments

Comments
 (0)