cloudflare/json-schema-docs-generator

View on GitHub
lib/pointer.js

Summary

Maintainability
A
55 mins
Test Coverage
/**
 * JSON Pointer implementation
 * Modified from: Alexey Kuzmin <alex.s.kuzmin@gmail.com>
 * see: http://tools.ietf.org/html/rfc6901
 *
 * @module lib/pointer
 */
'use strict';

var _ = require('lodash');
var pointer = {};
// A list of special characters and their escape sequences.
// Special characters will be unescaped in order they are listed.
// Section 3 of spec.
var specialChars = [
  ['/', '~1'],
  ['~', '~0']
];
// Token separator in JSON pointer string. Section 3 of spec.
var tokenSeparator = '/';
// Validates a pointer string.
var validPointerRegex = /(\/[^\/]*)+/;
// Possible errors during parsing
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 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 {Object} 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 new ReferenceError(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())) {
          // Evaluate the 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;
  };
}


/**
 * Validates JSON pointer string.
 *
 * @param pointer
 * @returns {boolean}
 */
function isValidJSONPointer(pointer) {
  if (!_.isString(pointer)) {
      // If it's not a string, it obviously is not valid.
      return false;
  }

  // If it is string and is an empty string, it's valid.
  if ('' === pointer) {
      return true;
  }

  // If it is non-empty string, it must match spec defined format.
  // Check Section 3 of specification for concrete syntax.
  return validPointerRegex.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) {
  // 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(tokenSeparator).reverse();
  // Last item in resulting array is always an empty string, we don't need it.
  tokens.pop();
  // Now tokens' array is ready to use
  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.
  specialChars.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 {Array|Object} 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.

  // In array context there are more strict requirements for token value.
  if (_.isArray(context)) {
      if ('-' === token) {
          // Token cannot be a "-" character,
          // it has no sense in current implementation.
          throw new SyntaxError(ErrorMessage.HYPHEN_IS_NOT_SUPPORTED_IN_ARRAY_CONTEXT);
      }
      if (_.isNaN(+token)) {
          // Token cannot be non-number.
          throw new ReferenceError(ErrorMessage.NON_NUMBER_TOKEN_IN_ARRAY_CONTEXT);
      }
      if (token.length > 1 && '0' === token[0]) {
          // Token cannot be non-zero number with leading zero.
          throw new ReferenceError(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 (_.isPlainObject(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 target object's value pointed by pointer, returns undefined
 * if |pointer| points to non-existing value.
 * If pointer is not provided, validates first argument and returns
 * evaluator function that takes pointer as argument.
 *
 * @param {Object} target
 * @param {string} [pointer]
 * @returns {*} pointer JSON Pointer string
 */
function getPointedValue(target, pointer) {
  // If not object, an exception will be thrown.
  if (!_.isPlainObject(target)) {
    throw new ReferenceError(ErrorMessage.INVALID_DOCUMENT_TYPE);
  }

  // target is already parsed, create an evaluator for it.
  var evaluator = createPointerEvaluator(target);

  // If no pointer was provided, return evaluator function.
  if (_.isUndefined(pointer)) {
    return evaluator;
  } else {
    return evaluator(pointer);
  }
}

/**
 *
 * @module lib/pointer
 */
module.exports = {
  get: getPointedValue
};