четверг, 28 июля 2016 г.

Мой вариант JSON Schema

;(function (global, factory) {
    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else if (typeof module === 'object' && typeof module.exports === 'object') {
        module.exports = factory();
    } else {
        global.JSONSchema = factory();
    }
})(this, function () {

    // Валидация данных в формате JSON согласно схеме проверки.

    // Допустимые для типы значений:
    // - types - массив с возможными вариантами типов значений
    // - type - тип значения: null, boolean, number, string, array, object
    // - min - минимальная длина значения или число элементов, входящих в него
    // - max - максимальная длина значения или число элементов, входящих в него
    // - regexp: {pattern: 'abcde', flags: 'gi'} - регулярное выражение для проверки содержимого строки
    // - items - объект, содержащий в себе описание схемы для проверки элементов внутри массива, имеющих числовые индексы
    // - allItems - массив, содержащий в себе описание схемы для проверки всех элементов внутри массива, имеющих одинаковый тип
    // - properties - объект, содержащий в себе описание схемы для проверки элементов внутри объекта
    // - optional: true - булево значение, всегда равное true и указывающее на то, что не является ошибкой, если данный элемент будет отсутствовать
    //
    // undefined:
    // - type
    // - optional: true
    //
    // null:
    // - type
    // - optional: true
    //
    // boolean:
    // - type
    // - optional: true
    //
    // number:
    // - type
    // - min
    // - max
    // - optional: true
    //
    // string:
    // - type
    // - min
    // - max
    // - regexp
    // - optional: true
    //
    // array:
    // - type
    // - min
    // - max
    // - items
    // - allItems
    // - optional: true
    //
    // object:
    // - type
    // - min
    // - max
    // - properties
    // - optional: true

    // Пример использования:
    //
    // var data = {
    //       undef: undefined
    //     , nul: null
    //     , bool: false
    //     , num: 5
    //     , str: 'ab1c2de'
    //     , arr: [1, 2, [3, 4]]
    //     , obj: {one: '', two: {three: 3}}
    //     , anyTypeFrom: 5
    //     , all: [1, 2, 3, 4]
    // };
    //
    // var schema = {
    //       undef: {type: 'undefined'}
    //     , nul: {type: 'null'}
    //     , bool: {type: 'boolean'}
    //     , num: {type: 'number', min: 0, max: 10}
    //     , str: {type: 'string', min: 0, max: 10, regexp: {pattern: 'abcde', flags: 'gi'}}
    //     , arr: {type: 'array', min: 0, max: 10, items: {
    //             '0': {type: 'number'}
    //           , '1': {type: 'number'}
    //           , '2': {type: 'array', items: {
    //                     '0': {type: 'number'}
    //                   , '1': {type: 'string'}
    //               }
    //             }
    //         }
    //       }
    //     , obj: {type: 'object', min: 0, max: 10, properties: {
    //              one: {type: 'number'}
    //            , two: {type: 'object', properties: {
    //                   three: {type: 'string'}
    //                 , four: {type: 'string', optional: true}
    //              }
    //            }
    //        }
    //       }
    //     , anyTypeFrom: {
    //         types: [
    //              {type: 'number', min: 0, max: 10}
    //            , {type: 'string', min: 0, max: 10, regexp: {pattern: 'abcde', flags: 'gi'}}
    //         ]
    //       }
    //     , all: {type: 'array', allItems: {type: 'number'}}
    //     , notExist1: {type: 'array', optional: true}
    //     , notExist2: {
    //           types: [
    //                  {type: 'number', min: 0, max: 10}
    //                , {type: 'string', min: 0, max: 10, regexp: {pattern: 'abcde', flags: 'gi'}}
    //           ]
    //         , optional: true
    //       }
    // };
    //
    // var result = JSONSchema.validate(data, schema);
    //
    // if (!result.valid) {
    //     for (var i = 0, len = result.errors.length; i < len; i++) {
    //         console.log('Ошибка: ' + result.errors[i].message);
    //         console.log('Путь до элемента: ' + result.errors[i].path);
    //     }
    // }

    function has (object, key) {
        return Object.prototype.hasOwnProperty.call(object, key);
    }

    function isTypeOf (value, type) {
        return Object.prototype.toString.call(value).toLowerCase().slice(8, -1) === type;
    }

    function objectLength (object) {
        var length = 0
            , key;
        for (key in object) {if (has(object, key)) {
            length++;
        }}
        return length;
    }

    function validate (json, schema) {

        var result = {
              valid: true
            , errors: []
        };

        function addError (path, message) {
            result.valid = false;
            result.errors.push({path: path, message: message});
        }

        if (!isTypeOf(json, 'object')) {
            addError('корневой элемент', 'Корневой элемент должен быть объектом.');
        } else {
            for (var key in schema) {if (has(schema, key)) {
                if (
                          !has(json, key)
                    && (
                            (has(schema[key], 'type') && schema[key].type !== 'undefined' && !has(schema[key], 'optional'))
                        || (has(schema[key], 'types') && !has(schema[key], 'optional'))
                    )
                ) {
                    addError('корневой объект', 'Элемент "' + key + '" должен присутствовать в корневом объекте.');
                }
                           if (has(schema[key], 'type')) {validateSingleRootType(json, schema, key);
                } else if (has(schema[key], 'types')) {validateManyRootTypes(json, schema, key);
                }
            }}
        }

        function validateSingleRootType (json, schema, key) {
            if (!has(schema[key], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента "' + key + '" корневого объекта.');}
                       if (schema[key].type === 'undefined') {validateUndefined(key, json[key], schema[key], '');
            } else if (schema[key].type === 'null') {validateNull(key, json[key], schema[key], '');
            } else if (schema[key].type === 'boolean') {validateBoolean(key, json[key], schema[key], '');
            } else if (schema[key].type === 'number') {validateNumber(key, json[key], schema[key], '');
            } else if (schema[key].type === 'string') {validateString(key, json[key], schema[key], '');
            } else if (schema[key].type === 'array') {validateArray(key, json[key], schema[key], '');
            } else if (schema[key].type === 'object') {validateObject(key, json[key], schema[key], '');
            }
        }

        function validateManyRootTypes (json, schema, key) {
            var elementType
                , requiredElements = [];
            for (var i = 0, len = schema[key].types.length; i < len; i++) {
                if (!has(schema[key].types[i], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента "' + key + '" корневого объекта.');}
                           if (schema[key].types[i].type === 'undefined' && isTypeOf(json[key], 'undefined')) {elementType = 'undefined'; break;
                } else if (schema[key].types[i].type === 'null' && isTypeOf(json[key], 'null')) {elementType = 'null'; break;
                } else if (schema[key].types[i].type === 'boolean' && isTypeOf(json[key], 'boolean')) {elementType = 'boolean'; break;
                } else if (schema[key].types[i].type === 'number' && isTypeOf(json[key], 'number')) {elementType = 'number'; validateNumber(key, json[key], schema[key].types[i], ''); break;
                } else if (schema[key].types[i].type === 'string' && isTypeOf(json[key], 'string')) {elementType = 'string'; validateString(key, json[key], schema[key].types[i], ''); break;
                } else if (schema[key].types[i].type === 'array' && isTypeOf(json[key], 'array')) {elementType = 'array'; validateArray(key, json[key], schema[key].types[i], ''); break;
                } else if (schema[key].types[i].type === 'object' && isTypeOf(json[key], 'object')) {elementType = 'object'; validateObject(key, json[key], schema[key].types[i], ''); break;
                } else {requiredElements.push('"' + schema[key].types[i].type + '"');
                }
            }
            if (elementType === undefined && !has(schema[key], 'optional')) {addError('корневой объект', 'Элемент "' + key + '" корневого объекта должен иметь значение с типом: ' + requiredElements.join(', ') + '.');}
        }

        function validateUndefined (/*key, value, schemaForUndefined, pathToElement*/) {
            // Сообщение об ошибке выводить не нужно. Если элемента нет, то это допустимо.
            // if (!isTypeOf(value, 'undefined') && !has(schemaForNull, 'optional')) {addError(pathToElement + key, 'Элемент "' + key + '" не должен присутствовать.');}
        }

        function validateNull (key, value, schemaForNull, pathToElement) {
            if (isTypeOf(value, 'undefined') && has(schemaForNull, 'optional')) {return;}
            if (!isTypeOf(value, 'null')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь значение null.');}
        }

        function validateBoolean (key, bool, schemaForBoolean, pathToElement) {
            if (isTypeOf(bool, 'undefined') && has(schemaForBoolean, 'optional')) {return;}
            if (!isTypeOf(bool, 'boolean')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь значение true или false.');}
        }

        function validateNumber (key, number, schemaForNumber, pathToElement) {
            if (isTypeOf(number, 'undefined') && has(schemaForNumber, 'optional')) {return;}
            if (!isTypeOf(number, 'number')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь в качестве значения число.');
            } else {
                if (number !== number) {addError(pathToElement + key, 'Элемент "' + key + '" не должен иметь в качестве значения NaN.');}
                if (has(schemaForNumber, 'min') && number < schemaForNumber.min) {addError(pathToElement + key, 'Значение элемента "' + key + '" должно быть больше или равно ' + schemaForNumber.min + '.');}
                if (has(schemaForNumber, 'max') && number > schemaForNumber.max) {addError(pathToElement + key, 'Значение элемента "' + key + '" должно быть меньше или равно ' + schemaForNumber.max + '.');}
            }
        }

        function validateString (key, string, schemaForString, pathToElement) {
            if (isTypeOf(string, 'undefined') && has(schemaForString, 'optional')) {return;}
            if (!isTypeOf(string, 'string')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь в качестве значения строку.');
            } else {
                if (has(schemaForString, 'min') && schemaForString.min === 0 && string.length === 0) {return;}
                if (has(schemaForString, 'min') && string.length < schemaForString.min) {addError(pathToElement + key, 'Число символов в строке "' + key + '" должно быть больше или равно ' + schemaForString.min + '.');}
                if (has(schemaForString, 'max') && string.length > schemaForString.max) {addError(pathToElement + key, 'Число символов в строке "' + key + '" должно быть меньше или равно ' + schemaForString.max + '.');}
                if (has(schemaForString, 'regexp') && !(new RegExp(schemaForString.regexp.pattern, schemaForString.regexp.flags).test(string))) {addError(pathToElement + key, 'Значение элемента "' + key + '" не соответствует регулярному выражению: new RegExp("' + schemaForString.regexp.pattern + '", "' + schemaForString.regexp.flags + '")');}
            }
        }

        function validateArray (key, array, schemaForArray, pathToElement) {
            var index
                , arrayLength;
            if (isTypeOf(array, 'undefined') && has(schemaForArray, 'optional')) {return;}
            if (!isTypeOf(array, 'array')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь в качестве значения массив.');
            } else {
                if (has(schemaForArray, 'min') && schemaForArray.min === 0 && array.length === 0) {return;}
                if (has(schemaForArray, 'min') && array.length < schemaForArray.min) {addError(pathToElement + key, 'Число элементов в массиве "' + key + '" должно быть больше или равно ' + schemaForArray.min + '.');}
                if (has(schemaForArray, 'max') && array.length > schemaForArray.max) {addError(pathToElement + key, 'Число элементов в массиве "' + key + '" должно быть меньше или равно ' + schemaForArray.max + '.');}
                if (has(schemaForArray, 'items')) {
                    for (index in schemaForArray.items) {if (has(schemaForArray.items, index)) {
                        if (array[index] === undefined) {addError(pathToElement + key, 'Элемент с индексом "' + index + '" должен присутствовать в массиве "' + key + '".');}
                                   if (has(schemaForArray.items[index], 'type')) {validateSingleArrayType(array, schemaForArray, index, key, pathToElement);
                        } else if (has(schemaForArray.items[index], 'types')) {validateManyArrayTypes(array, schemaForArray, index, key, pathToElement);
                        }
                    }}
                }
                if (has(schemaForArray, 'allItems')) {
                    for (index = 0, arrayLength = array.length; index < arrayLength; index++) {
                        if (!has(schemaForArray.allItems, 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для всех элементов массива "' + key + '".');}
                                   if (schemaForArray.allItems.type === 'undefined') {validateUndefined(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'null') {validateNull(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'boolean') {validateBoolean(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'number') {validateNumber(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'string') {validateString(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'array') {validateArray(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        } else if (schemaForArray.allItems.type === 'object') {validateObject(index, array[index], schemaForArray.allItems, pathToElement + key + '.');
                        }
                    }
                }
            }
        }

        function validateSingleArrayType (array, schemaForArray, index, key, pathToElement) {
            if (!has(schemaForArray.items[index], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента с индексом "' + index + '" массива "' + key + '".');}
                       if (schemaForArray.items[index].type === 'undefined') {validateUndefined(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'null') {validateNull(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'boolean') {validateBoolean(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'number') {validateNumber(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'string') {validateString(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'array') {validateArray(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            } else if (schemaForArray.items[index].type === 'object') {validateObject(index, array[index], schemaForArray.items[index], pathToElement + key + '.');
            }
        }

        function validateManyArrayTypes (array, schemaForArray, index, key, pathToElement) {
            var elementType
                , requiredElements = [];
            for (var i = 0, len = schemaForArray.items[index].types.length; i < len; i++) {
                if (!has(schemaForArray.items[index].types[i], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента с индексом "' + index + '" массива "' + key + '".');}
                           if (schemaForArray.items[index].types[i].type === 'undefined' && isTypeOf(array[index], 'undefined')) {elementType = 'undefined'; break;
                } else if (schemaForArray.items[index].types[i].type === 'null' && isTypeOf(array[index], 'null')) {elementType = 'null'; break;
                } else if (schemaForArray.items[index].types[i].type === 'boolean' && isTypeOf(array[index], 'boolean')) {elementType = 'boolean'; break;
                } else if (schemaForArray.items[index].types[i].type === 'number' && isTypeOf(array[index], 'number')) {elementType = 'number'; validateNumber(index, array[index], schemaForArray.items[index].types[i], pathToElement + key + '.'); break;
                } else if (schemaForArray.items[index].types[i].type === 'string' && isTypeOf(array[index], 'string')) {elementType = 'string'; validateString(index, array[index], schemaForArray.items[index].types[i], pathToElement + key + '.'); break;
                } else if (schemaForArray.items[index].types[i].type === 'array' && isTypeOf(array[index], 'array')) {elementType = 'array'; validateArray(index, array[index], schemaForArray.items[index].types[i], pathToElement + key + '.'); break;
                } else if (schemaForArray.items[index].types[i].type === 'object' && isTypeOf(array[index], 'object')) {elementType = 'object'; validateObject(index, array[index], schemaForArray.items[index].types[i], pathToElement + key + '.'); break;
                } else {requiredElements.push('"' + schemaForArray.items[index].types[i].type + '"');
                }
            }
            if (elementType === undefined && !has(schemaForArray.items[index], 'optional')) {addError(pathToElement + key + '.' + index, 'Элемент с индексом "' + index + '" массива "' + key + '" должен иметь значение с типом: ' + requiredElements.join(', ') + '.');}
        }

        function validateObject (key, object, schemaForObject, pathToElement) {
            if (isTypeOf(object, 'undefined') && has(schemaForObject, 'optional')) {return;}
            if (!isTypeOf(object, 'object')) {addError(pathToElement + key, 'Элемент "' + key + '" должен иметь в качестве значения объект.');
            } else {
                if (has(schemaForObject, 'min') && schemaForObject.min === 0 && objectLength(object) === 0) {return;}
                if (has(schemaForObject, 'min') && objectLength(object) < schemaForObject.min) {addError(pathToElement + key, 'Число свойств в объекте "' + key + '" должно быть больше или равно ' + schemaForObject.min + '.');}
                if (has(schemaForObject, 'max') && objectLength(object) > schemaForObject.max) {addError(pathToElement + key, 'Число свойств в объекте "' + key + '" должно быть меньше или равно ' + schemaForObject.max + '.');}
                if (has(schemaForObject, 'properties')) {
                    for (var property in schemaForObject.properties) {if (has(schemaForObject.properties, property)) {
                        if (
                                  !has(object, property)
                            && (
                                    (has(schemaForObject.properties[property], 'type') && schemaForObject.properties[property].type !== 'undefined'  && !has(schemaForObject.properties[property], 'optional'))
                                || (has(schemaForObject.properties[property], 'types') && !has(schemaForObject.properties[property], 'optional'))
                            )
                        ) {
                            addError(pathToElement + key, 'Элемент "' + property + '" должен присутствовать в объекте "' + key + '".');
                        }
                                   if (has(schemaForObject.properties[property], 'type')) {validateSingleObjectType(object, schemaForObject, property, key, pathToElement);
                        } else if (has(schemaForObject.properties[property], 'types')) {validateManyObjectTypes(object, schemaForObject, property, key, pathToElement);
                        }
                    }}
                }
            }
        }

        function validateSingleObjectType (object, schemaForObject, property, key, pathToElement) {
            if (!has(schemaForObject.properties[property], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента "' + property + '" объекта "' + key + '".');}
                       if (schemaForObject.properties[property].type === 'undefined') {validateUndefined(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'null') {validateNull(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'boolean') {validateBoolean(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'number') {validateNumber(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'string') {validateString(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'array') {validateArray(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            } else if (schemaForObject.properties[property].type === 'object') {validateObject(property, object[property], schemaForObject.properties[property], pathToElement + key + '.');
            }
        }

        function validateManyObjectTypes (object, schemaForObject, property, key, pathToElement) {
            var elementType
                , requiredElements = [];
            for (var i = 0, len = schemaForObject.properties[property].types.length; i < len; i++) {
                if (!has(schemaForObject.properties[property].types[i], 'type')) {throw new Error('Описание "type" всегда должно присутствовать в схеме для элемента "' + property + '" объекта "' + key + '".');}
                           if (schemaForObject.properties[property].types[i].type === 'undefined' && isTypeOf(object[property], 'undefined')) {elementType = 'undefined'; break;
                } else if (schemaForObject.properties[property].types[i].type === 'null' && isTypeOf(object[property], 'null')) {elementType = 'null'; break;
                } else if (schemaForObject.properties[property].types[i].type === 'boolean' && isTypeOf(object[property], 'boolean')) {elementType = 'boolean'; break;
                } else if (schemaForObject.properties[property].types[i].type === 'number' && isTypeOf(object[property], 'number')) {elementType = 'number'; validateNumber(property, object[property], schemaForObject.properties[property].types[i], pathToElement + key + '.'); break;
                } else if (schemaForObject.properties[property].types[i].type === 'string' && isTypeOf(object[property], 'string')) {elementType = 'string'; validateString(property, object[property], schemaForObject.properties[property].types[i], pathToElement + key + '.'); break;
                } else if (schemaForObject.properties[property].types[i].type === 'array' && isTypeOf(object[property], 'array')) {elementType = 'array'; validateArray(property, object[property], schemaForObject.properties[property].types[i], pathToElement + key + '.'); break;
                } else if (schemaForObject.properties[property].types[i].type === 'object' && isTypeOf(object[property], 'object')) {elementType = 'object'; validateObject(property, object[property], schemaForObject.properties[property].types[i], pathToElement + key + '.'); break;
                } else {requiredElements.push('"' + schemaForObject.properties[property].types[i].type + '"');
                }
            }
            if (elementType === undefined && !has(schemaForObject.properties[property], 'optional')) {addError(pathToElement + key + '.' + property, 'Элемент "' + property + '" объекта "' + key + '" должен иметь значение с типом: ' + requiredElements.join(', ') + '.');}
        }

        if (result.errors.length > 0) {
            (function(){
                var pathElements
                    , pathElementsLength
                    , resultPath = ''
                    , separator
                    , errorsLength = result.errors.length
                    , i;
                while (errorsLength--) {
                    pathElements = result.errors[errorsLength].path.split('.');
                    pathElementsLength = pathElements.length;
                    for (i = 0; i < pathElementsLength; i++) {
                        if (i === 0) {
                            separator = '';
                        } else {
                            separator = '.';
                        }
                                   if ((/^\d+$/g).test(pathElements[i])) {resultPath += '[' + pathElements[i] + ']';
                        } else if ((/^\d+/g).test(pathElements[i])) {resultPath += '["' + pathElements[i] + '"]';
                        } else {resultPath += separator + pathElements[i];
                        }
                    }
                    result.errors[errorsLength].path = resultPath;
                    resultPath = '';
                }
            })();
        }

        return result;

    }

    return {validate: validate};

});

Комментариев нет:

Отправить комментарий