/**
* @copyright 2015 Tridium, Inc. All Rights Reserved.
* @author Gareth Johnson
*/
/**
* Defines {@link baja.Facets}.
* @module baja/obj/Facets
*/
define([ 'bajaScript/sys',
'bajaScript/baja/obj/Simple',
'bajaScript/baja/obj/objUtil',
'bajaPromises' ], function (
baja,
Simple,
objUtil,
Promise) {
'use strict';
var subclass = baja.subclass,
callSuper = baja.callSuper,
strictArg = baja.strictArg,
bajaDef = baja.def,
cacheEncode = objUtil.cacheEncode,
cacheDecode = objUtil.cacheDecode,
facetsDefault;
/**
* Mapping of data type symbol chars to their respective type specs. c&p'ed
* from DataTypes.java.
*
* @inner
* @type {Object}
*/
var symbolsToDataTypes = {
b: 'baja:Boolean',
i: 'baja:Integer',
l: 'baja:Long',
f: 'baja:Float',
d: 'baja:Double',
s: 'baja:String',
e: 'baja:DynamicEnum',
E: 'baja:EnumRange',
a: 'baja:AbsTime',
r: 'baja:RelTime',
u: 'baja:Unit',
z: 'baja:TimeZone',
o: 'baja:Ord',
m: 'baja:Marker'
};
////////////////////////////////////////////////////////////////
// Utility functions
////////////////////////////////////////////////////////////////
function getDefaultInstance(symbol) {
return baja.$(symbolsToDataTypes[symbol]);
}
/**
* Split the encoded string into an array of objects for decoding.
* @param {string} str
* @returns {Array.<Array.<string>>} array of keys, symbols, and encoded strings
*/
function toSplitArray(str) {
var keys = [];
var symbols = [];
var strings = [];
var split = str.split('|');
for (var i = 0, len = split.length; i < len; ++i) {
var keyAndValue = split[i].split(/=(.*)/);
var key = keyAndValue[0];
var value = keyAndValue[1] || '';
var symbolAndEncodedString = value.split(/:(.*)/); //split on first :
var symbol = symbolAndEncodedString[0];
var encodedString = symbolAndEncodedString[1];
if (symbol === 's' || symbol === 'o') {
//strings and ords are encoded into facets as slot-escaped
encodedString = baja.SlotPath.unescape(encodedString);
}
keys.push(key);
symbols.push(symbol);
strings.push(encodedString);
}
return [ keys, symbols, strings ];
}
/**
* Get the facets from a baja.Complex
*
* @param {baja.Complex} c
*/
function getFacetsFromComplex(c) {
// First, work with the 'facets' slot
var slotFacets = c.get('facets');
if (baja.hasType(slotFacets, 'baja:Facets')) {
return slotFacets;
}
// Second, work with the 'out' slot
// True for writable points and consts
var out = c.get('out');
if (out && typeof out.getStatusValueFacets === 'function') {
return out.getStatusValueFacets();
}
if (baja.hasType(out, 'baja:StatusValue')) {
return out.get('status').getFacets();
}
return baja.Facets.DEFAULT;
}
////////////////////////////////////////////////////////////////
// Facets implementation
////////////////////////////////////////////////////////////////
/**
* Represents a `baja:Facets` in BajaScript.
*
* `BFacets` is a map of name/value pairs used to annotate a
* `BComplex`'s Slot or to just provide additional metadata
* about something. The values of facets may only be
* `BIDataValue`s which are a predefined subset of simples.
*
* When creating a `Simple`, always use the `make()` method instead of
* creating a new Object.
*
* @class
* @alias baja.Facets
* @extends baja.Simple
*/
var Facets = function Facets(keys, vals) {
callSuper(Facets, this, arguments);
this.$map = new baja.OrderedMap();
strictArg(keys, Array, "Keys array required");
strictArg(vals, Array, "Values array required");
if (keys.length !== vals.length) {
throw new Error("baja.Facets Constructor must have an equal number of " +
"keys and values");
}
if (keys.length === 0) {
return;
}
const SlotPath = baja.SlotPath;
// Iterate through key, value pair arguments and add to the internal Map
const dvt = baja.lt("baja:IDataValue");
for (let i = 0; i < keys.length; ++i) {
if (typeof keys[i] !== 'string') {
throw new Error("Facets keys must be a String");
}
if (SlotPath) {
// some Facets instances are constructed before baja namespace - these are internal to BajaScript itself so ok
SlotPath.verifyValidName(keys[i]);
// Check everything being added here is a DataValue
if (!vals[i].getType) {
throw new Error("Facet value has no Baja Type associated with it");
}
if (!vals[i].getType().is(dvt)) {
throw new Error("Can only add baja:IDataValue Types to BFacets: " +
vals[i].getType().getTypeSpec());
}
}
this.$map.put(keys[i], vals[i]);
}
};
subclass(Facets, Simple);
facetsDefault = new Facets([], []);
/**
* Default `Facets` instance.
* @type {baja.Facets}
*/
Facets.DEFAULT = facetsDefault;
/**
* NULL `Facets` instance.
* @type {baja.Facets}
*/
Facets.NULL = facetsDefault;
function facetsExtend(orig, toAdd) {
var obj = {}, newKeys = [], newValues = [];
baja.iterate(orig.getKeys(), function (key) {
obj[key] = orig.get(key);
});
//overwrite any existing values with ones from toAdd
baja.iterate(toAdd.getKeys(), function (key) {
obj[key] = toAdd.get(key);
});
baja.iterate(obj, function (value, key) {
newKeys.push(key);
newValues.push(value);
});
return Facets.make(newKeys, newValues);
}
function facetsFromObj(obj) {
var keys = [],
values = [];
baja.iterate(obj, function (v, k) {
keys.push(k);
values.push(v);
});
return Facets.make(keys, values);
}
/**
* Make a `Facets` object. This function can either take two `Array` objects
* for keys and values, two `Facets` or two object literals. In the latter two
* cases, a new `Facets` object will be returned containing a combination of
* keys and values from both input `Facets` objects. If a key exists on both
* `Facets` objects, the value from the second Facets will take precedence.
*
* @param {Array.<String>|baja.Facets|Object} keys an array of keys for the
* facets. The keys must be `String`s. (This may also be a `Facets` (or object
* literal) whose values will be combined with the second parameter. Values in
* this object will be overwritten by corresponding values from the other).
* @param {Array.<baja.Simple>|baja.Facets|Object} [values] an array of values
* for the facets. The values must be BajaScript Objects whose `Type`
* implements `BIDataValue`. (This may also be a `Facets` (or object literal)
* object whose values will be combined with the first parameter. Values in
* this object will overwrite corresponding values on the other.)
* @returns {baja.Facets} the Facets
*
* @example
* var facets1 = baja.Facets.make(['a', 'b'], ['1', '2']);
* var facets2 = baja.Facets.make(['b', 'c'], ['3', '4']);
* var facets3 = baja.Facets.make(facets1, facets2);
* baja.outln(facets3.get('a')); //1
* baja.outln(facets3.get('b')); //3 - facets2 overwrote value in facets1
* baja.outln(facets3.get('c')); //4
*/
Facets.make = function (keys, values) {
// If there are no arguments are defined then return the default
if (arguments.length === 0) {
return facetsDefault;
}
// Throw an error if no keys
if (!keys) {
throw new Error("Keys required");
}
// If the keys are an Object then convert to Facets
if (keys.constructor === Object) {
keys = facetsFromObj(keys);
}
// If the values are an Object then convert to Facets
if (values && values.constructor === Object) {
values = facetsFromObj(values);
}
if (keys instanceof Facets) {
// If keys and values are facets then merge
if (values && values instanceof Facets) {
return facetsExtend(keys, values);
}
// If just the keys are facets then just return them
return keys;
}
// If we've got here then we assume the keys and values are arrays...
if (keys.length === 0) {
return facetsDefault;
}
// Note: I could do more argument checking here but I don't want to slow this down
// more than necessary.
return new Facets(keys, values);
};
/**
* Make a `Facets` object. Same as {@link baja.Facets.make}.
*
* @see baja.Facets.make
*/
Facets.prototype.make = function (args) {
return Facets.make.apply(Facets, arguments);
};
/**
* Decode a `String` to a `Facets`.
*
* @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.Facets}
*/
Facets.prototype.decodeFromString = cacheDecode(function (str, { unsafe = false } = {}) {
if (!unsafe) { throw new Error('Facets#decodeAsync should be called instead to ensure all types are loaded for the decode'); }
if (str.length === 0) { return facetsDefault; }
var arr = toSplitArray(str);
var keys = arr[0];
var symbols = arr[1];
var strings = arr[2];
var values = [];
for (var i = 0, len = symbols.length; i < len; ++i) {
values.push(getDefaultInstance(symbols[i]).decodeFromString(strings[i], { unsafe }));
}
return Facets.make(keys, values);
});
/**
* Decode a `String` to a `Facets`, doing an async decode on each individual
* encoded Simple.
*
* @param {String} str
* @param {baja.comm.Batch} [batch]
* @returns {Promise.<baja.Facets>}
*/
Facets.prototype.decodeAsync = function (str, batch) {
if (!str) { return Promise.resolve(facetsDefault); }
var arr = toSplitArray(str);
var keys = arr[0];
var symbols = arr[1];
var strings = arr[2];
var decodes = [];
for (var i = 0, len = symbols.length; i < len; ++i) {
decodes.push(getDefaultInstance(symbols[i]).decodeAsync(strings[i], batch));
}
return Promise.all(decodes)
.then(function (vals) {
return Facets.make(keys, vals);
});
};
/**
* Encode `Facets` to a `String`.
*
* @method
* @returns {String}
*/
Facets.prototype.encodeToString = cacheEncode(function () {
var s = "", // TODO: This needs more work for data encoding
k = this.$map.getKeys(),
v, i, symbol;
for (i = 0; i < k.length; ++i) {
if (i > 0) {
s += "|";
}
v = this.$map.get(k[i]);
if (v.getDataTypeSymbol === undefined) {
throw new Error("Cannot encode data type as 'getDataTypeSymbol' is not defined: " + v.getType());
}
symbol = v.getDataTypeSymbol();
// If a String or Ord then escape it
if (symbol === 's' || symbol === 'o') {
v = baja.SlotPath.escape(String(v));
}
s += k[i] + "=" + symbol + ":" + v.encodeToString();
}
return s;
});
/**
* Return a value from the map for the given key.
*
* @param {String} key the key used to look up the data value
* @param [def] if defined, this value is returned if the key can't be found.
* @returns the data value for the key (null if not found)
*/
Facets.prototype.get = function (key, def) {
strictArg(key, String);
def = bajaDef(def, null);
var v = this.$map.get(key);
return v === null ? def : v;
};
/**
* Return a copy of the `Facets` keys.
*
* @returns {Array.<String>} all of the keys used in the `Facets`
*/
Facets.prototype.getKeys = function () {
return this.$map.getKeys();
};
/**
* Return a `String` representation of the `Facets`.
*
* @param {Object} [cx] - Passed directly into the toString() functions of the
* individual Simples within. Object Literal used to specify formatting
* facets.
*
* @returns {String|Promise.<String>} returns a Promise if a cx is passed in.
*/
Facets.prototype.toString = function (cx) {
var str = "",
keys = this.$map.getKeys(),
unescape = baja.SlotPath.unescape,
i;
if (cx) {
var promises = [];
for (i = 0; i < keys.length; ++i) {
promises.push(this.$map.get(keys[i]).toString(cx));
}
return Promise.all(promises)
.then(function (values) {
for (i = 0; i < values.length; ++i) {
if (i > 0) {
str += ",";
}
str += unescape(keys[i]) + "=" + values[i];
}
return str;
});
}
for (i = 0; i < keys.length; ++i) {
if (i > 0) {
str += ",";
}
str += unescape(keys[i]) + "=" + this.$map.get(keys[i]).toString();
}
return str;
};
/**
* Return the value of the `Facets`.
*
* @method
* @returns {String}
*/
Facets.prototype.valueOf = Facets.prototype.toString;
/**
* Converts the `Facets` (itself) to an object literal.
*
* @returns {Object}
*/
Facets.prototype.toObject = function () {
var obj = {},
keys = this.$map.getKeys(),
i;
for (i = 0; i < keys.length; ++i) {
obj[keys[i]] = this.$map.get(keys[i]);
}
return obj;
};
/**
* Removes the key from the provided facets and returns a new facets. If the orig facets doesn't
* contain the specified key then return the original instance.
* @since Niagara 4.14
* @param {String} key the key in the facets to remove
* @returns {baja.Facets}
*/
Facets.prototype.makeRemove = function (key) {
const obj = this.toObject();
if (obj[key]) {
delete obj[key];
return Facets.make(obj);
} else {
return this;
}
};
/**
* Return the facets for the given object.
*
* If the object is a `Facets`, that object is returned.
*
* If the object has a `facets` slot, the value of that slot is returned.
*
* If the object has a parent, the `Facets` from the object's parent slot is
* returned.
*
* If the facets can't be found then {@link baja.Facets.DEFAULT} is returned.
*
* @param obj
* @returns {baja.Facets}
*/
Facets.getFacetsFromObject = function (obj) {
var val = facetsDefault;
if (obj.getType().is("baja:Facets")) {
val = obj;
} else if (obj.getType().isComplex()) {
val = getFacetsFromComplex(obj);
}
return val;
};
/**
* Get the slot facets from a container, merged all the way down through a
* property path or slot.
* @param {baja.Complex} [container]
* @param {Array.<baja.Property|string>|baja.Property|string} [propertyPath]
* @returns {baja.Facets}
* @since Niagara 4.10
*/
Facets.mergeSlotFacets = function (container, propertyPath) {
var facets = baja.Facets.DEFAULT;
if (!propertyPath) { return facets; }
if (!Array.isArray(propertyPath)) {
propertyPath = [ propertyPath ];
}
for (var i = 0, len = propertyPath.length; i < len; i++) {
if (!container) { return facets; }
var prop = propertyPath[i];
if (typeof container.getFacets === 'function') {
var containerFacets = container.getFacets(prop);
facets = containerFacets ? baja.Facets.make(containerFacets, facets) : facets;
}
if (typeof container.get === 'function' && typeof container.getSlot === 'function') {
try {
const slot = container.getSlot(prop);
if (slot && slot.isProperty()) {
container = container.get(prop);
} else {
container = null;
}
} catch (e) {
container = null;
}
} else {
return facets;
}
}
return facets;
};
return Facets;
});