Skip to content

Commit

Permalink
Merge pull request #1 from Takayuki-Y5991/feature/fix-pipeline
Browse files Browse the repository at this point in the history
🍏 fix pipe
  • Loading branch information
Takayuki-Y5991 committed Jul 22, 2024
2 parents 6761309 + a3d4833 commit c4f7dfa
Show file tree
Hide file tree
Showing 24 changed files with 969 additions and 164 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@

node_modules/
dist/
4 changes: 4 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
src
lib
tsconfig.json
181 changes: 180 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,180 @@
# pipeline-ts
# PipeLineTS

PipeLineTS is a minimal dependency-free library for composing pipelines in TypeScript. It allows you to compose both synchronous and asynchronous functions, ensuring type safety throughout the pipeline.

=
## Features

- Type-safe function composition
- Supports both synchronous and asynchronous functions
- Handles success and failure cases with a unified `Result` type

## Installation

You can install this package using pnpm:
```sh
pnpm add my-pipeline-ts
```

## Usage
Define Result Types and Utility Functions

Create a file named `result.ts` with the following content:

```typescript

export class Success<T> {
readonly isSuccess = true;
readonly isFailure = false;

constructor(public readonly value: T) {}
}

export class Failure<E> {
readonly isSuccess = false;
readonly isFailure = true;

constructor(public readonly error: E) {}
}

export type Result<T, E> = Success<T> | Failure<E>;
export type AsyncResult<T, E> = Result<T, E> | Promise<Result<T, E>>;

export const isSuccess = <T, E>(result: Result<T, E>): result is Success<T> =>
result.isSuccess;
export const isFailure = <T, E>(result: Result<T, E>): result is Failure<E> =>
result.isFailure;

export const success = <T>(value: T): Result<T, never> => new Success(value);
export const failure = <E>(error: E): Result<never, E> => new Failure(error);

export const of = <T>(value: T): Result<T, never> => success(value);

export const match =
<T, E, RS, RF>({
onSuccess,
onFailure,
}: {
onSuccess: (value: T) => RS | Promise<RS>;
onFailure: (error: E) => RF | Promise<RF>;
}) =>
async (result: Result<T, E>): Promise<RS | RF> =>
isSuccess(result) ? onSuccess(result.value) : onFailure(result.error);

export const fromPromise = async <T, E extends Error = Error>(
promise: Promise<T>
): Promise<Result<T, E>> => {
try {
const value = await promise;
return success(value);
} catch (error) {
return failure(error as E);
}
};
```

## Define pipe Function

Create a file named pipeline.ts with the following content:

```typescript

import { AsyncResult, isFailure, Result, Success } from "./result";

type Last<T extends any[]> = T extends [...infer _, infer L] ? L : never;
type PipeFn<In, Out, E> = (arg: In) => AsyncResult<Out, E>;
type LastReturnType<T extends any[], K> = K extends keyof T
? T[K] extends PipeFn<any, infer S, any>
? S
: never
: never;
type ExtractError<T extends any[]> = T[number] extends PipeFn<any, any, infer E>
? E
: never;

type Pipeline<T extends any[], E> = {
[K in keyof T]: K extends "0"
? T[K] extends PipeFn<infer R, infer S, E>
? PipeFn<R, S, E>
: never
: T[K] extends PipeFn<infer R, infer S, E>
? PipeFn<LastReturnType<T, K>, S, E>
: never;
};

/**
* The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes.
* @template T - The tuple of functions.
* @template ExtractError<T> - The error type extracted from the tuple of functions.
* @param {...Pipeline<T, ExtractError<T>>} fns - The functions to compose.
* @returns {Function} A function that takes the input of the first function and returns a `Promise` resolving to a `Result` of the last function's output type and the error type.
*
* @example
* const pipeline = pipe(
* async (input: string) => success(parseInt(input)),
* async (num: number) => success(num + 1),
* async (num: number) => success(num.toString())
* );
*
* pipeline("42").then(result =>
* match({
* onSuccess: val => console.log("Success:", val),
* onFailure: err => console.log("Error:", err)
* })(result)
* );
*/
export const pipe = <
T extends [PipeFn<any, any, any>, ...PipeFn<any, any, any>[]]
>(
...fns: Pipeline<T, ExtractError<T>>
): ((
arg: Parameters<T[0]>[0]
) => Promise<
Result<LastReturnType<T, keyof T & (keyof T)[]["length"]>, ExtractError<T>>
>) => {
return async (
arg: Parameters<T[0]>[0]
): Promise<
Result<LastReturnType<T, keyof T & (keyof T)[]["length"]>, ExtractError<T>>
> => {
let result: Result<any, ExtractError<T>> = await fns[0](arg);
for (const fn of fns.slice(1)) {
if (isFailure(result)) break;
result = await fn((result as Success<any>).value);
}
return result;
};
};
```
## Example Usage

