Skip to content

Commit

Permalink
chore: Finalise the initial implementation
Browse files Browse the repository at this point in the history
Whoop! This is an implementation of the core functionality, basic
documentation, and some thorough type level tests to lock down the
packages base level behaviour early on.
  • Loading branch information
iainjreid committed Jun 23, 2022
1 parent 987dda6 commit 7e471cd
Show file tree
Hide file tree
Showing 12 changed files with 369 additions and 206 deletions.
34 changes: 34 additions & 0 deletions .github/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Compose

Welcome to the source repository of Compose, our functional composition library.
If you're looking to contribute, raise an issue, or explore the code, you're in
the right place!

> Not what you were looking for? Head over to the [package page][readme] for
> users documentation, where you'll find usage information and examples instead.
[readme]: https://github.com/emphori/compose/blob/HEAD/README.md


## Contributing

We welcome contributions of any size from anyone. Please do take a moment to
read our [contribution guidelines][contributing] to familiarise yourself with
our process.

[contributing]: https://github.com/emphori/.github/blob/HEAD/CONTRIBUTING.md


## Issues

If you've found a bug in the code, log it! Raise an appropriate ticket here in
GitHub and include as _much_ useful information as you possibly can.

Once triaged you should expect a reply within a few working days.


## License

This project is released under the [MIT License][license]. Enjoy responsibly ❤️

[license]: https://github.com/emphori/compose/blob/HEAD/LICENSE
71 changes: 51 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,71 @@
A drop in extension for Promises, adding oodles of functional goodness through
composition and taking error handling to an entirely different level.

## Installing

## Features

* **Lightweight and with almost no memory footprint,** the main package export
comes in at well below 1KB, unminified, ungzipped.

* **A familiar Promise like interface** reduces the learning curve
dramatically.

* **Robust types for TypeScript** are included in the package by default, with
specific attention in areas such as scope narrowing, for heavily nested and
complex compositions.

* **Interoperable with existing code by design,** to ensure that it's easy to
introduce incrementally to your project without any pesky migrations.


## Installation

Compose is distributed to both NPM and GitHub Packages. Whichever registry you
prefer to use, the installation instructions should remain the same.

```sh
# Using NPM
npm install @emphori/compose -S

# Using Yarn
# Or, using Yarn
yarn add @emphori/compose
```

## Getting started

```sh
# Install the dependencies
npm ci
```
## Examples

## Building
```ts
import { compose } from '@emphori/compose'

```sh
# Build the package
npm run build
// (userId: string) => Promise<Org, UserNotFound | OrgNotFound>
const getUserOrg = compose(getUser).then(getOrgForUser)

function getUser(userId: string): Promise<User, UserNotFound> {
return User.getById(userId).then((user) => {
return user ?? Promise.reject(UserNotFound)
})
}

