ikuseiGmbH/smart-village-app-cms

View on GitHub
lib/jsonform-defaults.js

Summary

Maintainability
C
1 day
Test Coverage
/**
 * @fileoverview The JSON Form "defaults" library exposes a setDefaultValues
 * method that extends the object passed as argument so that it includes
 * values for all required fields of the JSON schema it is to follow that
 * define a default value.
 *
 * The library is called to complete the configuration settings of a template in
 * the Factory and to complete datasource settings.
 *
 * The library is useless if the settings have already been validated against the
 * schema using the JSON schema validator (typically, provided the validator is
 * loaded, submitting the form created from the schema will raise an error when
 * required properties are missing).
 *
 * Note the library does not validate the created object, it merely sets missing
 * values to the default values specified in the schema. All other values may
 * be invalid.
 *
 * Nota Bene:
 * - in data-joshfire, the runtime/nodejs/lib/jsonform-defaults.js file is a
 * symbolic link to the jsonform submodule in deps/jsonform
 * - in platform-joshfire, the server/public/js/libs/jsonform-defaults.js file
 * is a symbolic link to the jsonform submodule in deps/jsonform
 */

(function () {
  // Establish the root object:
  // that's "window" in the browser, "global" in node.js
  var root = this;

  /**
   * Sets default values, ensuring that fields defined as "required" in the
   * schema appear in the object. If missing, the hierarchy that leads to
   * a required key is automatically created.
   *
   * @function
   * @param {Object} obj The object to complete with default values according
   *  to the schema
   * @param {Object} schema The JSON schema that the object follows
   * @param {boolean} includeOptionalValues Include default values for fields
   *  that are not "required"
   * @param {boolean} skipFieldsWithoutDefaultValue Set flag not to include a
   *  generated empty default value for fields marked as "required" in the
   *  schema but that do not define a default value.
   * @return {Object} The completed object (same instance as obj)
   */
  var setDefaultValues = function (obj, schema, includeOptionalValues, skipFieldsWithoutDefaultValue) {
    if (!obj || !schema) return obj;
    if (!schema.properties) {
      schema = { properties: schema };
    }

    // Inner function that parses the schema recursively to build a flat
    // list of defaults
    var defaults = {};
    var extractDefaultValues = function (schemaItem, path) {
      var properties = null;
      var child = null;

      if (!schemaItem || (schemaItem !== Object(schemaItem))) return null;

      if (schemaItem.required) {
        // Item is required
        if (schemaItem['default']) {
          // Item defines a default value, let's use it,
          // no need to continue in that case, we have the default value
          // for the whole subtree starting at schemaItem.
          defaults[path] = schemaItem['default'];
          return;
        }
        else if (skipFieldsWithoutDefaultValue) {
          // Required but no default value and caller explicitly asked not
          // include such fields in the returned object.
        }
        else if ((schemaItem.type === 'object') || schemaItem.properties) {
          // Item is a required object
          defaults[path] = {};
        }
        else if ((schemaItem.type === 'array') || schemaItem.items) {
          // Item is a required array
          defaults[path] = [];
        }
        else if (schemaItem.type === 'string') {
          defaults[path] = '';
        }
        else if ((schemaItem.type === 'number') || (schemaItem.type === 'integer')) {
          defaults[path] = 0;
        }
        else if (schemaItem.type === 'boolean') {
          defaults[path] = false;
        }
        else {
          // Unknown type, use an empty object by default
          defaults[path] = {};
        }
      }
      else if (schemaItem['default'] && includeOptionalValues) {
        // Item is not required but defines a default value and the
        // include optional values flag is set, so let's use it.
        // No need to continue in that case, we have the default value
        // for the whole subtree starting at schemaItem.
        defaults[path] = schemaItem['default'];
        return;
      }

      // Parse schema item's properties recursively
      properties = schemaItem.properties;
      if (properties) {
        for (var key in properties) {
          if (properties.hasOwnProperty(key)) {
            extractDefaultValues(properties[key], path + '.' + key);
          }
        }
      }

      // Parse schema item's children recursively
      if (schemaItem.items) {
        // Items may be a single item or an array composed of only one item
        child = schemaItem.items;
        if (_isArray(child)) {
          child = child[0];
        }

        extractDefaultValues(child, path + '[]');
      }
    };

    // Build a flat list of default values
    extractDefaultValues(schema, '');

    // Ensure the object's default values are correctly set
    for (var key in defaults) {
      if (defaults.hasOwnProperty(key)) {
        setObjKey(obj, key, defaults[key]);
      }
    }
  };


  /**
   * Retrieves the default value for the given key in the schema
   *
   * Levels in the path are separated by a dot. Array items are marked
   * with []. For instance:
   *  foo.bar[].baz
   *
   * @function
   * @param {Object} schema The schema to parse
   * @param {String} key The path to the key whose default value we're
   *  looking for. each level is separated by a dot, and array items are
   *  flagged with [x].
   * @return {Object} The default value, null if not found.
   */
  var getSchemaKeyDefaultValue = function(schema,key) {
    var schemaKey = key
      .replace(/\./g, '.properties.')
      .replace(/\[.*\](\.|$)/g, '.items$1');
    var schemaDef = getObjKey(schema, schemaKey);
    if (schemaDef) return schemaDef['default'];
    return null;
  };

  /**
   * Retrieves the key identified by a path selector in the structured object.
   *
   * Levels in the path are separated by a dot. Array items are marked
   * with []. For instance:
   *  foo.bar[].baz
   *
   * @function
   * @param {Object} obj The object to parse
   * @param {String} key The path to the key whose default value we're
   *  looking for. each level is separated by a dot, and array items are
   *  flagged with [x].
   * @return {Object} The key definition, null if not found.
   */
  var getObjKey = function (obj, key) {
    var innerobj = obj;
    var keyparts = key.split('.');
    var subkey = null;
    var arrayMatch = null;
    var reArraySingle = /\[([0-9]*)\](?:\.|$)/;

    for (var i = 0; i < keyparts.length; i++) {
      if (typeof innerobj !== 'object') return null;
      subkey = keyparts[i];
      arrayMatch = subkey.match(reArraySingle);
      if (arrayMatch) {
        // Subkey is part of an array
        subkey = subkey.replace(reArraySingle, '');
        if (!_isArray(innerobj[subkey])) {
          return null;
        }
        innerobj = innerobj[subkey][parseInt(arrayMatch[1], 10)];
      }
      else {
        innerobj = innerobj[subkey];
      }
    }

    return innerobj;
  };


  /**
   * Sets the key identified by a path selector to the given value.
   *
   * Levels in the path are separated by a dot. Array items are marked
   * with []. For instance:
   *  foo.bar[].baz
   *
   * The hierarchy is automatically created if it does not exist yet.
   *
   * Default values are added to all array items. Array items are not
   * automatically created if they do not exist (in particular, the
   * minItems constraint is not enforced)
   *
   * @function
   * @param {Object} obj The object to build
   * @param {String} key The path to the key to set where each level
   *  is separated by a dot, and array items are flagged with [x].
   * @param {Object} value The value to set, may be of any type.
   */
  var setObjKey = function (obj, key, value) {
    var keyparts = key.split('.');

    // Recursive version of setObjKey
    var recSetObjKey = function (obj, keyparts, value) {
      var arrayMatch = null;
      var reArray = /\[([0-9]*)\]$/;
      var subkey = keyparts.shift();
      var idx = 0;

      if (keyparts.length > 0) {
        // Not the end yet, build the hierarchy
        arrayMatch = subkey.match(reArray);
        if (arrayMatch) {
          // Subkey is part of an array, check all existing array items
          // TODO: review that! Only create the right item!!!
          subkey = subkey.replace(reArray, '');
          if (!_isArray(obj[subkey])) {
            obj[subkey] = [];
          }
          obj = obj[subkey];
          if (arrayMatch[1] !== '') {
            idx = parseInt(arrayMatch[1], 10);
            if (!obj[idx]) {
              obj[idx] = {};
            }
            recSetObjKey(obj[idx], keyparts, value);
          }
          else {
            for (var k = 0; k < obj.length; k++) {
              recSetObjKey(obj[k], keyparts, value);
            }
          }
          return;
        }
        else {
          // "Normal" subkey
          if (typeof obj[subkey] !== 'object') {
            obj[subkey] = {};
          }
          obj = obj[subkey];
          recSetObjKey(obj, keyparts, value);
        }
      }
      else {
        // Last key, time to set the value, unless already defined
        arrayMatch = subkey.match(reArray);
        if (arrayMatch) {
          subkey = subkey.replace(reArray, '');
          if (!_isArray(obj[subkey])) {
            obj[subkey] = [];
          }
          idx = parseInt(arrayMatch[1], 10);
          if (!obj[subkey][idx]) {
            obj[subkey][idx] = value;
          }
        }
        else if (!obj[subkey]) {
          obj[subkey] = value;
        }
      }
    };

    // Skip first item if empty (key starts with a '.')
    if (!keyparts[0]) {
      keyparts.shift();
    }
    recSetObjKey(obj, keyparts, value);
  };

  // Taken from Underscore.js (not included to save bytes)
  var _isArray = Array.isArray || function (obj) {
    return Object.prototype.toString.call(obj) == '[object Array]';
  };


  // Export the code as:
  //  1. an AMD module (the "define" method exists in that case), or
  //  2. a node.js module ("module.exports" is defined in that case), or
  //  3. a global JSONForm object (using "root")
  if (typeof define !== 'undefined') {
    // AMD module
    define([], function () {
      return {
        setDefaultValues: setDefaultValues,
        setObjKey: setObjKey,
        getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
      };
    });
  }
  else if ((typeof module !== 'undefined') && module.exports) {
    // Node.js module
    module.exports = {
      setDefaultValues: setDefaultValues,
      setObjKey: setObjKey,
      getSchemaKeyDefaultValue: getSchemaKeyDefaultValue
    };
  }
  else {
    // Export the function to the global context, using a "string" for
    // Google Closure Compiler "advanced" mode
    // (not sure why it's needed, done by Underscore)
    root['JSONForm'] = root['JSONForm'] || {};
    root['JSONForm'].setDefaultValues = setDefaultValues;
    root['JSONForm'].setObjKey = setObjKey;
    root['JSONForm'].getSchemaKeyDefaultValue = getSchemaKeyDefaultValue;
  }
})();