core/default_value.js

/**
 * Default value wrappers, merging, and comment annotation, independent of schema loading/validation.
 * @module core/default_value
 */
const { typeOf, hasOwnProperty } = require('./schema_util');
const { MAGIC, stringify } = require('./commented_yaml');

/** The default value wrapper class. */
class DefaultValue {
  /**
   * @param {any} value The wrapped default value.
   * @param {string} description A description used to produce the comment string.
   */
  constructor(value, description) {
    this.value = value;
    this.description = description;
  }

  /**
   * Duplicate this wrapper, shallow-copying its wrapped arrays and objects.
   * @returns {DefaultValue} A new wrapper.
   */
  clone() {
    const result = new DefaultValue(this.value, this.description);
    if (result.value instanceof DefaultValue) {
      result.value = result.value.clone();
    } else if (typeOf(result.value) === 'array') {
      result.value = [].concat(result.value);
    } else if (typeOf(result.value) === 'object') {
      result.value = Object.assign({}, result.value);
    }
    return result;
  }

  /** Unwrap nested wrappers, preferring the innermost non-empty description. */
  flatten() {
    if (this.value instanceof DefaultValue) {
      this.value.flatten();
      if (hasOwnProperty(this.value, 'description') && this.value.description) {
        this.description = this.value.description;
      }
      this.value = this.value.value;
    }
  }

  /**
   * Merge a wrapper with the same value type, concatenating arrays and assigning object properties.
   * @param {DefaultValue} source The wrapper to merge.
   */
  merge(source) {
    if (hasOwnProperty(source, 'value') && source.value !== null) {
      this.flatten();
      if (source.value instanceof DefaultValue) {
        source = source.clone();
        source.flatten();
      }
      if (typeOf(source.value) === typeOf(this.value)) {
        if (typeOf(this.value) === 'array') {
          this.value = this.value.concat(source.value);
        } else if (typeOf(this.value) === 'object') {
          Object.keys(source.value).forEach((key) => {
            this.value[key] = source.value[key];
          });
        } else {
          this.value = source.value;
        }
      } else if (typeOf(this.value) === 'undefined' || typeOf(this.value) === 'null') {
        this.value = source.value;
      }
    }
    if (hasOwnProperty(source, 'description') && source.description) {
      this.description = source.description;
    }
  }

  /**
   * Insert description markers before each wrapped array element.
   * @returns {Array} A new array of comments and annotated values.
   */
  toCommentedArray() {
    return [].concat(
      ...this.value.map((item) => {
        if (item instanceof DefaultValue) {
          if (typeOf(item.description) !== 'string' || !item.description.trim()) {
            return [item.toCommented()];
          }
          return item.description
            .split('\n')
            .map((line, i) => {
              return MAGIC + i + ': ' + line;
            })
            .concat(item.toCommented());
        } else if (typeOf(item) === 'array' || typeOf(item) === 'object') {
          return new DefaultValue(item).toCommented();
        }
        return [item];
      }),
    );
  }

  /**
   * Insert description markers before each wrapped object property.
   * @returns {Object} A new object of comments and annotated values.
   */
  toCommentedObject() {
    if (this.value instanceof DefaultValue) {
      return this.value.toCommented();
    }
    const result = {};
    for (const key in this.value) {
      const item = this.value[key];
      if (item instanceof DefaultValue) {
        if (typeOf(item.description) === 'string' && item.description.trim()) {
          item.description.split('\n').forEach((line, i) => {
            result[MAGIC + key + i] = line;
          });
        }
        result[key] = item.toCommented();
      } else if (typeOf(item) === 'array' || typeOf(item) === 'object') {
        result[key] = new DefaultValue(item).toCommented();
      } else {
        result[key] = item;
      }
    }
    return result;
  }

  /**
   * Annotate arrays and objects with comment markers, or return a primitive value unchanged.
   * @returns {any} The annotated value.
   */
  toCommented() {
    if (typeOf(this.value) === 'array') {
      return this.toCommentedArray();
    } else if (typeOf(this.value) === 'object') {
      return this.toCommentedObject();
    }
    return this.value;
  }

  /**
   * Create YAML including the descriptions of nested values as comments.
   * @returns {string} The formatted YAML string.
   */
  toYaml() {
    return stringify(this.toCommented());
  }
}

module.exports = DefaultValue;