mirror of
https://github.com/barsdeveloper/ueblueprint.git
synced 2026-02-21 06:05:45 +08:00
Refactoring entities (#23)
* Still WIP * WIP * ArrayEntity parsing fixed * Fix format text entity * Tests for various entity classes and update entity class implementations * More tests and fixed * More entities fixed * Simple entities serialization fixed * Entities tests fixed * Remove serialization bits * Fix Function reference * CustomProperties creating fixed * WIP * Better typing for grammars * Decoding code fixes * Fixing still * Several fixes * rename toString to serialize * Several fixes * More fixes * Moving more stuff out of Utility * Several fixes * Fixing Linear color entity print * Serialization fixes * Fix serialization * Method to compute grammar * Renaming fix * Fix array grammar and equality check * Fix inlined keys * Fix type * Several serialization fixes * Fix undefined dereference * Several fixes * More fixes and cleanup * Fix keys quoting mechanism * Fix natural number assignment * Fix Int64 toString() * Fix quoted keys for inlined arrays * Fix PG pins * Fix several test cases * Types fixes * New pin default value empty * Fix non existing DefaultValue for variadic nodes * Smaller fixes for crashes * Fix link color when attached to knot * Linking test and more reliability operations for adding pins * Improve issue 18 test * More tests and fixes * Fix enum pin entity * Remove failing test
This commit is contained in:
@@ -1,232 +1,410 @@
|
||||
import Configuration from "../Configuration.js"
|
||||
import P from "parsernostrum"
|
||||
import Utility from "../Utility.js"
|
||||
import Serializable from "../serialization/Serializable.js"
|
||||
import SerializerFactory from "../serialization/SerializerFactory.js"
|
||||
import AttributeInfo from "./AttributeInfo.js"
|
||||
import ComputedType from "./ComputedType.js"
|
||||
import MirroredEntity from "./MirroredEntity.js"
|
||||
import Union from "./Union.js"
|
||||
|
||||
/** @abstract */
|
||||
export default class IEntity extends Serializable {
|
||||
export default class IEntity {
|
||||
|
||||
/** @type {{ [attribute: String]: AttributeInfo }} */
|
||||
static attributes = {
|
||||
attributes: new AttributeInfo({
|
||||
ignored: true,
|
||||
}),
|
||||
lookbehind: new AttributeInfo({
|
||||
default: /** @type {String | Union<String[]>} */(""),
|
||||
ignored: true,
|
||||
uninitialized: true,
|
||||
}),
|
||||
}
|
||||
/** @type {(v: String) => String} */
|
||||
static same = v => v
|
||||
/** @type {(entity: IEntity, 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 = ","
|
||||
static keySeparator = "="
|
||||
/** @type {(k: String) => String} */
|
||||
static printKey = k => k
|
||||
static grammar = P.lazy(() => this.createGrammar())
|
||||
/** @type {P<IEntity>} */
|
||||
static unknownEntityGrammar
|
||||
static unknownEntity
|
||||
/** @type {{ [key: String]: typeof IEntity }} */
|
||||
static attributes = {}
|
||||
/** @type {String | String[]} */
|
||||
static lookbehind = ""
|
||||
/** @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)
|
||||
/** @type {Boolean} */
|
||||
static quoted // 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[]} */
|
||||
#_keys
|
||||
get _keys() {
|
||||
return this.#_keys
|
||||
#keys
|
||||
get keys() {
|
||||
return this.#keys ?? Object.keys(this)
|
||||
}
|
||||
set _keys(keys) {
|
||||
this.#_keys = keys
|
||||
set keys(value) {
|
||||
this.#keys = [... new Set(value)]
|
||||
}
|
||||
|
||||
constructor(values = {}, suppressWarns = false) {
|
||||
super()
|
||||
const Self = /** @type {typeof IEntity} */(this.constructor)
|
||||
/** @type {AttributeDeclarations?} */ this.attributes
|
||||
/** @type {String} */ this.lookbehind
|
||||
const valuesKeys = Object.keys(values)
|
||||
const attributesKeys = values.attributes
|
||||
? Utility.mergeArrays(Object.keys(values.attributes), Object.keys(Self.attributes))
|
||||
: Object.keys(Self.attributes)
|
||||
const allAttributesKeys = Utility.mergeArrays(valuesKeys, attributesKeys)
|
||||
for (const key of allAttributesKeys) {
|
||||
let value = values[key]
|
||||
if (!suppressWarns && !(key in values)) {
|
||||
if (!(key in Self.attributes) && !key.startsWith(Configuration.subObjectAttributeNamePrefix)) {
|
||||
const typeName = value instanceof Array ? `[${value[0]?.constructor.name}]` : value.constructor.name
|
||||
console.warn(
|
||||
`UEBlueprint: Attribute ${key} (of type ${typeName}) in the serialized data is not defined in ${Self.name}.attributes`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (!(key in Self.attributes)) {
|
||||
// Remember attributeName can come from the values and be not defined in the attributes.
|
||||
// In that case just assign it and skip the rest.
|
||||
this[key] = value
|
||||
continue
|
||||
}
|
||||
Self.attributes.lookbehind
|
||||
const predicate = AttributeInfo.getAttribute(values, key, "predicate", Self)
|
||||
const assignAttribute = !predicate
|
||||
? v => this[key] = v
|
||||
: v => {
|
||||
Object.defineProperties(this, {
|
||||
["#" + key]: {
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
},
|
||||
[key]: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this["#" + key]
|
||||
},
|
||||
set(v) {
|
||||
if (!predicate(v)) {
|
||||
console.warn(
|
||||
`UEBlueprint: Tried to assign attribute ${key} to ${Self.name} not satisfying the predicate`
|
||||
)
|
||||
return
|
||||
}
|
||||
this["#" + key] = v
|
||||
}
|
||||
},
|
||||
})
|
||||
this[key] = v
|
||||
}
|
||||
#lookbehind = /** @type {String} */(this.constructor.lookbehind)
|
||||
get lookbehind() {
|
||||
return this.#lookbehind.trim()
|
||||
}
|
||||
set lookbehind(value) {
|
||||
this.#lookbehind = value
|
||||
}
|
||||
|
||||
let defaultValue = AttributeInfo.getAttribute(values, key, "default", Self)
|
||||
if (defaultValue instanceof Function) {
|
||||
defaultValue = defaultValue(this)
|
||||
}
|
||||
let defaultType = AttributeInfo.getAttribute(values, key, "type", Self)
|
||||
if (defaultType instanceof ComputedType) {
|
||||
defaultType = defaultType.compute(this)
|
||||
}
|
||||
if (defaultType instanceof Array) {
|
||||
defaultType = Array
|
||||
}
|
||||
if (defaultType === undefined) {
|
||||
defaultType = Utility.getType(defaultValue)
|
||||
}
|
||||
#ignored = this.constructor.ignored
|
||||
get ignored() {
|
||||
return this.#ignored
|
||||
}
|
||||
set ignored(value) {
|
||||
this.#ignored = value
|
||||
}
|
||||
|
||||
if (value !== undefined) {
|
||||
// Remember value can still be null
|
||||
if (
|
||||
value?.constructor === String
|
||||
&& AttributeInfo.getAttribute(values, key, "serialized", Self)
|
||||
&& defaultType !== String
|
||||
) {
|
||||
try {
|
||||
value = SerializerFactory
|
||||
.getSerializer(defaultType)
|
||||
.read(/** @type {String} */(value))
|
||||
} catch (e) {
|
||||
assignAttribute(value)
|
||||
continue
|
||||
#quoted
|
||||
get quoted() {
|
||||
return /** @type {typeof IEntity} */(this.constructor).quoted ?? this.#quoted ?? false
|
||||
}
|
||||
set quoted(value) {
|
||||
this.#quoted = value
|
||||
}
|
||||
|
||||
#trailing = this.constructor.trailing
|
||||
get trailing() {
|
||||
return this.#trailing
|
||||
}
|
||||
set trailing(value) {
|
||||
this.#trailing = value
|
||||
}
|
||||
|
||||
constructor(values = {}) {
|
||||
const attributes = /** @type {typeof IEntity} */(this.constructor).attributes
|
||||
const keys = Utility.mergeArrays(
|
||||
Object.keys(values),
|
||||
Object.entries(attributes).filter(([k, v]) => v.default !== undefined).map(([k, v]) => k)
|
||||
)
|
||||
for (const key of keys) {
|
||||
if (values[key] !== undefined) {
|
||||
if (values[key].constructor === Object) {
|
||||
// It is part of a nested key (words separated by ".")
|
||||
values[key] = new (
|
||||
attributes[key] !== undefined ? attributes[key] : IEntity.unknownEntity
|
||||
)(values[key])
|
||||
}
|
||||
const computedEntity = /** @type {ComputedTypeEntityConstructor} */(attributes[key])
|
||||
this[key] = values[key]
|
||||
if (computedEntity?.compute) {
|
||||
/** @type {typeof IEntity} */
|
||||
const actualEntity = computedEntity.compute(this)
|
||||
const parsed = actualEntity.grammar.run(values[key].toString())
|
||||
if (parsed.status) {
|
||||
this[key] = parsed.value
|
||||
}
|
||||
}
|
||||
assignAttribute(Utility.sanitize(value, /** @type {AttributeConstructor<Attribute>} */(defaultType)))
|
||||
continue // We have a value, need nothing more
|
||||
continue
|
||||
}
|
||||
if (defaultValue !== undefined && !AttributeInfo.getAttribute(values, key, "uninitialized", Self)) {
|
||||
assignAttribute(defaultValue)
|
||||
const attribute = attributes[key]
|
||||
if (attribute.default !== undefined) {
|
||||
this[key] = attribute.default(attribute)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {AttributeTypeDescription} attributeType */
|
||||
static defaultValueProviderFromType(attributeType) {
|
||||
if (attributeType === Boolean) {
|
||||
return false
|
||||
} else if (attributeType === Number) {
|
||||
return 0
|
||||
} else if (attributeType === BigInt) {
|
||||
return 0n
|
||||
} else if (attributeType === String) {
|
||||
return ""
|
||||
} else if (attributeType === Array || attributeType instanceof Array) {
|
||||
return () => []
|
||||
} else if (attributeType instanceof Union) {
|
||||
return this.defaultValueProviderFromType(attributeType.values[0])
|
||||
} else if (attributeType instanceof MirroredEntity) {
|
||||
return () => new MirroredEntity(attributeType.type, attributeType.getter)
|
||||
} else if (attributeType instanceof ComputedType) {
|
||||
return undefined
|
||||
} else {
|
||||
return () => new /** @type {AnyConstructor<Attribute>} */(attributeType)()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {new (...args: any) => any} C
|
||||
* @param {C} type
|
||||
* @returns {value is InstanceType<C>}
|
||||
* @protected
|
||||
* @returns {P<IEntity>}
|
||||
*/
|
||||
static isValueOfType(value, type) {
|
||||
return value != null && (value instanceof type || value.constructor === type)
|
||||
static createGrammar() {
|
||||
return this.unknownEntityGrammar
|
||||
}
|
||||
|
||||
static defineAttributes(object, attributes) {
|
||||
Object.defineProperty(object, "attributes", {
|
||||
writable: true,
|
||||
configurable: false,
|
||||
})
|
||||
object.attributes = attributes
|
||||
static actualClass() {
|
||||
let self = this
|
||||
while (!self.name) {
|
||||
self = Object.getPrototypeOf(self)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
static className() {
|
||||
return this.actualClass().name
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @template {typeof IEntity} T
|
||||
* @this {T}
|
||||
* @returns {T}
|
||||
*/
|
||||
static asUniqueClass() {
|
||||
let result = this
|
||||
if (this.name.length) {
|
||||
// @ts-expect-error
|
||||
result = (() => class extends this { })() // Comes from a lambda otherwise the class will have name "result"
|
||||
result.grammar = result.createGrammar() // Reassign grammar to capture the correct this from subclass
|
||||
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @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}
|
||||
* @param {(type: T) => (InstanceType<T> | NullEntity)} value
|
||||
* @returns {T}
|
||||
*/
|
||||
static withDefault(value = 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 flagIgnored(value = true) {
|
||||
const result = this.asUniqueClass()
|
||||
result.ignored = 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
|
||||
}
|
||||
|
||||
/**
|
||||
* @protected
|
||||
* @param {String} string
|
||||
*/
|
||||
static asSerializedString(string) {
|
||||
return `"${string.replaceAll(/(?<=(?:[^\\]|^)(?:\\\\)*?)"/g, '\\"')}"`
|
||||
}
|
||||
|
||||
/** @param {String} key */
|
||||
showProperty(key) {
|
||||
/** @type {IEntity} */
|
||||
let value = this[key]
|
||||
const valueType = /** @type {typeof IEntity} */(value.constructor)
|
||||
if (valueType.silent && valueType.default !== undefined) {
|
||||
if (valueType["#default"] === undefined) {
|
||||
valueType["#default"] = valueType.default(valueType)
|
||||
}
|
||||
const defaultValue = valueType["#default"]
|
||||
return !value.equals(defaultValue)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {String} attribute
|
||||
* @param {String} attributeName
|
||||
* @param {(v: any) => void} callback
|
||||
*/
|
||||
listenAttribute(attribute, callback) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(this, attribute)
|
||||
listenAttribute(attributeName, callback) {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(this, attributeName)
|
||||
const setter = descriptor.set
|
||||
if (setter) {
|
||||
descriptor.set = v => {
|
||||
setter(v)
|
||||
callback(v)
|
||||
}
|
||||
Object.defineProperties(this, { [attribute]: descriptor })
|
||||
Object.defineProperties(this, { [attributeName]: descriptor })
|
||||
} else if (descriptor.value) {
|
||||
|
||||
Object.defineProperties(this, {
|
||||
["#" + attribute]: {
|
||||
["#" + attributeName]: {
|
||||
value: descriptor.value,
|
||||
writable: true,
|
||||
enumerable: false,
|
||||
},
|
||||
[attribute]: {
|
||||
[attributeName]: {
|
||||
enumerable: true,
|
||||
get() {
|
||||
return this["#" + attribute]
|
||||
return this["#" + attributeName]
|
||||
},
|
||||
set(v) {
|
||||
if (v == this["#" + attribute]) {
|
||||
return
|
||||
}
|
||||
callback(v)
|
||||
this["#" + attribute] = v
|
||||
}
|
||||
this["#" + attributeName] = v
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
getLookbehind() {
|
||||
let lookbehind = this.lookbehind ?? AttributeInfo.getAttribute(this, "lookbehind", "default")
|
||||
lookbehind = lookbehind instanceof Union ? lookbehind.values[0] : lookbehind
|
||||
return lookbehind
|
||||
/** @this {IEntity | Array} */
|
||||
doSerialize(
|
||||
insideString = false,
|
||||
indentation = "",
|
||||
Self = /** @type {typeof IEntity} */(this.constructor),
|
||||
printKey = Self.printKey,
|
||||
keySeparator = Self.keySeparator,
|
||||
attributeSeparator = Self.attributeSeparator,
|
||||
wrap = Self.wrap,
|
||||
) {
|
||||
let result = ""
|
||||
let first = true
|
||||
const keys = this instanceof IEntity ? this.keys : Object.keys(this)
|
||||
for (const key of keys) {
|
||||
/** @type {IEntity} */
|
||||
const value = this[key]
|
||||
const valueType = /** @type {typeof IEntity} */(value?.constructor)
|
||||
if (value === undefined || this instanceof IEntity && !this.showProperty(key)) {
|
||||
continue
|
||||
}
|
||||
if (first) {
|
||||
first = false
|
||||
} else {
|
||||
result += attributeSeparator
|
||||
}
|
||||
let keyValue = this instanceof Array ? `(${key})` : key
|
||||
if (keyValue.length && (Self.attributes[key]?.quoted === true || value.quoted === true)) {
|
||||
keyValue = `"${keyValue}"`
|
||||
}
|
||||
if (valueType.inlined) {
|
||||
const inlinedPrintKey = valueType.className() === "ArrayEntity"
|
||||
? k => printKey(`${keyValue}${k}`)
|
||||
: k => printKey(`${keyValue}.${k}`)
|
||||
result += value.serialize(
|
||||
insideString,
|
||||
indentation,
|
||||
undefined,
|
||||
inlinedPrintKey,
|
||||
keySeparator,
|
||||
attributeSeparator,
|
||||
Self.notWrapped
|
||||
)
|
||||
continue
|
||||
}
|
||||
keyValue = printKey(keyValue)
|
||||
if (keyValue.length) {
|
||||
result += (attributeSeparator.includes("\n") ? indentation : "") + keyValue + keySeparator
|
||||
}
|
||||
let serialization = value?.serialize(insideString, indentation)
|
||||
result += serialization
|
||||
}
|
||||
if (this instanceof IEntity && this.trailing && result.length) {
|
||||
result += attributeSeparator
|
||||
}
|
||||
return wrap(/** @type {IEntity} */(this), result)
|
||||
}
|
||||
|
||||
unexpectedKeys() {
|
||||
return Object.keys(this).length - Object.keys(/** @type {typeof IEntity} */(this.constructor).attributes).length
|
||||
/** @this {IEntity | Array} */
|
||||
serialize(
|
||||
insideString = false,
|
||||
indentation = "",
|
||||
Self = /** @type {typeof IEntity} */(this.constructor),
|
||||
printKey = Self.printKey,
|
||||
keySeparator = Self.keySeparator,
|
||||
attributeSeparator = Self.attributeSeparator,
|
||||
wrap = Self.wrap,
|
||||
) {
|
||||
let result = this instanceof Array
|
||||
? IEntity.prototype.doSerialize.bind(this)(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap)
|
||||
: this.doSerialize(insideString, indentation, Self, printKey, keySeparator, attributeSeparator, wrap)
|
||||
if (Self.serialized) {
|
||||
result = IEntity.asSerializedString(result)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** @param {IEntity} other */
|
||||
equals(other) {
|
||||
const thisKeys = Object.keys(this)
|
||||
const otherKeys = Object.keys(other)
|
||||
if (thisKeys.length != otherKeys.length) {
|
||||
if (!(other instanceof IEntity)) {
|
||||
return false
|
||||
}
|
||||
for (const key of thisKeys) {
|
||||
if (this[key] instanceof IEntity && !this[key].equals(other[key])) {
|
||||
return false
|
||||
} else if (!Utility.equals(this[key], other[key])) {
|
||||
const thisKeys = Object.keys(this)
|
||||
const otherKeys = Object.keys(other)
|
||||
const thisType = /** @type {typeof IEntity} */(this.constructor).actualClass()
|
||||
const otherType = /** @type {typeof IEntity} */(other.constructor).actualClass()
|
||||
if (
|
||||
thisKeys.length !== otherKeys.length
|
||||
|| this.lookbehind != other.lookbehind
|
||||
|| !(other instanceof thisType) && !(this instanceof otherType)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < thisKeys.length; ++i) {
|
||||
const k = thisKeys[i]
|
||||
if (!otherKeys.includes(k)) {
|
||||
return false
|
||||
}
|
||||
const a = this[k]
|
||||
const b = other[k]
|
||||
if (a instanceof IEntity) {
|
||||
if (!a.equals(b)) {
|
||||
return false
|
||||
}
|
||||
} else if (a instanceof Array && b instanceof Array) {
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
for (let j = 0; j < a.length; ++j) {
|
||||
if (!(a[j] instanceof IEntity && a[j].equals(b[j])) && a[j] !== b[j]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (a !== b) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user