UNPKG

immutable-class

Version:

A template for creating immutable classes

328 lines (327 loc) 12.3 kB
"use strict"; 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'); }, } });