From 19dfe747325d48d713225be90cb29eb60f29caf1 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Tue, 17 Jan 2017 18:20:11 -0800 Subject: [PATCH] feat(@ngtools/json-schema): add support for enums. Close #4082 --- .../json-schema/src/schema-tree.spec.ts | 59 +++++++++++++------ .../@ngtools/json-schema/src/schema-tree.ts | 53 +++++++++++++++-- .../@ngtools/json-schema/tests/schema2.json | 13 ++++ .../@ngtools/json-schema/tests/value2-1.json | 8 +++ 4 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 packages/@ngtools/json-schema/tests/schema2.json create mode 100644 packages/@ngtools/json-schema/tests/value2-1.json diff --git a/packages/@ngtools/json-schema/src/schema-tree.spec.ts b/packages/@ngtools/json-schema/src/schema-tree.spec.ts index 46fee6f9815b..604edaa182a0 100644 --- a/packages/@ngtools/json-schema/src/schema-tree.spec.ts +++ b/packages/@ngtools/json-schema/src/schema-tree.spec.ts @@ -4,31 +4,54 @@ import {join} from 'path'; import {RootSchemaTreeNode} from './schema-tree'; -describe('SchemaTreeNode', () => { +describe('@ngtools/json-schema', () => { -}); + describe('OneOfSchemaTreeNode', () => { + const schemaJsonFilePath = join(__dirname, '../tests/schema1.json'); + const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8')); + const valueJsonFilePath = join(__dirname, '../tests/value1-1.json'); + const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8')); -describe('OneOfSchemaTreeNode', () => { - const schemaJsonFilePath = join(__dirname, '../tests/schema1.json'); - const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8')); - const valueJsonFilePath = join(__dirname, '../tests/value1-1.json'); - const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8')); + it('works', () => { + const proto: any = Object.create(null); + new RootSchemaTreeNode(proto, { + value: valueJson, + schema: schemaJson + }); + expect(proto.oneOfKey2 instanceof Array).toBe(true); + expect(proto.oneOfKey2.length).toBe(2); - it('works', () => { - const proto: any = Object.create(null); - new RootSchemaTreeNode(proto, { - value: valueJson, - schema: schemaJson + // Set it to a string, which is valid. + proto.oneOfKey2 = 'hello'; + expect(proto.oneOfKey2 instanceof Array).toBe(false); }); + }); + + + describe('EnumSchemaTreeNode', () => { + const schemaJsonFilePath = join(__dirname, '../tests/schema2.json'); + const schemaJson = JSON.parse(readFileSync(schemaJsonFilePath, 'utf-8')); + const valueJsonFilePath = join(__dirname, '../tests/value2-1.json'); + const valueJson = JSON.parse(readFileSync(valueJsonFilePath, 'utf-8')); + - expect(proto.oneOfKey2 instanceof Array).toBe(true); - expect(proto.oneOfKey2.length).toBe(2); + it('works', () => { + const proto: any = Object.create(null); + new RootSchemaTreeNode(proto, { + value: valueJson, + schema: schemaJson + }); - // Set it to a string, which is valid. - proto.oneOfKey2 = 'hello'; - expect(proto.oneOfKey2 instanceof Array).toBe(false); + expect(proto.a instanceof Array).toBe(true); + expect(proto.a).toEqual([null, 'v1', null, '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']); + }); }); -}); +}); diff --git a/packages/@ngtools/json-schema/src/schema-tree.ts b/packages/@ngtools/json-schema/src/schema-tree.ts index 75d1fe2fe814..df732d41d72d 100644 --- a/packages/@ngtools/json-schema/src/schema-tree.ts +++ b/packages/@ngtools/json-schema/src/schema-tree.ts @@ -4,6 +4,7 @@ import {SchemaNode, TypeScriptType} from './node'; export class InvalidSchema extends JsonSchemaErrorBase {} +export class InvalidValueError extends JsonSchemaErrorBase {} export class MissingImplementationError extends JsonSchemaErrorBase {} export class SettingReadOnlyPropertyError extends JsonSchemaErrorBase {} @@ -151,8 +152,9 @@ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { // Helper function to create a child based on its schema. protected _createChildProperty(name: string, value: T, forward: SchemaTreeNode, schema: Schema, define = true): SchemaTreeNode { - - let type: string = schema['oneOf'] ? 'oneOf' : schema['type']; + const type: string = + ('oneOf' in schema) ? 'oneOf' : + ('enum' in schema) ? 'enum' : schema['type']; let Klass: { new (arg: TreeNodeConstructorArgument): SchemaTreeNode } = null; switch (type) { @@ -163,6 +165,7 @@ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { case 'number': Klass = NumberSchemaTreeNode; break; case 'integer': Klass = IntegerSchemaTreeNode; break; + case 'enum': Klass = EnumSchemaTreeNode; break; case 'oneOf': Klass = OneOfSchemaTreeNode; break; default: @@ -327,7 +330,8 @@ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode> { this._set(metaData.value, true, false); // 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']); + this._itemPrototype = this._createChildProperty( + '', null, null, metaData.schema['items'], false); } _set(value: any, init: boolean, force: boolean) { @@ -397,7 +401,7 @@ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { super(metaData); this._defined = !(metaData.value === undefined || metaData.value === null); if ('default' in metaData.schema) { - this._default = metaData.schema['default']; + this._default = this.convert(metaData.schema['default']); } } @@ -415,8 +419,15 @@ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { throw new SettingReadOnlyPropertyError(); } + let convertedValue: T | null = this.convert(v); + if (convertedValue === null || convertedValue === undefined) { + if (this.required) { + throw new InvalidValueError(`Invalid value "${v}" on a required field.`); + } + } + this.dirty = true; - this._value = this.convert(v); + this._value = convertedValue; } destroy() { @@ -448,6 +459,38 @@ class StringSchemaTreeNode extends LeafSchemaTreeNode { } +class EnumSchemaTreeNode extends StringSchemaTreeNode { + private _enumValues: string[]; + + constructor(metaData: TreeNodeConstructorArgument) { + super(metaData); + + if (!Array.isArray(metaData.schema['enum'])) { + throw new InvalidSchema(); + } + this._enumValues = [].concat(metaData.schema['enum']); + this.set(metaData.value, true); + } + + protected _isInEnum(value: string) { + return this._enumValues.some(v => v === value); + } + + isCompatible(v: any) { + return (typeof v == 'string' || v instanceof String) && this._isInEnum('' + v); + } + convert(v: any) { + if (v === undefined) { + return undefined; + } + if (v === null || !this._isInEnum('' + v)) { + return null; + } + return '' + v; + } +} + + class BooleanSchemaTreeNode extends LeafSchemaTreeNode { serialize(serializer: Serializer) { serializer.outputBoolean(this); } diff --git a/packages/@ngtools/json-schema/tests/schema2.json b/packages/@ngtools/json-schema/tests/schema2.json new file mode 100644 index 000000000000..8afa29a93b42 --- /dev/null +++ b/packages/@ngtools/json-schema/tests/schema2.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "id": "JsonSchema", + "type": "object", + "properties": { + "a": { + "type": "array", + "items": { + "enum": [ "v1", "v2", "v3" ] + } + } + } +} diff --git a/packages/@ngtools/json-schema/tests/value2-1.json b/packages/@ngtools/json-schema/tests/value2-1.json new file mode 100644 index 000000000000..d646965f6fec --- /dev/null +++ b/packages/@ngtools/json-schema/tests/value2-1.json @@ -0,0 +1,8 @@ +{ + "a": [ + "INVALID", + "v1", + "INVALID", + "v3" + ] +}