Skip to content

Commit 46d35e7

Browse files
committed
feat(@ngtools/json-schema): add support for enums.
1 parent 088ebf0 commit 46d35e7

File tree

4 files changed

+109
-23
lines changed

4 files changed

+109
-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

+47-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,7 @@ 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('', null, null, metaData.schema['items'], false);
331334
}
332335

333336
_set(value: any, init: boolean, force: boolean) {
@@ -397,7 +400,7 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
397400
super(metaData);
398401
this._defined = !(metaData.value === undefined || metaData.value === null);
399402
if ('default' in metaData.schema) {
400-
this._default = metaData.schema['default'];
403+
this._default = this.convert(metaData.schema['default']);
401404
}
402405
}
403406

@@ -415,8 +418,15 @@ export abstract class LeafSchemaTreeNode<T> extends SchemaTreeNode<T> {
415418
throw new SettingReadOnlyPropertyError();
416419
}
417420

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

422432
destroy() {
@@ -448,6 +458,38 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode<string> {
448458
}
449459

450460

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

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)