@focuson/lens
Version:
A simple implementation of lens using type script
538 lines (536 loc) • 24.2 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.asGetNameFn = exports.nameLensFn = exports.massTransform = exports.displayTransformsInState = exports.secondIn2 = exports.firstIn2 = exports.nthItem = exports.updateThreeValues = exports.updateTwoValues = exports.transformTwoValues = exports.Iso = exports.iso = exports.Prism = exports.DirtyPrism = exports.dirtyPrism = exports.prism = exports.isFOPPath = exports.isFOPNth = exports.isFOPSingle = exports.Lenses = exports.Lens = exports.lens = exports.castIfOptional = exports.orUndefined = exports.identityOptional = exports.optional = exports.Optional = exports.identityOptics = void 0;
const utils_1 = require("@focuson/utils");
const identityOptics = (description) => new Iso(x => x, x => x, description ? description : "I");
exports.identityOptics = identityOptics;
/** An Optional is like a lens, except that it is not guaranteed to 'work'. Specifically if you ask for a child... maybe that child isn't there.
*
* This is great for things like 'optional values' which are often written as 'name?: type' in typescript.
*
* It is rare that you create one directly. Usually it is created using 'focusQuery' on a lens
*/
class Optional {
constructor(getOption, optionalSet, description) {
this.set = (m, c) => (0, utils_1.useOrDefault)(m)(this.setOption(m, c));
this.map = (m, fn) => this.set(m, fn(this.getOption(m)));
this.mapDefined = (m, fn) => (0, utils_1.applyOrDefault)(this.getOption(m), c => this.set(m, fn(c)), m);
this.getOption = getOption;
this.setOption = optionalSet;
this.description = description ? description : "";
}
/** This is identical to this.setOption(m, undefined) */
clearJson(m) {
// @ts-ignore
return this.setOption(m, undefined);
}
/** Allows us to change the focuson-ed child based on it's existing value
* @fn a function that will be given the old value and will calculate the new
* @returns a function that given a Main will return a new main with the child transformed as per 'fn' */
transform(fn) {
return m => this.map(m, fn);
}
/** This is used when the 'parameter' points to definite value. i.e. it isn't 'x: X | undefined' or 'x?: X'. If you want to
* walk through those you probably want to use 'focusQuery'
*
* If the type system is complaining and you are sure that it should be OK, check if the previous focusOn should be a focusQuery
* @param k
*/
focusOn(k) {
return new Optional((m) => (0, utils_1.apply)(this.getOption(m), c => c[k]), (m, v) => (0, utils_1.apply)(this.getOption(m), c => this.set(m, (0, utils_1.copyWithFieldSet)(c, k, v))), this.description + ".focusOn(" + k.toString() + ")");
}
/** Used to focus onto a child that might not be there. If you don't use this, then the type system is likely to complain if you try and carry on focusing. */
focusQuery(k) {
return new Optional(
// @ts-ignore
m => (0, utils_1.apply)(this.getOption(m), c => c[k]), (m, v) => {
let child = this.getOption(m);
let result = this.setOption(m, (0, utils_1.copyWithFieldSet)(child, k, v));
return result;
}, this.description + ".focus?(" + k.toString() + ")");
}
chain(o) {
return new Optional(m => (0, utils_1.apply)(this.getOption(m), c => o.getOption(c)), (m, t) => this.map(m, oldC => o.set(oldC, t)), this.description + ".chain(" + o.description + ")");
}
combine(other) {
return new Optional(m => (0, utils_1.apply)(this.getOption(m), (c) => (0, utils_1.apply)(other.getOption(m), nc => [c, nc])), (m, newChild) => {
let [nc, noc] = newChild;
let m1 = other.setOption(m, noc);
return m1 && this.setOption(m1, nc);
}, "combine(" + this.description + "," + other.description + ")");
}
/** If you desire to change the description this will do that. It is rarely called outside the Lens code itself */
withDescription(description) {
return new Optional(this.getOption, this.set, description);
}
combineAs(other, iso) {
return this.combine(other).chain(iso);
}
chainIntoArray(a) {
const opt = this;
function getter(m) {
const res = opt.getOption(m);
if (res === undefined)
return undefined;
let t = typeof res;
if (t === 'boolean')
return a[res ? 1 : 0];
if (t === 'number')
return a[res];
throw Error(`Tried to access data ${a} at ${opt.description} and value was type ${t}\n${JSON.stringify(m)}`);
}
function setter(m, s) {
const index = a.indexOf(s);
if (index < 0)
throw Error(`Cannot give value ${s}. Legal values are ${a}`);
return opt.setOption(m, index); //Note this will change booleans to 0/1
}
return new Optional(getter, setter, `chainIntoArray(${a})`);
}
chainNthFromPath(pathL) {
// @ts-ignore --Typescripts type system doesn't support type guards so we cannot express that Child must be an array and do better than any on the output
return Lenses.calculatedNth(pathL, this);
}
chainCalc(pathL) {
const lens = this;
function getter(main) {
const index = pathL.getOption(main);
const to = lens.getOption(main);
if (index !== undefined && to !== undefined) {
if (typeof index === 'boolean')
return to[index ? 1 : 0];
return to[index];
}
}
function setter(main, newChild) {
const index = pathL.getOption(main);
const oldTo = lens.getOption(main);
if (Array.isArray(oldTo)) {
const newArray = [...oldTo];
if (typeof index === 'boolean')
newArray[index ? 1 : 0] = newChild;
else
newArray[index] = newChild;
return lens.set(main, newArray);
}
const newTo = Object.assign({}, oldTo);
newTo[index] = newChild;
return lens.set(main, newTo);
}
return optional(getter, setter, `${this.description}.chainCalc(${pathL.description})`);
}
toString() {
return "Optional(" + this.description + ")";
}
}
exports.Optional = Optional;
function optional(getOption, setOption, description) {
return new Optional(getOption, setOption, description);
}
exports.optional = optional;
function identityOptional() {
return optional(m => m, (m, c) => c);
}
exports.identityOptional = identityOptional;
function orUndefined(description) {
const getOption = (t) => t;
const setOption = (t, child) => child;
return optional(getOption, setOption, description);
}
exports.orUndefined = orUndefined;
function castIfOptional(cond, description) {
function getOption(t) {
// @ts-ignore
return cond(t) ? t : undefined;
}
function setOption(ignore, child) {
// @ts-ignore
return child;
}
return optional(getOption, setOption, description ? description : `castIf`);
}
exports.castIfOptional = castIfOptional;
/**
* Creates a lens with two generics. Lens<Main,Child>. Main is the main 'object' that we start with, and Child is the part of Main that the lens is focuson-ed
* @param get should be a sideeffect free function that goes from 'Main' to the focuson-ed child. When called it 'gets' the Child from the Main
* @param set should be a sideeffect free function that creates a new Main out of an old main and a new child. It returns the old main with the 'focuson-ed' part replaced by the new child
* @param description should probably be the string representation of the class 'Main'. If the main object is of type Dragon, this could be the string 'dragon'.
*
* Usually these are created by code like
*
* identityOptics<Dragon>().focuson('head')
*/
function lens(get, set, description) {
(0, utils_1.checkIsFunction)(get);
(0, utils_1.checkIsFunction)(set);
return new Lens(get, set, description ? description : "lens");
}
exports.lens = lens;
/** This is the class that represents a Lens<Main,Child> which focuses on Child which is a part of the Main */
class Lens extends Optional {
constructor(get, set, description) {
super(get, set, description);
this.chainLens = (o) => new Lens(m => o.get(this.get(m)), (m, t) => this.set(m, o.set(this.get(m), t)), this.description + ".chain(" + o.description + ")");
/** @deprecated */
this.chainWith = this.chainLens;
this.get = get;
this.set = set;
}
/** this is the 'normal' focuson. We use it when we know that the result is there. i.e. if we have
*
* interface AB{
* a: string,
* b?: SomeInterface | undefined
* }
*
* In this case focusOn('a') will give us a Lens<AB,string> but focusOn('b') will give a lens<AB, SomeInterface|undefined>.
* @param k
*/
focusOn(k) {
return new Lens((m) => this.get(m)[k], (m, c) => this.set(m, (0, utils_1.copyWithFieldSet)(this.get(m), k, c)), this.description + ".focusOn(" + k.toString() + ")");
}
/** interface AB{
* a: string,
* b?: SomeInterface | undefined
* }
*
* In this case it would be redundant to have focusWithDefault('a', "someA") because 'a' should never be undefined.
* However (if someValue is a SomeInterface) focusWithDfeault('b', someValue) return Lens<AB,SomeInterface> and if we do a get, and b was undefined, we use 'someValue'
* @param k
*/
focusWithDefault(k, def) {
// @ts-ignore
return new Lens((m) => (0, utils_1.useOrDefault)(def)(this.get(m)[k]), (m, v) => this.set(m, (0, utils_1.copyWithFieldSet)(
// @ts-ignore
this.get(m), k, v)), this.description + ".focusWithDefault(" + k.toString() + ")");
}
combineLens(other) {
return new Lens(m => [this.get(m), other.get(m)], (m, newChild) => this.set(other.set(m, newChild[1]), newChild[0]), "combine(" + this.description + "," + other.description + ")");
}
/** If you desire to change the description this will do that. It is rarely called outside the Lens code itself */
withDescription(description) {
return new Lens(this.get, this.set, description);
}
toString() {
return "Lens(" + this.description + ")";
}
}
exports.Lens = Lens;
/** A factory class that allows us to create new Lens. Every method on it is static, so you would never create one of these
*
* This class will be removed and replaced with just 'plain functions'
* */
class Lenses {
/** This is a the normal way to generate lens. It create a link that goes from Main to itself */
static build(description) {
return Lenses.identity().withDescription(description);
}
/** Given a main which is an object, with a field name, this returns a lens that goes from the Main to the contents of the field name */
static identity(description) {
return (0, exports.identityOptics)(description);
}
/**This should no longer be needed. It was in fact the need for this method that drove the rewrite using Optionals/Prisms and Isos.
*
* Nowadays we can use focusQuery
*
* It should only be used when we 'know' that a Lens<Main,Child|undefined> is really a Lens<Main,Child>.
* @deprecated */
static define() {
return lens(main => {
if (main != undefined)
return main;
else
throw new Error("undefined");
}, (main, child) => child);
}
/** This returns a lens from an array of T to the last item of the array */
static last() {
return lens(ts => ts === null || ts === void 0 ? void 0 : ts[(ts === null || ts === void 0 ? void 0 : ts.length) - 1], (ts, t) => {
let result = [...ts];
result[ts.length - 1] = t;
return result;
}, '[last]');
}
/** This returns a lens from an array of T to the next item of the array */
static append() {
return lens(ts => undefined, (ts, t) => {
let result = [...ts];
result[ts.length] = t;
return result;
}, '[append]');
}
/** This returns a lens from an array of T to the nth member of the array */
static nth(n) {
const check = (verb, length) => {
if (n > length)
throw Error(`Cannot Lens.nth(${n}).${verb}. arr.length is ${length}`);
};
if (n < 0)
throw Error(`Cannot give Lens.nth a negative n [${n}]`);
return optional(arr => {
// check ( 'get', arr.length );
return arr[n];
}, (main, value) => {
// check ( 'set', main.length )
let result = main.slice();
result[n] = value;
return result;
}, `[${n}]`);
}
static chainNthFromOptionalFn(lens, newToFnL, newFragDescription) {
const getter = (f) => {
const l = newToFnL(f);
const to = lens.getOption(f);
return to && l.getOption(to);
};
const setter = (f, newTo) => {
const l = newToFnL(f);
const to = lens.getOption(f);
const toPrime = to && l.set(to, newTo);
return toPrime && lens.set(f, toPrime);
};
return new Optional(getter, setter, lens.description + ".chainNthFrom(" + newFragDescription + ')');
}
static chainNthRef(lens, lookup, name, description) {
if (!lookup)
throw new Error('lookup must not be undefined');
function findIndex(f) {
const index = lookup(name);
if (index === undefined)
throw new Error(`nthRef of [${name}] doesn't exist.`);
if (typeof index !== 'number')
throw new Error(`nthRef of [${name}] has type ${typeof index} which is not a number. Value is ${index}.`);
if (index < 0)
throw new Error(`nthRef of ${name} maps to ${index} in ${JSON.stringify(f)}`);
return index;
}
const getter = (f) => { var _a; return (_a = (lens.getOption(f))) === null || _a === void 0 ? void 0 : _a[findIndex(f)]; };
function setter(f, t) {
const index = findIndex(f);
const array = lens.getOption(f);
const res = [...array];
res[index] = t;
return lens.set(f, res);
}
return new Lens(getter, setter, description ? description : `${lens.description}.{${name}}`);
}
/** Used to to go from ids to values and back again. Obvious if the mapping isn't one to one there will be loss of data */
static chainLookup(opt, lookupL) {
const getter = (m) => {
const table = (0, utils_1.or)(() => ({}))(lookupL.getOption(m));
return table === null || table === void 0 ? void 0 : table[opt.getOption(m)];
};
const setter = (m, value) => {
var _a;
const table = Object.entries((0, utils_1.or)(() => ({}))(lookupL.getOption(m)));
const newId = (_a = table.find(([k, v]) => v === value)) === null || _a === void 0 ? void 0 : _a[0];
return newId ? opt.setOption(m, newId) : undefined;
};
return optional(getter, setter, `${opt.description}.chainLookup(${lookupL.description})`);
}
/** Used to to go from ids to values and back again. Obvious if the mapping isn't one to one there will be loss of data */
static chainLookupTable(opt, lookupL, idName, valueName) {
const getter = (m) => {
var _a;
const table = (0, utils_1.or)(() => ([]))(lookupL.getOption(m));
const id = opt.getOption(m);
return (_a = table.find(obj => obj[idName] === id)) === null || _a === void 0 ? void 0 : _a[valueName];
};
const setter = (m, value) => {
var _a;
const table = (0, utils_1.or)(() => ([]))(lookupL.getOption(m));
const newId = (_a = table.find(obj => obj[valueName] === value)) === null || _a === void 0 ? void 0 : _a[idName];
return newId ? opt.setOption(m, newId) : undefined;
};
return optional(getter, setter, `${opt.description}.chainLookup(${lookupL.description})`);
}
static safeList() {
return lens((list) => list ? list : [], (main, list) => list, 'removeUndefined');
}
static if(cond, trueL, falseL) {
return Lenses.condition(cond, c => c, trueL, falseL);
}
static condition(cond, fn, trueL, falseL) {
function getter(m) {
const c = cond.getOption(m);
if (c !== undefined && fn(c))
return trueL.getOption(m);
return falseL.getOption(m);
}
function setter(m, t) {
const c = cond.getOption(m);
if (c !== undefined && fn(c))
return trueL.setOption(m, t);
return falseL.setOption(m, t);
}
return new Optional(getter, setter, `If(${cond.description}) then ${trueL.description} else ${falseL.description}`);
}
static calculatedNth(nL, opt) {
function getter(m) {
var _a;
const rawN = nL.getOption(m);
return (_a = opt.getOption(m)) === null || _a === void 0 ? void 0 : _a[rawN ? rawN : 0];
}
function setter(m, t) {
const rawN = nL.getOption(m);
const newArray = [...opt.getOption(m)];
newArray[rawN] = t;
return opt.setOption(m, newArray);
}
return new Optional(getter, setter, `calculatedNth(${nL.description}, ${opt.description}`);
}
}
/** Given a main which is an object, with a field name, this returns a lens that goes from the Main to the contents of the field name */
Lenses.field = (fieldName) => lens(m => m[fieldName], (m, c) => {
let result = Object.assign({}, m);
result[fieldName] = c;
return result;
}, fieldName.toString());
Lenses.constant = (value, description) => lens(m => value, (m, c) => m, description);
exports.Lenses = Lenses;
function isFOPSingle(f) {
// @ts-ignore
return f.action === '$last' || f.action === '$append';
}
exports.isFOPSingle = isFOPSingle;
function isFOPNth(f) {
// @ts-ignore
return f.action === '[n]';
}
exports.isFOPNth = isFOPNth;
function isFOPPath(f) {
// @ts-ignore
return f.action === '[path]';
}
exports.isFOPPath = isFOPPath;
function prism(getOption, reverseGet, description) {
return new Prism(getOption, reverseGet, description ? description : "prism");
}
exports.prism = prism;
function dirtyPrism(getOption, reverseGet, description) {
return new DirtyPrism(getOption, reverseGet, description ? description : "prism");
}
exports.dirtyPrism = dirtyPrism;
class DirtyPrism extends Optional {
constructor(getOption, reverseGet, description) {
super(getOption, (m, c) => reverseGet(c), description);
this.reverseGet = reverseGet;
}
toString() {
return "DirtyPrism(" + this.description + ")";
}
}
exports.DirtyPrism = DirtyPrism;
class Prism extends DirtyPrism {
constructor(getOption, reverseGet, description) {
super(getOption, reverseGet, description);
}
toString() {
return "Prims(" + this.description + ")";
}
}
exports.Prism = Prism;
function iso(get, reverseGet, description) {
return new Iso(get, reverseGet, description ? description : "iso");
}
exports.iso = iso;
class Iso extends Lens {
constructor(get, reverseGet, description) {
super(get, (m, c) => reverseGet(c), description);
this.reverseGet = reverseGet;
this.optional = new Optional(get, (s, c) => reverseGet(c));
}
toString() {
return "Iso(" + this.description + ")";
}
}
exports.Iso = Iso;
/** This 'changes' two parts of Main simultaneously.
*
* @param lens1 This is focused in on a part of main that we want to change
* @param lens2 This is focused in on a second part of main that we want to change
* @param fn1 Given the old values that lens1 and lens2 are focused on, this gives us a new value for the part of main that lens1 is focused on
* @param fn2 Given the old values that lens1 and lens2 are focused on, this gives us a new value for the part of main that lens2 is focused on
* @returns a function that given a Main will return a new main with the two functions used to modify the parts of Main that the two lens are focused in on
*
*/
const transformTwoValues = (lens1, lens2) => (fn1, fn2) => (main) => {
let c1 = lens1.getOption(main);
let c2 = lens2.getOption(main);
return c1 && c2 ? lens1.set(lens2.set(main, fn2(c1, c2)), fn1(c1, c2)) : main;
};
exports.transformTwoValues = transformTwoValues;
/** This 'changes' two parts of Main simultaneously.
*
* @param lens1 This is focused in on a part of main that we want to change
* @param lens2 This is focused in on a second part of main that we want to change
* @param main A value that is to be 'changed' by the method. Changed means that we will make a copy of it with changes
* @param c1 The new value for the part that lens1 is focused on
* @param c2 The new value for the part that lens2 is focused on
* @returns a new main with the parts the two lens are focused on changed by the new values
*
*/
const updateTwoValues = (lens1, lens2) => (main, c1, c2) => lens1.set(lens2.set(main, c2), c1);
exports.updateTwoValues = updateTwoValues;
/** This 'changes' three parts of Main simultaneously.
*
* @param lens1 This is focused in on a part of main that we want to change
* @param lens2 This is focused in on a second part of main that we want to change
* @param lens3 This is focused in on a third part of main that we want to change
* @param main A value that is to be 'changed' by the method. Changed means that we will make a copy of it with changes
* @param c1 The new value for the part that lens1 is focused on
* @param c2 The new value for the part that lens2 is focused on
* @param c3 The new value for the part that lens3 is focused on
* @returns a new main with the parts the three lens are focused on changed by the new values
*
*/
const updateThreeValues = (lens1, lens2, lens3) => (main, c1, c2, c3) => lens1.set(lens2.set(lens3.set(main, c3), c2), c1);
exports.updateThreeValues = updateThreeValues;
function nthItem(n) {
return new Optional(t => t[n], (arr, t) => {
let result = [...arr];
result[n] = t;
return result;
}, "nth(" + n + ")");
}
exports.nthItem = nthItem;
function firstIn2() {
return new Optional(arr => arr[0], (arr, t1) => [t1, arr[1]], "firstIn2");
}
exports.firstIn2 = firstIn2;
function secondIn2() {
return new Optional(arr => arr[1], (arr, t2) => [arr[0], t2], "secondIn2");
}
exports.secondIn2 = secondIn2;
function displayTransformsInState(main, txs) {
return txs.map(([l, tx]) => ({ opt: l.description, value: tx(l.getOption(main)) }));
}
exports.displayTransformsInState = displayTransformsInState;
function massTransform(main, ...transforms) {
return transforms.reduce((acc, [o, fn]) => {
try {
let result = o.setOption(acc, fn(o.getOption(acc)));
if (result === undefined)
throw new Error(`Cannot transform ${o.description}`);
return result;
}
catch (e) {
console.error(`Error in massTransform with ${o.description}`, o, fn, acc);
throw e;
}
}, main);
}
exports.massTransform = massTransform;
function nameLensFn(lens) {
return (name) => lens.focusQuery(name);
}
exports.nameLensFn = nameLensFn;
function asGetNameFn(nl) {
return name => {
const l = nl[name];
if (l === undefined)
throw new Error(`Cannot find lens for name ${name}`);
return l;
};
}
exports.asGetNameFn = asGetNameFn;
;