Source: wk/config/Config.js

/**
 * @author    Wolfgang Kowarschick <kowa@hs-augsburg.de>
 * @copyright 2017-2018
 * @license   CC-BY-NC-SA-4.0
 */

import {concretize} from './concretize';

/** @private */
const
  C_EMPTY             = Object.freeze({}),
  C_EMPTY_CONFIG_NAME = Symbol(),

  c_cache = new Map(),

  // private members
  v_config      = Symbol('v_config'),
  v_environment = Symbol('v_environment'),
  v_default     = Symbol('v_default');

/**
 * A class for storing configuration data. It does not define
 * any attribute. A <code>Config</code> object, however,
 * yields a (readonly) result for every attribute name
 * you pass to it. In all cases the result is a <code>Config</code> object,
 * which is initialized by the appropriate subobject of <code>p_config</code>.
 * By default <code>{@link Config.EMPTY}</code> is returned.
 *
 * @class
 */
class Config
{ /**
   * @param {object} p_config
   *          A configuration template object (a JSON object!).
   *          As a template object it may contain @-values that
   *          are replaced by concrete values by means of the
   *          function <code>{@link module:wk/config/concretize}</code>.
   * @param {object} p_environment
   *          An environment object that is used for computing
   *          a concrete value. It should contain concrete values
   *          for @-values that are referenced by <code>p_config</code>.
   *          For example:
   *<pre>
   *{ '@width':  500,
   *   '@height': 400
   *}
   *</pre>
   */
  constructor(p_config, p_environment = {})
  { const
      c_config_string = JSON.stringify(p_config),
      c_config        = c_cache.get(c_config_string);

    if (c_config != null)
       return c_config;

    c_cache.set(c_config_string, this);

    this[v_config] = p_config===C_EMPTY ? p_config : JSON.parse(c_config_string);
    this[v_environment] = p_environment;

    if (p_config !== C_EMPTY && p_environment[C_EMPTY_CONFIG_NAME] == null)
      p_environment[C_EMPTY_CONFIG_NAME] = new Config(C_EMPTY, p_environment);

    this[v_default] = false;

    return new Proxy
           ( this,
             { get(p_this, p_name)
               { let l_member = p_this[p_name];

                 if (typeof l_member === 'function')
                   return l_member;

                 if (l_member == null)
                 { const c_config = p_this[v_config][p_name];
                   if (c_config == null)
                   { return p_environment[C_EMPTY_CONFIG_NAME]; }
                   l_member = p_this[p_name]
                            = new Config(c_config, p_this[v_environment]);
                 }
                 return l_member;
               }
             }
           );
  }

  /**
   * The environment to be used for <code>this.config</code>.
   * As all sub objects of a configuration object share the same
   * environment, it suffices to change the content of the environment
   * one of those objects to change the environment of all related
   * config objects.
   *
   * @return {Object}
   */
  environment()
  { return this[v_environment]; }

  /**
   * The original configuration object
   * @return {Object}
   */
  config()
  { return this[v_config]; }

  /**
   * @param {Object}          p_config
   * @param {Object}          [p_config.environment = this.environment()]
   * @param {?Array.<String>} p_config.specials
   * @param {?number}         p_config.level
   *
   * @return {Object} The concretized value of <code>this.config()</code>
   */
  value({environment: p_environment = this.environment(),
         specials:    p_specials = null,
         level:       p_level = null
        } = {}
       )
  { return this.isUndefined()
             ? null
             : concretize({config:      this[v_config],
                           environment: p_environment,
                           specials:    p_specials,
                           level:       p_level
                         });
  }

  /**
   *  Returns <code>true</code>, if <code>this.config</code>
   *  has been defined.
   *  @return {Boolean}
   */
  isDefined()
  { return this[v_config] !== C_EMPTY; }

  /**
   *  Returns <code>true</code>, if <code>this.config</code>
   *  has not been defined.
   *  @return {Boolean}
   */
  isUndefined()
  { return this[v_config] === C_EMPTY; }

  /**
   *  Sets default values for JSON attributes which are not found in
   *  the configuration object itself.
   *
   *  @param {object} p_default
   */
  default(p_default) // deep prototype mixin should be used
  { Object.setPrototypeOf
           ( this[v_config],
             {...Object.getPrototypeOf(this[v_config]), ...p_default}
            );
  }

  /**
   * An empty config Object which can be used as a default value.
   * @type {Config}
   */
  static get EMPTY()
  { return C_EMPTY_CONFIG; }
}

const C_EMPTY_CONFIG = new Config(C_EMPTY);

export {Config};
export default Config;