/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.EnumRange}.
* @module baja/obj/EnumRange
*/
define([ "bajaScript/sys",
"bajaScript/baja/obj/Facets",
"bajaScript/baja/obj/Simple",
"bajaScript/baja/obj/objUtil",
"bajaPromises" ], function (baja, Facets, Simple, objUtil, Promise) {
'use strict';
var subclass = baja.subclass,
callSuper = baja.callSuper,
strictArg = baja.strictArg,
strictAllArgs = baja.strictAllArgs,
objectify = baja.objectify,
bajaDef = baja.def,
strictNumber = baja.strictNumber,
cacheDecode = objUtil.cacheDecode,
cacheEncode = objUtil.cacheEncode,
uncacheConstantEncodeDecode = objUtil.uncacheConstantEncodeDecode,
facetsDefault = Facets.DEFAULT;
/**
* Represents a `baja:EnumRange` in BajaScript.
*
* An `EnumRange` stores a range of ordinal/name pairs for Enumerations.
*
* When creating a `Simple`, always use the `make()` method instead of
* creating a new Object.
*
* @class
* @alias baja.EnumRange
* @extends baja.Simple
*/
var EnumRange = function EnumRange(frozen, dynamic, byOrdinal, byTag, options) {
callSuper(EnumRange, this, arguments);
this.$frozen = frozen;
this.$dynamic = strictArg(dynamic, Array);
this.$byOrdinal = strictArg(byOrdinal, Object);
this.$byTag = strictArg(byTag, Object);
this.$options = strictArg(options, baja.Facets);
};
subclass(EnumRange, Simple);
/**
* Make an EnumRange.
*
* The TypeSpec for a FrozenEnum can be used as the first argument. If other arguments
* are required then an Object Literal is used to to specify the method's arguments.
*
* @param {Object} [obj] the Object Literal that holds the method's arguments.
* @param {String|Type} [obj.frozen] the Type or TypeSpec for the FrozenEnum.
* @param {Array.<Number|baja.Simple>} [obj.ordinals] an array of numbers (any
* `baja:Number` type) that specify the dynamic enum ordinals.
* @param {Array.<String>} [obj.tags] an array of strings that specify the
* dynamic enum tags.
* @param {baja.Facets} [obj.options] optional facets.
* @returns {baja.EnumRange} the EnumRange.
*
* @example
* var er = baja.EnumRange.make({
* ordinals: [0, 1, 2],
* tags: ["A", "B", "C"]
* });
*/
EnumRange.make = function (obj) {
obj = objectify(obj, "frozen");
var frozen = bajaDef(obj.frozen, null),
ordinals = obj.ordinals,
tags = obj.tags,
count = obj.count,
options = obj.options,
byOrdinal = {},
byTag = {},
dynaOrdinals = [],
o, t, i;
// Support String typespec as well as type
if (typeof frozen === "string") {
frozen = baja.lt(frozen);
}
if (tags && !ordinals) {
if (!frozen) {
ordinals = tags.map(function (tag, index) { return index; });
} else {
throw new Error("Ordinals array required with FrozenEnum");
}
}
if (!ordinals) {
ordinals = [];
}
if (!tags) {
tags = [];
}
if (count === undefined && ordinals instanceof Array) {
count = ordinals.length;
}
if (!options) {
options = facetsDefault;
}
strictAllArgs([ ordinals, tags, count, options ], [ Array, Array, Number, baja.Facets ]);
if (ordinals.length !== tags.length) {
throw new Error("Ordinals and tags arrays must match in length");
}
// optimization
if (count === 0 && options === facetsDefault) {
if (frozen === null) {
return EnumRange.DEFAULT;
}
return new EnumRange(frozen, [], {}, {}, options);
}
for (i = 0; i < count; ++i) {
o = ordinals[i];
t = tags[i];
// Check for undefined due to BajaScript loading sequence
if (baja.SlotPath !== undefined) {
baja.SlotPath.verifyValidName(t);
}
// check against frozen
if (frozen !== null && frozen.isOrdinal(o)) {
continue;
}
// check duplicate ordinal
if (byOrdinal.hasOwnProperty(o)) {
throw new Error("Duplicate ordinal: " + t + "=" + o);
}
// check duplicate tag
if (byTag.hasOwnProperty(t) ||
(frozen && frozen.isTag(t))) {
throw new Error("Duplicate tag: " + t + "=" + o);
}
// put into map
byOrdinal[o] = t;
byTag[t] = o;
dynaOrdinals.push(o);
}
return new EnumRange(frozen, dynaOrdinals, byOrdinal, byTag, options);
};
/**
* Make an EnumRange.
*
* The TypeSpec for a FrozenEnum can be used as the first argument. If other arguments
* are required then an Object Literal is used to to specify the method's arguments.
*
* @param {Object} [obj] the Object Literal that holds the method's arguments.
* @param {String|Type} [obj.frozen] the Type or TypeSpec for the FrozenEnum.
* @param {Array.<Number|baja.Simple>} [obj.ordinals] an array of numbers (any
* `baja:Number` type) that specify the dynamic enum ordinals.
* @param {Array.<String>} [obj.tags] an array of strings that specify the
* dynamic enum tags.
* @param {baja.Facets} [obj.options] optional facets.
* @returns {baja.EnumRange} the EnumRange .
*
* @example
* var er = baja.$("baja:EnumRange").make({
* ordinals: [0, 1, 2],
* tags: ["A", "B", "C"]
* });
*/
EnumRange.prototype.make = function (obj) {
return EnumRange.make.apply(EnumRange, arguments);
};
function splitFrozenDynamic(s) {
var frozen = null,
dynamic = null,
plus = s.indexOf('+');
if (plus < 0) {
if (s.indexOf("{") === 0) {
dynamic = s;
} else {
frozen = s;
}
} else {
if (s.indexOf("{") === 0) {
dynamic = s.substring(0, plus);
frozen = s.substring(plus + 1);
} else {
frozen = s.substring(0, plus);
dynamic = s.substring(plus + 1);
}
}
return [ frozen, dynamic ];
}
function splitRangeAndOptions(str) {
var range, options, question = str.indexOf('?');
if (question >= 0) {
range = str.substring(0, question);
options = str.substring(question + 1);
} else {
range = str;
options = '';
}
return [ range, options ];
}
function parseDynamic(s, ordinals, tags) {
var count = 0,
ordinal,
st = s ? s.split(/[=,]/) : [],
i;
for (i = 0; i < st.length; ++i) {
tags[count] = st[i];
ordinal = parseInt(st[++i], 10);
if (isNaN(ordinal)) {
throw new Error("Invalid ordinal: " + st[i]);
}
ordinals[count] = ordinal;
count++;
}
return count;
}
/**
* Decode an `EnumRange` 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.EnumRange}
*/
EnumRange.prototype.decodeFromString = function (str, { unsafe = false } = {}) {
if (!unsafe) { throw new Error('EnumRange#decodeAsync should be called instead to ensure all types are loaded for the decode'); }
if (str === "{}") {
return EnumRange.DEFAULT;
}
// split body from options (there can't be a question mark
// anywhere until after the frozen type or dynamic {}
var rangeAndOptions = splitRangeAndOptions(str),
rangeStr = rangeAndOptions[0],
options = facetsDefault.decodeFromString(rangeAndOptions[1], { unsafe }),
split = splitFrozenDynamic(rangeStr),
frozenStr = split[0],
dynamicStr = split[1],
frozen = null,
ordinals,
tags,
count;
// get frozen
if (frozenStr !== null) {
frozen = baja.lt(frozenStr);
if (frozen === null) {
throw new Error("Invalid frozen EnumRange spec: " + frozenStr);
}
}
if (dynamicStr === null) {
return this.make({
"frozen": frozen,
"options": options
});
}
// check for required braces on dynamic
if (dynamicStr.charAt(0) !== "{") {
throw new Error("Missing {");
}
if (dynamicStr.charAt(dynamicStr.length - 1) !== "}") {
throw new Error("Missing }");
}
dynamicStr = dynamicStr.substring(1, dynamicStr.length - 1);
// get dynamic
ordinals = [];
tags = [];
count = parseDynamic(dynamicStr, ordinals, tags);
return this.make({
"frozen": frozen,
"ordinals": ordinals,
"tags": tags,
"count": count,
"options": options
});
};
/**
* If the string encoding includes a frozen type, ensure that that type is
* imported before decoding the EnumRange.
*
* @param {string} str
* @param {baja.comm.Batch} [batch]
* @returns {Promise.<baja.EnumRange>}
*/
EnumRange.prototype.decodeAsync = function (str, batch) {
var frozenType = splitFrozenDynamic(splitRangeAndOptions(str)[0])[0];
return Promise.resolve(frozenType && baja.importTypes({
typeSpecs: [ frozenType ],
batch: batch
}))
.then(function () {
return baja.EnumRange.DEFAULT.decodeFromString(str, baja.Simple.$unsafeDecode);
});
};
/**
* Encode an `EnumRange` to a `String`.
*
* @returns {String}
*/
EnumRange.prototype.encodeToString = function () {
// Optimization for encoding
if (this === EnumRange.DEFAULT) {
return "{}";
}
var s = "",
key,
tag, i;
if (this.$frozen !== null) {
s += this.$frozen.getTypeSpec();
}
if (this.$dynamic.length > 0 || this.$frozen === null) {
if (s.length > 0) {
s += "+";
}
s += "{";
for (i = 0; i < this.$dynamic.length; ++i) {
key = this.$dynamic[i];
tag = this.$byOrdinal[key];
if (i > 0) {
s += ",";
}
s += tag + "=" + key;
}
s += "}";
}
if (this.$options !== facetsDefault) {
s += "?" + this.$options.encodeToString();
}
return s;
};
/**
* Equality test.
*
* @param obj
* @returns {Boolean} true if valid
*/
EnumRange.prototype.equals = function (obj) {
if (!baja.hasType(obj, 'baja:EnumRange')) { return false; }
var ords1 = this.getOrdinals(),
ords2 = obj.getOrdinals();
if (ords1.length !== ords2.length) { return false; }
for (var i = 0; i < ords1.length; i++) {
if (!obj.isOrdinal(ords1[i]) || this.getTag(ords1[i]) !== obj.getTag(ords1[i])) {
return false;
}
}
if (!(obj.getOptions().equals(this.getOptions()))) {
return false;
}
return true;
};
/**
* Default EnumRange instance.
* @type {baja.EnumRange}
*/
EnumRange.DEFAULT = uncacheConstantEncodeDecode(new EnumRange(null, [], {}, {}, facetsDefault));
EnumRange.prototype.decodeFromString = cacheDecode(EnumRange.prototype.decodeFromString);
EnumRange.prototype.encodeToString = cacheEncode(EnumRange.prototype.encodeToString);
/**
* Return the data type symbol.
*
* @returns {String} data type symbol.
*/
EnumRange.prototype.getDataTypeSymbol = function () {
return "E";
};
/**
* Return all of the ordinals for the `EnumRange`.
*
* The returned array contains both frozen and enum ordinals.
*
* @returns {Array.<Number>} an array of numbers that represents the ordinals for this
* `EnumRange`.
*/
EnumRange.prototype.getOrdinals = function () {
var ordinals, i;
if (this.$frozen !== null) {
ordinals = this.$frozen.getOrdinals();
} else {
ordinals = [];
}
for (i = 0; i < this.$dynamic.length; ++i) {
ordinals.push(this.$dynamic[i]);
}
return ordinals;
};
/**
* Return true if the ordinal is valid in this `EnumRange`.
*
* @param {Number|baja.Simple} ordinal (any `baja:Number` type)
* @returns {Boolean} true if valid
*/
EnumRange.prototype.isOrdinal = function (ordinal) {
ordinal = strictNumber(ordinal);
if (this.$frozen !== null && this.$frozen.isOrdinal(ordinal)) {
return true;
}
return this.$byOrdinal.hasOwnProperty(ordinal);
};
/**
* Return the tag for the specified ordinal.
*
* If the ordinal isn't valid then the ordinal is returned
* as a `String`.
*
* @param {Number|baja.Simple} ordinal (any `baja:Number` type)
* @returns {String} tag
*/
EnumRange.prototype.getTag = function (ordinal) {
ordinal = strictNumber(ordinal);
if (this.$byOrdinal.hasOwnProperty(ordinal)) {
return this.$byOrdinal[ordinal];
}
if (this.$frozen !== null && this.$frozen.isOrdinal(ordinal)) {
return this.$frozen.getTag(ordinal);
}
return String(ordinal);
};
/**
* Return the display tag for the specified ordinal.
*
* If the ordinal isn't valid then the ordinal is returned
* as a `String`.
*
* @param {Number|baja.Simple} ordinal (any `baja:Number` type)
* @returns {String} tag
*/
EnumRange.prototype.getDisplayTag = function (ordinal) {
var that = this,
tag;
ordinal = strictNumber(ordinal);
if (that.$frozen !== null && that.$frozen.isOrdinal(ordinal)) {
return that.$frozen.getDisplayTag(ordinal);
}
if (that.$byOrdinal.hasOwnProperty(ordinal)) {
tag = that.$byOrdinal[ordinal];
// TODO: should look up lexicon.
return baja.SlotPath.unescape(tag);
}
return String(ordinal);
};
/**
* Return true if the tag is used within the `EnumRange`.
*
* @param {String} tag
* @returns {Boolean} true if valid.
*/
EnumRange.prototype.isTag = function (tag) {
strictArg(tag, String);
if (this.$frozen !== null && this.$frozen.isTag(tag)) {
return true;
}
return this.$byTag.hasOwnProperty(tag);
};
/**
* Convert the tag to its ordinal within the `EnumRange`.
*
* @param {String} tag
* @returns {Number} ordinal for the tag.
* @throws {Error} if the tag is invalid.
*/
EnumRange.prototype.tagToOrdinal = function (tag) {
strictArg(tag, String);
if (this.$frozen !== null && this.$frozen.isTag(tag)) {
return this.$frozen.tagToOrdinal(tag);
}
if (!this.$byTag.hasOwnProperty(tag)) {
throw new Error("Invalid tag: " + tag);
}
return this.$byTag[tag];
};
/**
* 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|baja.FrozenEnum|Boolean} the enum for the tag or
* ordinal.
* @throws {Error} if the tag or ordinal is invalid.
*/
EnumRange.prototype.get = function (arg) {
if (typeof arg === "string") {
if (this === EnumRange.BOOLEAN_RANGE) {
return arg !== "false";
}
// Look up via tag name
if (this.$frozen !== null && this.$frozen.isTag(arg)) {
return this.$frozen.getFrozenEnum(arg);
}
if (this.$byTag.hasOwnProperty(arg)) {
return baja.DynamicEnum.make({ "ordinal": this.$byTag[arg], "range": this });
}
} else {
arg = strictNumber(arg);
if (this === EnumRange.BOOLEAN_RANGE) {
return arg !== 0;
}
// Look up via ordinal
if (this.$frozen !== null && this.$frozen.isOrdinal(arg)) {
return this.$frozen.getFrozenEnum(arg);
}
if (this.isOrdinal(arg)) {
return baja.DynamicEnum.make({ "ordinal": arg, "range": this });
}
}
throw new Error("Unable to access enum");
};
/**
* Return true if the ordinal is a valid ordinal in the frozen range.
*
* @param {Number|baja.Simple} ordinal (any `baja:Number` type)
* @returns {Boolean} true if valid.
*/
EnumRange.prototype.isFrozenOrdinal = function (ordinal) {
ordinal = strictNumber(ordinal);
return this.$frozen !== null && this.$frozen.isOrdinal(ordinal);
};
/**
* Return true if the ordinal is a valid ordinal in the dynamic range.
*
* @param {Number|baja.Simple} ordinal (any `baja:Number` type)
* @returns {Boolean} true if valid
*/
EnumRange.prototype.isDynamicOrdinal = function (ordinal) {
ordinal = strictNumber(ordinal);
return this.$byOrdinal.hasOwnProperty(ordinal);
};
/**
* Return the Type used for the frozen enum range or null if this range
* has no frozen ordinal/tag pairs.
*
* @returns {Type} the Type for the FrozenEnum or null.
*/
EnumRange.prototype.getFrozenType = function () {
return this.$frozen;
};
/**
* Get the options for this range stored as a Facets instance.
*
* @returns {baja.Facets} facets
*/
EnumRange.prototype.getOptions = function () {
return this.$options;
};
/**
* Boolean EnumRange.
* @type {baja.EnumRange}
*/
EnumRange.BOOLEAN_RANGE = EnumRange.make({
ordinals: [ 0, 1 ],
tags: [ "false", "true" ],
options: Facets.make({ lexicon: 'baja' })
});
return EnumRange;
});