-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: Finalise the initial implementation
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
Showing
12 changed files
with
369 additions
and
206 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} |
Oops, something went wrong.