angular2-json-schema-form
Version:
Angular 2 JSON Schema Form builder
573 lines (544 loc) • 20.5 kB
text/typescript
import { AbstractControl } from '@angular/forms';
import { toPromise } from 'rxjs/operator/toPromise';
/**
* Validator utility function library:
*
* Validator and error utilities:
* _executeValidators, _executeAsyncValidators, _mergeObjects, _mergeErrors
*
* Individual value checking:
* isDefined, hasValue, isEmpty
*
* Individual type checking:
* isString, isNumber, isInteger, isBoolean, isFunction, isObject, isArray,
* isMap, isSet, isPromise
*
* Multiple type checking and fixing:
* getType, isType, isPrimitive, toJavaScriptType, toSchemaType, _convertToPromise
*
* Utility functions:
* inArray, xor
*
* Typescript types and interfaces:
* SchemaPrimitiveType, SchemaType, JavaScriptPrimitiveType, JavaScriptType,
* PrimitiveValue, PlainObject, IValidatorFn, AsyncIValidatorFn
*
* Note: 'IValidatorFn' is short for 'invertable validator function',
* which is a validator functions that accepts an optional second
* argument which, if set to TRUE, causes the validator to perform
* the opposite of its original function.
*/
export type SchemaPrimitiveType =
'string' | 'number' | 'integer' | 'boolean' | 'null';
export type SchemaType =
'string' | 'number' | 'integer' | 'boolean' | 'null' | 'object' | 'array';
export type JavaScriptPrimitiveType =
'string' | 'number' | 'boolean' | 'null' | 'undefined';
export type JavaScriptType =
'string' | 'number' | 'boolean' | 'null' | 'undefined' | 'object' | 'array' |
'map' | 'set' | 'arguments' | 'date' | 'error' | 'function' | 'json' |
'math' | 'regexp'; // Note: this list is incomplete
export type PrimitiveValue = string | number | boolean | null | undefined;
export type PlainObject = { [k: string]: any };
export interface IValidatorFn { (c: AbstractControl, i?: boolean): PlainObject; }
export interface AsyncIValidatorFn { (c: AbstractControl, i?: boolean): any; }
/**
* '_executeValidators' utility function
*
* Validates a control against an array of validators, and returns
* an array of the same length containing a combination of error messages
* (from invalid validators) and null values (from valid validators)
*
* @param {AbstractControl} control - control to validate
* @param {IValidatorFn[]} validators - array of validators
* @return {any[]} - array of nulls and error message
*/
export function _executeValidators(
control: AbstractControl, validators: IValidatorFn[], invert: boolean = false
): PlainObject[] {
return validators.map(validator => validator(control, invert));
}
/**
* '_executeAsyncValidators' utility function
*
* Validates a control against an array of async validators, and returns
* an array of observabe results of the same length containing a combination of
* error messages (from invalid validators) and null values (from valid ones)
*
* @param {AbstractControl} control - control to validate
* @param {AsyncIValidatorFn[]} validators - array of async validators
* @return {any[]} - array of observable nulls and error message
*/
export function _executeAsyncValidators(
control: AbstractControl, validators: AsyncIValidatorFn[], invert: boolean = false
): any[] {
return validators.map(v => v(control, invert));
}
/**
* '_mergeObjects' utility function
*
* Recursively Merges one or more objects into a single object with combined keys.
* Automatically detects and ignores null and undefined inputs.
* Also detects duplicated boolean 'not' keys and XORs their values.
*
* @param {PlainObject[]} object - one or more objects to merge
* @return {PlainObject} - merged object
*/
export function _mergeObjects(...object: PlainObject[]): PlainObject {
let mergedObject: PlainObject = { };
for (let i = 0, l = arguments.length; i < l; i++) {
const currentObject = arguments[i];
if (isObject(currentObject)) {
for (let key of Object.keys(currentObject)) {
const currentValue = currentObject[key];
const mergedValue = mergedObject[key];
if (isDefined(mergedValue)) {
if ( key === 'not' &&
isBoolean(mergedValue, 'strict') &&
isBoolean(currentValue, 'strict')
) {
mergedObject[key] = xor(mergedValue, currentValue);
} else if (
getType(mergedValue) === 'object' &&
getType(currentValue) === 'object'
) {
mergedObject[key] = _mergeObjects(mergedValue, currentValue);
} else {
mergedObject[key] = currentValue;
}
} else {
mergedObject[key] = currentValue;
}
}
}
}
return mergedObject;
}
/**
* '_mergeErrors' utility function
*
* Merges an array of objects.
* Used for combining the validator errors returned from 'executeValidators'
*
* @param {PlainObject[]} arrayOfErrors - array of objects
* @return {PlainObject} - merged object, or null if no usable input objectcs
*/
export function _mergeErrors(arrayOfErrors: PlainObject[]): PlainObject {
let mergedErrors: PlainObject = _mergeObjects.apply(null, arrayOfErrors);
return isEmpty(mergedErrors) ? null : mergedErrors;
}
/**
* 'isDefined' utility function
*
* Checks if a variable contains a value of any type.
* Returns true even for otherwise 'falsey' values of 0, '', and false.
*
* @param {any} object - the value to check
* @return {boolean} - false if undefined or null, otherwise true
*/
export function isDefined(value: any): boolean {
return value !== undefined && value !== null;
}
/**
* 'hasValue' utility function
*
* Checks if a variable contains a value.
* Returs false for null, undefined, or a zero-length strng, '',
* otherwise returns true.
* (Stricter than 'isDefined' because it also returns false for '',
* though it stil returns true for otherwise 'falsey' values 0 and false.)
*
* @param {any} object - the value to check
* @return {boolean} - false if undefined, null, or '', otherwise true
*/
export function hasValue(value: any): boolean {
return value !== undefined && value !== null && value !== '';
}
/**
* 'isEmpty' utility function
*
* Similar to !hasValue, but also returns true for empty arrays and objects.
*
* @param {any} object - the value to check
* @return {boolean} - false if undefined, null, or '', otherwise true
*/
export function isEmpty(value: any): boolean {
if (isArray(value)) { return !value.length; }
if (isObject(value)) { return !Object.keys(value).length; }
return value === undefined || value === null || value === '';
}
/**
* 'isString' utility function
*
* Checks if a value is a string.
*
* @param {any} object - the value to check
* @return {boolean} - true if string, false if not
*/
export function isString(value: any): value is string {
return typeof value === 'string';
}
/**
* 'isNumber' utility function
*
* Checks if a value is a regular number or a numeric string.
*
* @param {any} object - the value to check
* @param {any = false} strict - if truthy, also checks JavaScript tyoe
* @return {boolean} - true if number, false if not
*/
export function isNumber(value: any, strict: any = false): boolean {
if (strict && typeof value !== 'number') { return false; }
return !isNaN(value) && value !== value / 0;
}
/**
* 'isInteger' utility function
*
* Checks if a value is an integer.
*
* @param {any} object - the value to check
* @param {any = false} strict - if truthy, also checks JavaScript tyoe
* @return {boolean} - true if number, false if not
*/
export function isInteger(value: any, strict: any = false): boolean {
if (strict && typeof value !== 'number') { return false; }
return !isNaN(value) && value !== value / 0 && value % 1 === 0;
}
/**
* 'isBoolean' utility function
*
* Checks if a value is a boolean.
*
* @param {any} object - the value to check
* @param {any = null} option - if 'strict', also checks JavaScript type
* if TRUE or FALSE, checks only for that value
* @return {boolean} - true if boolean, false if not
*/
export function isBoolean(value: any, option: any = null): boolean {
if (option === 'strict') { return value === true || value === false; }
if (option === true) {
return value === true || value === 1 || value === 'true' || value === '1';
}
if (option === false) {
return value === false || value === 0 || value === 'false' || value === '0';
}
return value === true || value === 1 || value === 'true' || value === '1' ||
value === false || value === 0 || value === 'false' || value === '0';
}
export function isFunction(item: any): boolean {
return typeof item === 'function';
}
export function isObject(item: any): boolean {
return item !== null && typeof item === 'object' &&
Object.prototype.toString.call(item) === '[object Object]';
}
export function isArray(item: any): boolean {
return Array.isArray(item) ||
Object.prototype.toString.call(item) === '[object Array]';
}
export function isMap(item: any): boolean {
return typeof item === 'object' &&
Object.prototype.toString.call(item) === '[object Map]';
}
export function isSet(item: any): boolean {
return typeof item === 'object' &&
Object.prototype.toString.call(item) === '[object Set]';
}
/**
* 'getType' function
*
* Detects the JSON Schema Type of a value.
* By default, detects numbers and integers even if formatted as strings.
* (So all integers are also numbers, and any number may also be a string.)
* However, it only detects true boolean values (to detect boolean values
* in non-boolean formats, use isBoolean() instead).
*
* If passed a second optional parameter of 'strict', it will only detect
* numbers and integers if they are JavaScript numbers.
*
* Examples:
* getType('10.5') = 'number'
* getType(10.5) = 'number'
* getType('10') = 'integer'
* getType(10) = 'integer'
* getType('true') = 'string'
* getType(true) = 'boolean'
* getType(null) = 'null'
* getType({ }) = 'object'
* getType([]) = 'array'
*
* getType('10.5', 'strict') = 'string'
* getType(10.5, 'strict') = 'number'
* getType('10', 'strict') = 'string'
* getType(10, 'strict') = 'integer'
* getType('true', 'strict') = 'string'
* getType(true, 'strict') = 'boolean'
*
* @param {any} value - value to check
* @param {any = false} strict - if truthy, also checks JavaScript tyoe
* @return {boolean}
*/
export function getType(value: any, strict: any = false): SchemaType {
if (!isDefined(value)) { return 'null'; }
if (isArray(value)) { return 'array'; }
if (isObject(value)) { return 'object'; }
if (isBoolean(value, 'strict')) { return 'boolean'; }
if (isInteger(value, strict)) { return 'integer'; }
if (isNumber(value, strict)) { return 'number'; }
if (isString(value)) { return 'string'; }
return null;
}
/**
* 'isType' function
*
* Checks wether an input (probably string) value contains data of
* a specified JSON Schema type
*
* @param {PrimitiveValue} value - value to check
* @param {SchemaPrimitiveType} type - type to check
* @return {boolean}
*/
export function isType(value: PrimitiveValue, type: SchemaPrimitiveType): boolean {
switch (type) {
case 'string':
return isString(value);
case 'number':
return isNumber(value);
case 'integer':
return isInteger(value);
case 'boolean':
return isBoolean(value);
case 'null':
return !hasValue(value);
default:
console.error('isType error: "' + type + '" is not a recognized type.');
return null;
}
}
/**
* 'isPrimitive' function
*
* Checks wether an input value is a JavaScript primitive type:
* string, number, boolean, or null.
*
* @param {any} value - value to check
* @return {boolean}
*/
export function isPrimitive(value: any): boolean {
return (isString(value) || isNumber(value) ||
isBoolean(value, 'strict') || value === null);
}
/**
* 'toJavaScriptType' function
*
* Converts an input (probably string) value to a JavaScript primitive type -
* 'string', 'number', 'boolean', or 'null' - before storing in a JSON object.
*
* Does not coerce values (other than null), and only converts the types
* of values that would otherwise be valid.
*
* If the optional third parameter 'strictIntegers' is TRUE, and the
* JSON Schema type 'integer' is specified, it also verifies the input value
* is an integer and, if it is, returns it as a JaveScript number.
* If 'strictIntegers' is FALSE (or not set) the type 'integer' is treated
* exactly the same as 'number', and allows decimals.
*
* Examples:
* toJavaScriptType('10', 'number') = 10
* toJavaScriptType('10', 'integer') = 10
* toJavaScriptType('10.5', 'number') = 10.5
* toJavaScriptType('10.5', 'integer') = null // '10.5' is not an integer
* toJavaScriptType(10.5, 'integer') = null // 10.5 is still not an integer
*
* @param {PrimitiveValue} value - value to convert
* @param {SchemaPrimitiveType | SchemaPrimitiveType[]} types - types to convert to
* @param {boolean = false} strictIntegers - if FALSE, treat integers as numbers
* @return {boolean}
*/
export function toJavaScriptType(
value: any,
types: SchemaPrimitiveType | SchemaPrimitiveType[],
strictIntegers: boolean = true
): PrimitiveValue {
if (!isDefined(value)) { return null; }
if (isString(types)) { types = [types]; }
if (strictIntegers && inArray('integer', types)) {
if (isInteger(value, 'strict')) { return value; }
if (isInteger(value)) { return parseInt(value, 10); }
}
if (inArray('number', types) || (!strictIntegers && inArray('integer', types))) {
if (isNumber(value, 'strict')) { return value; }
if (isNumber(value)) { return parseFloat(value); }
}
if (inArray('string', types)) {
if (isString(value)) { return value; }
}
if (inArray('boolean', types)) {
if (isBoolean(value, true)) { return true; }
if (isBoolean(value, false)) { return false; }
}
return null;
}
/**
* 'toSchemaType' function
*
* Converts an input (probably string) value to the "best" JavaScript
* equivalent available from an allowed list of JSON Schema types, which may
* contain 'string', 'number', 'integer', 'boolean', and/or 'null'.
* If necssary, it does progressively agressive type coersion.
* It will not return null unless null is in the list of allowed types.
*
* Number conversion examples:
* toSchemaType('10', ['number','integer','string']) = 10 // integer
* toSchemaType('10', ['number','string']) = 10 // number
* toSchemaType('10', ['string']) = '10' // string
* toSchemaType('10.5', ['number','integer','string']) = 10.5 // number
* toSchemaType('10.5', ['integer','string']) = '10.5' // string
* toSchemaType('10.5', ['integer']) = 10 // integer
* toSchemaType(10.5, ['null','boolean','string']) = '10.5' // string
* toSchemaType(10.5, ['null','boolean']) = true // boolean
*
* String conversion examples:
* toSchemaType('1.5x', ['boolean','number','integer','string']) = '1.5x' // string
* toSchemaType('1.5x', ['boolean','number','integer']) = '1.5' // number
* toSchemaType('1.5x', ['boolean','integer']) = '1' // integer
* toSchemaType('1.5x', ['boolean']) = true // boolean
* toSchemaType('xyz', ['number','integer','boolean','null']) = true // boolean
* toSchemaType('xyz', ['number','integer','null']) = null // null
* toSchemaType('xyz', ['number','integer']) = 0 // number
*
* Boolean conversion examples:
* toSchemaType('1', ['integer','number','string','boolean']) = 1 // integer
* toSchemaType('1', ['number','string','boolean']) = 1 // number
* toSchemaType('1', ['string','boolean']) = '1' // string
* toSchemaType('1', ['boolean']) = true // boolean
* toSchemaType('true', ['number','string','boolean']) = 'true' // string
* toSchemaType('true', ['boolean']) = true // boolean
* toSchemaType('true', ['number']) = 0 // number
* toSchemaType(true, ['number','string','boolean']) = true // boolean
* toSchemaType(true, ['number','string']) = 'true' // string
* toSchemaType(true, ['number']) = 1 // number
*
* @param {PrimitiveValue} value - value to convert
* @param {SchemaPrimitiveType[]} types - allowed types to convert to
* @return {boolean}
*/
export function toSchemaType(
value: PrimitiveValue, types: SchemaPrimitiveType | SchemaPrimitiveType[]
): PrimitiveValue {
if (!isArray(<SchemaPrimitiveType>types)) {
types = <SchemaPrimitiveType[]>[types];
}
if ((<SchemaPrimitiveType[]>types).indexOf('null') !== -1 && !hasValue(value)) {
return null;
}
if ((<SchemaPrimitiveType[]>types).indexOf('boolean') !== -1 && !isBoolean(value, 'strict')) {
return value;
}
if ((<SchemaPrimitiveType[]>types).indexOf('integer') !== -1) {
let testValue = toJavaScriptType(value, 'integer');
if (testValue !== null) { return +testValue; }
}
if ((<SchemaPrimitiveType[]>types).indexOf('number') !== -1) {
let testValue = toJavaScriptType(value, 'number');
if (testValue !== null) { return +testValue; }
}
if (
(isString(value) || isNumber(value, 'strict')) &&
(<SchemaPrimitiveType[]>types).indexOf('string') !== -1
) { // Convert number to string
return toJavaScriptType(value, 'string');
}
if ((<SchemaPrimitiveType[]>types).indexOf('boolean') !== -1 && isBoolean(value)) {
return toJavaScriptType(value, 'boolean');
}
if ((<SchemaPrimitiveType[]>types).indexOf('string') !== -1) { // Convert null & boolean to string
if (value === null) { return ''; }
let testValue = toJavaScriptType(value, 'string');
if (testValue !== null) { return testValue; }
}
if ((
(<SchemaPrimitiveType[]>types).indexOf('number') !== -1 ||
(<SchemaPrimitiveType[]>types).indexOf('integer') !== -1)
) {
if (value === true) { return 1; } // Convert boolean & null to number
if (value === false || value === null || value === '') { return 0; }
}
if ((<SchemaPrimitiveType[]>types).indexOf('number') !== -1) { // Convert mixed string to number
let testValue = parseFloat(<string>value);
if (!!testValue) { return testValue; }
}
if ((<SchemaPrimitiveType[]>types).indexOf('integer') !== -1) { // Convert string or number to integer
let testValue = parseInt(<string>value, 10);
if (!!testValue) { return testValue; }
}
if ((<SchemaPrimitiveType[]>types).indexOf('boolean') !== -1) { // Convert anything to boolean
return !!value;
}
if ((
(<SchemaPrimitiveType[]>types).indexOf('number') !== -1 ||
(<SchemaPrimitiveType[]>types).indexOf('integer') !== -1
) && (<SchemaPrimitiveType[]>types).indexOf('null') === -1
) {
return 0; // If null not allowed, return 0 for non-convertable values
}
}
/**
* 'isPromise' function
*
* @param {object} object
* @return {object}
*/
export function isPromise(object: any): object is Promise<any> {
return !!object && typeof object.then === 'function';
}
/**
* '_convertToPromise' function
*
* @param {object} object
* @return {Promise<any>}
*/
export function _convertToPromise(object: any): Promise<any> {
return isPromise(object) ? object : toPromise.call(object);
}
/**
* 'inArray' function
*
* Searches an array for an item, or one of a list of items, and returns true
* as soon as a match is found, or false if no match.
*
* If the optional third parameter allIn is set to TRUE, and the item to find
* is an array, then the function returns true only if all elements from item
* are found in the list, and false if any element is not found. If the item to
* find is not an array, setting allIn to TRUE has no effect.
*
* @param {any|any[]} item - the item to search for
* @param {any[]} array - the array to search
* @param {boolean = false} allIn - if TRUE, all items must be in array
* @return {boolean} - true if item(s) in array, false otherwise
*/
export function inArray(
item: any|any[], array: any[], allIn: boolean = false
): boolean {
if (!isDefined(item) || !isArray(array)) { return false; }
if (isArray(item)) {
for (let subItem of item) {
if (xor(array.indexOf(subItem) !== -1, allIn)) { return !allIn; }
}
return allIn;
} else {
return array.indexOf(item) !== -1;
}
}
/**
* 'xor' utility function - exclusive or
*
* Returns true if exactly one of two values is truthy.
*
* @param {any} value1 - first value to check
* @param {any} value2 - second value to check
* @return {boolean} - true if exactly one input value is truthy, false if not
*/
export function xor(value1: any, value2: any): boolean {
return (!!value1 && !value2) || (!value1 && !!value2);
}