# Or, watch for changes
npm run build -- -w
function getOrgForUser(user: User): Promise<Org, OrgNotFound> {
return Org.getById(user.orgId).then((org) => {
return org ?? Promise.reject(OrgNotFound)
})
}
```

## Testing

```sh
# Run the tests
npm test
```
## Contributing

If you're interested in contributing, or just want to learn more about Compose,
then head over to the [repository][repo] where you'll hopefully find all the
information you need.

[repo]: https://github.com/emphori/compose


## Licence

This project is released under the [MIT License][license]. Enjoy responsibly ❤️

## License
[license]: https://github.com/emphori/compose/blob/HEAD/LICENSE

[The MIT License](./LICENSE)
41 changes: 41 additions & 0 deletions lib/compose.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export declare type ComposableTarget<C1, I1 extends any[], R1, E1> =
CallableFunction & ((this: C1, ...args: I1) => Promise<R1, E1>);

export type UnreachableFunctionWarning = "Your function will never be run";

export declare type ComposableSafeFunction<C1, R1, R2> =
[R1] extends [never] ? UnreachableFunctionWarning : ((this: C1, val: R1) => R2 | Promise<R2, never>)

export declare type ComposableUnsafeFunction<C1, R1, R2, E1 = unknown> =
[R1] extends [never] ? UnreachableFunctionWarning : ((this: C1, val: R1) => Promise<R2, E1>)

export interface SafeComposable<C1, I1 extends any[], R1> extends ComposableTarget<C1, I1, R1, never> {
then<R2, E1, C2 extends C1 = C1>(fn: ComposableUnsafeFunction<C2, R1, R2, E1>):
[E1] extends [never] ? never : UnsafeComposable<C2, I1, R2, E1>;

then<R2, __, C2 extends C1 = C1>(fn: ComposableSafeFunction<C2, R1, R2>):
SafeComposable<C2, I1, R2>;

catch: never;
}

export interface UnsafeComposable<C1, I1 extends any[], R1, E1> extends ComposableTarget<C1, I1, R1, never> {
__ErrorTypeCheck__: [E1] extends [never] ? 'Please use a "SafeComposable"' : E1;

then<R2, E2, C2 extends C1 = C1>(fn: ComposableUnsafeFunction<C2, R1, R2, E2>):
UnsafeComposable<C2, I1, R2, E1 | E2>;

then<R2, __, C2 extends C1 = C1>(fn: ComposableSafeFunction<C2, R1, R2>):
UnsafeComposable<C2, I1, R2, E1>;

catch<E2, C2 extends C1 = C1>(fn: ComposableUnsafeFunction<C2, E1, R1, E2>):
[E2] extends [never] ? never : UnsafeComposable<C2, I1, R1, E2>;

catch<__, C2 extends C1 = C1>(fn: ComposableSafeFunction<C2, E1, R1>):
SafeComposable<C2, I1, R1>;
}

export declare function compose<C1, I1 extends any[], V1, E1 = never>(fn: ComposableTarget<C1, I1, V1, E1>):
[E1] extends [never]
? SafeComposable<C1, I1, V1>
: UnsafeComposable<C1, I1, V1, E1>;
29 changes: 29 additions & 0 deletions lib/compose.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// @ts-nocheck
"use strict";

/**
* A composition factory that chains Promises together in a functional manner.
*
* @param fn - The function to compose
*/
function compose(fn) {
return Object.setPrototypeOf(function (...args) {
return fn.apply(this, args);
}, compose);
}

compose.then = function (fn) {
const run = this;
return compose(function (...args) {
return run.apply(this, args).then((val) => fn.call(this, val));
});
};

compose.catch = function (fn) {
const run = this;
return compose(function (...args) {
return run.apply(this, args).catch((val) => fn.call(this, val));
});
}

exports.compose = compose;
193 changes: 193 additions & 0 deletions lib/compose.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
// @ts-check
"use strict";

/**
* @typedef {import('./compose').SafeComposable<C1, R1, R2>} SafeComposable
* @template C1, R1, R2
*/

/**
* @typedef {import('./compose').UnsafeComposable<C1, R1, R2, E1>} UnsafeComposable
* @template C1, R1, R2, E1
*/

/**
* @typedef {import('./compose').UnreachableFunctionWarning} UnreachableFunctionWarning
*/

/**
* @typedef {import('./compose').ComposableSafeFunction<C1, R1, R2>} ComposableSafeFunction
* @template C1, R1, R2
*/

/**
* @typedef {import('./compose').ComposableUnsafeFunction<C1, R1, R2, E1>} ComposableUnsafeFunction
* @template C1, R1, R2, E1
*/

const { compose } = require('./compose');

/**
* Maintaining safe compositions.
*/
{
/** @type {SafeComposable<any, [string, string], string>} */
const composition1 = compose(safeTarget);

/** @type {SafeComposable<any, [string, string], [string]>} */
const composition2 = composition1.then(safeWrapArray);

/** @type {SafeComposable<any, [string, string], string>} */
const composition3 = composition2.then(safeUnwrapArray);

/** @type {SafeComposable<any, [string, string], number>} */
const composition4 = composition3.then(safeStringLength);
}

/**
* Adding errors to originally safe compositions.
*/
{
/** @type {SafeComposable<any, [string, string], string>} */
const composition1 = compose(safeTarget);

/** @type {SafeComposable<any, [string, string], number>} */
const composition2 = composition1.then(safeStringLength);

/** @type {UnsafeComposable<any, [string, string], number, string>} */
const composition3 = composition2.then(unsafeGeneric);
}

/**
* Keeping track of errors in originally unsafe compositions.
*/
{
/** @type {UnsafeComposable<any, [string, string], string, string>} */
const composition1 = compose(unsafeTarget);

/** @type {UnsafeComposable<any, [string, string], [string], string>} */
const composition2 = composition1.then(safeWrapArray);

/** @type {UnsafeComposable<any, [string, string], string, string>} */
const composition3 = composition2.then(safeUnwrapArray);

/** @type {UnsafeComposable<any, [string, string], number, string>} */
const composition4 = composition3.then(safeStringLength);
}

/**
* Discarding errors in originally unsafe compositions.
*/
{
/** @type {UnsafeComposable<any, [string, string], string, string>} */
const composition1 = compose(unsafeTarget);

/** @type {UnsafeComposable<any, [string, string], number, string>} */
const composition2 = composition1.then(safeStringLength);

/** @type {SafeComposable<any, [string, string], number>} */
const composition3 = composition2.catch(resolveErrors);
}

/**
* Unreachable error path compositions.
*
* The below tests confirm that compositions that will never fail are properly
* typed.
*/
{
/** @type {SafeComposable<any, [string, string], string>} */
const composition1 = compose(safeTarget);

/** @type {(fn: ComposableSafeFunction<any, string, any>) => any} */
const _ = composition1.then

/** @type {(fn: UnreachableFunctionWarning) => any} */
const __ = composition1.catch
}

/**
* Unreachable happy path compositions.
*
* Although this sort of composition is highly undesireable, the below tests
* ensure that "fail only" compositions are possible.
*/
{
/** @type {UnsafeComposable<any, [string, string], never, string>} */
const composition1 = compose(brokenTarget);

/** @type {(fn: UnreachableFunctionWarning) => any} */
const _ = composition1.then

/** @type {(fn: ComposableUnsafeFunction<any, string, never, any>) => any} */
const __ = composition1.catch
}

/**
* @param {string} input1
* @param {string} input2
* @returns {Promise<string, never>}
*/
function safeTarget(input1, input2) {
return Promise.resolve(input1 + input2);
}

/**
* @param {string} input1
* @param {string} input2
* @returns {Promise<string, string>}
*/
function unsafeTarget(input1, input2) {
return Promise.resolve(input1 + input2);
}

/**
* @param {string} input1
* @param {string} input2
* @returns {Promise<never, string>}
*/
function brokenTarget(input1, input2) {
return Promise.reject(input1 + input2);
}

/**
* @param {string} input
* @returns {Promise<[string], never>}
*/
function safeWrapArray(input) {
return Promise.resolve([input]);
}

/**
* @param {[string]} input
* @returns {Promise<string, never>}
*/
function safeUnwrapArray([input]) {
return Promise.resolve(input);
}

/**
* @param {string} input
* @returns {Promise<number, never>}
*/
function safeStringLength(input) {
return Promise.resolve(input.length);
}

/**
* @template T
* @param {T} input
* @returns {Promise<T, string>}
*/
function unsafeGeneric(input) {
return Promise.resolve(input);
}

/**
* @template T
* @returns {Promise<T, never>}
*/
function resolveErrors() {
// @ts-ignore
return Promise.resolve();
}
Loading

0 comments on commit 7e471cd

Please sign in to comment.