import {JsonSchemaErrorBase} from './error'; import {Serializer} from './serializer'; 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 {} export interface Schema { [key: string]: any; } /** This interface is defined to simplify the arguments passed in to the SchemaTreeNode. */ export type TreeNodeConstructorArgument = { parent?: SchemaTreeNode; name?: string; value: T; forward?: SchemaTreeNode; schema: Schema; }; /** * Holds all the information, including the value, of a node in the schema tree. */ export abstract class SchemaTreeNode implements SchemaNode { // Hierarchy objects protected _parent: SchemaTreeNode; protected _defined: boolean = false; protected _dirty: boolean = false; protected _schema: Schema; protected _name: string; protected _value: T; protected _forward: SchemaTreeNode; constructor(nodeMetaData: TreeNodeConstructorArgument) { this._schema = nodeMetaData.schema; this._name = nodeMetaData.name; this._value = nodeMetaData.value; this._forward = nodeMetaData.forward; this._parent = nodeMetaData.parent; } dispose() { this._parent = null; this._schema = null; this._value = null; if (this._forward) { this._forward.dispose(); } this._forward = null; } get defined() { return this._defined; } get dirty() { return this._dirty; } set dirty(v: boolean) { if (v) { this._defined = true; this._dirty = true; if (this._parent) { this._parent.dirty = true; } } } get value(): T { return this.get(); } abstract get type(): string; abstract get tsType(): TypeScriptType; abstract destroy(): void; abstract get defaultValue(): any | null; get name() { return this._name; } get readOnly(): boolean { return this._schema['readOnly']; } get frozen(): boolean { return true; } get description() { return 'description' in this._schema ? this._schema['description'] : null; } get required() { if (!this._parent) { return false; } return this._parent.isChildRequired(this.name); } isChildRequired(name: string) { return false; } get parent(): SchemaTreeNode { return this._parent; } get children(): { [key: string]: SchemaTreeNode } | null { return null; } get items(): SchemaTreeNode[] | null { return null; } get itemPrototype(): SchemaTreeNode | null { return null; } abstract get(): T; set(v: T, force = false) { if (!this.readOnly) { throw new MissingImplementationError(); } throw new SettingReadOnlyPropertyError(); }; isCompatible(v: any) { return false; } abstract serialize(serializer: Serializer): void; protected static _defineProperty(proto: any, treeNode: SchemaTreeNode): void { if (treeNode.readOnly) { Object.defineProperty(proto, treeNode.name, { enumerable: true, get: () => treeNode.get() }); } else { Object.defineProperty(proto, treeNode.name, { enumerable: true, get: () => treeNode.get(), set: (v: T) => treeNode.set(v) }); } } } /** Base Class used for Non-Leaves TreeNode. Meaning they can have children. */ export abstract class NonLeafSchemaTreeNode extends SchemaTreeNode { dispose() { for (const key of Object.keys(this.children || {})) { this.children[key].dispose(); } for (let item of this.items || []) { item.dispose(); } super.dispose(); } get() { if (this.defined) { return this._value; } else { return undefined; } } destroy() { this._defined = false; this._value = null; } // Helper function to create a child based on its schema. protected _createChildProperty(name: string, value: T, forward: SchemaTreeNode, schema: Schema, define = true): SchemaTreeNode { const type: string = ('oneOf' in schema) ? 'oneOf' : ('enum' in schema) ? 'enum' : schema['type']; let Klass: { new (arg: TreeNodeConstructorArgument): SchemaTreeNode } = null; switch (type) { case 'object': Klass = ObjectSchemaTreeNode; break; case 'array': Klass = ArraySchemaTreeNode; break; case 'string': Klass = StringSchemaTreeNode; break; case 'boolean': Klass = BooleanSchemaTreeNode; break; case 'number': Klass = NumberSchemaTreeNode; break; case 'integer': Klass = IntegerSchemaTreeNode; break; case 'enum': Klass = EnumSchemaTreeNode; break; case 'oneOf': Klass = OneOfSchemaTreeNode; break; default: throw new InvalidSchema('Type ' + type + ' not understood by SchemaClassFactory.'); } const metaData = new Klass({ parent: this, forward, value, schema, name }); if (define) { SchemaTreeNode._defineProperty(this._value, metaData); } return metaData; } } export class OneOfSchemaTreeNode extends NonLeafSchemaTreeNode { protected _typesPrototype: SchemaTreeNode[]; protected _currentTypeHolder: SchemaTreeNode | null; constructor(metaData: TreeNodeConstructorArgument) { super(metaData); let { value, forward, schema } = metaData; this._typesPrototype = schema['oneOf'].map((schema: Object) => { return this._createChildProperty('', '', forward, schema, false); }); this._currentTypeHolder = null; this._set(value, true, false); } _set(v: any, init: boolean, force: boolean) { if (!init && this.readOnly && !force) { throw new SettingReadOnlyPropertyError(); } // Find the first type prototype that is compatible with the let proto: SchemaTreeNode = null; for (let i = 0; i < this._typesPrototype.length; i++) { const p = this._typesPrototype[i]; if (p.isCompatible(v)) { proto = p; break; } } if (proto == null) { return; } if (!init) { this.dirty = true; } this._currentTypeHolder = proto; this._currentTypeHolder.set(v, true); } set(v: any, force = false) { return this._set(v, false, force); } get(): any { return this._currentTypeHolder ? this._currentTypeHolder.get() : null; } get defaultValue(): any | null { return null; } get defined() { return this._currentTypeHolder ? this._currentTypeHolder.defined : false; } get items() { return this._typesPrototype; } get type() { return 'oneOf'; } get tsType(): null { return null; } serialize(serializer: Serializer) { serializer.outputOneOf(this); } } /** A Schema Tree Node that represents an object. */ export class ObjectSchemaTreeNode extends NonLeafSchemaTreeNode<{[key: string]: any}> { // The map of all children metadata. protected _children: { [key: string]: SchemaTreeNode }; protected _frozen: boolean = false; constructor(metaData: TreeNodeConstructorArgument) { super(metaData); this._set(metaData.value, true, false); } _set(value: any, init: boolean, force: boolean) { if (!init && this.readOnly && !force) { throw new SettingReadOnlyPropertyError(); } const schema = this._schema; const forward = this._forward; this._defined = !!value; this._children = Object.create(null); this._value = Object.create(null); this._dirty = this._dirty || !init; if (schema['properties']) { for (const name of Object.keys(schema['properties'])) { const propertySchema = schema['properties'][name]; this._children[name] = this._createChildProperty( name, value ? value[name] : null, forward ? (forward as ObjectSchemaTreeNode).children[name] : null, propertySchema); } } else if (!schema['additionalProperties']) { throw new InvalidSchema('Schema does not have a properties, but doesnt allow for ' + 'additional properties.'); } if (!schema['additionalProperties']) { this._frozen = true; Object.freeze(this._value); Object.freeze(this._children); } else if (value) { // Set other properties which don't have a schema. for (const key of Object.keys(value)) { if (!this._children[key]) { this._value[key] = value[key]; } } } } set(v: any, force = false) { return this._set(v, false, force); } get frozen(): boolean { return this._frozen; } get children(): { [key: string]: SchemaTreeNode } | null { return this._children; } get type() { return 'object'; } get tsType() { return Object; } get defaultValue(): any | null { return null; } isCompatible(v: any) { return typeof v == 'object' && v !== null; } isChildRequired(name: string) { if (this._schema['required']) { return this._schema['required'].indexOf(name) != -1; } return false; } serialize(serializer: Serializer) { serializer.object(this); } } /** A Schema Tree Node that represents an array. */ export class ArraySchemaTreeNode extends NonLeafSchemaTreeNode> { // The map of all items metadata. protected _items: SchemaTreeNode[]; protected _itemPrototype: SchemaTreeNode; constructor(metaData: TreeNodeConstructorArgument>) { super(metaData); 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'], false); } _set(value: any, init: boolean, force: boolean) { const schema = this._schema; const forward = this._forward; this._defined = !!value; this._value = Object.create(null); this._dirty = this._dirty || !init; if (value) { this._defined = true; } else { this._defined = false; value = []; } this._items = []; this._value = []; for (let index = 0; index < value.length; index++) { this._items[index] = this._createChildProperty( '' + index, value && value[index], forward && (forward as ArraySchemaTreeNode).items[index], schema['items'] ); } } set(v: any, force = false) { return this._set(v, false, force); } isCompatible(v: any) { return Array.isArray(v); } get type() { return 'array'; } get tsType() { return Array; } get items(): SchemaTreeNode[] { return this._items; } get itemPrototype(): SchemaTreeNode { return this._itemPrototype; } get defaultValue(): any | null { return null; } serialize(serializer: Serializer) { serializer.array(this); } } /** * The root class of the tree node. Receives a prototype that will be filled with the * properties of the Schema root. */ export class RootSchemaTreeNode extends ObjectSchemaTreeNode { constructor(proto: any, metaData: TreeNodeConstructorArgument) { super(metaData); for (const key of Object.keys(this._children)) { if (this._children[key]) { SchemaTreeNode._defineProperty(proto, this._children[key]); } } } } /** A leaf in the schema tree. Must contain a single primitive value. */ export abstract class LeafSchemaTreeNode extends SchemaTreeNode { private _default: T; constructor(metaData: TreeNodeConstructorArgument) { super(metaData); this._defined = !(metaData.value === undefined || metaData.value === null); if ('default' in metaData.schema) { this._default = this.convert(metaData.schema['default']); } } get() { if (!this.defined && this._forward) { return this._forward.get(); } if (!this.defined && this._default !== undefined) { return this._default; } return this._value === undefined ? undefined : this.convert(this._value); } set(v: T, force = false) { if (this.readOnly && !force) { 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 = convertedValue; } destroy() { this._defined = false; this._value = null; } get defaultValue(): T { return 'default' in this._schema ? this._default : null; } abstract convert(v: any): T; abstract isCompatible(v: any): boolean; serialize(serializer: Serializer) { serializer.outputValue(this); } } /** Basic primitives for JSON Schema. */ class StringSchemaTreeNode extends LeafSchemaTreeNode { serialize(serializer: Serializer) { serializer.outputString(this); } isCompatible(v: any) { return typeof v == 'string' || v instanceof String; } convert(v: any) { return v === undefined ? undefined : '' + v; } get type() { return 'string'; } get tsType() { return String; } } 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); } isCompatible(v: any) { return typeof v == 'boolean' || v instanceof Boolean; } convert(v: any) { return v === undefined ? undefined : !!v; } get type() { return 'boolean'; } get tsType() { return Boolean; } } class NumberSchemaTreeNode extends LeafSchemaTreeNode { serialize(serializer: Serializer) { serializer.outputNumber(this); } isCompatible(v: any) { return typeof v == 'number' || v instanceof Number; } convert(v: any) { return v === undefined ? undefined : +v; } get type() { return 'number'; } get tsType() { return Number; } } class IntegerSchemaTreeNode extends NumberSchemaTreeNode { convert(v: any) { return v === undefined ? undefined : Math.floor(+v); } }