UNPKG

json-dup-key-validator

Version:

A json validator that has an option to check for duplicated keys

492 lines (455 loc) 12.1 kB
var backslash = require('backslash'); module.exports = { validate: validate, parse: parse }; /** * Validates a json string. * Errors are returned * @param jsonString * @param allowDuplicatedKeys * @returns {String} error. undefined if no error */ function validate(jsonString, allowDuplicatedKeys) { var error; allowDuplicatedKeys = allowDuplicatedKeys || false; if (typeof jsonString !== 'string') { error = 'Input must be a string'; } else { try { // Try to find a value starting from index 0 _findValue(jsonString, 0, allowDuplicatedKeys, false); } catch(e) { error = e.message; } } return error; } /** * Parses a json. Errors are thrown if any * @param jsonString * @param allowDuplicatedKeys * @returns {Object} */ function parse(jsonString, allowDuplicatedKeys) { if (typeof jsonString !== 'string') { throw new Error('Input must be a string'); } allowDuplicatedKeys = allowDuplicatedKeys || false; // Try to find a value starting from index 0 var value = _findValue(jsonString, 0, allowDuplicatedKeys, true); return value.value; } /** * Find the comma separator, ], } or end of file * @param {String} str - original json string * @param {Number} startInd - starting index * @returns {{start: Number, end: Number, value: String}} value: the separator found * @private */ function _findSeparator(str, startInd) { var len = str.length; var sepStartInd = startInd; var sepEndInd; for (var i = startInd; i < len; i++) { var ch = str[i]; if (ch === ',') { sepEndInd = i; break; } else if ( ch === ']' || ch === '}') { sepEndInd = i - 1; break; } else if (!_isWhiteSpace(ch)) { throw _syntaxError(str, i, 'expecting end of expression or separator'); } } var value; if (sepEndInd === undefined) { sepEndInd = len; value = str[sepEndInd]; } else { value = str[sepEndInd]; sepEndInd++; } return { start: sepStartInd, end: sepEndInd, value: value }; } /** * Find the semi-colon separator ':' * @param {String} str - original json string * @param {Number} startInd * @returns {{start: Number, end: Number}} * @private */ function _findSemiColonSeparator(str, startInd) { var len = str.length; var semiColStartInd = startInd; var semiColEndInd; for (var i = startInd; i < len; i++) { var ch = str[i]; if (ch === ':') { semiColEndInd = i; break; } else if (!_isWhiteSpace(ch)) { throw _syntaxError(str, i, 'expecting \':\''); } } if (semiColEndInd === undefined) { throw _syntaxError(str, i, 'expecting \':\''); } semiColEndInd++; return { start: semiColStartInd, end: semiColEndInd }; } /** * Find a value it can be number, array, object, strings or boolean * @param {String} str - original json string * @param {Number} startInd * @param {Boolean} allowDuplicatedKeys - allow duplicated keys in objects or not * @returns {{value: *, start: Number, end: Number}} * @private */ function _findValue(str, startInd, allowDuplicatedKeys, parse) { var len = str.length; var valueStartInd; var valueEndInd; var isArray = false; var isObject = false; var isString = false; var isNumber = false; var dotFound = false; var exponentInd; var exponentPostNumberCount = 0; var whiteSpaceInNumber = false; var value; for (var i = startInd; i < len; i++) { var ch = str[i]; if (valueStartInd === undefined) { if (!_isWhiteSpace(ch)) { if (ch === '[') { isArray = true; } else if (ch === '{') { isObject = true; } else if (ch === '"') { isString = true; } else if (_isTrueFromIndex(str, i)) { valueStartInd = i; i = i + 3; valueEndInd = i; value = true; break; } else if (_isFalseFromIndex(str, i)) { valueStartInd = i; i = i + 4; valueEndInd = i; value = false; break; } else if (_isNullFromIndex(str, i)) { valueStartInd = i; i = i + 3; valueEndInd = i; value = null; break; } else if (_isNumber(ch)) { isNumber = true; } else if (ch === '-') { isNumber = true; } else { throw _syntaxError(str, i, ''); } valueStartInd = i; } } else { if (isArray) { var arr = _findArray(str, i, allowDuplicatedKeys, parse); valueEndInd = arr.end; value = arr.value; break; } else if (isObject) { var obj = _findObject(str, i, allowDuplicatedKeys, parse); valueEndInd = obj.end; value = obj.value; break; } else if (isString && ch === '"' && _hasEvenNumberOfBackSlash(str, i - 1)) { valueEndInd = i; value = backslash(str.substring(valueStartInd + 1, valueEndInd)); break; } else if (isNumber) { if(_isWhiteSpace(ch)) { whiteSpaceInNumber = true; } else if (ch === ',' || ch === ']' || ch === '}') { value = parseFloat(str.substring(valueStartInd, valueEndInd), 10); valueEndInd = i - 1; break; } else if (_isNumber(ch) && !whiteSpaceInNumber) { if (exponentInd) { exponentPostNumberCount++; } continue; } else if (ch === '.' && !dotFound && !exponentInd && !whiteSpaceInNumber) { dotFound = true; } else if ((ch === 'e' || ch === 'E') && !exponentInd && !whiteSpaceInNumber) { exponentInd = i; } else if ((ch === '+' || ch === '-') && exponentInd === i - 1 && !whiteSpaceInNumber) { continue; } else { throw _syntaxError(str, i, 'expecting number'); } } } } if (isNumber && exponentInd && !exponentPostNumberCount) { throw _syntaxError(str, i, 'expecting number'); } if (valueEndInd === undefined) { if (isNumber) { value = parseFloat(str.substring(valueStartInd, i), 10); valueEndInd = i - 1; } else { throw _syntaxError(str, i, 'unclosed statement'); } } valueEndInd++; return { value: value, start: valueStartInd, end: valueEndInd }; } /** * Find a key in an object * @param {String} str - original json string * @param {Number} startInd * @returns {{start: Number, end: Number, value: String}} * @private */ function _findKey(str, startInd) { var len = str.length; var keyStartInd; var keyEndInd; for (var i = startInd; i < len; i++) { var ch = str[i]; if (keyStartInd === undefined) { if (!_isWhiteSpace(ch)) { if (ch !== '"') { throw _syntaxError(str, i, 'expecting String'); } keyStartInd = i; } } else { if (ch === '"' && _hasEvenNumberOfBackSlash(str, i - 1)) { keyEndInd = i; break; } } } if (keyEndInd === undefined) { throw _syntaxError(str, len, 'expecting String'); } var value = backslash(str.substring(keyStartInd + 1, keyEndInd)); if (value === '') { throw _syntaxError(str, keyStartInd, 'empty string'); } keyEndInd++; return { start: keyStartInd, end: keyEndInd, value: value }; } /** * Find an object by identifying the key, ':' separator and value * @param {String} str - original json string * @param {Number} startInd * @param {Boolean} allowDuplicatedKeys * @returns {{start: Number, end: Number, value: Object}} * @private */ function _findObject(str, startInd, allowDuplicatedKeys, parse) { var i = startInd; var sepValue = ','; var obj = {}; var keys = []; var values = []; var j = startInd; while (_isWhiteSpace(str[j])) { j++; } if (str[j] === '}') { return { start: startInd, end: j, value: obj }; } while (sepValue === ',') { var key = _findKey(str, i); var semi = _findSemiColonSeparator(str, key.end); var value = _findValue(str, semi.end, allowDuplicatedKeys, parse); var sepIndex = _findSeparator(str, value.end); if (!allowDuplicatedKeys) { if(keys.indexOf(key.value) !== -1) { throw _syntaxError(str, key.end, 'duplicated keys "' + key.value + '"'); } } keys.push(key.value); values.push(value.value); i = sepIndex.end; sepValue = sepIndex.value; } if (parse) { var indx = 0; for(indx = 0; indx < keys.length; indx++) { obj[keys[indx]] = values[indx]; } } return { start: startInd, end: i, value: obj }; } /** * Going backward from an index, determine if there are even number * of consecutive backslashes in the string * @param {String} str - original json string * @param {Number} endInd * @returns {Boolean} * @private */ function _hasEvenNumberOfBackSlash(str, endInd) { var i = endInd; var count = 0; while(i > -1 && str[i] === '\\') { count++; i--; } return (count % 2) === 0; } /** * Find an array by identifying values separated by ',' separator * @param {String} str - original json string * @param {Number} startInd * @returns {{start: Number, end: Number, value: Array}} * @private */ function _findArray(str, startInd, allowDuplicatedKeys, parse) { var i = startInd; var sepValue = ','; var arr = []; var j = startInd; while (_isWhiteSpace(str[j])) { j++; } if (str[j] === ']') { return { start: startInd, end: j, value: arr }; } while (sepValue === ',') { var value = _findValue(str, i, allowDuplicatedKeys, parse); var sepIndex = _findSeparator(str, value.end); if (parse) { arr.push(value.value); } i = sepIndex.end; sepValue = sepIndex.value; } return { start: startInd, end: i, value: arr }; } /** * Determine if the string is 'true' from specified index * @param {String} str - original json string * @param {Number} ind * @returns {Boolean} * @private */ function _isTrueFromIndex(str, ind) { return (str.substr(ind, 4) === 'true'); } /** * Determine if the string is 'false' from specified index * @param {String} str - original json string * @param {Number} ind * @returns {Boolean} * @private */ function _isFalseFromIndex(str, ind) { return (str.substr(ind, 5) === 'false'); } /** * Determine if the string is 'null' from specified index * @param {String} str - original json string * @param {Number} ind * @returns {Boolean} * @private */ function _isNullFromIndex(str, ind) { return (str.substr(ind, 4) === 'null'); } var white = new RegExp(/^\s$/); /** * Determine if this character is a white space * @param {String} ch - single character string * @returns {Boolean} * @private */ function _isWhiteSpace(ch){ return white.test(ch); } var numberReg = new RegExp(/^\d$/); /** * Determine if this character is a numeric character * @param {String} ch - single character string * @returns {Boolean} * @private */ function _isNumber(ch) { return numberReg.test(ch); } /** * Generate syntax error * @param {String} str - original json string * @param {Number} index - index in which the error was detected * @param {String} reason * @returns {Error} * @private */ function _syntaxError(str, index, reason) { var regionLen = 10; var regionStr; if (str.length < index + regionLen) { regionStr = str.substr(_normalizeNegativeNumber(str.length - regionLen), str.length); } else if (index - (regionLen/2) < 0) { regionStr = str.substr(0, regionLen); } else { regionStr = str.substr(_normalizeNegativeNumber(index - (regionLen/2)), regionLen); } var message; if (reason) { message = 'Syntax error: ' + reason + ' near ' + regionStr; } else { message = 'Syntax error near ' + regionStr; } return new Error(message); } /** * Return 0 if number is negative, the original number otherwise * @param {Number} num * @returns {Number} * @private */ function _normalizeNegativeNumber(num) { return (num < 0) ? 0 : num; }