Skip to content

Commit c034a44

Browse files
hanslfilipesilva
authored andcommitted
feat(@ngtools/json-schema): add support for enums.
Close #4082
1 parent 2677138 commit c034a44

File tree

4 files changed

+110
-23
lines changed

4 files changed

+110
-23
lines changed

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

+41-18
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,54 @@ import {join} from 'path';
44
import {RootSchemaTreeNode} from './schema-tree';
55

66

7-
describe('SchemaTreeNode', () => {
7+
describe('@ngtools/json-schema', () => {
88

9-
});
9+
describe('OneOfSchemaTreeNode', () => {
10+
const schemaJsonFilePath = join(__dirname, '../tests/schema1.json');
11+
const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8'));
12+
const valueJsonFilePath = join(__dirname, '../tests/value1-1.json');
13+
const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8'));
1014

1115

12-
describe('OneOfSchemaTreeNode', () => {
13-
const schemaJsonFilePath = join(__dirname, '../tests/schema1.json');
14-
const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8'));
15-
const valueJsonFilePath = join(__dirname, '../tests/value1-1.json');
16-
const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8'));
16+
it('works', () => {
17+
const proto: any = Object.create(null);
18+
new RootSchemaTreeNode(proto, {
19+
value: valueJson,
20+
schema: schemaJson
21+
});
1722

23+
expect(proto.oneOfKey2 instanceof Array).toBe(true);
24+
expect(proto.oneOfKey2.length).toBe(2);
1825

19-
it('works', () => {
20-
const proto: any = Object.create(null);
21-
new RootSchemaTreeNode(proto, {
22-
value: valueJson,
23-
schema: schemaJson
26+
// Set it to a string, which is valid.
27+
proto.oneOfKey2 = 'hello';
28+
expect(proto.oneOfKey2 instanceof Array).toBe(false);
2429
});
30+
});
31+
32+
33+
describe('EnumSchemaTreeNode', () => {
34+
const schemaJsonFilePath = join(__dirname, '../tests/schema2.json');
35+
const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8'));
36+
const valueJsonFilePath = join(__dirname, '../tests/value2-1.json');
37+
const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8'));
38+
2539

26-
expect(proto.oneOfKey2 instanceof Array).toBe(true);
27-
expect(proto.oneOfKey2.length).toBe(2);
40+
it('works', () => {
41+
const proto: any = Object.create(null);
42+
new RootSchemaTreeNode(proto, {
43+
value: valueJson,
44+
schema: schemaJson
45+
});
2846

29-
// Set it to a string, which is valid.
30-
proto.oneOfKey2 = 'hello';
31-
expect(proto.oneOfKey2 instanceof Array).toBe(false);
47+
expect(proto.a instanceof Array).toBe(true);
48+
expect(proto.a).toEqual([null, 'v1', null, 'v3']);
49+
50+
// Set it to a string, which is valid.
51+
proto.a[0] = 'v2';
52+
proto.a[1] = 'INVALID';
53+
expect(proto.a).toEqual(['v2', null, null, 'v3']);
54+
});
3255
});
33-
});
3456

57+
});

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

+48-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {SchemaNode, TypeScriptType} from './node';
44

55

66
export class InvalidSchema extends JsonSchemaErrorBase {}
7+
export class InvalidValueError extends JsonSchemaErrorBase {}
78
export class MissingImplementationError extends JsonSchemaErrorBase {}
89
export class SettingReadOnlyPropertyError extends JsonSchemaErrorBase {}
910

@@ -151,8 +152,9 @@ export abstract class NonLeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
151152
// Helper function to create a child based on its schema.
152153
protected _createChildProperty<T>(name: string, value: T, forward: SchemaTreeNode<T>,
153154
schema: Schema, define = true): SchemaTreeNode<T> {
154-
155-
let type: string = schema['oneOf'] ? 'oneOf' : schema['type'];
155+
const type: string =
156+
('oneOf' in schema) ? 'oneOf' :
157+
('enum' in schema) ? 'enum' : schema['type'];
156158
let Klass: { new (arg: TreeNodeConstructorArgument<any>): SchemaTreeNode<any> } = null;
157159

158160
switch (type) {
@@ -163,6 +165,7 @@ export abstract class NonLeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
163165
case 'number': Klass = NumberSchemaTreeNode; break;
164166
case 'integer': Klass = IntegerSchemaTreeNode; break;
165167

168+
case 'enum': Klass = EnumSchemaTreeNode; break;
166169
case 'oneOf': Klass = OneOfSchemaTreeNode; break;
167170

168171
default:
@@ -327,7 +330,8 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode<Array<any>> {
327330
this._set(metaData.value, true, false);
328331

329332
// Keep the item's schema as a schema node. This is important to keep type information.
330-
this._itemPrototype = this._createChildProperty('', null, null, metaData.schema['items']);
333+
this._itemPrototype = this._createChildProperty(
334+
'', null, null, metaData.schema['items'], false);
331335
}
332336

333337
_set(value: any, init: boolean, force: boolean) {
@@ -397,7 +401,7 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
397401
super(metaData);
398402
this._defined = !(metaData.value === undefined || metaData.value === null);
399403
if ('default' in metaData.schema) {
400-
this._default = metaData.schema['default'];
404+
this._default = this.convert(metaData.schema['default']);
401405
}
402406
}
403407

@@ -415,8 +419,15 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
415419
throw new SettingReadOnlyPropertyError();
416420
}
417421

422+
let convertedValue: T | null = this.convert(v);
423+
if (convertedValue === null || convertedValue === undefined) {
424+
if (this.required) {
425+
throw new InvalidValueError(`Invalid value "${v}" on a required field.`);
426+
}
427+
}
428+
418429
this.dirty = true;
419-
this._value = this.convert(v);
430+
this._value = convertedValue;
420431
}
421432

422433
destroy() {
@@ -448,6 +459,38 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode<string> {
448459
}
449460

450461

462+
class EnumSchemaTreeNode extends StringSchemaTreeNode {
463+
private _enumValues: string[];
464+
465+
constructor(metaData: TreeNodeConstructorArgument<string>) {
466+
super(metaData);
467+
468+
if (!Array.isArray(metaData.schema['enum'])) {
469+
throw new InvalidSchema();
470+
}
471+
this._enumValues = [].concat(metaData.schema['enum']);
472+
this.set(metaData.value, true);
473+
}
474+
475+
protected _isInEnum(value: string) {
476+
return this._enumValues.some(v => v === value);
477+
}
478+
479+
isCompatible(v: any) {
480+
return (typeof v == 'string' || v instanceof String) && this._isInEnum('' + v);
481+
}
482+
convert(v: any) {
483+
if (v === undefined) {
484+
return undefined;
485+
}
486+
if (v === null || !this._isInEnum('' + v)) {
487+
return null;
488+
}
489+
return '' + v;
490+
}
491+
}
492+
493+
451494
class BooleanSchemaTreeNode extends LeafSchemaTreeNode<boolean> {
452495
serialize(serializer: Serializer) { serializer.outputBoolean(this); }
453496

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"id": "JsonSchema",
4+
"type": "object",
5+
"properties": {
6+
"a": {
7+
"type": "array",
8+
"items": {
9+
"enum": [ "v1", "v2", "v3" ]
10+
}
11+
}
12+
}
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"a": [
3+
"INVALID",
4+
"v1",
5+
"INVALID",
6+
"v3"
7+
]
8+
}

0 commit comments

Comments
 (0)