| /** |
| * @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); |
| })()); |