sdj-esm
Version:
Self Described JSON - ESM
362 lines (317 loc) • 11.7 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 {
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);
}
}