baja/obj/DynamicEnum.js

/**
 * @copyright 2015 Tridium, Inc. All Rights Reserved.
 * @author Gareth Johnson
 */

/**
 * Defines {@link baja.DynamicEnum}.
 * @module baja/obj/DynamicEnum
 */
define([ "lex!",
        "bajaScript/sys",
        "bajaScript/baja/obj/Enum",
        "bajaScript/baja/obj/EnumRange",
        "bajaScript/baja/obj/objUtil",
        "bajaPromises" ], function (
         lex,
         baja,
         Enum,
         EnumRange,
         objUtil,
         Promise) {
  
  "use strict";
  
  var subclass = baja.subclass,
      callSuper = baja.callSuper,
      objectify = baja.objectify,
      bajaDef = baja.def,
      bajaHasType = baja.hasType,
      strictArg = baja.strictArg,
      cacheDecode = objUtil.cacheDecode,
      cacheEncode = objUtil.cacheEncode;
  
  /**
   * Represents a `baja:DynamicEnum` in BajaScript.
   * 
   * `DynamicEnum` stores an ordinal state variable as 
   * a `Number`.  An instance of `EnumRange` may be used to specify the range.
   * 
   * When creating a `Simple`, always use the `make()` method instead of 
   * creating a new Object.
   *
   * @class
   * @alias baja.DynamicEnum
   * @extends baja.Enum
   */
  var DynamicEnum = function DynamicEnum(ordinal, range) {
    callSuper(DynamicEnum, this, arguments);
    this.$ordinal = strictArg(ordinal, Number);
    this.$range = strictArg(range || baja.EnumRange.DEFAULT, baja.EnumRange);    
  };
  
  subclass(DynamicEnum, Enum);
  
  /**
   * Make a `DynamicEnum`.
   * 
   * @param {Object|Number} [obj] the Object Literal for the method's arguments or an ordinal.
   * @param {Number} [obj.ordinal] the ordinal for the enum. If omitted it
   * defaults to the first ordinal in the range or 0 if the range has no
   * ordinals.
   * @param {baja.EnumRange} [obj.range] the range for the enum.
   * @param {baja.DynamicEnum|baja.FrozenEnum|Boolean|String} [obj.en] if defined, this enum will be used for the ordinal and range. 
   *                                                                   As well as an enum, this can also be a TypeSpec 
   *                                                                   String (moduleName:typeName) for a FrozenEnum.
   * @returns {baja.DynamicEnum} the DynamicEnum.
   * 
   * @example
   *   //An ordinal or an Object Literal can be used for the method's arguments...
   *   var de1 = baja.DynamicEnum.make(0); // Just with an ordinal
   *
   *   //... or with an Object Literal...
   *
   *   var de2 = baja.DynamicEnum.make({ordinal: 0, range: enumRange});
   *   
   *   //...or create from another enumeration...
   *
   *   var de3 = baja.DynamicEnum.make({en: anEnum});
   */
  DynamicEnum.make = function (obj) {
    obj = objectify(obj, "ordinal");  

    var ordinal = 0,
        range = bajaDef(obj.range, EnumRange.DEFAULT),
        en;

    if (obj.ordinal !== undefined) {
      ordinal = obj.ordinal;
    } else if (range.getOrdinals().length > 0) {
      ordinal = range.getOrdinals()[0];
    }

    // Create from another enumeration...
    if (obj.en !== undefined) {
      en = obj.en;
    
      if (!bajaHasType(en)) {
        throw new Error("Invalid Enum Argument");
      }
      
      if (en.getType().is("baja:DynamicEnum")) {
        return en;
      }
      
      // If a type spec is passed in then resolve it to the enum instance
      if (typeof en === "string") {
        en = baja.$(en);
      }
      
      if (en.getType().isFrozenEnum() || en.getType().is("baja:Boolean")) {
        ordinal = en.getOrdinal();
        range = en.getRange();
      } else {
        throw new Error("Argument must be an Enum");
      }
    }
    
    // Check for default
    if (ordinal === 0 && range === EnumRange.DEFAULT) {
      return DynamicEnum.DEFAULT;
    }
    
    return new DynamicEnum(ordinal, range);
  };
  
  /**
   * Make a DynamicEnum.
   *
   * @param {Object|Number} [obj] the Object Literal for the method's arguments or an ordinal.
   * @param {Number} [obj.ordinal] the ordinal for the enum. If omitted it
   * defaults to the first ordinal in the range or 0 if the range has no
   * ordinals.
   * @param {baja.DynamicEnum|baja.FrozenEnum|Boolean|String} [obj.en] if defined, this enum will be used for the ordinal and range. 
   *                                                                   As well as an enum, this can also be a TypeSpec 
   *                                                                   String (moduleName:typeName) for a FrozenEnum.
   * @returns {baja.DynamicEnum} the DynamicEnum.
   * 
   * @example
   *   // An ordinal or an Object Literal can be used for the method's arguments...
   *   var de1 = baja.$("baja:DynamicEnum").make(0); // Just with an ordinal
   *
   *   //... or with an Object Literal...
   *
   *   var de2 = baja.$("baja:DynamicEnum").make({ordinal: 0, range: enumRange});
   *   
   *   //...or create from another enumeration...
   *
   *   var de3 = baja.$("baja:DynamicEnum").make({en: anEnum});
   */
  DynamicEnum.prototype.make = function (obj) {
    return DynamicEnum.make.apply(DynamicEnum, arguments);
  };
    
  /**
   * Decode a `DynamicEnum` from a `String`.
   *
   * @param {String} str
   * @param {Object} [params]
   * @param {Boolean} [params.unsafe=false] if set to true, this will allow
   * decodeFromString to continue. If not, decodeFromString will throw an error. This flag is for
   * internal bajaScript use only. All external implementations should use decodeAsync instead.
   * @returns {baja.DynamicEnum}
   */  
  DynamicEnum.prototype.decodeFromString = function (str, { unsafe = false } = {}) {
    if (!unsafe) { throw new Error('DynamicEnum#decodeAsync should be called instead to ensure all types are loaded for the decode'); }

    const [ o, r ] = str.split('@');
    const defaultRange = EnumRange.DEFAULT;
    const range = r ? defaultRange.decodeFromString(r, { unsafe }) : defaultRange;

    return this.make({ ordinal: parseInt(o), range });
  };

  /**
   * Asynchronously decode a `DynamicEnum` from a string, including importing
   * any type spec encoded in the range.
   * @param {string} str
   * @param {baja.comm.Batch} [batch]
   * @returns {Promise.<string>}
   */
  DynamicEnum.prototype.decodeAsync = function (str, batch) {
    const [ o, r ] = str.split('@');
    const defaultRange = EnumRange.DEFAULT;

    return Promise.resolve(r ? defaultRange.decodeAsync(r, batch) : defaultRange)
      .then((range) => this.make({ ordinal: parseInt(o), range }));
  };
  
  DynamicEnum.prototype.decodeFromString = cacheDecode(DynamicEnum.prototype.decodeFromString);
  
  /**
   * Encode a `DynamicEnum` to a `String`.
   *                     
   * @returns {String}
   */  
  DynamicEnum.prototype.encodeToString = function () {       
    var s = "";
    s += this.$ordinal;
    if (this.$range !== baja.EnumRange.DEFAULT) {
      s += "@" + this.$range.encodeToString();
    }
    
    return s;
  };
  
  DynamicEnum.prototype.encodeToString = cacheEncode(DynamicEnum.prototype.encodeToString);
  
  /**
   * Default DynamicEnum instance.
   * @type {baja.DynamicEnum}
   */   
  DynamicEnum.DEFAULT = new DynamicEnum(0, EnumRange.DEFAULT); 
  
  /**
   * Return the data type symbol.
   *
   * @returns {String} the data type symbol.
   */
  DynamicEnum.prototype.getDataTypeSymbol = function () {
    return "e";
  };
  
  /**
   * Return whether the enum is active or not.
   *
   * @returns {Boolean} true if active.
   */
  DynamicEnum.prototype.isActive = function () {
    return this.$ordinal !== 0;
  };
  
  /**
   * Return the ordinal.
   *
   * @returns {Number} the ordinal.
   */
  DynamicEnum.prototype.getOrdinal = function () {
    return this.$ordinal;
  };
  
  /**
   * Return the range.
   *
   * @returns {baja.EnumRange} the enum range.
   */
  DynamicEnum.prototype.getRange = function () {
    return this.$range;
  };
  
  /**
   * Return the tag for the ordinal.
   *
   * @returns {String} the tag.
   */
  DynamicEnum.prototype.getTag = function () {
    return this.$range.getTag(this.$ordinal);
  };
  
  /**
   * Return the String representation of the DynamicEnum.
   *
   * @param {Object} [cx]
   * @param {baja.EnumRange} [cx.range] range to use when formatting the string.
   * If not provided, the range configured on the DynamicEnum directly will be
   * used.
   * @returns {String|Promise.<String>} a string representation of this
   * DynamicEnum (Promise if context given; String if no context given)
   */
  DynamicEnum.prototype.toString = function (cx) {
    var ordinal = this.$ordinal;

    if (!cx) {
      return this.$range.getDisplayTag(ordinal);
    } else {
      return toEnumString(this, cx.range || this.getRange(), ordinal);
    }
  };

  function toEnumString(en, range, ordinal) {
    if (!range || !range.isOrdinal(ordinal)) {
      return Promise.resolve(String(en));
    }

    var lexicon = range.getOptions().get('lexicon'),
      got = range.get(ordinal),
      tag = got.getTag();

    if (lexicon) {
      return lex.module(lexicon)
        .then(function (lex) {
          const tagKey = baja.SlotPath.unescape(tag);
          let lexValue = lex.get(tag);

          if (!lexValue && tagKey !== tag) {
            lexValue = lex.get({ key: tagKey, def: tagKey });  
          }

          return lexValue || tagKey;
        })
        .catch(function () { //module not found
          return String(got);
        });
    } else {
      return Promise.resolve(String(got));
    }
  }

  /**
   * Get the enum for the specified tag or ordinal.
   *
   * This method is used to access an enum based upon a tag or ordinal.
   *
   * @param {String|Number|baja.Simple} arg a tag or ordinal (any `baja:Number`
   * type).
   * @returns {baja.DynamicEnum} the enum for the tag or ordinal.
   * @throws {Error} if the tag or ordinal is invalid.
   * @since Niagara 4.6
   */
  DynamicEnum.prototype.get = function (arg) {
    const range = this.getRange();
    const enumFromRange = range.get(arg);
    if (enumFromRange instanceof DynamicEnum) {
      return enumFromRange;
    }
    return DynamicEnum.make({ ordinal: enumFromRange.getOrdinal(), range });
  };
  
  /**
   * Equals comparison via tag or ordinal.
   *
   * @param {String|Number|baja.DynamicEnum} arg the enum, tag or ordinal used for comparison.
   * @returns {Boolean} true if equal.
   */
  DynamicEnum.prototype.is = function (arg) {  
    var tof = typeof arg;
    if (tof === "number") {
      return arg === this.$ordinal;
    }    
    
    if (tof === "string") {
      return arg === this.getTag();
    }
    
    return this.equals(arg);
  };

  return DynamicEnum;
});