Files
ueblueprint/js/entity/IEntity.js
barsdeveloper ecc71b76d1 WIP
2024-05-31 15:09:48 +02:00

298 lines
8.2 KiB
JavaScript

import P from "parsernostrum"
import Utility from "../Utility.js"
/** @abstract */
export default class IEntity {
/** @type {(v: String) => String} */
static same = v => v
/** @type {(entity: Attribute, serialized: String) => String} */
static notWrapped = (entity, serialized) => serialized
/** @type {(entity: IEntity, serialized: String) => String} */
static defaultWrapped = (entity, serialized) => `${entity.lookbehind}(${serialized})`
static wrap = this.defaultWrapped
static attributeSeparator = ","
/** @type {(k: String) => String} */
static printKey = k => k
/** @type {P<InstanceType<typeof this>>} */
static grammar = /** @type {any} */(P.failure())
/** @type {{ [key: String]: typeof IEntity }} */
static attributes = {}
/** @type {(type: typeof IEntity) => InstanceType<typeof IEntity>} */
static default
static nullable = false
static ignored = false // Never serialize or deserialize
static serialized = false // Value is written and read as string
static expected = false // Must be there
static inlined = false // The key is a subobject or array and printed as inlined (A.B=123, A(0)=123)
static quoted = false // Key is serialized with quotes
static silent = false // Do not serialize if default
static trailing = false // Add attribute separator after the last attribute when serializing
/** @type {String | String[]} */
static lookbehind = ""
#lookbehind = /** @type {String} */(this.Self().lookbehind)
get lookbehind() {
return this.#lookbehind
}
set lookbehind(value) {
this.#lookbehind = value
}
/** @type {String[]} */
#keys
get keys() {
return this.#keys ?? Object.keys(this)
}
set keys(value) {
this.#keys = [... new Set(value)]
}
constructor(values = {}) {
const keys = Utility.mergeArrays(Object.keys(values), Object.keys(this.Self().attributes))
for (const key of keys) {
if (values[key] !== undefined) {
this[key] = values[key]
continue
}
const attribute = this.Self().attributes[key]
if (attribute.default !== undefined) {
this[key] = attribute.default(attribute)
continue
}
}
}
static className() {
let self = this
while (!self.name) {
self = Object.getPrototypeOf(self)
}
return self.name
}
/** @param {String} key */
showProperty(key) {
/** @type {IEntity} */
let value = this[key]
const Self = value.Self()
if (Self.silent && Self.default !== undefined) {
if (Self["#default"] === undefined) {
Self["#default"] = Self.default(Self)
}
const defaultValue = Self["#default"]
return !value.equals(defaultValue)
}
return true
}
/**
* @protected
* @template {typeof IEntity} T
* @this {T}
* @returns {T}
*/
static asUniqueClass() {
if (this.name.length) {
// @ts-expect-error
return class extends this { }
}
return this
}
/**
* @template {typeof IEntity} T
* @this {T}
* @param {String} value
*/
static withLookbehind(value) {
const result = this.asUniqueClass()
result.lookbehind = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static withDefault(value = /** @type {(type: T) => InstanceType<T>} */(type => new type())) {
const result = this.asUniqueClass()
result.default = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagNullable(value = true) {
const result = this.asUniqueClass()
result.nullable = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagSerialized(value = true) {
const result = this.asUniqueClass()
result.serialized = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagInlined(value = true) {
const result = this.asUniqueClass()
result.inlined = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagQuoted(value = true) {
const result = this.asUniqueClass()
result.quoted = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagSilent(value = true) {
const result = this.asUniqueClass()
result.silent = value
return result
}
/**
* @template {typeof IEntity} T
* @this {T}
*/
static flagTrailing(value = true) {
const result = this.asUniqueClass()
result.trailing = value
return result
}
/**
* @template {typeof IEntity} T
* @this {InstanceType<T>}
*/
Self() {
return /** @type {T} */(this.constructor)
}
/**
*
* @param {String} attribute
* @param {(v: any) => void} callback
*/
listenAttribute(attribute, callback) {
const descriptor = Object.getOwnPropertyDescriptor(this, attribute)
const setter = descriptor.set
if (setter) {
descriptor.set = v => {
setter(v)
callback(v)
}
Object.defineProperties(this, { [attribute]: descriptor })
} else if (descriptor.value) {
Object.defineProperties(this, {
["#" + attribute]: {
value: descriptor.value,
writable: true,
enumerable: false,
},
[attribute]: {
enumerable: true,
get() {
return this["#" + attribute]
},
set(v) {
if (v != this["#" + attribute]) {
callback(v)
this["#" + attribute] = v
}
},
},
})
}
}
/** @param {IEntity} other */
equals(other) {
const thisKeys = Object.keys(this)
const otherKeys = Object.keys(other)
if (thisKeys.length != otherKeys.length) {
return false
}
if (this.valueOf && other.valueOf) {
return this.valueOf() === other.valueOf()
}
for (let i = 0; i < thisKeys.length; ++i) {
if (!(this[thisKeys[i]] instanceof IEntity && this[thisKeys[i]].equals(other[otherKeys[i]]))) {
return false
}
}
return true
}
toString(
insideString = false,
indentation = "",
printKey = this.Self().printKey,
) {
const Self = this.Self()
let result = ""
let first = true
for (const key of this.keys) {
/** @type {IEntity} */
const value = this[key]
if (value === undefined || !this.showProperty(key)) {
continue
}
if (first) {
first = false
} else {
result += Self.attributeSeparator
}
if (Self.inlined) {
result += value.toString(insideString, indentation, k => printKey(`${key}.${k}`))
continue
}
let keyValue = printKey(key)
if (keyValue.length) {
if (Self.quoted) {
keyValue = `"${keyValue}"`
}
result += Self.attributeSeparator.includes("\n") ? indentation : ""
}
let serialization = value.toString(insideString, indentation, printKey)
if (Self.serialized) {
serialization = `"${serialization.replaceAll(/(?<=(?:[^\\]|^)(?:\\\\)*?)"/, '\\"')}"`
}
result += serialization
}
if (Self.trailing && result.length) {
result += Self.attributeSeparator
}
return Self.wrap(this, result)
}
}