sdj-esm
Version:
Self Described JSON - ESM
355 lines (310 loc) • 12.4 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, DataJI, EntityJI, GenKeyStore, JIValue, NumKeyStore, SdKeyProps} from "../core/interfaces.js";
import {ESDJ_CLASS, ESDJ_LIMIT, GRAPH_ID, SYS_RESERVED} from "../core/statics.js";
import {genSdKeyProps} from "../util/immutables.js";
import {isArrayWithLen} from "../core/validators.js";
import {restrictCoreSD, restrictLimiter, restrictToAllowedKeys} from "../core/restrict.js";
import type {IDescriptionSdj, IEntitySdj, IItemSdj} from "./class-interfaces.js";
import {
each,
isBoolean,
isEmpty,
isEqual,
isFunction,
isNull,
isNumber,
isObject,
isString,
isUndefined,
uniq
} from "lodash-es";
import {validIntArray, validSDKey} from "../core/sdj-types.js";
import type {numArrayOrUnDef} from "../core/internal.js";
/*
Entities provide the connective information between the data represented
in the SDJ.
sdItems* Items that are assigned to entity, unique pos-int values only
extendIds* Entities extended by current entity
parentIds* Entities that are allowed to contain current entity
childIds* Entities that are allowed to be children of current entity
limiter 4 Avail: (ESDJ_LIMIT)
.REQ Entity is required by parent
.REQ_HIDE Entity is required by parent hidden from user (for only one entity)
.ONE_NONE Entity is singular on parent or not-present
.REQ_ONE At least one Entity is required by parent
.KEY_IDX Child Entities of singular type
sdProps Optional properties for entities and used by lexicons or manually
dataInfo Boolean switch for if Entity stores creation/modification information (sdInfo object)
*All pos-int number[] are uniq'd and sorted before operations
Extending Rules -
- sdProps/limiter and all number[] are considered static during runtime and should be re-validated if changed
- auto-inherent all sdItems from lower entities, auto-remove duplicate items
- Does not affect childIds or parentIds, which are unique per Entity
- sdProps are chained assigned based on entity/order - genJI sdProps will show final values
- limiter value matches last extended item if not provided
Note: in Sdj "$" functions for deliberate class-only implementations which don't appear on IEntitySdj
and should only be used by classes/files that implement "new SdjEntity" in any way.
*/
export class SdjEntity implements CoreSD, IEntitySdj {
private _sdId: number;
private _sdKey: string;
private _parentIds: number[];
private _sdItems: number[];
private _extendIds: number[] | undefined;
private _childIds: number[] | undefined;
private _dataInfo: boolean | undefined;
private _limiter: ESDJ_LIMIT;
private _sdProps: SdKeyProps | undefined;
private _description: IDescriptionSdj | undefined;
private _childRefs?: NumKeyStore<IEntitySdj>;
private _itemRefs?: GenKeyStore<IItemSdj>;
constructor(inEnt: EntityJI, description?: IDescriptionSdj | undefined) {
this._description = this.confirmDescript(description);
if (this._description) {
this._description.host.checkClassInst(inEnt, ESDJ_CLASS.ENTITY, false);
}
SdjEntity.VerifyJI(inEnt);
this.graphZeroLockout(inEnt);
this._sdId = inEnt.sdId;
this._sdKey = inEnt.sdKey;
if (isArrayWithLen(inEnt.extendIds)) {
this._extendIds = uniq([...<number[]>inEnt.extendIds]);
}
if (isArrayWithLen(inEnt.parentIds)) {
this._parentIds = uniq([...<number[]>inEnt.parentIds]);
} else {
this._parentIds = [0];
}
if (isArrayWithLen(inEnt.childIds)) {
this._childIds = uniq([...<number[]>inEnt.childIds]);
}
if (isArrayWithLen(inEnt.sdItems)) {
const inEntItems =[...<number[]>inEnt.sdItems];
this._sdItems = uniq([0, 1].concat(inEntItems));
} else {
this._sdItems = [0, 1];
}
if (inEnt.dataInfo) {
this._dataInfo = inEnt.dataInfo;
}
if (inEnt.sdProps) {
this._sdProps = genSdKeyProps(inEnt.sdProps);
}
this._limiter = inEnt.limiter || ESDJ_LIMIT.NONE;
}
get sdKey() {
return this._sdKey;
}
get sdId() {
return this._sdId;
}
get parentIds(): number[] {
return this._parentIds;
}
get sdItems(): number[] {
return this._sdItems;
}
get extendIds(): number[] | undefined {
return this._extendIds;
}
get childIds(): number[] | undefined {
return this._childIds;
}
get childRefs(): NumKeyStore<IEntitySdj> {
return (this._childRefs) ? this._childRefs : {};
}
get itemRefs(): GenKeyStore<IItemSdj> {
return (this._itemRefs) ? this._itemRefs : {};
}
get description(): IDescriptionSdj {
if (!this._description) {
throw new ReferenceError("[SDJ] Entity has no description;");
}
return this._description;
}
get sdProps(): SdKeyProps | undefined {
return this._sdProps;
}
get limiter(): ESDJ_LIMIT {
return this._limiter;
}
get dataInfo(): boolean | undefined{
return this._dataInfo;
}
set description(inDesc: IDescriptionSdj | undefined) {
if (!this._description) {
this._description = this.confirmDescript(inDesc);
if (this._description) {
this.$refreshRefs();
} else {
throw new Error("[SDJ] Incorrect or missing description/host;");
}
} else {
throw new Error("[SDJ] an Entity item is prevented from having its description overwritten;");
}
}
validStruct(dataSdj: DataJI, parentRef: DataJI | undefined, strict = false): boolean {
if (!this._description) {
return false;
}
return this._description.host.searchMgr.validStruct(this, dataSdj, parentRef, strict);
}
validData(dataSdj: DataJI, strict = false): boolean {
if (!this._description) {
return false;
}
return this._description.host.searchMgr.validData(this, dataSdj, strict);
}
genJI(): EntityJI {
const rtnEntJI: EntityJI = {
sdId: this._sdId,
sdKey: this._sdKey,
};
// parents will always be at least [0] and should only be eliminated when === [0]
if (this._parentIds.length > 1 || this._parentIds[0] !== 0) {
rtnEntJI.parentIds = [...this._parentIds];
}
each(this._sdItems, (itemId: number) => {
if (itemId !== 0 && itemId !== 1) {
if (!rtnEntJI.sdItems) {
rtnEntJI.sdItems = [];
}
rtnEntJI.sdItems.push(itemId);
}
});
if (isArrayWithLen(this._extendIds)) {
rtnEntJI.extendIds = [...<number[]>this._extendIds];
}
if (isArrayWithLen(this._childIds)) {
rtnEntJI.childIds = [...<number[]>this._childIds];
}
if (this._dataInfo) {
rtnEntJI.dataInfo = true;
}
if (this.sdProps) {
let rtnProps: SdKeyProps | undefined;
if (this._description) {
rtnProps = this._description.calcSdKeyProps(this);
if (!isEmpty(rtnProps)) {
rtnEntJI.sdProps = <SdKeyProps>rtnProps;
}
} else {
rtnEntJI.sdProps = genSdKeyProps(this.sdProps);
}
}
if (this._limiter !== ESDJ_LIMIT.NONE) {
rtnEntJI.limiter = this._limiter;
}
return rtnEntJI;
}
$refreshRefs() {
if (this._description) {
this._itemRefs = {};
this._childRefs = {};
each(this._childIds, (num: number) => {
const childRef = this._description?.getEntity(num);
if (childRef) {
this.childRefs[num] = childRef;
} else {
throw new Error("[SDJ] if this happens something else was missed presence of entity refs should be confirmed;");
}
});
each(this._description.getItemsByEntity(this._sdId), (sdItem: IItemSdj) => {
this.itemRefs[sdItem.sdKey] = sdItem;
});
}
}
private confirmDescript(inDesc: IDescriptionSdj | undefined): IDescriptionSdj | undefined {
let rtnVal = false;
if (!isNull(inDesc) && !isUndefined(inDesc)) {
if (inDesc.host?.checkClassInst && isFunction(inDesc.host.checkClassInst)) {
inDesc.host.checkClassInst(inDesc, ESDJ_CLASS.DESCRIPTION, true);
rtnVal = true;
} else if (!inDesc.host && isObject(inDesc)) {
throw new Error("[SDJ] Improper Entity init attempt use IDescriptionSdj;");
}
}
return (rtnVal) ? inDesc : undefined;
}
private graphZeroLockout(inEnt: EntityJI) {
if (inEnt.sdId === 0 || inEnt.sdKey === GRAPH_ID) {
if (this._description) {
// throw nothing for now
} else {
throw new Error("[SDJ] Entity 0 is reserved for system requires SdjDescription to build;");
}
}
}
static VerifyJI(inEnt: EntityJI) {
restrictCoreSD(inEnt);
if ((inEnt.sdId === 0 && inEnt?.sdKey !== GRAPH_ID) || inEnt.sdId < 0) {
throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' jixId: ${inEnt.sdId} <= 0;`);
} else if (inEnt.sdId === 0 && inEnt.sdKey === GRAPH_ID) {
return;
}
SdjEntity.checkArrays(inEnt);
if (inEnt.sdProps && !isObject(inEnt.sdProps)) {
throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' invalid keyValues;`);
} else if (inEnt.sdProps) {
each(inEnt.sdProps, (jValue: JIValue, key: string) => {
if (SYS_RESERVED.indexOf(key) !== -1 || !validSDKey(key)) {
throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' has an illegal keyName: ${key};`);
} else if (!(isString(jValue) || isNumber(jValue) || isBoolean(jValue))) {
throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' key '${key}' has illegal value, not string|number|boolean;`);
}
});
}
if (inEnt.limiter && !restrictLimiter(inEnt.limiter)) {
throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' has unrecognized limiter;`);
} else if (inEnt.limiter && inEnt.limiter === ESDJ_LIMIT.KEY_IDX) {
if (inEnt.parentIds && !isEqual(inEnt.parentIds, [0])) {
throw new TypeError("[SDJ] sdIndexed Entities can only exist on root;");
}
if (!inEnt.childIds) {
throw new TypeError("[SDJ] sdIndexed Entities are required to have children;");
}
}
if (inEnt.dataInfo && !isBoolean(inEnt.dataInfo)) {
throw new TypeError("[SDJ] Entity dataInfo value boolean only if present;");
}
restrictToAllowedKeys("EntityJI: "+ inEnt.sdKey,
["sdKey", "sdId", "sdItems", "extendIds", "parentIds", "childIds", "limiter", "sdProps", "dataInfo"], inEnt);
}
private static checkArrays(inEnt: EntityJI) {
const nameRefs = ["childIds", "parentIds", "extendIds", "childIds", "sdItems"],
valueRefArray: numArrayOrUnDef[] = [inEnt.childIds, inEnt.parentIds, inEnt.extendIds, inEnt.childIds, inEnt.sdItems];
each(nameRefs, (keyId, idx) => {
const valueRef = valueRefArray[idx];
if (isArrayWithLen(valueRef)) {
if (!validIntArray(valueRef)) {
throw new TypeError(`[SDJ] Entity '${inEnt.sdKey}' invalid '${keyId}' array;`);
} else {
switch(keyId) {
case "childIds":
if (valueRef && valueRef.indexOf(0) !== -1) {
throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' GraphZero[0] is not allowed to be a child of an entity;`);
}
break;
case "extendIds":
if (valueRef && valueRef.indexOf(0) !== -1) {
throw new Error(`[SDJ] Entity '${inEnt.sdKey}' cannot extend graphZero [0];`);
} else if (valueRef && valueRef.indexOf(inEnt.sdId) !== -1) {
throw new Error(`[SDJ] Entity: '${inEnt.sdKey}' cannot extend itself;`);
}
break;
default:
// no default;
}
}
}
if (keyId === "sdItems" && !inEnt.sdItems && !inEnt.extendIds && !inEnt.childIds) {
throw new Error(`[SDJ] Entity '${inEnt.sdKey}' has no items, does not extend, & has no children;`);
}
});
}
}