@javelin/core
Version:
This package is a general catch-all for stuff shared by many Javelin packages, including:
285 lines • 8.27 kB
JavaScript
import { mutableEmpty } from "../utils";
import { $flat, $kind, FieldKind, } from "./model";
/**
* Object used in a schema to declare a numeric field.
* @example
* const Wallet = { money: number }
*/
export const number = {
[$kind]: FieldKind.Number,
get: () => 0,
};
/**
* Object used in a schema to declare a string field.
* @example
* const Book = { title: string }
*/
export const string = {
[$kind]: FieldKind.String,
get: () => "",
};
/**
* Object used in a schema to declare a boolean field.
* @example
* const Controller = { jumping: boolean }
*/
export const boolean = {
[$kind]: FieldKind.Boolean,
get: () => false,
};
/**
* Build a field that represents an array within a schema. The sole parameter
* defines the array's element type, which can be another field or Schema.
* @example <caption>primitive element</caption>
* const Bag = { items: arrayOf(number) }
* @example <caption>complex element</caption>
* const Shape = { vertices: arrayOf(arrayOf(number)) }
*/
export function arrayOf(element) {
return {
[$kind]: FieldKind.Array,
get: (array = []) => mutableEmpty(array),
element,
};
}
/**
* Build a field that represents an object within a schema. The first parameter
* defines the object's element type, which can be another field or Schema. If
* provided, the second argument will override the default key type of `string`
* with another field derived from `string`.
* @example <caption>primitive element</caption>
* const Stats = { values: objectOf(number) }
* @example <caption>`key` type override (e.g. for `@javelin/pack` encoding)</caption>
* const Stat = { min: number, max: number, value: number }
* const Stats = { values: objectOf(Stat, { ...string, length: 20 }) }
*/
export function objectOf(element, key = string) {
return {
[$kind]: FieldKind.Object,
get: (object = {}) => {
for (const prop in object) {
delete object[prop];
}
return object;
},
key,
element,
};
}
/**
* Build a field that represents an Set within a schema. The sole parameter
* defines the Set's element type, which can be another field or Schema.
* @example <caption>primitive element</caption>
* const Buffs = { values: setOf(number) }
* @example <caption>complex element</caption>
* const Body = { colliders: setOf(Collider) }
*/
export function setOf(element) {
return {
[$kind]: FieldKind.Set,
get: (set = new Set()) => {
set.clear();
return set;
},
element,
};
}
/**
* Build a field that represents an Map within a schema. The first parameter
* defines the Map's key type, which must be a primitive field. The second
* argument defines the Map's value type, which can be a field or schema.
* @example <caption>primitive element</caption>
* const Disabled = { entities: mapOf(number, boolean) }
* @example <caption>complex element</caption>
* const PrivateChat = { messagesByClientId: mapOf(string, arrayOf(ChatMessage)) }
*/
export function mapOf(key, element) {
return {
[$kind]: FieldKind.Map,
get: (map = new Map()) => {
map.clear();
return map;
},
key,
element,
};
}
/**
* Build a field that represents an unknown type in a schema. Accepts an
* optional parameter which is a factory function that returns an initial
* value for each field.
* @example <caption>`unknown` type</caption>
* const RigidBody = { value: dynamic() }
* @example <caption>library type</caption>
* const RigidBody = { value: dynamic(() => new Rapier.RigidBody()) }
*/
export function dynamic(get = () => null) {
return {
[$kind]: FieldKind.Dynamic,
get,
};
}
/**
* Determine if an object is a Javelin model field.
*/
export function isField(object) {
return $kind in object;
}
/**
* Determine if an object is a primitive Javelin model field.
*/
export function isPrimitiveField(object) {
if (!isField(object)) {
return false;
}
const kind = object[$kind];
return (kind === FieldKind.Number ||
kind === FieldKind.String ||
kind === FieldKind.Boolean ||
kind === FieldKind.Dynamic);
}
/**
* Determine if a Javelin model node represents a schema.
*/
export function isSchema(node) {
return !($kind in node);
}
/**
* Determine if a Javelin model node is simple. A node is simple if:
* 1. it is primitive
* 2. it is a schema and each of its fields are primitive
* 3. it is a complex field (e.g. array, map) and its element is primitive
*/
export function isSimple(node) {
if (isSchema(node)) {
return node.fields.every(isPrimitiveField);
}
else if ("element" in node) {
return isPrimitiveField(node.element);
}
return true;
}
/**
* Create a recursively annotated schema where each field is uniquely
* addressed by an integer id, indexed using `lo` and `hi` fields, and
* annotated with useful metadata.
*/
function collate(visiting, cursor, traverse = []) {
let base = {
id: cursor.id,
lo: cursor.id,
hi: cursor.id,
deep: traverse.length > 0,
traverse,
};
cursor.id++;
let node;
if (isField(visiting)) {
node = { ...base, ...visiting };
if ("element" in node) {
node.element = collate(node.element, cursor, "key" in node
? [...traverse, node.key]
: traverse);
}
}
else {
const keys = Object.keys(visiting);
const keysByFieldId = [];
const fields = [];
const fieldsByKey = {};
const fieldIdsByKey = {};
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const child = collate(visiting[key], cursor, traverse);
keysByFieldId[child.id] = key;
fieldsByKey[key] = child;
fieldIdsByKey[key] = child.id;
fields.push(child);
}
node = {
...base,
keys,
keysByFieldId,
fields,
fieldsByKey,
fieldIdsByKey,
};
}
node.hi = cursor.id;
return node;
}
/**
* Recursively flatten a model node, updating the `flat` argument with
* key/value pairs where the key is a field id and the value is a its
* corresponding model node.
*/
function flattenModelNode(node, flat) {
flat[node.id] = node;
if (isField(node)) {
if ("element" in node) {
flattenModelNode(node.element, flat);
}
}
else {
for (let i = 0; i < node.fields.length; i++) {
flattenModelNode(node.fields[i], flat);
}
}
}
/**
* Recursively flatten a model, creating a new object containing key/value
* pairs where keys are field ids and values are corresponding model nodes.
*/
function flattenModel(model) {
const flat = {};
for (const prop in model) {
flattenModelNode(model[prop], (flat[prop] = {}));
}
return flat;
}
/**
* Produce a graph from a model and assign each writable field a unique integer
* id.
*/
export function createModel(config) {
const model = {};
config.forEach((schema, t) => (model[t] = collate(schema, { id: 0 })));
return Object.defineProperty(model, $flat, {
enumerable: false,
writable: false,
value: flattenModel(model),
});
}
/**
* Create an instance of a schema.
*/
export function createSchemaInstance(schema, object = {}) {
for (const prop in schema) {
const type = schema[prop];
let value;
if (isField(type)) {
value = type.get();
}
else {
value = createSchemaInstance({}, type);
}
object[prop] = value;
}
return object;
}
/**
* Reset an instance of a schema.
*/
export function resetSchemaInstance(object, schema) {
for (const prop in schema) {
const type = schema[prop];
if (isField(type)) {
object[prop] = type.get(object[prop]);
}
else {
resetSchemaInstance(object[prop], type);
}
}
return object;
}
//# sourceMappingURL=model_helpers.js.map