immutable-class
Version:
A template for creating immutable classes
328 lines (327 loc) • 12.3 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseImmutable = exports.PropertyType = void 0;
const tslib_1 = require("tslib");
const has_own_prop_1 = tslib_1.__importDefault(require("has-own-prop"));
const equality_1 = require("../equality/equality");
const named_array_1 = require("../named-array/named-array");
function firstUp(name) {
return name[0].toUpperCase() + name.slice(1);
}
function isDefined(v, emptyArrayIsOk) {
return Array.isArray(v) ? v.length || emptyArrayIsOk : v != null;
}
function noop(v) {
return v;
}
const EXPLAIN_UDFCF = 'This might indicate that you are using "useDefineForClassFields" and forgot to use "declare" on an auto-generated getter property.';
exports.PropertyType = {
DATE: 'date',
ARRAY: 'array',
};
class BaseImmutable {
static jsToValue(properties, js, backCompats, context) {
if (properties == null) {
throw new Error(`JS is not defined`);
}
if (Array.isArray(backCompats)) {
let jsCopied = false;
for (const backCompat of backCompats) {
if (backCompat.condition(js)) {
if (!jsCopied) {
js = JSON.parse(JSON.stringify(js));
jsCopied = true;
}
backCompat.action(js);
}
}
}
const value = {};
for (const property of properties) {
const propertyName = property.name;
if (typeof propertyName !== 'string')
continue;
const contextTransform = property.contextTransform || noop;
let pv = js[propertyName];
if (pv != null) {
if (property.type === exports.PropertyType.DATE) {
pv = new Date(pv);
}
else if (property.immutableClass) {
pv = property.immutableClass.fromJS(pv, context ? contextTransform(context) : undefined);
}
else if (property.immutableClassArray) {
if (!Array.isArray(pv))
throw new Error(`expected ${propertyName} to be an array`);
const propertyImmutableClassArray = property.immutableClassArray;
pv = pv.map((v) => propertyImmutableClassArray.fromJS(v, context ? contextTransform(context) : undefined));
}
}
value[propertyName] = pv;
}
return value;
}
static finalize(ClassFn) {
const proto = ClassFn.prototype;
ClassFn.PROPERTIES.forEach((property) => {
const propertyName = property.name;
if (typeof propertyName !== 'string')
return;
const defaultValue = property.defaultValue;
const upped = firstUp(propertyName);
const getUpped = 'get' + upped;
const changeUpped = 'change' + upped;
proto[getUpped] =
proto[getUpped] ||
function () {
const pv = this[propertyName];
return pv != null ? pv : defaultValue;
};
proto[changeUpped] =
proto[changeUpped] ||
function (newValue) {
if (this[propertyName] === newValue)
return this;
const value = this.valueOf();
value[propertyName] = newValue;
return new this.constructor(value);
};
});
}
constructor(value) {
const properties = this.ownProperties();
for (const property of properties) {
const propertyName = property.name;
if (typeof propertyName !== 'string')
continue;
const propertyType = (0, has_own_prop_1.default)(property, 'isDate') ? exports.PropertyType.DATE : property.type;
const pv = value[propertyName];
if (pv == null) {
if (propertyType === exports.PropertyType.ARRAY) {
this[propertyName] = [];
continue;
}
if (!(0, has_own_prop_1.default)(property, 'defaultValue')) {
throw new Error(`${this.constructor.name}.${propertyName} must be defined`);
}
}
else {
const possibleValues = property.possibleValues;
if (possibleValues && !possibleValues.includes(pv)) {
throw new Error(`${this.constructor.name}.${propertyName} can not have value '${pv}' must be one of [${possibleValues.join(', ')}]`);
}
if (property.type === exports.PropertyType.DATE) {
if (isNaN(pv)) {
throw new Error(`${this.constructor.name}.${propertyName} must be a Date`);
}
}
if (property.type === exports.PropertyType.ARRAY) {
if (!Array.isArray(pv)) {
throw new Error(`${this.constructor.name}.${propertyName} must be an Array`);
}
}
const validate = property.validate;
if (validate) {
const validators = Array.isArray(validate) ? validate : [validate];
try {
for (const validator of validators)
validator(pv);
}
catch (e) {
throw new Error(`${this.constructor.name}.${propertyName} ${e.message}`);
}
}
}
Object.defineProperty(this, propertyName, {
value: pv,
configurable: true,
enumerable: true,
writable: false,
});
}
}
ownProperties() {
return this.constructor.PROPERTIES;
}
findOwnProperty(propName) {
const properties = this.ownProperties();
return named_array_1.NamedArray.findByName(properties, propName);
}
hasProperty(propName) {
return this.findOwnProperty(propName) !== null;
}
valueOf() {
const value = {};
const properties = this.ownProperties();
for (const property of properties) {
const propertyName = property.name;
value[propertyName] = this[propertyName];
}
return value;
}
toJS() {
const js = {};
const properties = this.ownProperties();
for (const property of properties) {
const propertyName = property.name;
let pv = this[propertyName];
if (isDefined(pv, property.emptyArrayIsOk) || property.preserveUndefined) {
if (typeof property.toJS === 'function') {
const toJS = property.toJS;
pv = property.immutableClassArray ? pv.map(toJS) : toJS(pv);
}
else if (property.immutableClass) {
pv = pv.toJS();
}
else if (property.immutableClassArray) {
pv = pv.map((v) => v.toJS());
}
js[propertyName] = pv;
}
}
return js;
}
toJSON() {
return this.toJS();
}
toString() {
const name = this.name;
const extra = name === 'string' ? `: ${name}` : '';
return `[ImmutableClass${extra}]`;
}
getDifference(other, returnOnFirstDifference = false) {
if (!other)
return ['__no_other__'];
if (this === other)
return [];
if (!(other instanceof this.constructor))
return ['__different_constructors__'];
const differences = [];
const properties = this.ownProperties();
for (const property of properties) {
const equal = property.equal || equality_1.generalEqual;
if (!equal(this[property.name], other[property.name])) {
const difference = property.name;
if (returnOnFirstDifference)
return [difference];
differences.push(difference);
}
}
return differences;
}
equals(other) {
return this.getDifference(other, true).length === 0;
}
equivalent(other) {
if (!other)
return false;
if (this === other)
return true;
if (!(other instanceof this.constructor))
return false;
const properties = this.ownProperties();
for (const property of properties) {
const propertyName = property.name;
const equal = property.equal || equality_1.generalEqual;
if (!equal(this.get(propertyName), other.get(propertyName)))
return false;
}
return true;
}
get(propName) {
const getter = this['get' + firstUp(propName)];
if (!getter) {
const msg = `No getter was found for "${propName}"`;
if (Object.getOwnPropertyDescriptor(this, 'get' + firstUp(propName))) {
throw new Error(msg + ' but it is defined as a property. ' + EXPLAIN_UDFCF);
}
else {
throw new Error(msg + '.');
}
}
return getter.call(this);
}
change(propName, newValue) {
const changer = this['change' + firstUp(propName)];
if (!changer) {
const msg = `No changer was found for "${propName}"`;
if (Object.getOwnPropertyDescriptor(this, 'change' + firstUp(propName))) {
throw new Error(msg + ' but it is defined as a property. ' + EXPLAIN_UDFCF);
}
else {
throw new Error(msg + '.');
}
}
return changer.call(this, newValue);
}
changeMany(properties) {
if (!properties)
throw new TypeError('Invalid properties object');
let o = this;
for (const propName in properties) {
if (!this.hasProperty(propName))
throw new Error('Unknown property: ' + propName);
o = o.change(propName, properties[propName]);
}
return o;
}
deepChange(propName, newValue) {
const bits = propName.split('.');
let lastObject = newValue;
let currentObject;
const getLastObject = () => {
let o = this;
for (let i = 0; i < bits.length; i++) {
o = o['get' + firstUp(bits[i])]();
}
return o;
};
while (bits.length) {
const bit = bits.pop();
currentObject = getLastObject();
if (currentObject.change instanceof Function) {
lastObject = currentObject.change(bit, lastObject);
}
else {
const message = "Can't find `change()` method on " + currentObject.constructor.name;
throw new Error(message);
}
}
return lastObject;
}
deepGet(propName) {
let value = this;
const bits = propName.split('.');
let bit;
while ((bit = bits.shift())) {
const specializedGetterName = `get${firstUp(bit)}`;
const specializedGetter = value[specializedGetterName];
value = specializedGetter
? specializedGetter.call(value)
: value.get
? value.get(bit)
: value[bit];
}
return value;
}
}
exports.BaseImmutable = BaseImmutable;
Object.defineProperty(BaseImmutable, "ensure", {
enumerable: true,
configurable: true,
writable: true,
value: {
number: (n) => {
if (isNaN(n) || typeof n !== 'number')
throw new Error(`must be a number`);
},
positive: (n) => {
if (n < 0)
throw new Error('must be positive');
},
nonNegative: (n) => {
if (n < 0)
throw new Error('must be non negative');
},
}
});
;