X-Git-Url: http://source.jalview.org/gitweb/?a=blobdiff_plain;f=examples%2Fbiojson-doc%2Flib%2Fjsonpointer.js;fp=examples%2Fbiojson-doc%2Flib%2Fjsonpointer.js;h=3a662e7450b3d53ed4d5a284ec07834c8ed865d8;hb=1ba05544b34253204ef6410017a179c31fe5e23d;hp=0000000000000000000000000000000000000000;hpb=f8ce78dc02ce9d96129fcc4882749f43869928ff;p=jalview.git diff --git a/examples/biojson-doc/lib/jsonpointer.js b/examples/biojson-doc/lib/jsonpointer.js new file mode 100755 index 0000000..3a662e7 --- /dev/null +++ b/examples/biojson-doc/lib/jsonpointer.js @@ -0,0 +1,343 @@ +/** + * @author Alexey Kuzmin + * @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.>} + * @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); +})());