@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
364 lines (267 loc) • 8.92 kB
JavaScript
import LabelView from "../../../src/view/common/LabelView.js";
import EmptyView from "../../../src/view/elements/EmptyView.js";
import { createFieldEditor } from "./createFieldEditor.js";
import { FieldDescriptor } from "./FieldDescriptor.js";
import { FieldValueAdapter } from "./FieldValueAdapter.js";
import { findNearestRegisteredType } from "./findNearestRegisteredType.js";
/**
* @template T
* @param {T|undefined} value
*/
function objectToClass(value) {
if (value === null || value === undefined) {
return;
}
const type = typeof value;
switch (type) {
case "object":
const proto = Object.getPrototypeOf(value);
if (proto === null) {
return;
}
return proto.constructor;
case "boolean":
return Boolean;
case "function":
return Function;
case "string":
return String;
case "number":
return Number;
default:
throw new Error(`Unsupported type '${type}'`);
}
}
/**
*
* @param {Object} object
* @param {string} field_name
* @returns {boolean}
*/
function isPublicField(object, field_name) {
if (field_name.startsWith('_')) {
// private
return false;
}
return true;
}
/**
*
* @param {Object} object
* @param {string} field_name
* @returns {boolean}
*/
function isReservedField(object, field_name) {
if (field_name.startsWith('@')) {
return true;
}
return false;
}
/**
*
* @type {string[]}
*/
const reserved_prototype_names = [
"constructor"
];
const reserved_prototypes = [
Object,
Object.prototype
];
/**
*
* @param {Object} object
* @param {FieldDescriptor[]} result
* @param {*} object_proto
*/
function extract_object_fields_reflection(object, result, object_proto) {
const keys = Object.keys(object);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!isPublicField(object, key)) {
continue;
}
if (isReservedField(object, key)) {
continue;
}
const field_type = objectToClass(object[key]);
if (field_type === undefined) {
continue;
}
const descriptor = new FieldDescriptor();
descriptor.name = key;
descriptor.type = field_type;
descriptor.adapter = new FieldValueAdapter();
result.push(descriptor);
}
// TODO exclude value fields that belong to the prototype, such as .isTransform
if (!reserved_prototypes.includes(object_proto)) {
const descriptors = Object.getOwnPropertyDescriptors(object_proto);
for (let d_name in descriptors) {
if (reserved_prototype_names.includes(d_name)) {
// reserved name, skip
continue;
}
const d = descriptors[d_name];
const adapter = new FieldValueAdapter();
if (d.get === undefined && d.value === undefined) {
adapter.readable = false;
}
if (d.get !== undefined && d.set === undefined) {
adapter.writable = false;
}
if (!adapter.readable) {
continue;
}
let field_value;
try {
field_value = adapter.read(object, d_name);
} catch (e) {
console.warn(e);
continue;
}
const field_type = objectToClass(field_value);
if (field_type === undefined) {
continue;
}
const descriptor = new FieldDescriptor();
descriptor.name = d_name;
descriptor.type = field_type;
descriptor.adapter = adapter;
result.push(descriptor);
}
}
}
/**
*
* @param {Object} object
* @param {Map<*, TypeEditor>} registry
* @returns {FieldDescriptor[]}
*/
function extractObjectFields(object, registry) {
const result = [];
const object_proto = Object.getPrototypeOf(object);
const Klass = object_proto.constructor;
const editor = registry.get(Klass);
if (!(editor?.schema?.additionalProperties === false)) {
extract_object_fields_reflection(object, result, object_proto);
}
if (editor !== undefined) {
const schema = editor.schema;
if (schema !== undefined) {
// has schema, apply
const prop_keys = Object.keys(schema.properties);
for (let i = 0; i < prop_keys.length; i++) {
const key = prop_keys[i];
const property = schema.properties[key];
const field_index = result.findIndex(a => a.name === key);
let field;
if (field_index === -1) {
// not in schema yet, build
field = new FieldDescriptor();
field.name = key;
field.adapter = new FieldValueAdapter();
result.push(field);
} else {
field = result[field_index];
}
let actual_type = field.type;
if (actual_type === undefined || actual_type === null) {
let actual_value;
try {
actual_value = field.adapter.read(object, key);
} catch (e) {
// silent failure
}
if (actual_value !== undefined && actual_value !== null) {
actual_type = objectToClass(actual_value);
}
}
if (property.type) {
// validate type
if (actual_type !== undefined && actual_type !== null) {
if (actual_type !== property.type) {
console.error(`Schema violation at field '${key}', schema type is different from actual type`, schema, actual_type);
}
}
field.type = property.type;
} else if (actual_type !== undefined && field.type !== actual_type) {
field.type = actual_type;
}
field.schema = property;
if (property.transient === true) {
// no point in editing transient
result.splice(field_index, 1);
}
}
}
}
return result;
}
const build_path = [];
let build_path_offset = 0;
/**
* @param {Object} object
* @param {Map<*, TypeEditor>} registry
* @param {FieldDescriptor} [field_descriptor]
*/
export function buildObjectEditorFromRegistry(object, registry, field_descriptor) {
const editor = findNearestRegisteredType(registry, object);
let fd;
if (field_descriptor === undefined) {
fd = new FieldDescriptor();
fd.adapter = new FieldValueAdapter();
fd.adapter.read = () => object;
fd.adapter.writable = false;
if (editor.schema) {
fd.schema = editor.schema;
}
} else {
fd = field_descriptor;
}
return editor.build(null, fd, registry);
}
const MAX_DEPTH = 7;
/**
* @param {Object} object
* @param {Map<*, TypeEditor>} registry
*/
export function createObjectEditor(object, registry) {
if (build_path_offset > MAX_DEPTH) {
// too deep
return undefined;
}
const seen_index = build_path.indexOf(object);
if (seen_index >= 0 && seen_index < build_path_offset) {
// recursion
return undefined;
}
build_path[build_path_offset++] = object;
const vResult = new EmptyView({
classList: ['auto-object-editor'],
css: {}
});
const fields = extractObjectFields(object, registry);
const field_views = [];
for (let i = 0; i < fields.length; i++) {
const field = fields[i];
let vField;
try {
vField = createFieldEditor(object, field, registry);
} catch (e) {
vField = new LabelView('ERROR');
console.warn(e);
}
if (vField === null) {
continue;
}
field_views.push(vField);
}
build_path_offset--;
if (field_views.length === 0) {
// skip containers with no data
return null;
}
vResult.addChildren(field_views);
return vResult;
}