UNPKG

@focuson/lens

Version:

A simple implementation of lens using type script

538 lines (536 loc) 24.2 kB
"use strict"; 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;