/** * @author Alexey Kuzmin <alex.s.kuzmin@gmail.com> * @fileoverview JavaScript implementation of JSON Pointer. * @see http://tools.ietf.org/html/rfc6901 */ ;(function() { 'use strict'; /** * List of special characters and their escape sequences. * Special characters will be unescaped in order they are listed. * Section 3 of spec. * @type {Array.<Array.<string>>} * @const */ var SPECIAL_CHARACTERS = [ ['/', '~1'], ['~', '~0'] ]; /** * Tokens' separator in JSON pointer string. * Section 3 of spec. * @type {string} * @const */ var TOKENS_SEPARATOR = '/'; /** * Prefix for error messages. * @type {string} * @const */ var ERROR_MESSAGE_PREFIX = 'JSON Pointer: '; /** * Validates non-empty pointer string. * @type {RegExp} * @const */ var NON_EMPTY_POINTER_REGEXP = /(\/[^\/]*)+/; /** * List of error messages. * Please keep it in alphabetical order. * @enum {string} */ var ErrorMessage = { HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT: 'Implementation does not support "-" token for arrays.', INVALID_DOCUMENT: 'JSON document is not valid.', INVALID_DOCUMENT_TYPE: 'JSON document must be a string or object.', INVALID_POINTER: 'Pointer is not valid.', NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT: 'Non-number tokens cannot be used in array context.', TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT: 'Token with leading zero cannot be used in array context.' }; /** * Returns |target| object's value pointed by |opt_pointer|, returns undefined * if |opt_pointer| points to non-existing value. * If pointer is not provided, validates first argument and returns * evaluator function that takes pointer as argument. * @param {(string|Object|Array)} target Evaluation target. * @param {string=} opt_pointer JSON Pointer string. * @returns {*} Some value. */ function getPointedValue(target, opt_pointer) { // .get() method implementation. // First argument must be either string or object. if (isString(target)) { // If string it must be valid JSON document. try { // Let's try to parse it as JSON. target = JSON.parse(target); } catch (e) { // If parsing failed, an exception will be thrown. throw getError(ErrorMessage.INVALID_DOCUMENT); } } else if (!isObject(target)) { // If not object or string, an exception will be thrown. throw getError(ErrorMessage.INVALID_DOCUMENT_TYPE); } // |target| is already parsed, let's create evaluator function for it. var evaluator = createPointerEvaluator(target); if (isUndefined(opt_pointer)) { // If pointer was not provided, return evaluator function. return evaluator; } else { // If pointer is provided, return evaluation result. return evaluator(opt_pointer); } } /** * Returns function that takes JSON Pointer as single argument * and evaluates it in given |target| context. * Returned function throws an exception if pointer is not valid * or any error occurs during evaluation. * @param {*} target Evaluation target. * @returns {Function} */ function createPointerEvaluator(target) { // Use cache to store already received values. var cache = {}; return function(pointer) { if (!isValidJSONPointer(pointer)) { // If it's not, an exception will be thrown. throw getError(ErrorMessage.INVALID_POINTER); } // First, look up in the cache. if (cache.hasOwnProperty(pointer)) { // If cache entry exists, return it's value. return cache[pointer]; } // Now, when all arguments are valid, we can start evaluation. // First of all, let's convert JSON pointer string to tokens list. var tokensList = parsePointer(pointer); var token; var value = target; // Evaluation will be continued till tokens list is not empty // and returned value is not an undefined. while (!isUndefined(value) && !isUndefined(token = tokensList.pop())) { // Let's evaluate token in current context. // `getValue()` might throw an exception, but we won't handle it. value = getValue(value, token); } // Pointer evaluation is done, save value in the cache and return it. cache[pointer] = value; return value; }; } /** * Returns true if given |pointer| is valid, returns false otherwise. * @param {!string} pointer * @returns {boolean} Whether pointer is valid. */ function isValidJSONPointer(pointer) { // Validates JSON pointer string. if (!isString(pointer)) { // If it's not a string, it obviously is not valid. return false; } if ('' === pointer) { // If it is string and is an empty string, it's valid. return true; } // If it is non-empty string, it must match spec defined format. // Check Section 3 of specification for concrete syntax. return NON_EMPTY_POINTER_REGEXP.test(pointer); } /** * Returns tokens list for given |pointer|. List is reversed, e.g. * '/simple/path' -> ['path', 'simple'] * @param {!string} pointer JSON pointer string. * @returns {Array} List of tokens. */ function parsePointer(pointer) { // Converts JSON pointer string into tokens list. // Let's split pointer string by tokens' separator character. // Also we will reverse resulting array to simplify it's further usage. var tokens = pointer.split(TOKENS_SEPARATOR).reverse(); // Last item in resulting array is always an empty string, // we don't need it, let's remove it. tokens.pop(); // Now tokens' array is ready to use, let's return it. return tokens; } /** * Decodes all escape sequences in given |rawReferenceToken|. * @param {!string} rawReferenceToken * @returns {string} Unescaped reference token. */ function unescapeReferenceToken(rawReferenceToken) { // Unescapes reference token. See Section 3 of specification. var referenceToken = rawReferenceToken; var character; var escapeSequence; var replaceRegExp; // Order of unescaping does matter. // That's why an array is used here and not hash. SPECIAL_CHARACTERS.forEach(function(pair) { character = pair[0]; escapeSequence = pair[1]; replaceRegExp = new RegExp(escapeSequence, 'g'); referenceToken = referenceToken.replace(replaceRegExp, character); }); return referenceToken; } /** * Returns value pointed by |token| in evaluation |context|. * Throws an exception if any error occurs. * @param {*} context Current evaluation context. * @param {!string} token Unescaped reference token. * @returns {*} Some value or undefined if value if not found. */ function getValue(context, token) { // Reference token evaluation. See Section 4 of spec. // First of all we should unescape all special characters in token. token = unescapeReferenceToken(token); // Further actions depend of context of evaluation. if (isArray(context)) { // In array context there are more strict requirements // for token value. if ('-' === token) { // Token cannot be a "-" character, // it has no sense in current implementation. throw getError(ErrorMessage.HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT); } if (!isNumber(token)) { // Token cannot be non-number. throw getError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT); } if (token.length > 1 && '0' === token[0]) { // Token cannot be non-zero number with leading zero. throw getError(ErrorMessage.TOKEN_WITH_LEADING_ZERO_IN_ARRAY_CONTEXT); } // If all conditions are met, simply return element // with token's value index. // It might be undefined, but it's ok. return context[token]; } if (isObject(context)) { // In object context we can simply return element w/ key equal to token. // It might be undefined, but it's ok. return context[token]; } // If context is not an array or an object, // token evaluation is not possible. // This is the expected situation and so we won't throw an error, // undefined value is perfectly suitable here. return; } /** * Returns Error instance for throwing. * @param {string} message Error message. * @returns {Error} */ function getError(message) { return new Error(ERROR_MESSAGE_PREFIX + message); } function isObject(o) { return 'object' === typeof o && null !== o; } function isArray(a) { return Array.isArray(a); } function isNumber(n) { return !isNaN(Number(n)); } function isString(s) { return 'string' === typeof s || s instanceof String; } function isUndefined(v) { return 'undefined' === typeof v; } // Let's expose API to the world. var jsonpointer = { get: getPointedValue }; if ('object' === typeof exports) { // If `exports` is an object, we are in Node.js context. // We are supposed to act as Node.js package. module.exports = jsonpointer; } else if ('function' === typeof define && define.amd) { // If there is global function `define()` and `define.amd` is defined, // we are supposed to act as AMD module. define(function() { return jsonpointer; }); } else { // Last resort. // Let's create global `jsonpointer` object. this.jsonpointer = jsonpointer; } }).call((function() { 'use strict'; return (typeof window !== 'undefined' ? window : global); })());