diff --git a/README.md b/README.md index c2fba68..578c2aa 100644 --- a/README.md +++ b/README.md @@ -31,9 +31,10 @@

-**typescript-monads** helps you write safer code by using abstractions over dubious program state and control flow. +**typescript-monads** helps you write safer code by using abstractions over messy control flow and state. -# Getting Started +# Installation +You can use this library in the browser, node, or a bundler ## Node or as a module ```bash @@ -45,7 +46,7 @@ npm install typescript-monads - + ``` @@ -54,7 +55,7 @@ var someRemoteValue; typescriptMonads.maybe(someRemoteValue).tapSome(console.log) ``` -# Usage +# Example Usage * [Maybe](#maybe) * [Either](#either) @@ -92,7 +93,6 @@ maybe(process.env.DB_URL) }) ``` - # Either TODO diff --git a/src/interfaces/maybe.interface.ts b/src/interfaces/maybe.interface.ts index 5d0c034..fb726f5 100644 --- a/src/interfaces/maybe.interface.ts +++ b/src/interfaces/maybe.interface.ts @@ -1,4 +1,4 @@ -import { IMonad } from "./monad.interface" +import { IMonad } from './monad.interface' /** * Define a contract to unwrap Maybe object diff --git a/src/interfaces/result.interface.ts b/src/interfaces/result.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/monads/either.ts b/src/monads/either.ts index 955f223..a880c1e 100644 --- a/src/monads/either.ts +++ b/src/monads/either.ts @@ -1,4 +1,4 @@ -import { IEither, IEitherPattern } from "../interfaces" +import { IEither, IEitherPattern } from '../interfaces' const exists = (t: T) => t !== null && t !== undefined const bothExist = (left?: L) => (right?: R) => exists(left) && exists(right) diff --git a/src/monads/index.ts b/src/monads/index.ts index 975cd4a..9f55089 100644 --- a/src/monads/index.ts +++ b/src/monads/index.ts @@ -1,4 +1,5 @@ -export { monad } from './monad' -export { maybe } from './maybe' -export { either } from './either' -export { reader } from './reader' \ No newline at end of file +export * from './monad' +export * from './maybe' +export * from './either' +export * from './reader' +export * from './result' \ No newline at end of file diff --git a/src/monads/maybe.ts b/src/monads/maybe.ts index afa58f9..16741eb 100644 --- a/src/monads/maybe.ts +++ b/src/monads/maybe.ts @@ -1,4 +1,4 @@ -import { IMaybe, IMaybePattern } from "../interfaces" +import { IMaybe, IMaybePattern } from '../interfaces' const isEmpty = (value: T) => value === null || value === undefined const isNotEmpty = (value: T) => !isEmpty(value) diff --git a/src/monads/monad.ts b/src/monads/monad.ts index 38829cc..0515304 100644 --- a/src/monads/monad.ts +++ b/src/monads/monad.ts @@ -1,4 +1,4 @@ -import { mapping, IMonad } from "../interfaces" +import { mapping, IMonad } from '../interfaces' // tslint:disable:readonly-array export const monad = (x: T, ...args: any[]): IMonad => { diff --git a/src/monads/reader.ts b/src/monads/reader.ts index 3f64374..f0cfea7 100644 --- a/src/monads/reader.ts +++ b/src/monads/reader.ts @@ -1,4 +1,4 @@ -import { IReader } from "../interfaces" +import { IReader } from '../interfaces' // tslint:disable:no-this export const reader = (fn: (config: E) => A): IReader => { diff --git a/src/monads/result.ts b/src/monads/result.ts new file mode 100644 index 0000000..ea70ee3 --- /dev/null +++ b/src/monads/result.ts @@ -0,0 +1,97 @@ +import { IMaybe } from '../interfaces' +import { maybe } from './maybe' + +const returnTrue = () => true +const returnFalse = () => false +const returnValue = (val: T) => () => val +const returnMaybe = (val: T) => () => maybe(val) +const throwReferenceError = (message: string) => () => { throw new ReferenceError(message) } + +type Predicate = () => boolean + +export interface IResultMatchPattern { + readonly ok: (val: T) => U + readonly fail: (val: E) => U +} + +export interface IResult { + isOk(): boolean + isFail(): boolean + maybeOk(): IMaybe + maybeFail(): IMaybe + unwrap(): T | never + unwrapOr(opt: T): T + unwrapFail(): E | never + match(fn: IResultMatchPattern): M + map(fn: (val: T) => M): IResult + mapFail(fn: (err: E) => M): IResult + flatMap(fn: (val: T) => IResult): IResult +} + +export interface IResultOk extends IResult { + unwrap(): T + unwrapOr(opt: T): T + unwrapFail(): never + match(fn: IResultMatchPattern): M + map(fn: (val: T) => M): IResultOk + mapFail(fn: (err: E) => M): IResultOk +} + +export interface IResultFail extends IResult { + unwrap(): never + unwrapOr(opt: T): T + unwrapFail(): E + match(fn: IResultMatchPattern): M + map(fn: (val: T) => M): IResultFail + mapFail(fn: (err: E) => M): IResultFail + flatMap(fn: (val: T) => IResult): IResultFail +} + +export const ok = (val: T): IResultOk => { + return { + isOk: returnTrue, + isFail: returnFalse, + maybeOk: returnMaybe(val), + maybeFail: maybe, + unwrap: returnValue(val), + unwrapOr: _ => val, + unwrapFail: throwReferenceError('Cannot unwrap a success'), + map: (fn: (val: T) => M) => ok(fn(val)), + mapFail: (_: (err: E) => M) => ok(val), + flatMap: (fn: (val: T) => IResult) => fn(val), + match: (fn: IResultMatchPattern) => fn.ok(val) + } +} + +export const fail = (err: E): IResultFail => { + return { + isOk: returnFalse, + isFail: returnTrue, + maybeOk: maybe, + maybeFail: returnMaybe(err), + unwrap: throwReferenceError('Cannot unwrap a failure'), + unwrapOr: opt => opt, + unwrapFail: returnValue(err), + map: (_: (val: T) => M) => fail(err), + mapFail: (fn: (err: E) => M) => fail(fn(err)), + flatMap: (_: (val: T) => IResult) => fail(err), + match: (fn: IResultMatchPattern) => fn.fail(err) + } +} + +/** + * Utility function to quickly create ok/fail pairs. + */ +export const result = (predicate: Predicate, okValue: T, failValue: E): IResult => + predicate() + ? ok(okValue) + : fail(failValue) + +/** +* Utility function to quickly create ok/fail pairs, curried variant. +*/ +export const curriedResult = + (predicate: Predicate) => + (okValue: T) => + (failValue: E): IResult => + result(predicate, okValue, failValue) diff --git a/src/util/maybe-env.ts b/src/util/maybe-env.ts index 5f634f7..a1c22e2 100644 --- a/src/util/maybe-env.ts +++ b/src/util/maybe-env.ts @@ -1,4 +1,4 @@ -import { reader, maybe } from ".." +import { reader, maybe } from '..' export interface GetFromEnvironmentReader { readEnv(key: string): string | undefined diff --git a/test/interfaces/index.spec.ts b/test/interfaces/index.spec.ts index 2a9c05e..2e26f43 100644 --- a/test/interfaces/index.spec.ts +++ b/test/interfaces/index.spec.ts @@ -1,8 +1,7 @@ import '../../src/interfaces' -describe('', () => { - it('should', () => { +describe('Interfaces', () => { + it('should cover interfaces', () => { // stubbed just to get coverage up from interfaces - // which are not used }) }) \ No newline at end of file diff --git a/test/monads/result.spec.ts b/test/monads/result.spec.ts new file mode 100644 index 0000000..199e663 --- /dev/null +++ b/test/monads/result.spec.ts @@ -0,0 +1,164 @@ +import { ok, fail, result, curriedResult } from '../../src/monads' + +describe('result', () => { + describe('ok', () => { + it('should return true when "isOk" invoked on a success path', () => { + expect(ok(1).isOk()).toEqual(true) + }) + + it('should return false when "isFail" invoked on a success path', () => { + expect(ok(1).isFail()).toEqual(false) + }) + + it('should unwrap', () => { + expect(ok(1).unwrap()).toEqual(1) + expect(ok('Test').unwrap()).toEqual('Test') + }) + + it('should return proper value when "unwrapOr" is applied', () => { + expect(ok(1).unwrapOr(25)).toEqual(1) + expect(ok('Test').unwrapOr('Some Other')).toEqual('Test') + }) + + it('should throw an exception whe "unwrapOrFail" called on an ok value', () => { + expect(() => { + ok(1).unwrapFail() + }).toThrowError() + }) + + it('should ...', () => { + const _sut = ok('Test') + .maybeOk() + .valueOr('Some Other') + + expect(_sut).toEqual('Test') + }) + + it('should ...', () => { + const _sut = ok('Test') + .maybeFail() + .valueOrUndefined() + + expect(_sut).toEqual(undefined) + }) + + it('should map function', () => { + const sut = ok(1) + .map(b => b.toString()) + .unwrap() + expect(sut).toEqual('1') + }) + + it('should not mapFail', () => { + const sut = ok(1) + .mapFail(b => '') + .unwrap() + expect(sut).toEqual(1) + }) + + it('should flatMap', () => { + const sut = ok(1) + .flatMap(a => ok(a.toString())) + .unwrap() + + expect(sut).toEqual('1') + }) + + it('should match', () => { + const sut = ok(1) + .match({ + fail: _ => 2, + ok: val => val + }) + + expect(sut).toEqual(1) + }) + }) + + describe('fail', () => { + it('should return false when "isOk" invoked', () => { + expect(fail(1).isOk()).toEqual(false) + }) + + it('should return true when "isFail" invoked', () => { + expect(fail(1).isFail()).toEqual(true) + }) + + it('should return empty maybe when "maybeOk" is invoked', () => { + const _sut = fail('Test') + .maybeOk() + .valueOr('Some Other1') + + expect(_sut).toEqual('Some Other1') + }) + + it('should return fail object when "maybeFail" is invoked', () => { + const _sut = fail('Test') + .maybeFail() + .valueOr('Some Other2') + + expect(_sut).toEqual('Test') + }) + + it('should throw an exception on "unwrap"', () => { + expect(() => { fail(1).unwrap() }).toThrowError() + }) + + it('should return fail object on "unwrapFail"', () => { + expect(fail('123').unwrapFail()).toEqual('123') + }) + + it('should return input object on "unwrapOr"', () => { + expect(fail('123').unwrapOr('456')).toEqual('456') + }) + + it('should not map', () => { + const sut = fail(1) + .map(b => b.toString()) + .unwrapFail() + expect(sut).toEqual(1) + }) + + it('should mapFail', () => { + const sut = fail(1) + .mapFail(b => b.toString()) + .unwrapFail() + expect(sut).toEqual('1') + }) + + it('should not flatMap', () => { + const sut = fail(1) + .flatMap(a => ok(a.toString())) + .unwrapFail() + + expect(sut).toEqual(1) + }) + + it('should match', () => { + const sut = fail(1) + .match({ + fail: _ => 2, + ok: val => val + }) + + expect(sut).toEqual(2) + }) + }) + + describe('result', () => { + it('should return failure when predicate yields false', () => { + const sut = result(() => 1 + 1 === 3, true, 'FAILURE!') + expect(sut.isFail()).toEqual(true) + }) + + it('should return ok when predicate yields true', () => { + const sut = result(() => 1 + 1 === 2, true, 'FAILURE!') + expect(sut.isOk()).toEqual(true) + }) + + it('should return curried', () => { + const sut = curriedResult(() => 1 + 1 === 2)(true)('FAILURE!') + expect(sut.isOk()).toEqual(true) + }) + }) +}) diff --git a/tslint.json b/tslint.json index ecf3f72..18a8d24 100644 --- a/tslint.json +++ b/tslint.json @@ -25,6 +25,7 @@ "no-this": true, "no-class": true, "no-expression-statement": false, - "no-if-statement": true + "no-if-statement": true, + "quotemark": [true, "single"] } } \ No newline at end of file