This is a small library that allows you to define your calls as a testable list of steps that map your abstract data to the specific logic associated with that data.
I.e.: Define a flow for your data in small composable parts and combine those parts to solve the bigger problems.
So you can easily compose and test them
Suppose you want to write a function that queries a database based on some input and transform the results in order to send them to another system.
Normally you would create something to mock the server or even test against a local full-blown instance of the database. But why is that? You don't want to test the database, do you? The provider of the database should have already tested that.
What you want to test is:
- whether the correct query is being used and whether the result is transformed the way you expect it.
- how the function behaves if the query returns something unexpected
For that you'd have to write and export a function that takes your arguments and returns a query, and another function that handles the results of the query. Sure, you could do that, but why export functions that have no use otherwise? That are only relevant in the context of this one call?
Enter, the future(-fun): with this library you can define a "Call" that does exactly the same with the added benefit that you can structure it into defined "steps" and test each of those steps individually.
Not only does this make every important step of your "function" testable, you can also create some sort of pipelines and combine several pipelines.
For example, in a typical mongodb setup, you could define a call that returns a Database based on some configuration. And then, based on that call, another one that returns all collections. Then, from that, a specific collection. And all of these calls have the same "root" argument, namely the configuration of the database, a specific collection is, after all, nothing more than the configuration for the database, enriched with its name.
Think of this package as a way to wrap your logic into a list of actions, where each action has a defined (typesafe) input and a defined (typesafe) output. The resulting list gives you access to each action and lets you test this action independently by providing it with fake input. This way you can simulate how the action will behave even though you don't actually have the prerequisites.
Create a new ICallMonad
from scratch a.k.a. lift/of
Call.of: <In, Out>(fn: (arg?: In) => Out, thisArg?: any) => ICallMonad<Out, In>
fn
- any function that takes zero or one arguments of typeIn
and produces some output of typeOut
thisArg?
- an optional argument on whichfn
will be called. Necessary if you want to pass a function from a certain object i.e.Promise.resolve
needsPromise
asthisArg
An ICallMonad<Out, In>
representing the fn
as well as the context that enables you to .pipe
your fn
into other functions
const c = Call.of((x: number) => Promise.resolve(x))
// executing
const number$ = c(1)
// testing
const testResult = testCall(c, 1)
assert(testResult instanceof Promise)
testResult.then(num => assert(num === 1))
aggregate any number of calls into a single call
Call.all: (...calls: ICallMonad[]): ICallMonad<any[], any[]>
...calls
- theICallMonad
s to aggregate
A new ICallMonad
that takes an array of all the arguments of the passed calls as its argument and returns all their results
const c = Call.all(Call.of((x: number) => x), Call.of((y: string) => Promise.resolve(y)))
// executing
const [num, str$] = c([1, 'a'])
// testing
const [num, str$] = testCall(c, [1, 'a'])
assert(num === 1)
assert(str$ instanceof Promise)
str$.then(str => assert(str === 'a'))
pipe from one call to another
call.pipe: <I extends ICallMonad, O1, M1 extends Morphism> (this: I, op1: IOperator<OutOf<I>, O1, M1>): IPipedCallMonad<O1, I>
this
- the instance of anICallMonad
thatpipe
is executed onop1 - op5
- the operators used to transformthis
call
An IPipedCallMonad<O1, I>
where I
is the instance of the ICallMonad<Out, In>
that .pipe
was called on. This means that the resulting IPipedCallMonad
takes the result of the previous ICallMonad
as its argument.
const c = Call.of((x: number) => x * 2)
.pipe(map(x => x + ''))
// executing
const stringified = c(1)
// testing
assert(testCall(c.previous, 1) === 2)
assert(testCall(c, 1) === '1')
It is important to note that with each .pipe
you create a new ICallMonad
that has a link to the previous step under previous
. That is why testCall(c, 1)
does not double the number but only stringifies it
Operators are at the heart of this library and are basically functions that take any instance of an ICallMonad
and transform the instance into another ICallMonad
. This can happen based on any morphism you want to implement or statically for things you need really often.
future-fun ships with three basic operators and two utility functions to create them
Operators only transform calls, they do not keep track of the "previous step".
map from one value to another
function map<From, To> (morphism: UnaryFunction<From, To>): IOperator<From, To, UnaryFunction<From, To>>
morphism
- the function that transforms the output of the previousICallMonad
to a new value
An IOperator
that transforms the call it is applied on so that it changes the output type of the resulting call to the result of the morphism
const c = Call.of(parseInt)
const double = map((x: number) => x * 2)
// executing
const parseAndDouble = double(c)
// testing
assert(double.morphism(1) === 2)
assert(testCall(parseAndDouble, '1') === 2)
map from one value to the result of another ICallMonad
function flatMap<From, To> (morphism: UnaryFunction<From, ICallMonad<To, any, From>>): IOperator<From, To, typeof morphism>
morphism
- a function that takes the result of the previousICallMonad
and returns anotherICallMonad
that takes the same type as its argument
An IOperator
that transforms the call it is applied on so that it changes the output type of the resulting call to the result of the ICallMonad
returned from the morphism
const identity = Call.of((x: number) => x)
const stringify = Call.of((x: number) => x + '')
const conditional = flatMap((x: number) => x > 9999 ? stringify : identity)
// executing
const stringifyLarge = conditional(identity)
// testing
assert(conditional.morphism(10000) === stringify)
assert(conditional.morphism(1) === identity)
map the value that a promise will resolve with to another value
function mapPromise<From, To> (morphism: UnaryFunction<From, To | Promise<To>>): IOperator<Promise<From>, Promise<To>, UnaryFunction<From, To | Promise<To>>>
morphism
- a function that receives the value the promise is going to resolve with and maps it to another value
An IOperator
that transforms the call it is applied on so that it changes the resolved value of the promise returned from the call to a promise that resolves with another value
const doublePromise = mapPromise((x: number) => x * 2)
const identity$ = Call.of((x: number) => Promise.resolve(x))
// executing
const double$ = doublePromise(identity$)
// testing
assert(doublePromise.morphism(1) === 2)
testCall(double$, Promise.resolve(1)).then(num => assert(num === 2))
map to the result of a nested ICallMonad
const double = Call.of((x: number) => x).pipe(flatMapTo(Call.of((x: number) => x * 2)))
// executing
assert(double(1) === 2)
This is technically just an alias to map
. Works because ICallMonads
are functions themselves.
put multiple operators together into one
function(...operators: IOperator<any, any, any>[]): IOperator<any, any, any>
operators
- any number ofIOperator
s
A new IOperator
that simply combines all the morphisms into one and can transform any call by putting it through all the passed operators.
const doubleAndIncrement = aggregate(map((x: number) => x * 2), map((x: number) => x + 1))
const identity = Call.of((x: number) => x)
// executing
const doubleInc = doubleAndIncrement(identity)
// testing
assert(doubleAndIncrement.morphism(1) === 3)
create a new custom operator
function createOperator<In, Out> (morphism: UnaryFunction<In, Out> | NullaryFunction<Out>, transform: (result: In) => Out): IOperator<In, Out, typeof morphism>
morphism
- aUnaryFunction
orNullaryFunction
that defines any mapping from zero or one arguments to any outputtransform
- the logic for transforming the result of the call the operator is applied on.
A new IOperator
that transforms a call that returns In
into a call that returns Out
// actual implementation of `mapPromise` operator
const mapPromise = createOperator(morphism, result => result.then(morphism))
See demo.spec.ts (WIP)