UNPKG

sdj-esm

Version:
362 lines (317 loc) 11.7 kB
/* 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 { DataJI, DescriptionJI, EntitySearch, FuncStrNumVoid, Info, ItemSearch, SdJsonJI } from "../core/interfaces.js"; import {ESDJ_CLASS, SDJ_SCHEMA} from "../core/statics.js"; import {blankDescriptionJI, blankInfoJI, genInfoJI, isBlankInfo, newInfoJI} from "../util/immutables.js"; import {checkResetInfo, verifyUniqKeys} from "../util/verify.js"; import {restrictToAllowedKeys} from "../core/restrict.js"; import {isInfo} from "../core/validators.js"; import type {IDataSdj, IDescriptionSdj, IJsonSdj} from "./class-interfaces.js"; import {SdjHost} from "../global/host.js"; import {SdjData} from "./data.js"; import {cloneDeep, each, has, isArray, isEmpty, isEqual, isObject} from "lodash-es"; import {getRegEx} from "../util/regex.js"; export class SdJson implements IJsonSdj{ private _data: SdjData[] = []; private _description: IDescriptionSdj; private _lang: string = "en"; private _sdInfo: Info; private _isLocked = true; private _host: SdjHost = SdjHost.getHost(); private readonly log: FuncStrNumVoid; constructor(inJI: SdJsonJI | DescriptionJI) { const initKey: string = (inJI.sdInfo?.name) ? inJI.sdInfo.name : "", inJson: SdJsonJI = this.modifyInJson(inJI); this.log = this._host.getLogFunc(`sdJson:${initKey}`); this.log("Initialize"); if (<SdJsonJI>inJson instanceof SdJson) { throw new Error("[SDJ] input for SdJson cannot be another SdJson;"); } SdJson.VerifyJI(inJson); if (isBlankInfo(inJson.sdInfo)) { inJson.sdInfo = newInfoJI(inJson.sdInfo.name); } this._description = this._host.makeDescript(inJson.description); this._sdInfo = genInfoJI(inJson.sdInfo); this._host.lexiconMgr.verifyData(inJson, true); this._lang = (inJson.lang) ? String(inJson.lang).toLowerCase() : "en"; this.build(inJson); this.log("JSON Build Complete"); } get data(): IDataSdj[] { return <IDataSdj[]>this._data; } set data(inval: SdjData[]) { this._data = inval; } get description(): IDescriptionSdj { return this._description; } set description(changeDesc: IDescriptionSdj) { if (!this.isLocked) { this._description = changeDesc; } else { // no - op } } get sdInfo(): Info { return this._sdInfo; } get $sdInfo(): Info { return this._sdInfo; } set $sdInfo(inInfo: Info) { this._sdInfo = inInfo; } get lang() { return this._lang; } get $lang(): string { return this._lang; } set $lang(inStr: string) { if (getRegEx("lang").test(inStr)) { this._lang = String(inStr).toLowerCase(); } } get isLocked(): boolean { return this._isLocked; } get isValid(): boolean { let rtnBool = true; if (!this._isLocked) { try { rtnBool = this.validate(); if (rtnBool) { rtnBool = this._host.lexiconMgr.validateGraph(this._description.genJI()); if (rtnBool) { rtnBool = this._host.lexiconMgr.verifyData(this.internalJI(), false); } } } catch (e: unknown) { this.log("Validate error:" + String(e).toString()); rtnBool = false; } } return rtnBool; } lock(setLock: boolean): IDescriptionSdj | undefined { const boolLock = (inValue: boolean) => (inValue ? "locked" : "unlocked"); let descJI: DescriptionJI, orgDescUD: IDescriptionSdj | undefined, newDescJI: DescriptionJI; if ((!setLock && !this._isLocked) || (setLock && this._isLocked)) { this.log(`Unable to set as sdJson(${boolLock(this.isLocked)}) & request to move to '${boolLock(!setLock)}' is incorrect`, 3); return undefined; } else if (this._isLocked && !setLock) { descJI = this._description.genJI(); this._isLocked = false; orgDescUD = this._description; this._description = this._host.makeDescript(descJI, true); return orgDescUD; } else if (this.isValid) { newDescJI = this._description.genJI(); newDescJI.sdInfo.modified = Date.now(); this._sdInfo.modified = Date.now(); this._description = this._host.makeDescript(newDescJI); this._isLocked = true; return this._description; } else { throw new Error("[SDJ] Unable to unlock while data/description are invalid;"); } } dataByPath(dataPath: string): IDataSdj | undefined { return this._host.searchMgr.dataByPath(this, dataPath); } dataByEntity(searchEnt: EntitySearch, dataPath?: string): IDataSdj[] { return this._host.searchMgr.dataByEntity(this, searchEnt, dataPath); } dataByItem(searchItem: ItemSearch, dataPath?: string): IDataSdj[] { return this._host.searchMgr.dataByItem(this, searchItem, dataPath); } genJI(): SdJsonJI { const unlockedName = "genJI_silent_error_while_unlocked", rtnJsonJI:SdJsonJI = this.internalJI(); if (!this._isLocked) { rtnJsonJI.description = blankDescriptionJI(unlockedName); rtnJsonJI.sdInfo = blankInfoJI(unlockedName); if (rtnJsonJI.lang) { delete rtnJsonJI.lang; } rtnJsonJI.data = []; } return rtnJsonJI; } private build(inJson: SdJsonJI) { this.log("In Json: " + inJson.sdInfo.name); let quickLocRef: SdjData[] = []; each(inJson.data, (topDataJI: DataJI) => { const entRef = this._description.getEntity(topDataJI.sdId); if (entRef) { entRef.validStruct(topDataJI, undefined, true); } else { throw new Error(`[SDJ] data error '${topDataJI.sdKey}';`); } }); each(inJson.data, (topDataJI: DataJI) => { const entRef = this._description.getEntity(topDataJI.sdId); let topBuildData: SdjData; if (!entRef) { throw new Error(`[SDJ] Description '${this._description.name}' has no entity with sdId:'${topDataJI.sdId}';`); } try { topBuildData = new SdjData(topDataJI, entRef, undefined); } catch(err) { throw new Error(`[SDJ] SdJson Data create/valid err:${err};`); } if (topDataJI.sdChildren) { this.createSubData(topDataJI.sdChildren, topBuildData); } quickLocRef.push(<SdjData>topBuildData); this._data.push(topBuildData); }); each(quickLocRef, (sdjData, idx) => { sdjData.$sdIndex = idx; }); } private createSubData(childrenJI: DataJI[], parentRef: SdjData) { each(childrenJI, (childRef) => { const entRef = this._description.getEntity(childRef.sdId); if (entRef) { entRef.validStruct(childRef, parentRef.genJI(false), true); } else { throw new Error(`[SDJ] data error '${childRef.sdKey}';`); } }); const quickLocRef: SdjData[] = []; each(childrenJI, (childJI: DataJI) => { const entRef = this._description.getEntity(childJI.sdId); let newSdjData: SdjData; if (!entRef) { throw new Error(`[SDJ] Description '${this._description.name}' has no entity with sdId:'${childJI.sdId}';`); } try { newSdjData = new SdjData(childJI, entRef, parentRef); } catch (err) { throw new Error(`[SDJ] SdJson Data create/valid err:${err};`); } if (childJI.sdChildren) { this.createSubData(childJI.sdChildren, newSdjData); } quickLocRef.push(newSdjData); parentRef.$addChild(newSdjData); }); each(quickLocRef, (sdjData, idx) => { sdjData.$sdIndex = idx; }); } private modifyInJson(inJI: SdJsonJI | DescriptionJI): SdJsonJI { let rtnJson: SdJsonJI, descOrJson: SdJsonJI | DescriptionJI, descJI: DescriptionJI; if (!isObject(inJI)) { throw new Error("[SDJ] Input JSON is not an object;"); } else { descOrJson = <SdJsonJI | DescriptionJI>inJI; if (!has(descOrJson, "items") && !has(descOrJson, "graph") && has(descOrJson, "description")) { rtnJson = <SdJsonJI>descOrJson; if (!rtnJson.data || !isArray(rtnJson.data)) { rtnJson = cloneDeep(rtnJson); rtnJson.data = []; } } else if (!has(inJI, "data") && inJI.sdInfo && has(descOrJson, "items") && has(descOrJson, "graph")) { descJI = <DescriptionJI>descOrJson; descJI.sdInfo = checkResetInfo(inJI.sdInfo); try { this._host.verifyJIbyType(descJI, ESDJ_CLASS.DESCRIPTION, true); this._host.checkClassInst(descJI, ESDJ_CLASS.DESCRIPTION, false); rtnJson = { $id: <string>SDJ_SCHEMA[0], sdInfo: blankInfoJI(descJI.sdInfo.name + ".json"), description: descJI, data: [] }; } catch (e) { throw new Error(`[SDJ] Improper Description :${e}`); } } else { // don't know what it is - send through std error rtnJson = <SdJsonJI>inJI; } } return rtnJson; } private validate(): boolean { let rtnVal = true; const recDataValid = (dataObj: IDataSdj) => { if (!dataObj.isValid()) { rtnVal = false; return false; } else if (dataObj.hasChildren) { each(dataObj.sdChildren, (subData) => { recDataValid(subData); }); } return true; }; each(this._data, (dataObj: IDataSdj) => { recDataValid(dataObj); }); return rtnVal; } private internalJI(): SdJsonJI { const rtnJsonJI: SdJsonJI = { $id: <string>SDJ_SCHEMA[0], description: this._description.genJI(), sdInfo: genInfoJI(this._sdInfo), data: [] }; each(this._data, (sdjData: IDataSdj) => { // @ts-expect-error .. just defined it above rtnJsonJI.data.push(sdjData.genJI(true)); }); if (this._lang !== "en") { rtnJsonJI.lang = this._lang; } return rtnJsonJI; } static VerifyJI(inJson: SdJsonJI) { if (!isEmpty(inJson) && !isEqual(inJson, {})) { if (SDJ_SCHEMA.indexOf(inJson.$id) === -1) { throw new Error(`[SDJ] Json: Unknown Schema '${inJson.$id}';`); } } else { throw new Error("[SDJ] Json: empty or blank;"); } if (!isInfo(inJson.sdInfo)) { throw new Error("[SDJ] Json: missing sdInfo;"); } else if (isInfo(inJson.sdInfo) && (!getRegEx("sdjFileName").test(inJson.sdInfo.name))) { throw new Error(`[SDJ] Json sdInfo.name '${inJson.sdInfo.name}' is not regEx fileName;`); } if (!inJson.description || !inJson.description.sdInfo || !isInfo(inJson.description.sdInfo)) { throw new Error("[SDJ] Json: missing description or description.sdInfo;"); } if (!inJson.data || !isArray(inJson.data)) { throw new Error("[SDJ] Json: data must be in array format;"); } else if (isArray(inJson.data) && inJson.data.length > 0 && !verifyUniqKeys(inJson.data, true)) { throw new Error("[SDJ] Json: top level data has duplicate child sdKeys;"); } if (inJson.lang && !getRegEx("lang").test(inJson.lang)) { throw new Error(`[SDJ] lang value can be 2-3 lower case chars; error with value:'${inJson.lang}';`); } restrictToAllowedKeys("SdJsonJI:" + inJson.sdInfo.name, ["$id", "description", "sdInfo", "data", "lang"], inJson); } }