diff --git a/change/@fluentui-codemods-2020-07-21-17-13-11-either.json b/change/@fluentui-codemods-2020-07-21-17-13-11-either.json new file mode 100644 index 0000000000000..04558b734b642 --- /dev/null +++ b/change/@fluentui-codemods-2020-07-21-17-13-11-either.json @@ -0,0 +1,8 @@ +{ + "type": "patch", + "comment": "Result: add in an result helper to assit in the creation of codemods", + "packageName": "@fluentui/codemods", + "email": "joschect@microsoft.com", + "dependentChangeType": "patch", + "date": "2020-07-22T00:13:11.108Z" +} diff --git a/packages/codemods/src/helpers/chainable.ts b/packages/codemods/src/helpers/chainable.ts index 4bf3474e9abbf..cacf9d59d1c41 100644 --- a/packages/codemods/src/helpers/chainable.ts +++ b/packages/codemods/src/helpers/chainable.ts @@ -1,8 +1,26 @@ -import { Mappable } from './mappable'; - export type Flattened = T extends Chainable ? T : V; -export interface Chainable extends Mappable { +export interface Chainable { + /** + * If this Chainables's T is an instance of Chainable, + * then it will flatten it so now Chainable now contains + * a value of whatever type T held + * Chainable> => Chainable + */ flatten: () => Flattened>; + + /** + * This takes in a lambda which maps from the current chainable + * type to the same type of chainable with a new type R. The entire function then returns + * a new Chainable with type R + * @param fn: A lambda that maps from a type T and returns a new Chainable that has type R. + */ chain: (fn: (v: T) => Chainable) => Chainable; + + /** + * A combination of chain, map, and flatten. Then can return either type R or + * Chainable and this value will get flattened appropriately so the + * returned Chainable is not nested. + * @param fn: A lambda that maps from a type T and returns a new type R. + */ then: (fn: (v: T) => R) => Chainable; } diff --git a/packages/codemods/src/helpers/mappable.ts b/packages/codemods/src/helpers/mappable.ts deleted file mode 100644 index e90a1e5d85636..0000000000000 --- a/packages/codemods/src/helpers/mappable.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface Mappable { - fmap: (fn: (v: F) => T) => Mappable; -} diff --git a/packages/codemods/src/helpers/maybe.ts b/packages/codemods/src/helpers/maybe.ts index 6d35ed00f5fb7..c9d63acb028b3 100644 --- a/packages/codemods/src/helpers/maybe.ts +++ b/packages/codemods/src/helpers/maybe.ts @@ -3,7 +3,6 @@ import { Chainable, Flattened } from './chainable'; interface MaybeChain extends Chainable { something?: boolean; __isMaybe: true; - fmap: (this: Maybe, fn: (v: NonNullable) => NonNullable) => Maybe; chain: (this: Maybe, fn: (v: NonNullable) => Maybe) => Maybe; flatten: () => Flattened>; then: (this: Maybe, fn: (v: NonNullable) => ReturnType | Maybe) => Maybe; @@ -22,10 +21,6 @@ class MB implements MaybeChain { public value: T | undefined; public __isMaybe: true = true; - public fmap(this: Maybe, fn: (v: NonNullable) => NonNullable): Maybe { - return this.something ? Something(fn(this.value)) : Nothing(); - } - public chain(this: Maybe, fn: (v: NonNullable) => Maybe): Maybe { return this.something ? fn(this.value) : Nothing(); } diff --git a/packages/codemods/src/helpers/result.ts b/packages/codemods/src/helpers/result.ts new file mode 100644 index 0000000000000..a570a51a0d812 --- /dev/null +++ b/packages/codemods/src/helpers/result.ts @@ -0,0 +1,119 @@ +import { Chainable, Flattened } from './chainable'; + +export interface Ok extends ResultInternal { + ok: true; + value: R; +} + +export interface Err extends ResultInternal { + ok: false; + value: E; +} + +class ResultInternal implements Chainable { + public ok: boolean; + public value: R | E; + + public constructor(options: { ok: boolean; value: R | E }) { + this.ok = options.ok; + this.value = options.value; + } + + public chain(this: Result, fn: (v: R) => Result): Result { + if (this.ok) { + return fn(this.value); + } + return Err(this.value); + } + + /** + * Works just like chain, but is only called if this Result is an error. + * This returns a new Result with type Result + */ + public errChain(this: Result, fn: (v: E) => Result): Result { + if (!this.ok) { + return fn(this.value); + } + return Ok(this.value); + } + + public flatten(): Flattened> { + if (this.value && this.value instanceof ResultInternal) { + return this.value as Flattened>; + } + return (this as unknown) as Flattened>; + } + + /** + * This allows users to opperate on a presumed result without needing to know whether or not + * the result was successful or not. + * At each call if it is a result type of ok, it will call the supplied function, otherwise it + * will return the current value with a new Err. + * + * @param fnOk Function that takes in an ok value of type R and returns either F or Result + */ + public then(this: Result, fnOk: (v: R) => F | Result): Result { + if (this.ok) { + return Ok(fnOk(this.value)).flatten() as Result; + } + + return Err(this.value); + } + + /** + * Works just like then, but is only called if this Result is an error. + * This returns a new Result with type Result + */ + public errThen(this: Result, fnErr: (v: E) => F | Result): Result { + if (!this.ok) { + return Err(fnErr(this.value)).flatten() as Result; + } + + return Ok(this.value); + } + + public okOrElse(this: Result, okElse: R): R { + if (this.ok) { + return this.value; + } + return okElse; + } + + public errOrElse(this: Result, errElse: E): E { + if (!this.ok) { + return this.value; + } + return errElse; + } + + /** + * Takes in two functions, one which takes type R and the other which takes type E and both return + * type T. This lets you handle either case and potentially return a unifying type. You might use this + * to create a message for users. + * + * @param fnOk Function that maps from Ok value(R) to new type T + * @param fnErr Function that maps from Err value(E) to new type T + */ + public resolve(this: Result, fnOk: (v: R) => T, fnErr: (v: E) => T): T { + if (this.ok) { + return fnOk(this.value); + } + return fnErr(this.value); + } +} + +export const Ok = (value: R): Ok => { + return new ResultInternal({ value: value, ok: true }) as Ok; +}; + +export const Err = (value: E): Err => { + return new ResultInternal({ value: value, ok: false }) as Err; +}; + +/** + * Result is a useful type for when you might want to handle errors down the line without swallowing them + * while still preforming potentially several operations that could result in an error. A simple example could + * be a dividing function that returns a Result. Instead of throwing an error that you cannot divide by zero, + * it would be returned in an Err. This allows the rest of the program to execute cleanly. + */ +export type Result = Err | Ok; diff --git a/packages/codemods/src/helpers/tests/result.test.ts b/packages/codemods/src/helpers/tests/result.test.ts new file mode 100644 index 0000000000000..0dff352b222d1 --- /dev/null +++ b/packages/codemods/src/helpers/tests/result.test.ts @@ -0,0 +1,113 @@ +import { Err, Ok, Result } from '../result'; + +const getOk = (ok: T, err: Z): Result => { + return Ok(ok!); +}; + +const getErr = (okay: T, err: Z): Result => { + return Err(err!); +}; + +describe('Result', () => { + it('chained Okay value is evaluated correctly', () => { + expect( + getOk(3, '4') + .chain(v => Ok(v + 3)) + .okOrElse(100), + ).toBe(6); + }); + + it('errChained Err value is evaluated correctly', () => { + expect( + getErr(3, '4') + .errChain(v => Err(7)) + .errOrElse(100), + ).toBe(7); + }); + + it('chained Err value is evaluated correctly', () => { + expect( + getErr(3, '4') + .chain(v => Ok(1)) + .okOrElse(100), + ).toBe(100); + }); + + it('chained Err value is evaluated correctly', () => { + expect( + getErr(3, '4') + .errChain(v => Ok(1)) + .okOrElse(100), + ).toBe(1); + }); + + it('chain returning a Err returns a Err correctly', () => { + expect(getOk(3, '4').chain(v => Err('Error')).ok).toBe(false); + }); + + it('errChain returning an Ok returns an Ok correctly', () => { + expect(getOk(3, '4').chain(v => Ok('Error')).ok).toBe(true); + }); + + it('Thens correctly on Ok', () => { + expect( + getOk(3, '4') + .then(v => 30) + .then(v => v.toString()) + .okOrElse('Bad'), + ).toBe('30'); + }); + + it('Thens correctly on Err', () => { + expect( + getErr(3, '4') + .then(v => 30) + .then(v => v.toString()) + .okOrElse('Bad'), + ).toBe('Bad'); + }); + + it('orThens correctly on Err', () => { + expect( + getErr(3, '4') + .errThen(v => 30) + .errThen(v => v.toString()) + .errOrElse('Bad'), + ).toBe('30'); + }); + + it('orThens correctly on Ok', () => { + expect( + getOk(3, '4') + .errThen(v => 30) + .errThen(v => v.toString()) + .errOrElse('Bad'), + ).toBe('Bad'); + }); + + it('resolve calls Ok function with Ok object value', () => { + const spyLeft = jest.fn(); + const spyRight = jest.fn(); + getOk(3, '4').resolve(spyRight, spyLeft); + expect(spyRight).toHaveBeenCalled(); + expect(spyRight).toHaveBeenCalledWith(3); + }); + + it('resolve calls Err function with Err object value', () => { + const spyLeft = jest.fn(); + const spyRight = jest.fn(); + getErr(3, '4').resolve(spyRight, spyLeft); + expect(spyLeft).toHaveBeenCalled(); + expect(spyLeft).toHaveBeenCalledWith('4'); + }); + + it('resolve calls Err function with Err object value after then', () => { + const spyLeft = jest.fn(); + const spyRight = jest.fn(); + getErr(3, '4') + .then(v => 10) + .resolve(spyRight, spyLeft); + expect(spyLeft).toHaveBeenCalled(); + expect(spyLeft).toHaveBeenCalledWith('4'); + }); +});