UNPKG

angular2-json-schema-form

Version:
690 lines (664 loc) 26 kB
import { Injectable } from '@angular/core'; import { isDefined, isEmpty, isObject, isArray, isMap } from './validator.functions'; import { hasOwn, copy } from './utility.functions'; /** * 'JsonPointer' class * * Some utilities for using JSON Pointers with JSON objects * https://tools.ietf.org/html/rfc6901 * * get, getFirst, set, setCopy, insert, insertCopy, remove, has, dict, * forEachDeep, forEachDeepCopy, escape, unescape, parse, compile, toKey, * isJsonPointer, isSubPointer, toIndexedPointer, toGenericPointer, * toControlPointer, parseObjectPath * * Partly based on manuelstofer's json-pointer utilities * https://github.com/manuelstofer/json-pointer */ export type Pointer = string | string[]; @Injectable() export class JsonPointer { /** * 'get' function * * Uses a JSON Pointer to retrieve a value from an object * * @param {object} object - Object to get value from * @param {Pointer} pointer - JSON Pointer (string or array) * @param {number = 0} startSlice - Zero-based index of first Pointer key to use * @param {number} endSlice - Zero-based index of last Pointer key to use * @param {boolean = false} getBoolean - Return only true or false? * @param {boolean = true} errors - Show error if not found? * @return {object} - Located value (or true or false if getBoolean = true) */ static get( object: any, pointer: Pointer, startSlice: number = 0, endSlice: number = null, getBoolean: boolean = false, errors: boolean = false ): any { if (object === null) { return getBoolean ? false : undefined; } let keyArray: any[] = this.parse(pointer); if (typeof object === 'object' && keyArray !== null) { let subObject = object; if (startSlice >= keyArray.length || endSlice <= -keyArray.length) { return object; } if (startSlice <= -keyArray.length) { startSlice = 0; } if (!isDefined(endSlice) || endSlice >= keyArray.length) { endSlice = keyArray.length; } keyArray = keyArray.slice(startSlice, endSlice); for (let key of keyArray) { if (key === '-' && isArray(subObject) && subObject.length) { key = subObject.length - 1; } if (typeof subObject === 'object' && subObject !== null && hasOwn(subObject, key) ) { subObject = subObject[key]; } else if (isMap(subObject) && subObject.has(key)) { subObject = subObject.get(key); } else { if (errors) { console.error('get error: "' + key + '" key not found in object.'); console.error(pointer); console.error(object); } return getBoolean ? false : undefined; } } return getBoolean ? true : subObject; } if (errors && keyArray === null) { console.error('get error: Invalid JSON Pointer: ' + pointer); } if (errors && typeof object !== 'object') { console.error('get error: Invalid object:='); console.error(object); } return getBoolean ? false : undefined; } /** * 'getFirst' function * * Takes an array of JSON Pointers and objects, and returns the value * from the first pointer to find a value in its object. * * @param {[object, pointer][]} items - array of objects and pointers to check * @param {any} defaultValue - Optional value to return if nothing found * @return {any} - first set value */ static getFirst(items: any, defaultValue: any = null): any { if (isEmpty(items)) { return; } if (isArray(items)) { for (let item of items) { if (isEmpty(item)) { continue; } if (isArray(item) && item.length >= 2) { if (isEmpty(item[0]) || isEmpty(item[1])) { continue; } const value: any = this.get(item[0], item[1]); if (value) { return value; } continue; } console.error('getFirst error: Input not in correct format.\n' + 'Should be: [ [ object1, pointer1 ], [ object 2, pointer2 ], etc... ]'); return; } return defaultValue; } if (isMap(items)) { for (let [object, pointer] of items) { if (object === null || !this.isJsonPointer(pointer)) { continue; } const value: any = this.get(object, pointer); if (value) { return value; } } return defaultValue; } console.error('getFirst error: Input not in correct format.\n' + 'Should be: [ [ object1, pointer1 ], [ object 2, pointer2 ], etc... ]'); } /** * 'set' function * * Uses a JSON Pointer to set a value on an object * * If the optional fourth parameter is TRUE and the inner-most container * is an array, the function will insert the value as a new item at the * specified location in the array, rather than overwriting the existing value * * @param {object} object - The object to set value in * @param {Pointer} pointer - The JSON Pointer (string or array) * @param {any} value - The value to set * @return {object} - The original object, modified with the set value */ static set( object: any, pointer: Pointer, value: any, insert: boolean = false ): any { const keyArray: string[] = this.parse(pointer); if (keyArray !== null) { let subObject: any = object; for (let i = 0, l = keyArray.length - 1; i < l; ++i) { let key: string = keyArray[i]; if (key === '-' && isArray(subObject)) { key = subObject.length; } if (isMap(subObject) && subObject.has(key)) { subObject = subObject.get(key); } else { if (!hasOwn(subObject, key)) { subObject[key] = (keyArray[i + 1].match(/^(\d+|-)$/)) ? [] : {}; } subObject = subObject[key]; } } let lastKey: string = keyArray[keyArray.length - 1]; if (isArray(subObject) && lastKey === '-') { subObject.push(value); } else if (insert && isArray(subObject) && !isNaN(+lastKey)) { subObject.splice(lastKey, 0, value); } else if (isMap(subObject)) { subObject.set(lastKey, value); } else { subObject[lastKey] = value; } return object; } console.error('set error: Invalid JSON Pointer: ' + pointer); } /** * 'setCopy' function * * Copies an object and uses a JSON Pointer to set a value on the copy. * * If the optional fourth parameter is TRUE and the inner-most container * is an array, the function will insert the value as a new item at the * specified location in the array, rather than overwriting the existing value. * * @param {object} object - The object to copy and set value in * @param {Pointer} pointer - The JSON Pointer (string or array) * @param {any} value - The value to set * @return {object} - The new object with the set value */ static setCopy( object: any, pointer: Pointer, value: any, insert: boolean = false ): any { const keyArray: string[] = this.parse(pointer); if (keyArray !== null) { let newObject: any = copy(object); let subObject: any = newObject; for (let i = 0, l = keyArray.length - 1; i < l; ++i) { let key: string = keyArray[i]; if (key === '-' && isArray(subObject)) { key = subObject.length; } if (isMap(subObject) && subObject.has(key)) { subObject.set(key, copy(subObject.get(key))); subObject = subObject.get(key); } else { if (!hasOwn(subObject, key)) { subObject[key] = (keyArray[i + 1].match(/^(\d+|-)$/)) ? [] : {}; } subObject[key] = copy(subObject[key]); subObject = subObject[key]; } } let lastKey: string = keyArray[keyArray.length - 1]; if (isArray(subObject) && lastKey === '-') { subObject.push(value); } else if (insert && isArray(subObject) && !isNaN(+lastKey)) { subObject.splice(lastKey, 0, value); } else if (isMap(subObject)) { subObject.set(lastKey, value); } else { subObject[lastKey] = value; } return newObject; } console.error('setCopy error: Invalid JSON Pointer: ' + pointer); } /** * 'insert' function * * Calls 'set' with insert = TRUE * * @param {object} object - object to insert value in * @param {Pointer} pointer - JSON Pointer (string or array) * @param {any} value - value to insert * @return {object} */ static insert(object: any, pointer: Pointer, value: any): any { this.set(object, pointer, value, true); } /** * 'insertCopy' function * * Calls 'setCopy' with insert = TRUE * * @param {object} object - object to insert value in * @param {Pointer} pointer - JSON Pointer (string or array) * @param {any} value - value to insert * @return {object} */ static insertCopy(object: any, pointer: Pointer, value: any): any { this.setCopy(object, pointer, value, true); } /** * 'remove' function * * Uses a JSON Pointer to remove a key and its attribute from an object * * @param {object} object - object to delete attribute from * @param {Pointer} pointer - JSON Pointer (string or array) * @return {object} */ static remove(object: any, pointer: Pointer): any { let keyArray: any[] = this.parse(pointer); if (keyArray !== null && keyArray.length) { let lastKey = keyArray.pop(); let parentObject = this.get(object, keyArray); if (isArray(parentObject)) { if (lastKey === '-') { lastKey = parentObject.length - 1; } parentObject.splice(lastKey, 1); } else if (isObject(parentObject)) { delete parentObject[lastKey]; } return object; } console.error('remove error: Invalid JSON Pointer: ' + pointer); } /** * 'has' function * * Tests if an object has a value at the location specified by a JSON Pointer * * @param {object} object - object to chek for value * @param {Pointer} pointer - JSON Pointer (string or array) * @return {boolean} */ static has(object: any, pointer: Pointer): boolean { return this.get(object, pointer, 0, null, true); } /** * 'dict' function * * Returns a (pointer -> value) dictionary for an object * * @param {Object} object - The object to create a dictionary from * @return {Object} - The resulting dictionary object */ static dict(object: any): any { let results: any = {}; this.forEachDeep(object, (value, pointer) => { if (typeof value !== 'object') { results[pointer] = value; } }); return results; } /** * 'forEachDeep' function * * Iterates over own enumerable properties of an object or items in an array * and invokes an iteratee function for each key/value or index/value pair. * By default, iterates over items within objects and arrays after calling * the iteratee function on the containing object or array itself. * * The iteratee is invoked with three arguments: (value, pointer, rootObject), * where pointer is a JSON pointer indicating the location of the current * value within the root object, and rootObject is the root object initially * submitted to th function. * * If a third optional parameter 'bottomUp' is set to TRUE, the iterator * function will be called on sub-objects and arrays after being * called on their contents, rather than before, which is the default. * * This function can also optionally be called directly on a sub-object by * including optional 4th and 5th parameterss to specify the initial * root object and pointer. * * @param {object} object - the initial object or array * @param {(v: any, k?: string, o?: any, p?: any) => any} function - iteratee function * @param {boolean = false} bottomUp - optional, set to TRUE to reverse direction * @param {object = object} rootObject - optional, root object or array * @param {string = ''} pointer - optional, JSON Pointer to object within rootObject */ static forEachDeep( object: any, fn: (v: any, p?: string, o?: any) => any, bottomUp: boolean = false, pointer: string = '', rootObject: any = object ): void { if (typeof fn === 'function') { if (!bottomUp) { fn(object, pointer, rootObject); } if (isObject(object) || isArray(object)) { for (let key of Object.keys(object)) { const newPointer: string = pointer + '/' + this.escape(key); this.forEachDeep(object[key], fn, bottomUp, newPointer, rootObject); } } if (bottomUp) { fn(object, pointer, rootObject); } } else { console.error('forEachDeep error: Iterator must be a function.'); } } /** * 'forEachDeepCopy' function * * Similar to forEachDeep, but returns a copy of the original object, with * the same keys and indexes, but with values replaced with the result of * the iteratee function. * * @param {object} object - the initial object or array * @param {(v: any, k?: string, o?: any, p?: any) => any} function - iteratee function * @param {boolean = false} bottomUp - optional, set to TRUE to reverse direction * @param {object = object} rootObject - optional, root object or array * @param {string = ''} pointer - optional, JSON Pointer to object within rootObject */ static forEachDeepCopy( object: any, fn: (v: any, p?: string, o?: any) => any, bottomUp: boolean = false, pointer: string = '', rootObject: any = object ): void { if (typeof fn === 'function') { if (isObject(object) || isArray(object)) { let newObject = Object.assign(isArray(object) ? [] : {}, object); if (!bottomUp) { fn(newObject, pointer, rootObject); } for (let key of Object.keys(newObject)) { const newPointer: string = pointer + '/' + this.escape(key); newObject[key] = this.forEachDeepCopy(object[key], fn, bottomUp, newPointer, rootObject); } if (bottomUp) { fn(newObject, pointer, rootObject); } } else { return fn(object, pointer, rootObject); } } console.error('forEachDeep error: Iterator must be a function.'); } /** * 'escape' function * * Escapes a string reference key * * @param {string} key - string key to escape * @return {string} - escaped key */ static escape(key: string): string { return key.toString().replace(/~/g, '~0').replace(/\//g, '~1'); } /** * 'unescape' function * Unescapes a string reference key * * @param {string} key - string key to unescape * @return {string} - unescaped key */ static unescape(key: string): string { return key.toString().replace(/~1/g, '/').replace(/~0/g, '~'); } /** * 'parse' function * * Converts a string JSON Pointer into a array of keys * (if input is already an an array of keys, it is returned unchanged) * * @param {Pointer} pointer - JSON Pointer (string or array) * @return {string[]} - JSON Pointer array of keys */ static parse(pointer: Pointer): string[] { if (isArray(pointer)) { return <string[]>pointer; } if (typeof pointer === 'string') { if ((<string>pointer)[0] === '#') { pointer = pointer.slice(1); } if (<string>pointer === '') { return []; } if ((<string>pointer)[0] !== '/') { console.error('parse error: Invalid JSON Pointer, does not start with "/": ' + pointer); return; } return (<string>pointer).slice(1).split('/').map(this.unescape); } console.error('parse error: Invalid JSON Pointer, not a string or array:'); console.error(pointer); } /** * 'compile' function * * Converts an array of keys into a JSON Pointer string * (if input is already a string, it is normalized and returned) * * The optional second parameter is a default which will replace any empty keys. * * @param {Pointer} keyArray - JSON Pointer (string or array) * @returns {string} - JSON Pointer string */ static compile(keyArray: Pointer, defaultValue: string | number = ''): string { if (isArray(keyArray)) { if ((<string[]>keyArray).length === 0) { return ''; } return '/' + (<string[]>keyArray).map( key => key === '' ? defaultValue : this.escape(key) ).join('/'); } if (typeof keyArray === 'string') { if (keyArray[0] === '#') { keyArray = keyArray.slice(1); } if (keyArray.length && keyArray[0] !== '/') { console.error('compile error: Invalid JSON Pointer, does not start with "/": ' + keyArray); return; } return keyArray; } console.error('compile error: Invalid JSON Pointer, not a string or array:'); console.error(keyArray); } /** * 'toKey' function * * Extracts name of the final key from a JSON Pointer. * * @param {Pointer} pointer - JSON Pointer (string or array) * @returns {string} - the extracted key */ static toKey(pointer: Pointer): string { let keyArray = this.parse(pointer); if (keyArray === null) { return null; } if (!keyArray.length) { return ''; } return keyArray[keyArray.length - 1]; } /** * 'isJsonPointer' function * * Checks a string value to determine if it is a valid JSON Pointer. * This function only checks for valid JSON Pointer strings, not arrays. * (Any array of string values is assumed to be a potentially valid JSON Pointer.) * * @param {any} value - value to check * @returns {boolean} - true if value is a valid JSON Pointer, otherwise false */ static isJsonPointer(value: any): boolean { if (typeof value === 'string') { if (value === '') { return true; } if (value[0] === '#') { value = value.slice(1); } if (value[0] === '/') { return true; } } return false; } /** * 'isSubPointer' function * * Checks whether one JSON Pointer is a subset of another. * * @param {Pointer} shortPointer - potential subset JSON Pointer * @param {Pointer} longPointer - potential superset JSON Pointer * @return {boolean} - true if shortPointer is a subset of longPointer, false if not */ static isSubPointer(shortPointer: Pointer, longPointer: Pointer): boolean { if (isArray(shortPointer)) { shortPointer = this.compile(shortPointer); } if (isArray(longPointer)) { longPointer = this.compile(longPointer); } if (typeof shortPointer !== 'string' || typeof longPointer !== 'string') { console.error('isSubPointer error: Invalid JSON Pointer, not a string or array:'); if (typeof shortPointer !== 'string') { console.error(shortPointer); } if (typeof longPointer !== 'string') { console.error(longPointer); } return; } return shortPointer === longPointer.slice(0, shortPointer.length); } /** * 'toIndexedPointer' function * * Merges an array of numeric indexes and a generic pointer to create an * indexed pointer for a specific item. * * For example, merging the generic pointer '/foo/-/bar/-/baz' and * the array [4, 2] would result in the indexed pointer '/foo/4/bar/2/baz' * * @function * @param {string | string[]} genericPointer - The generic pointer * @param {number[]} indexArray - The array of numeric indexes * @param {Map<string, number>} arrayMap - An optional array map * @return {string} - The merged pointer with indexes */ static toIndexedPointer( genericPointer: string, indexArray: number[], arrayMap: Map<string, number> = null ) { if (genericPointer[0] === '#') { genericPointer = genericPointer.slice(1); } if (this.isJsonPointer(genericPointer) && isArray(indexArray)) { if (isMap(arrayMap)) { let arrayIndex: number = 0; return genericPointer.replace(/\/\-(?=\/|$)/g, (key, stringIndex) => { const subPointer = genericPointer.slice(0, stringIndex); if (arrayMap.has(subPointer)) { return '/' + indexArray[arrayIndex++]; } }); } else { let indexedPointer = genericPointer; for (let pointerIndex of indexArray) { indexedPointer = indexedPointer.replace('/-', '/' + pointerIndex); } return indexedPointer; } } console.error('toIndexedPointer error: genericPointer must be ' + 'a JSON Pointer and indexArray must be an array.'); console.error(genericPointer); console.error(indexArray); }; /** * 'toGenericPointer' function * * Compares an indexed pointer to an array map and removes list array * indexes (but leaves tuple arrray indexes and all object keys, including * numeric keys) to create a generic pointer. * * For example, using the indexed pointer '/foo/1/bar/2/baz/3' and * the arrayMap [['/foo', 0], ['/foo/-/bar', 3], ['/foo/-/bar/2/baz', 0]] * would result in the generic pointer '/foo/-/bar/2/baz/-' * Using the indexed pointer '/foo/1/bar/4/baz/3' and the same arrayMap * would result in the generic pointer '/foo/-/bar/-/baz/-' * * The structure of the arrayMap is: [['path to array', number of tuple items]...] * * @function * @param {Pointer} indexedPointer - The indexed pointer (array or string) * @param {Map<string, number>} arrayMap - The optional array map (for preserving tuple indexes) * @return {string} - The generic pointer with indexes removed */ static toGenericPointer( indexedPointer: Pointer, arrayMap: Map<string, number> = new Map<string, number>() ) { if (this.isJsonPointer(indexedPointer) && isMap(arrayMap)) { let pointerArray = this.parse(indexedPointer); for (let i = 1, l = pointerArray.length; i < l; i++) { const subPointer = this.compile(pointerArray.slice(0, i)); if (arrayMap.has(subPointer) && arrayMap.get(subPointer) <= +pointerArray[i]) { pointerArray[i] = '-'; } } return this.compile(pointerArray); } console.error('toGenericPointer error: ' + 'indexedPointer must be a JSON Pointer and arrayMap must be a Map.'); console.error(indexedPointer); console.error(arrayMap); }; /** * 'toControlPointer' function * * Accepts a JSON Pointer for a data object and returns a JSON Pointer for the * matching control in an Angular 2 FormGroup. * * @param {FormGroup} formGroup - Angular 2 FormGroup to get value from * @param {Pointer} dataPointer - JSON Pointer (string or array) to a data object * @return {Pointer} - JSON Pointer (string) to the formGroup object */ static toControlPointer(formGroup: any, dataPointer: Pointer): string { const dataPointerArray: string[] = this.parse(dataPointer); let controlPointerArray: string[] = []; let subGroup = formGroup; if (dataPointerArray !== null) { for (let key of dataPointerArray) { if (subGroup.hasOwnProperty('controls')) { controlPointerArray.push('controls'); subGroup = subGroup.controls; } if (isArray(subGroup) && (key === '-')) { controlPointerArray.push((subGroup.length - 1).toString()); subGroup = subGroup[subGroup.length - 1]; } else if (subGroup.hasOwnProperty(key)) { controlPointerArray.push(key); subGroup = subGroup[key]; } else { console.error('toControlPointer error: Unable to find "' + key + '" item in FormGroup.'); console.error(dataPointer); console.error(formGroup); return; } } return this.compile(controlPointerArray); } console.error('getControl error: Invalid JSON Pointer: ' + dataPointer); } /** * 'parseObjectPath' function * * Parses a JavaScript object path into an array of keys, which * can then be passed to compile() to convert into a string JSON Pointer. * * Based on mike-marcacci's objectpath parse function: * https://github.com/mike-marcacci/objectpath * * @param {string} path - The object path to parse * @return {string[]} - The resulting array of keys */ static parseObjectPath(path: string | string[]): string[] { if (isArray(path)) { return <string[]>path; } if (typeof path === 'string') { let index: number = 0; let parts: string[] = []; while (index < path.length) { const nextDot: number = path.indexOf('.', index); const nextOB: number = path.indexOf('[', index); // next open bracket if (nextDot === -1 && nextOB === -1) { // last item parts.push(path.slice(index)); index = path.length; } else if (nextDot !== -1 && (nextDot < nextOB || nextOB === -1)) { // dot notation parts.push(path.slice(index, nextDot)); index = nextDot + 1; } else { // bracket notation if (nextOB > index) { parts.push(path.slice(index, nextOB)); index = nextOB; } const quote: string = path.charAt(nextOB + 1); if (quote === '"' || quote === "'") { // enclosing quotes let nextCB: number = path.indexOf(quote + ']', nextOB); // next close bracket while (nextCB !== -1 && path.charAt(nextCB - 1) === '\\') { nextCB = path.indexOf(quote + ']', nextCB + 2); } if (nextCB === -1) { nextCB = path.length; } parts.push(path.slice(index + 2, nextCB) .replace(new RegExp('\\' + quote, 'g'), quote)); index = nextCB + 2; } else { // no enclosing quotes let nextCB: number = path.indexOf(']', nextOB); // next close bracket if (nextCB === -1) { nextCB = path.length; } parts.push(path.slice(index + 1, nextCB)); index = nextCB + 1; } if (path.charAt(index) === '.') { index++; } } } return parts; } console.error('parseObjectPath error: Input object path must be a string.'); } }