/**
* @copyright 2021 Tridium, Inc. All Rights Reserved.
*/
/**
* Defines {@link baja.FacetsMap}.
* @module baja/obj/FacetsMap
*/
define([ 'bajaScript/sys',
'bajaScript/baja/obj/Simple',
'bajaPromises' ], function (
baja,
Simple,
Promise) {
'use strict';
/**
* Represents a `baja:FacetsMap` in BajaScript. `FacetsMap` is simply a
* mapping of String names to `Facets` instances. It is mostly intended for
* internal use by the framework.
*
* When creating a `Simple`, always use the `make()` method instead of
* creating a new Object.
*
* @class
* @alias baja.FacetsMap
* @extends baja.Simple
*/
class FacetsMap extends Simple {
constructor(obj = {}) {
super();
if (typeof obj !== 'object') { throw new Error('object required'); }
Object.values(obj).forEach((facets) => {
if (!baja.hasType(facets, 'baja:Facets')) { throw new Error('Facets required'); }
});
this.$obj = obj;
}
/**
* @param {object} obj a mapping of string keys to `baja.Facets` instances
* @returns {baja.FacetsMap}
*/
static make(obj) {
const facetsMap = new FacetsMap(obj);
return facetsMap.list().length ? facetsMap : DEFAULT;
}
/**
* @param {object} obj a mapping of string keys to `baja.Facets` instances
* @returns {baja.FacetsMap}
*/
make(obj) {
return FacetsMap.make(...arguments);
}
/**
* @returns {string[]} all string keys in this `FacetsMap` instance
*/
list() {
return Object.keys(this.$obj);
}
/**
* @param {string} key
* @returns {baja.Facets} the `Facets` at that key, or `baja.Facets.NULL` if
* not found
*/
get(key) {
return this.$obj[key] || baja.Facets.NULL;
}
/**
* @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.FacetsMap}
*/
decodeFromString(str, { unsafe = false } = {}) {
if (!unsafe) { throw new Error('FacetsMap#decodeAsync should be called instead to ensure all types are loaded for the decode'); }
const obj = parseEncoding(str);
Object.keys(obj).forEach((key) => {
obj[key] = baja.Facets.DEFAULT.decodeFromString(obj[key], baja.Simple.$unsafeDecode);
});
return FacetsMap.make(obj);
}
/**
* @param {string} str
* @param {baja.comm.Batch} [batch]
* @returns {Promise<baja.FacetsMap>}
*/
decodeAsync(str, batch) {
const obj = parseEncoding(str);
return Promise.all(Object.keys(obj).map((key) => {
return Promise.resolve(baja.Facets.DEFAULT.decodeAsync(obj[key], batch))
.then((facets) => { obj[key] = facets; });
}))
.then(() => FacetsMap.make(obj));
}
/**
* @returns {string}
*/
encodeToString() {
return '{' + this.list().map((key) => {
return `${ escape(key) }=${ escape(this.get(key).encodeToString()) };`;
}).join('') + '}';
}
/**
* @returns {boolean} true if empty
*/
isNull() {
return !this.list().length;
}
/**
* @returns {baja.FacetsMap} the DEFAULT instance
*/
static get DEFAULT() { return DEFAULT; }
/**
* @returns {baja.FacetsMap} the NULL instance
*/
static get NULL() { return DEFAULT; }
}
const DEFAULT = new FacetsMap({});
function escape(str) {
return str.replace(/[{}=;]/g, (s) => '\\' + s);
}
function unescape(str) {
return str.replace(/\\(.)/g, (m, c) => c);
}
/**
* @param {string} str string encoding from `BFacetsMap#encodeToString`
* @returns {object} mapping of string names to their corresponding
* `baja.Facets` string encodings
*/
function parseEncoding(str) {
const obj = {};
const [ , body ] = str.match(/^{(.*)}$/);
const escapedFacetsEncodings = splitOnUnescapedSeparator(body, ';');
const pairs = escapedFacetsEncodings.map((str) => splitOnUnescapedSeparator(str, '='));
pairs.forEach(([ key, value ]) => {
obj[unescape(key)] = unescape(value);
});
return obj;
}
// must be something that can never be in the actual string, or at least is vanishingly unlikely
// to be in production data. watch out for tech support complaints from Penguins Inc.
const SEPARATOR_REPLACEMENT = '🐧\ue000🐧\ue000🐧';
const replaceSeparator = (match, charBeforeSeparator) => charBeforeSeparator + SEPARATOR_REPLACEMENT;
function splitOnUnescapedSeparator(str, separator) {
// first match is the "real" character before the unescaped separator which we will put back in
const unescapedSeparatorRegex = new RegExp(`([^\\\\])${ separator }`, 'g');
const withRealSeparatorsReplaced = str.replace(unescapedSeparatorRegex, replaceSeparator);
const actualEncodings = withRealSeparatorsReplaced.split(SEPARATOR_REPLACEMENT);
return actualEncodings.filter((s) => s); // if last char is a separator, last string will be empty
}
return FacetsMap;
});