sdj-esm
Version:
Self Described JSON - ESM
346 lines (304 loc) • 12.9 kB
text/typescript
/*
Copyright (c) 2023-2024 Will Rudolph <@willrudolph.com>
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
import type {
CoreSD,
DescriptionJI,
EntityJI,
EntitySearch,
FuncStrNumVoid,
Info,
ItemJI,
ItemSearch,
SdKeyProps
} from "../core/interfaces.js";
import {checkResetInfo, verifyUniqKeys,} from "../util/verify.js";
import {genEntityJI, genInfoJI, genItemJI} from "../util/immutables.js";
import {BASE_ITEMS_JI, ESDJ_CLASS, GRAPH_ZERO, SYS_RESERVED} from "../core/statics.js";
import {cloneJI, getFromCoreArray} from "../util/func.std.js";
import {isArrayWithLen, isInfo, validLexiconArray} from "../core/validators.js";
import {restrictToAllowedKeys} from "../core/restrict.js";
import type {IDescriptionSdj, IEntitySdj, IItemSdj} from "./class-interfaces.js";
import {SdjEntity} from "./entity.js";
import {SdjItem} from "./item.js";
import type {ISdjHost, SdjJITypes} from "../global/global-interfaces.js";
import {clone, cloneDeep, each, find, isArray, isEqual, isFunction, times, uniq} from "lodash-es";
import {validSDKey} from "../core/sdj-types.js";
import {getRegEx} from "../util/regex.js";
// Descriptions created outside of new SdJson() or SdjHost.makeDescription
// using 'new SdjDescription(...)' ; will NOT automatically be added to a library.
// After initial creation they can be added as needed via library.storeDesc(...);
export class SdjDescription implements IDescriptionSdj {
log: FuncStrNumVoid;
private _lexicons: string[] = [];
private _graph: IEntitySdj[] = [];
private _sdInfo: Info;
private _lang: string = "en";
private _items: IItemSdj[] = [];
private _host: ISdjHost;
constructor(refDescJI: DescriptionJI, requiredHost: ISdjHost) {
this._host = requiredHost;
if (requiredHost?.checkClassInst && isFunction(requiredHost.checkClassInst)) {
this.host.checkClassInst(requiredHost, ESDJ_CLASS.HOST, true);
this.host.checkClassInst(refDescJI, ESDJ_CLASS.DESCRIPTION, false);
} else {
throw new Error("[SDJ] Improper required Host - use SdjHost.createDescription for auto provided host;");
}
SdjDescription.VerifyJI(refDescJI);
let inDescJI: DescriptionJI;
inDescJI = cloneJI(refDescJI);
inDescJI.sdInfo = checkResetInfo(inDescJI.sdInfo);
inDescJI = this._host.fullDescription(inDescJI);
this._sdInfo = inDescJI.sdInfo;
this.lang = (inDescJI.lang) ? String(inDescJI.lang).toLowerCase() : "en";
this.log = this._host.getLogFunc("Desc:" + this._sdInfo.name);
this.log("created");
this.entityItemBuild(inDescJI);
}
// vvv Below are frozen when description/sdJson locked
get graph(): SdjEntity[] {
return <SdjEntity[]>this._graph;
}
set graph(inGraph: SdjEntity[]) {
this._graph = inGraph;
}
set items(inItems: IItemSdj[]) {
this._items = <SdjItem[]>inItems;
}
get items(): SdjItem[] {
return <SdjItem[]>this._items;
}
get name(): string {
return this._sdInfo.name;
}
set name(name: string) {
this._sdInfo.name = name;
}
get sdInfo(): Info {
return this._sdInfo;
}
get lang() {
return this._lang;
}
set lang(inStr: string) {
if (getRegEx("lang").test(inStr)) {
this._lang = String(inStr).toLowerCase();
}
}
// ^^ Above Frozen when locked
get host(): ISdjHost {
return this._host;
}
searchEntities(searchEnt: EntitySearch): IEntitySdj[] {
return this._host.searchMgr.searchEntities(this, searchEnt);
}
searchItems(searchItem: ItemSearch): IItemSdj[] {
return this._host.searchMgr.searchItems(this, searchItem);
}
getEntity (entKeyNum: number | string): IEntitySdj | undefined {
return (entKeyNum === 0) ? undefined : <IEntitySdj>getFromCoreArray(entKeyNum, this._graph);
}
getEntityRefs (entArray: number[] | string[]): IEntitySdj[] {
const rtnAry: IEntitySdj[] = [];
each(entArray, (theValue: number | string) => {
const entRef = <IEntitySdj>getFromCoreArray(theValue, this._graph);
if (entRef && entRef.sdId !== 0) {
rtnAry.push(entRef);
} else {
this.log(`Unable to find item sdId/sdKey '${theValue}'`, 3);
}
});
return rtnAry;
}
calcSdKeyProps(entity: IEntitySdj): SdKeyProps {
// confirm entity name and sdId, and get from graph instead
const localEnt = (entity && this._graph[entity.sdId]) ? this._graph[entity.sdId] : undefined;
if (!localEnt || entity.sdKey !== localEnt.sdKey || !entity.sdProps) {
this.log(`entity '${entity.sdKey}' does match description '${localEnt?.sdKey}'`);
return (entity.sdProps) ? cloneDeep(entity.sdProps) : {}; // calcSdKeyProps shouldn't be called on entities that don't have sdProps
}
return this._host.lexiconMgr.calcSdKeyProps(entity, this._graph);
}
getItem (itemKeyNum: number | string): IItemSdj | undefined {
return <IItemSdj>getFromCoreArray(itemKeyNum, this._items);
}
getItemRefs(itemArray: number[] | string[]): IItemSdj[] {
const rtnAry: IItemSdj[] = [];
each(itemArray, (theValue: number | string) => {
const itemRef = <IItemSdj>getFromCoreArray(theValue, this._items);
if (itemRef) {
rtnAry.push(itemRef);
} else {
this.log(`Unable to find item sdId/sdKey '${theValue}'`, 3);
}
});
return rtnAry;
}
genJI(): DescriptionJI {
const rtnDescJI: DescriptionJI = {
sdInfo: genInfoJI(this._sdInfo),
graph: [],
items: [],
lang: this.lang
};
let lexEntRefs: EntityJI[] = [],
lexItemRefs: ItemJI[] = [];
if (this._lexicons && this._lexicons.length > 0) {
rtnDescJI.lexicons = clone(this._lexicons);
each(this._lexicons, (lexId) => {
const workLex = this._host.lexiconMgr.getByName(lexId);
lexEntRefs = (workLex?.entities) ? lexEntRefs.concat(workLex.entities) :lexEntRefs;
lexItemRefs = (workLex?.items) ? lexItemRefs.concat(workLex.items) : lexItemRefs;
});
}
each(this._items, (sdjItem: IItemSdj) => {
const itemInLex = find(lexItemRefs, {sdId: sdjItem.sdId}),
itemInBase = find(BASE_ITEMS_JI, {sdId: sdjItem.sdId});
if (!itemInLex && !itemInBase) {
rtnDescJI.items.push(sdjItem.genJI());
}
});
each(this._graph, (sdjEnt: IEntitySdj) => {
const entInLex = find(lexEntRefs, {sdId: sdjEnt.sdId});
if (sdjEnt.sdId !== 0 && !entInLex) {
rtnDescJI.graph.push(sdjEnt.genJI());
}
});
if (this._lang !== "en") {
rtnDescJI.lang = this._lang;
}
return rtnDescJI;
}
// Use searches for misses; use this function only for Verified Graphs
// Internals should not be calling for things that don't exist
getEntityRefById(entId: number): IEntitySdj | undefined {
return (entId > 0) ? this._graph[entId] : undefined;
}
getItemsByEntity(entKeyNum: string | number): IItemSdj[] {
const actualEnt = <IEntitySdj>getFromCoreArray(entKeyNum, this._graph);
if (!actualEnt) {
return [];
}
let extendsList = (actualEnt.extendIds) ? clone(actualEnt.extendIds) : [],
fullItemList: number[] = cloneDeep(actualEnt.sdItems),
getEnts = (entRef: IEntitySdj) => {
let rtnSdItems = cloneDeep(entRef.sdItems);
if (entRef.extendIds && isArrayWithLen(entRef.extendIds)) {
each(entRef.extendIds, (num: number) => {
const subEntRef = <IEntitySdj>getFromCoreArray(num, this._graph);
if (subEntRef) {
rtnSdItems = rtnSdItems.concat(getEnts(subEntRef));
}
});
}
return rtnSdItems;
};
each(extendsList, (entNum: number) => {
const topExtendRef = <IEntitySdj>getFromCoreArray(entNum, this._graph);
if (topExtendRef) {
fullItemList = fullItemList.concat(getEnts(topExtendRef));
}
});
fullItemList = uniq(fullItemList.sort());
return this.getItemRefs(fullItemList);
}
verifyJIbyType(ji: SdjJITypes, jiType: ESDJ_CLASS, strict: boolean = false): boolean {
return this._host.verifyJIbyType(ji, jiType, strict);
}
private entityItemBuild(inDescJI: DescriptionJI) {
this._items = this.buildBaseItems();
const descriptRef: IDescriptionSdj = <IDescriptionSdj>this;
each(inDescJI.items, (itemJI: ItemJI) => {
this.checkBase(itemJI, true);
this._items.push(new SdjItem(genItemJI(itemJI), descriptRef));
});
each(inDescJI.graph, (entityJI: EntityJI) => {
this.checkBase(entityJI, false);
let defItemIds = times(BASE_ITEMS_JI.length);
if (entityJI.sdItems && Array.isArray(entityJI.sdItems)) {
defItemIds = defItemIds.concat(entityJI.sdItems);
}
entityJI.sdItems = defItemIds;
this._graph.push(new SdjEntity(genEntityJI(entityJI), descriptRef));
});
this._graph.unshift(new SdjEntity(genEntityJI(GRAPH_ZERO), this));
this._lexicons = (inDescJI.lexicons) ? inDescJI.lexicons : [];
each(this._items, (itemJI: IItemSdj, idx: number) => {
// Has been pre-sorted, final confirmation to confirm sdId === _item index alignment
if (idx !== itemJI.sdId) {
throw new Error("[SDJ] Description validity error, missing entity or error;");
}
});
each(this._graph, (entity: IEntitySdj, idx: number) => {
// This has been presorted before this, but confirms sdId === _graph index alignment
if (idx !== entity.sdId) {
throw new Error("[SDJ] Description validity error, missing entity or error;");
}
const classRef: SdjEntity = <SdjEntity>entity;
classRef.$refreshRefs();
});
}
private buildBaseItems(): SdjItem[] {
const newBaseItems: ItemJI[] = this._host.lexiconMgr.newBaseItems(),
rtnArray: SdjItem[] = [];
each(newBaseItems, (itemJI: ItemJI) => {
rtnArray.push(new SdjItem(itemJI, this));
});
return rtnArray;
}
private checkBase(itemBase: CoreSD, isItem: boolean) {
const checkArray = (isItem) ? this._items : this._graph,
errorType = (isItem) ? "Item" : "Entity";
if (SYS_RESERVED.indexOf(itemBase.sdKey) !== -1) {
throw new Error(`[SDJ] Description: ${errorType} sdKey '${itemBase.sdKey}' is reserved;`);
}
if (find(checkArray, {sdKey: itemBase.sdKey})) {
throw new Error(`[SDJ] Description: ${errorType} sdKey '${itemBase.sdKey}' already exists;`);
}
if (find(checkArray, {sdId: itemBase.sdId})) {
throw new Error(`[SDJ] Description:: ${errorType} sdKey '${itemBase.sdKey}' has duplicate sdId [${itemBase.sdId}];`);
}
}
private strictLogError(logStr: string, strict: boolean) {
if (strict) {
throw new Error(logStr);
} else {
this.log(logStr, 3);
}
}
static VerifyJI(inDesc: DescriptionJI) {
if (!isInfo(inDesc.sdInfo)) {
throw new Error("[SDJ] Description - improperly formatted sdInfo;");
} else if (isInfo(inDesc.sdInfo) && !validSDKey(inDesc.sdInfo.name)) {
throw new Error(`[SDJ] Description name '${inDesc.sdInfo.name}' must be sdKey format;`);
}
if (inDesc.lexicons && !validLexiconArray(inDesc.lexicons)) {
throw new Error("[SDJ] Description.lexicons improperly formed;");
}
if ((!inDesc.items || !isArray(inDesc.items)) && !inDesc.lexicons) {
throw new Error("[SDJ] Description - items missing or array error;");
} else if (!inDesc.lexicons && !verifyUniqKeys(inDesc.items)) {
throw new Error("[SDJ] Description - items array must contain unique sdKeys and sdIds;");
}
if (!inDesc.graph) {
inDesc.graph = [];
} else if ((inDesc.graph && !isArray(inDesc.graph)) && !inDesc.lexicons) {
throw new Error("[SDJ] Description graph is not array;");
} else if (isArray(inDesc.graph) && !verifyUniqKeys(inDesc.graph)) {
throw new Error("[SDJ] Description graph does not have unique sdKeys/sdIds;");
}
if (inDesc.lang && !getRegEx("lang").test(inDesc.lang)) {
throw new Error(`[SDJ] lang value can be 2-3 lower case chars; error with value:'${inDesc.lang}';`);
}
restrictToAllowedKeys("DescriptionJI:" + inDesc.sdInfo.name,
["sdInfo", "items", "graph", "lexicons", "lang"], inDesc);
}
static IsEqual(alpha: SdjDescription | DescriptionJI, beta: SdjDescription | DescriptionJI) {
const betaJI = (beta instanceof SdjDescription) ? beta.genJI() : beta,
alphaJI = (alpha instanceof SdjDescription) ? alpha.genJI() : alpha;
return isEqual(alphaJI, betaJI);
}
}