```typescript

import { pipe } from './pipeline';
import { success, failure, match, Result } from './result';

// Define your functions
const parseNumber = async (input: string): Promise<Result<number, string>> =>
isNaN(Number(input)) ? failure("Not a number") : success(Number(input));

const increment = async (num: number): Promise<Result<number, string>> =>
success(num + 1);

const stringify = async (num: number): Promise<Result<string, string>> =>
success(num.toString());

// Create a pipeline
const pipeline = pipe(parseNumber, increment, stringify);

// Execute the pipeline
pipeline("42").then(result =>
match({
onSuccess: val => console.log("Success:", val),
onFailure: err => console.log("Error:", err)
})(result)
);
```

## License

This project is licensed under the MIT License
51 changes: 51 additions & 0 deletions dist/lib/pipe.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { AsyncResult, Result } from "./result";
/**
* Type for a function that takes an input of type `In` and returns an `AsyncResult` of type `Out` and `E`.
* @template In - The input type.
* @template Out - The output type.
* @template E - The error type.
*/
type PipeFn<In, Out, E> = (arg: In) => AsyncResult<Out, E>;
/**
* Type to extract the return type of the last function in a tuple of functions.
* @template T - The tuple of functions.
* @template K - The index of the function in the tuple.
*/
type LastReturnType<T extends any[], K> = K extends keyof T ? T[K] extends PipeFn<any, infer S, any> ? S : never : never;
/**
* Type to extract the error type from a tuple of functions.
* @template T - The tuple of functions.
*/
type ExtractError<T extends any[]> = T[number] extends PipeFn<any, any, infer E> ? E : never;
/**
* Type for a pipeline of functions, ensuring correct type inference for each function in the tuple.
* @template T - The tuple of functions.
* @template E - The error type.
*/
type Pipeline<T extends any[], E> = {
[K in keyof T]: K extends "0" ? T[K] extends PipeFn<infer R, infer S, E> ? PipeFn<R, S, E> : never : T[K] extends PipeFn<infer R, infer S, E> ? PipeFn<LastReturnType<T, K>, S, E> : never;
};
/**
* The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes.
* @template T - The tuple of functions.
* @template ExtractError<T> - The error type extracted from the tuple of functions.
* @param {...Pipeline<T, ExtractError<T>>} fns - The functions to compose.
* @returns {Function} A function that takes the input of the first function and returns a `Promise` resolving to a `Result` of the last function's output type and the error type.
*
* @example
* const pipeline = pipe(
* async (input: string) => success(parseInt(input)),
* async (num: number) => success(num + 1),
* async (num: number) => success(num.toString())
* );
*
* pipeline("42").then(result =>
* match({
* onSuccess: val => console.log("Success:", val),
* onFailure: err => console.log("Error:", err)
* })(result)
* );
*/
export declare const pipe: <T extends [PipeFn<any, any, any>, ...PipeFn<any, any, any>[]]>(...fns: Pipeline<T, ExtractError<T>>) => ((arg: Parameters<T[0]>[0]) => Promise<Result<LastReturnType<T, keyof T & (keyof T)[]["length"]>, ExtractError<T>>>);
export {};
//# sourceMappingURL=pipe.d.ts.map
1 change: 1 addition & 0 deletions dist/lib/pipe.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

43 changes: 43 additions & 0 deletions dist/lib/pipe.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions dist/lib/pipe.js.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit c4f7dfa

Please sign in to comment.