From bbc18dd0fb91d35b08cbbad14bb1bf257b3318b1 Mon Sep 17 00:00:00 2001 From: konkon Date: Thu, 18 Jul 2024 23:17:14 +0900 Subject: [PATCH 1/4] :green_apple: fix pipe --- src/pipe.ts | 24 ++++--- src/result.ts | 41 ++++++++---- tests/pipe.test.ts | 60 ++++++++++++----- tests/result.test.ts | 142 ++++++++++++++++++++++++++++++----------- tests/scenario.test.ts | 93 +++++++++++++++++++++++++++ 5 files changed, 281 insertions(+), 79 deletions(-) create mode 100644 tests/scenario.test.ts diff --git a/src/pipe.ts b/src/pipe.ts index f967100..2b4bff3 100644 --- a/src/pipe.ts +++ b/src/pipe.ts @@ -1,16 +1,14 @@ -import { failure, Result } from "./result"; +import { isFailure, Result, success, Success } from "../src/result"; -export const pipe = ( - ...fns: Array<(arg: T) => R | Promise> -): ((arg: T) => Promise>) => { - return (arg: T): Promise> => { - return fns.reduce(async (acc, fn) => { - try { - const result = await acc; - return Promise.resolve(fn(result as T)); - } catch (error) { - return failure(error as Error); - } - }, Promise.resolve(arg as any)) as Promise>; +export const pipe = ( + ...fns: Array<(arg: T) => Result | Promise>> +): ((arg: T) => Promise>) => { + return async (arg: T): Promise> => { + let result: Result = success(arg); + for (const fn of fns) { + if (isFailure(result)) break; + result = await fn((result as Success).value); + } + return result as Result; }; }; diff --git a/src/result.ts b/src/result.ts index 3ebe97f..e0f32dc 100644 --- a/src/result.ts +++ b/src/result.ts @@ -22,6 +22,8 @@ export const isFailure = (result: Result): result is Failure => export const success = (value: T): Result => new Success(value); export const failure = (error: E): Result => new Failure(error); +export const of = (value: T): Result => success(value); + export const map = (fn: (value: T) => R | Promise) => async (result: Result): Promise> => { @@ -32,26 +34,34 @@ export const map = }; export const chain = - (fn: (value: T) => Promise>) => - async (result: Result): Promise> => { + (fn: (value: T) => Result | Promise>) => + async (result: Result): Promise> => { if (isSuccess(result)) { - return fn(result.value); + return await fn(result.value); } - return result as Result; + return result as Result; }; export const bimap = - (fn: (value: T) => R, fnError: (error: E) => F) => - (result: Result): Result => { - return isSuccess(result) - ? success(fn(result.value)) - : failure(fnError(result.error)); + ( + fn: (value: T) => R | Promise, + fnError: (error: E) => F | Promise + ) => + async (result: Result): Promise> => { + if (isSuccess(result)) { + return success(await fn(result.value)); + } else { + return failure(await fnError(result.error)); + } }; export const mapError = - (fnError: (error: E) => F) => - (result: Result): Result => { - return isFailure(result) ? failure(fnError(result.error)) : result; + (fnError: (error: E) => F | Promise) => + async (result: Result): Promise> => { + if (isFailure(result)) { + return failure(await fnError(result.error)); + } + return result; }; export const match = @@ -73,5 +83,10 @@ export const match = export const fromPromise = async ( promise: Promise ): Promise> => { - return promise.then(success).catch(failure); + try { + const value = await promise; + return success(value); + } catch (error) { + return failure(error as E); + } }; diff --git a/tests/pipe.test.ts b/tests/pipe.test.ts index f106ff3..5d48a2c 100644 --- a/tests/pipe.test.ts +++ b/tests/pipe.test.ts @@ -1,25 +1,53 @@ import { describe, expect, it } from "vitest"; import { pipe } from "../src/pipe"; +import { failure, isFailure, isSuccess, Result, success } from "../src/result"; -describe("Pipe", () => { - it("should pipe synchronous functions", async () => { - const add = (x: number) => x + 1; - const double = (x: number) => x * 2; - const result = await pipe(add, double)(2); - expect(result).toBe(6); +// ユーティリティ関数の定義 +const double = (value: number): Result => success(value * 2); +const square = (value: number): Result => success(value * value); +const throwError = (): Result => failure(new Error("Error")); +const asyncDouble = async (value: number): Promise> => + success(value * 2); +const asyncSquare = async (value: number): Promise> => + success(value * value); +const asyncThrowError = async (): Promise> => + failure(new Error("Error")); + +// テストケースの定義 +describe("pipe", () => { + it("should apply a sequence of functions to a success result", async () => { + const fn = pipe(double, square); + const result = await fn(10); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) { + expect(result.value).toBe(400); + } + }); + + it("should return a failure result if any function throws an error", async () => { + const fn = pipe(double, throwError); + const result = await fn(10); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) { + expect(result.error.message).toBe("Error"); + } }); - it("should pipe asynchronous functions", async () => { - const add = async (x: number) => x + 1; - const double = async (x: number) => x * 2; - const result = await pipe(add, double)(2); - expect(result).toBe(6); + it("should handle asynchronous functions", async () => { + const fn = pipe(asyncDouble, asyncSquare); + const result = await fn(10); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) { + expect(result.value).toBe(400); + } }); - it("should pipe mixed synchronous and asynchronous functions", async () => { - const add = (x: number) => x + 1; - const double = async (x: number) => x * 2; - const result = await pipe(add, double)(2); - expect(result).toBe(6); + it("should return a failure result if any asynchronous function throws an error", async () => { + const fn = pipe(asyncDouble, asyncThrowError); + const result = await fn(10); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) { + expect(result.error.message).toBe("Error"); + } }); }); diff --git a/tests/result.test.ts b/tests/result.test.ts index 16fe696..be78b6c 100644 --- a/tests/result.test.ts +++ b/tests/result.test.ts @@ -1,63 +1,131 @@ import { describe, expect, it } from "vitest"; import { + bimap, chain, failure, + fromPromise, isFailure, isSuccess, map, + mapError, match, success, } from "../src/result"; describe("Result", () => { - it("should create a Success result", () => { - const result = success(42); - expect(result.isSuccess).toBe(true); - expect(result.isFailure).toBe(false); - if (isSuccess(result)) expect(result.value).toBe(42); + describe("success", () => { + it("should create a success result", () => { + const result = success(10); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(10); + }); }); - it("should create a Failure result", () => { - const result = failure("error"); - expect(result.isSuccess).toBe(false); - expect(result.isFailure).toBe(true); - if (isFailure(result)) expect(result.error).toBe("error"); + describe("failure", () => { + it("should create a failure result", () => { + const result = failure("Error"); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Error"); + }); }); - it("should map over a Success result", async () => { - const result = await map((x: number) => x + 1)(success(42)); - expect(result.isSuccess).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(43); + describe("map", () => { + it("should map a success result", async () => { + const result = await map((value: number) => value * 2)(success(10)); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(20); + }); + + it("should not map a failure result", async () => { + const result = await map((value: number) => value * 2)(failure("Error")); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Error"); + }); + }); + + describe("chain", () => { + it("should chain a success result", async () => { + const result = await chain((value: number) => + Promise.resolve(success(value * 2)) + )(success(10)); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(20); + }); + + it("should not chain a failure result", async () => { + const result = await chain((value: number) => + Promise.resolve(success(value * 2)) + )(failure("Error" as never)); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Error"); + }); }); - it("should not map over a Failure result", async () => { - const result = await map((x: number) => x + 1)(failure("error")); - expect(result.isFailure).toBe(true); - if (isFailure(result)) expect(result.error).toBe("error"); + describe("bimap", () => { + it("should bimap a success result", async () => { + const result = await bimap( + (value: number) => value * 2, + (error) => "Mapped Error" + )(success(10)); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(20); + }); + + it("should bimap a failure result", async () => { + const result = await bimap( + (value: number) => value * 2, + (error) => "Mapped Error" + )(failure("Error")); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Mapped Error"); + }); }); - it("should chain over a Success result", async () => { - const result = await chain((x: number) => Promise.resolve(success(x + 1)))( - success(42) - ); - if (isSuccess(result)) { - expect(result.value).toBe(43); - } + describe("mapError", () => { + it("should not map a success result", async () => { + const result = await mapError((error) => "Mapped Error")(success(10)); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(10); + }); + + it("should map a failure result", async () => { + const result = await mapError((error) => "Mapped Error")( + failure("Error") + ); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Mapped Error"); + }); }); - it("should match on a Success result", async () => { - const result = await match({ - onSuccess: (x: number) => x + 1, - onFailure: (err: string) => err, - })(success(42)); - expect(result).toBe(43); + describe("match", () => { + it("should match a success result", async () => { + const result = await match({ + onSuccess: (value: number) => value * 2, + onFailure: (error) => "Mapped Error", + })(success(10)); + expect(result).toBe(20); + }); + + it("should match a failure result", async () => { + const result = await match({ + onSuccess: (value: number) => value * 2, + onFailure: (error) => "Mapped Error", + })(failure("Error")); + expect(result).toBe("Mapped Error"); + }); }); - it("should match on a Failure result", async () => { - const result = await match({ - onSuccess: (x: number) => x + 1, - onFailure: (err: string) => err, - })(failure("error")); - expect(result).toBe("error"); + describe("fromPromise", () => { + it("should create a success result from a resolved promise", async () => { + const result = await fromPromise(Promise.resolve(10)); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(10); + }); + + it("should create a failure result from a rejected promise", async () => { + const result = await fromPromise(Promise.reject("Error")); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Error"); + }); }); }); diff --git a/tests/scenario.test.ts b/tests/scenario.test.ts new file mode 100644 index 0000000..21d4e8a --- /dev/null +++ b/tests/scenario.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { pipe } from "../src/pipe"; +import { + chain, + failure, + fromPromise, + match, + of, + Result, + success, +} from "../src/result"; + +// 同期関数 +const addOne = (x: number): Result => success(x + 1); +const double = (x: number): Result => success(x * 2); +const square = (x: number): Result => success(x * x); + +// 非同期関数 +const asyncAddOne = async (x: number): Promise> => + success(x + 1); +const asyncDouble = async (x: number): Promise> => + success(x * 2); +const asyncSquare = async (x: number): Promise> => + success(x * x); +const asyncAddTen = async (x: number): Promise> => + success(x + 10); + +// 混合型関数 +const numberToString = (x: number): Result => + success(`Number: ${x}`); +const asyncStringLength = async (s: string): Promise> => + success(s.length); + +// テストケース +describe("Result Pipe Scenarios", () => { + it("should handle a sequence of synchronous operations", async () => { + const initial = of(2); + const result = await pipe( + chain(addOne), + chain(double), + chain(square) + )(initial); + + expect(result).toEqual(success(36)); + }); + + it("should handle a sequence of asynchronous operations", async () => { + const initial = await fromPromise(Promise.resolve(2)); + const result = await pipe( + chain(asyncAddOne), + chain(asyncDouble), + chain(asyncSquare), + chain(asyncAddTen), + match({ + onSuccess: (value: number) => success(value), + onFailure: (error: string) => failure(error), + }) + )(initial); + + expect(result).toEqual(success(90)); + }); + + it("should handle a mix of synchronous and asynchronous operations", async () => { + const initial = of(2); + const result = await pipe( + chain(addOne), + chain(asyncDouble), + chain(square), + chain(asyncAddTen), + match({ + onSuccess: (value: number) => success(value), + onFailure: (error: string) => failure(error), + }) + )(initial); + + expect(result).toEqual(success(50)); + }); + + it("should handle a mix of different types in operations", async () => { + const initial = of(2); + const result = await pipe( + chain(addOne), + chain((value: number) => numberToString(value)), + chain(asyncStringLength), + match({ + onSuccess: (value: number) => success(value), + onFailure: (error: string) => failure(error), + }) + )(initial); + + expect(result).toEqual(success(9)); + }); +}); From 2b18dc8cabf9ffc59333edfdecc9bb675718bee1 Mon Sep 17 00:00:00 2001 From: konkon Date: Mon, 22 Jul 2024 08:50:14 +0900 Subject: [PATCH 2/4] :recycle: fix pipelines --- src/pipe.ts | 91 ++++++++++++++-- src/result.ts | 131 ++++++++++++++--------- tests/pipe.test.ts | 238 ++++++++++++++++++++++++++++++++--------- tests/result.test.ts | 154 +++++++++----------------- tests/scenario.test.ts | 93 ---------------- 5 files changed, 401 insertions(+), 306 deletions(-) delete mode 100644 tests/scenario.test.ts diff --git a/src/pipe.ts b/src/pipe.ts index 2b4bff3..55b05ab 100644 --- a/src/pipe.ts +++ b/src/pipe.ts @@ -1,14 +1,87 @@ -import { isFailure, Result, success, Success } from "../src/result"; - -export const pipe = ( - ...fns: Array<(arg: T) => Result | Promise>> -): ((arg: T) => Promise>) => { - return async (arg: T): Promise> => { - let result: Result = success(arg); - for (const fn of fns) { +import { AsyncResult, isFailure, Result, Success } 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 = (arg: In) => AsyncResult; + +/** + * 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 = K extends keyof T + ? T[K] extends PipeFn + ? S + : never + : never; + +/** + * Type to extract the error type from a tuple of functions. + * @template T - The tuple of functions. + */ +type ExtractError = T[number] extends PipeFn + ? 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 = { + [K in keyof T]: K extends "0" + ? T[K] extends PipeFn + ? PipeFn + : never + : T[K] extends PipeFn + ? PipeFn, S, E> + : never; +}; + +/** + * The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes. + * @template T - The tuple of functions. + * @template ExtractError - The error type extracted from the tuple of functions. + * @param {...Pipeline>} 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, ...PipeFn[]] +>( + ...fns: Pipeline> +): (( + arg: Parameters[0] +) => Promise< + Result, ExtractError> +>) => { + return async ( + arg: Parameters[0] + ): Promise< + Result, ExtractError> + > => { + let result: Result> = await fns[0](arg); + for (const fn of fns.slice(1)) { if (isFailure(result)) break; result = await fn((result as Success).value); } - return result as Result; + return result; }; }; diff --git a/src/result.ts b/src/result.ts index e0f32dc..8d621ef 100644 --- a/src/result.ts +++ b/src/result.ts @@ -1,69 +1,100 @@ +/** + * Represents a successful result. + * @template T - The type of the value. + */ export class Success { readonly isSuccess = true; readonly isFailure = false; - + /** + * Creates an instance of Success. + * @param {T} value - The value of the successful result. + */ constructor(public readonly value: T) {} } +/** + * Represents a failed result. + * @template E - The type of the error. + */ export class Failure { readonly isSuccess = false; readonly isFailure = true; - + /** + * Creates an instance of Failure. + * @param {E} error - The error of the failed result. + */ constructor(public readonly error: E) {} } +/** + * Type alias for a result which can be either a Success or a Failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + */ export type Result = Success | Failure; +/** + * Type alias for an asynchronous result. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + */ +export type AsyncResult = Result | Promise>; + +/** + * Checks if the given result is a Success. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Success} - True if the result is a Success, otherwise false. + */ export const isSuccess = (result: Result): result is Success => result.isSuccess; + +/** + * Checks if the given result is a Failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Failure} - True if the result is a Failure, otherwise false. + */ export const isFailure = (result: Result): result is Failure => result.isFailure; +/** + * Creates a success result. + * @template T - The type of the value. + * @param {T} value - The value of the success result. + * @returns {Result} - The success result. + */ export const success = (value: T): Result => new Success(value); + +/** + * Creates a failure result. + * @template E - The type of the error. + * @param {E} error - The error of the failure result. + * @returns {Result} - The failure result. + */ export const failure = (error: E): Result => new Failure(error); +/** + * Wraps a value in a success result. + * @template T - The type of the value. + * @param {T} value - The value to wrap. + * @returns {Result} - The success result. + */ export const of = (value: T): Result => success(value); -export const map = - (fn: (value: T) => R | Promise) => - async (result: Result): Promise> => { - if (isSuccess(result)) { - return success(await fn(result.value)); - } - return result; - }; - -export const chain = - (fn: (value: T) => Result | Promise>) => - async (result: Result): Promise> => { - if (isSuccess(result)) { - return await fn(result.value); - } - return result as Result; - }; - -export const bimap = - ( - fn: (value: T) => R | Promise, - fnError: (error: E) => F | Promise - ) => - async (result: Result): Promise> => { - if (isSuccess(result)) { - return success(await fn(result.value)); - } else { - return failure(await fnError(result.error)); - } - }; - -export const mapError = - (fnError: (error: E) => F | Promise) => - async (result: Result): Promise> => { - if (isFailure(result)) { - return failure(await fnError(result.error)); - } - return result; - }; - +/** + * Matches a result to a function based on whether it is a success or a failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @template RS - The return type of the success handler. + * @template RF - The return type of the failure handler. + * @param {Object} handlers - An object containing the success and failure handlers. + * @param {(value: T) => RS | Promise} handlers.onSuccess - The success handler. + * @param {(error: E) => RF | Promise} handlers.onFailure - The failure handler. + * @returns {(result: Result) => Promise} - A function that takes a result and returns a promise that resolves to the result of the appropriate handler. + */ export const match = ({ onSuccess, @@ -72,14 +103,16 @@ export const match = onSuccess: (value: T) => RS | Promise; onFailure: (error: E) => RF | Promise; }) => - async (result: Result): Promise => { - if (result.isSuccess) { - return onSuccess(result.value); - } else { - return onFailure(result.error); - } - }; + async (result: Result): Promise => + isSuccess(result) ? onSuccess(result.value) : onFailure(result.error); +/** + * Converts a promise to a result. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Promise} promise - The promise to convert. + * @returns {Promise>} - A promise that resolves to a result. + */ export const fromPromise = async ( promise: Promise ): Promise> => { diff --git a/tests/pipe.test.ts b/tests/pipe.test.ts index 5d48a2c..d50803f 100644 --- a/tests/pipe.test.ts +++ b/tests/pipe.test.ts @@ -1,53 +1,191 @@ import { describe, expect, it } from "vitest"; import { pipe } from "../src/pipe"; -import { failure, isFailure, isSuccess, Result, success } from "../src/result"; - -// ユーティリティ関数の定義 -const double = (value: number): Result => success(value * 2); -const square = (value: number): Result => success(value * value); -const throwError = (): Result => failure(new Error("Error")); -const asyncDouble = async (value: number): Promise> => - success(value * 2); -const asyncSquare = async (value: number): Promise> => - success(value * value); -const asyncThrowError = async (): Promise> => - failure(new Error("Error")); - -// テストケースの定義 -describe("pipe", () => { - it("should apply a sequence of functions to a success result", async () => { - const fn = pipe(double, square); - const result = await fn(10); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) { - expect(result.value).toBe(400); - } - }); - - it("should return a failure result if any function throws an error", async () => { - const fn = pipe(double, throwError); - const result = await fn(10); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) { - expect(result.error.message).toBe("Error"); - } - }); - - it("should handle asynchronous functions", async () => { - const fn = pipe(asyncDouble, asyncSquare); - const result = await fn(10); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) { - expect(result.value).toBe(400); - } - }); - - it("should return a failure result if any asynchronous function throws an error", async () => { - const fn = pipe(asyncDouble, asyncThrowError); - const result = await fn(10); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) { - expect(result.error.message).toBe("Error"); - } - }); +import { failure, match, Result, success } from "../src/result"; + +// Define interfaces +interface Person { + name: string; + age: number; +} + +interface Employee extends Person { + role: string; +} + +// Define functions for numbers and strings +const parseNumber = (input: string): Result => + isNaN(Number(input)) ? failure("Not a number") : success(Number(input)); +const increment = (num: number): Result => success(num + 1); +const stringify = (num: number): Result => + success(num.toString()); + +// Asynchronous functions for numbers and strings +const asyncParseNumber = async ( + input: string +): Promise> => + isNaN(Number(input)) ? failure("Not a number") : success(Number(input)); +const asyncIncrement = async (num: number): Promise> => + success(num + 1); +const asyncStringify = async (num: number): Promise> => + success(num.toString()); + +// Define functions for objects and interfaces +const createPerson = (name: string, age: number): Result => + success({ name, age }); +const promoteToEmployee = ( + person: Person, + role: string +): Result => success({ ...person, role }); + +const asyncCreatePerson = async ( + name: string, + age: number +): Promise> => success({ name, age }); +const asyncPromoteToEmployee = async ( + person: Person, + role: string +): Promise> => success({ ...person, role }); + +const personToString = (person: Person): Result => + success(`${person.name} is ${person.age} years old`); +const employeeToString = (employee: Employee): Result => + success( + `${employee.name} is ${employee.age} years old and works as a ${employee.role}` + ); +const asyncPersonToString = async ( + person: Person +): Promise> => + success(`${person.name} is ${person.age} years old`); +const asyncEmployeeToString = async ( + employee: Employee +): Promise> => + success( + `${employee.name} is ${employee.age} years old and works as a ${employee.role}` + ); +// Test cases +describe("Result Pipe Scenarios", () => { + it("should parse, increment and stringify a number synchronously", async () => + await pipe( + parseNumber, + increment, + stringify + )("42").then((result) => + match({ + onSuccess: (val) => expect(val).toBe("43"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should fail if input is not a number synchronously", async () => + await pipe( + parseNumber, + increment, + stringify + )("abc").then((result) => + match({ + onSuccess: () => expect.fail("Expected failure but got success"), + onFailure: (err) => expect(err).toBe("Not a number"), + })(result) + )); + + it("should parse, increment and stringify a number asynchronously", async () => + await pipe( + asyncParseNumber, + asyncIncrement, + asyncStringify + )("42").then((result) => + match({ + onSuccess: (val) => expect(val).toBe("43"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should fail if input is not a number asynchronously", async () => + await pipe( + asyncParseNumber, + asyncIncrement, + asyncStringify + )("abc").then((result) => + match({ + onSuccess: () => expect.fail("Expected failure but got success"), + onFailure: (err) => expect(err).toBe("Not a number"), + })(result) + )); + + it("should parse, increment and stringify a number with mixed functions", async () => + await pipe( + parseNumber, + asyncIncrement, + stringify, + asyncStringify + )("42").then((result) => + match({ + onSuccess: (val) => expect(val).toBe("43"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should fail if input is not a number with mixed functions", async () => + await pipe( + parseNumber, + asyncIncrement, + stringify, + asyncStringify + )("abc").then((result) => + match({ + onSuccess: () => expect.fail("Expected failure but got success"), + onFailure: (err) => expect(err).toBe("Not a number"), + })(result) + )); + + it("should map a number to a string synchronously", async () => + await pipe( + parseNumber, + stringify + )("42").then((result) => + match({ + onSuccess: (val) => expect(val).toBe("42"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should chain asynchronous functions correctly", async () => + await pipe( + parseNumber, + asyncIncrement, + asyncStringify + )("42").then((result) => + match({ + onSuccess: (val) => expect(val).toBe("43"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should create and promote a person synchronously", async () => + await pipe( + (input: { name: string; age: number }) => + createPerson(input.name, input.age), + (person) => promoteToEmployee(person, "Developer"), + employeeToString + )({ name: "John", age: 30 }).then((result) => + match({ + onSuccess: (val) => + expect(val).toBe("John is 30 years old and works as a Developer"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); + + it("should create and promote a person asynchronously", async () => + await pipe( + (input: { name: string; age: number }) => + asyncCreatePerson(input.name, input.age), + (person) => asyncPromoteToEmployee(person, "Developer"), + asyncEmployeeToString + )({ name: "John", age: 30 }).then((result) => + match({ + onSuccess: (val) => + expect(val).toBe("John is 30 years old and works as a Developer"), + onFailure: (err) => expect.fail(`Unexpected failure: ${err}`), + })(result) + )); }); diff --git a/tests/result.test.ts b/tests/result.test.ts index be78b6c..06e3fee 100644 --- a/tests/result.test.ts +++ b/tests/result.test.ts @@ -1,131 +1,75 @@ import { describe, expect, it } from "vitest"; import { - bimap, - chain, failure, fromPromise, isFailure, isSuccess, - map, - mapError, match, success, } from "../src/result"; -describe("Result", () => { - describe("success", () => { - it("should create a success result", () => { - const result = success(10); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(10); - }); +describe("Result Type Functions", () => { + it("should create a success result", () => { + const result = success(42); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(42); }); - describe("failure", () => { - it("should create a failure result", () => { - const result = failure("Error"); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Error"); - }); + it("should create a failure result", () => { + const result = failure("Error occurred"); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error).toBe("Error occurred"); }); - describe("map", () => { - it("should map a success result", async () => { - const result = await map((value: number) => value * 2)(success(10)); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(20); - }); - - it("should not map a failure result", async () => { - const result = await map((value: number) => value * 2)(failure("Error")); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Error"); - }); + it("should match a success result", async () => { + const result = success(42); + const matched = await match({ + onSuccess: (value) => `Success: ${value}`, + onFailure: (error) => `Failure: ${error}`, + })(result); + expect(matched).toBe("Success: 42"); }); - describe("chain", () => { - it("should chain a success result", async () => { - const result = await chain((value: number) => - Promise.resolve(success(value * 2)) - )(success(10)); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(20); - }); - - it("should not chain a failure result", async () => { - const result = await chain((value: number) => - Promise.resolve(success(value * 2)) - )(failure("Error" as never)); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Error"); - }); + it("should match a failure result", async () => { + const result = failure("Error occurred"); + const matched = await match({ + onSuccess: (value) => `Success: ${value}`, + onFailure: (error) => `Failure: ${error}`, + })(result); + expect(matched).toBe("Failure: Error occurred"); }); - describe("bimap", () => { - it("should bimap a success result", async () => { - const result = await bimap( - (value: number) => value * 2, - (error) => "Mapped Error" - )(success(10)); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(20); - }); - - it("should bimap a failure result", async () => { - const result = await bimap( - (value: number) => value * 2, - (error) => "Mapped Error" - )(failure("Error")); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Mapped Error"); - }); + it("should create a result from a resolved promise", async () => { + const promise = Promise.resolve(42); + const result = await fromPromise(promise); + expect(isSuccess(result)).toBe(true); + if (isSuccess(result)) expect(result.value).toBe(42); }); - describe("mapError", () => { - it("should not map a success result", async () => { - const result = await mapError((error) => "Mapped Error")(success(10)); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(10); - }); - - it("should map a failure result", async () => { - const result = await mapError((error) => "Mapped Error")( - failure("Error") - ); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Mapped Error"); - }); + it("should create a result from a rejected promise", async () => { + const promise = Promise.reject(new Error("Error occurred")); + const result = await fromPromise(promise); + expect(isFailure(result)).toBe(true); + if (isFailure(result)) expect(result.error.message).toBe("Error occurred"); }); - describe("match", () => { - it("should match a success result", async () => { - const result = await match({ - onSuccess: (value: number) => value * 2, - onFailure: (error) => "Mapped Error", - })(success(10)); - expect(result).toBe(20); - }); - - it("should match a failure result", async () => { - const result = await match({ - onSuccess: (value: number) => value * 2, - onFailure: (error) => "Mapped Error", - })(failure("Error")); - expect(result).toBe("Mapped Error"); - }); + it("should handle a promise with match function", async () => { + const promise = Promise.resolve(42); + const result = await fromPromise(promise); + const matched = await match({ + onSuccess: (value) => `Resolved: ${value}`, + onFailure: (error: Error) => `Rejected: ${error.message}`, + })(result); + expect(matched).toBe("Resolved: 42"); }); - describe("fromPromise", () => { - it("should create a success result from a resolved promise", async () => { - const result = await fromPromise(Promise.resolve(10)); - expect(isSuccess(result)).toBe(true); - if (isSuccess(result)) expect(result.value).toBe(10); - }); - - it("should create a failure result from a rejected promise", async () => { - const result = await fromPromise(Promise.reject("Error")); - expect(isFailure(result)).toBe(true); - if (isFailure(result)) expect(result.error).toBe("Error"); - }); + it("should handle a rejected promise with match function", async () => { + const promise = Promise.reject(new Error("Error occurred")); + const result = await fromPromise(promise); + const matched = await match({ + onSuccess: (value) => `Resolved: ${value}`, + onFailure: (error: Error) => `Rejected: ${error.message}`, + })(result); + expect(matched).toBe("Rejected: Error occurred"); }); }); diff --git a/tests/scenario.test.ts b/tests/scenario.test.ts deleted file mode 100644 index 21d4e8a..0000000 --- a/tests/scenario.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { pipe } from "../src/pipe"; -import { - chain, - failure, - fromPromise, - match, - of, - Result, - success, -} from "../src/result"; - -// 同期関数 -const addOne = (x: number): Result => success(x + 1); -const double = (x: number): Result => success(x * 2); -const square = (x: number): Result => success(x * x); - -// 非同期関数 -const asyncAddOne = async (x: number): Promise> => - success(x + 1); -const asyncDouble = async (x: number): Promise> => - success(x * 2); -const asyncSquare = async (x: number): Promise> => - success(x * x); -const asyncAddTen = async (x: number): Promise> => - success(x + 10); - -// 混合型関数 -const numberToString = (x: number): Result => - success(`Number: ${x}`); -const asyncStringLength = async (s: string): Promise> => - success(s.length); - -// テストケース -describe("Result Pipe Scenarios", () => { - it("should handle a sequence of synchronous operations", async () => { - const initial = of(2); - const result = await pipe( - chain(addOne), - chain(double), - chain(square) - )(initial); - - expect(result).toEqual(success(36)); - }); - - it("should handle a sequence of asynchronous operations", async () => { - const initial = await fromPromise(Promise.resolve(2)); - const result = await pipe( - chain(asyncAddOne), - chain(asyncDouble), - chain(asyncSquare), - chain(asyncAddTen), - match({ - onSuccess: (value: number) => success(value), - onFailure: (error: string) => failure(error), - }) - )(initial); - - expect(result).toEqual(success(90)); - }); - - it("should handle a mix of synchronous and asynchronous operations", async () => { - const initial = of(2); - const result = await pipe( - chain(addOne), - chain(asyncDouble), - chain(square), - chain(asyncAddTen), - match({ - onSuccess: (value: number) => success(value), - onFailure: (error: string) => failure(error), - }) - )(initial); - - expect(result).toEqual(success(50)); - }); - - it("should handle a mix of different types in operations", async () => { - const initial = of(2); - const result = await pipe( - chain(addOne), - chain((value: number) => numberToString(value)), - chain(asyncStringLength), - match({ - onSuccess: (value: number) => success(value), - onFailure: (error: string) => failure(error), - }) - )(initial); - - expect(result).toEqual(success(9)); - }); -}); From 16eb1b7bdcac23b0f81f5e6b9f8155f9686944dd Mon Sep 17 00:00:00 2001 From: konkon Date: Mon, 22 Jul 2024 22:30:29 +0900 Subject: [PATCH 3/4] :book: add docs --- .npmignore | 4 + README.md | 181 ++++++++++++++++++++++++++++++++++++++- dist/lib/pipe.d.ts | 51 +++++++++++ dist/lib/pipe.d.ts.map | 1 + dist/lib/pipe.js | 43 ++++++++++ dist/lib/pipe.js.map | 1 + dist/lib/result.d.ts | 101 ++++++++++++++++++++++ dist/lib/result.d.ts.map | 1 + dist/lib/result.js | 105 +++++++++++++++++++++++ dist/lib/result.js.map | 1 + dist/src/index.d.ts | 3 + dist/src/index.d.ts.map | 1 + dist/src/index.js | 3 + dist/src/index.js.map | 1 + {src => lib}/pipe.ts | 0 {src => lib}/result.ts | 0 package.json | 22 +++-- src/index.ts | 4 +- tests/pipe.test.ts | 4 +- tests/result.test.ts | 2 +- tsconfig.json | 2 +- 21 files changed, 519 insertions(+), 12 deletions(-) create mode 100644 .npmignore create mode 100644 dist/lib/pipe.d.ts create mode 100644 dist/lib/pipe.d.ts.map create mode 100644 dist/lib/pipe.js create mode 100644 dist/lib/pipe.js.map create mode 100644 dist/lib/result.d.ts create mode 100644 dist/lib/result.d.ts.map create mode 100644 dist/lib/result.js create mode 100644 dist/lib/result.js.map create mode 100644 dist/src/index.d.ts create mode 100644 dist/src/index.d.ts.map create mode 100644 dist/src/index.js create mode 100644 dist/src/index.js.map rename {src => lib}/pipe.ts (100%) rename {src => lib}/result.ts (100%) diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..c6f4d8a --- /dev/null +++ b/.npmignore @@ -0,0 +1,4 @@ +node_modules +src +lib +tsconfig.json \ No newline at end of file diff --git a/README.md b/README.md index 6402e6e..d23cac2 100644 --- a/README.md +++ b/README.md @@ -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 { + readonly isSuccess = true; + readonly isFailure = false; + + constructor(public readonly value: T) {} +} + +export class Failure { + readonly isSuccess = false; + readonly isFailure = true; + + constructor(public readonly error: E) {} +} + +export type Result = Success | Failure; +export type AsyncResult = Result | Promise>; + +export const isSuccess = (result: Result): result is Success => + result.isSuccess; +export const isFailure = (result: Result): result is Failure => + result.isFailure; + +export const success = (value: T): Result => new Success(value); +export const failure = (error: E): Result => new Failure(error); + +export const of = (value: T): Result => success(value); + +export const match = + ({ + onSuccess, + onFailure, + }: { + onSuccess: (value: T) => RS | Promise; + onFailure: (error: E) => RF | Promise; + }) => + async (result: Result): Promise => + isSuccess(result) ? onSuccess(result.value) : onFailure(result.error); + +export const fromPromise = async ( + promise: Promise +): Promise> => { + 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 [...infer _, infer L] ? L : never; +type PipeFn = (arg: In) => AsyncResult; +type LastReturnType = K extends keyof T + ? T[K] extends PipeFn + ? S + : never + : never; +type ExtractError = T[number] extends PipeFn + ? E + : never; + +type Pipeline = { + [K in keyof T]: K extends "0" + ? T[K] extends PipeFn + ? PipeFn + : never + : T[K] extends PipeFn + ? PipeFn, S, E> + : never; +}; + +/** + * The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes. + * @template T - The tuple of functions. + * @template ExtractError - The error type extracted from the tuple of functions. + * @param {...Pipeline>} 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, ...PipeFn[]] +>( + ...fns: Pipeline> +): (( + arg: Parameters[0] +) => Promise< + Result, ExtractError> +>) => { + return async ( + arg: Parameters[0] + ): Promise< + Result, ExtractError> + > => { + let result: Result> = await fns[0](arg); + for (const fn of fns.slice(1)) { + if (isFailure(result)) break; + result = await fn((result as Success).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> => + isNaN(Number(input)) ? failure("Not a number") : success(Number(input)); + +const increment = async (num: number): Promise> => + success(num + 1); + +const stringify = async (num: number): Promise> => + 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 \ No newline at end of file diff --git a/dist/lib/pipe.d.ts b/dist/lib/pipe.d.ts new file mode 100644 index 0000000..7eea83f --- /dev/null +++ b/dist/lib/pipe.d.ts @@ -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 = (arg: In) => AsyncResult; +/** + * 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 = K extends keyof T ? T[K] extends PipeFn ? S : never : never; +/** + * Type to extract the error type from a tuple of functions. + * @template T - The tuple of functions. + */ +type ExtractError = T[number] extends PipeFn ? 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 = { + [K in keyof T]: K extends "0" ? T[K] extends PipeFn ? PipeFn : never : T[K] extends PipeFn ? PipeFn, S, E> : never; +}; +/** + * The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes. + * @template T - The tuple of functions. + * @template ExtractError - The error type extracted from the tuple of functions. + * @param {...Pipeline>} 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: , ...PipeFn[]]>(...fns: Pipeline>) => ((arg: Parameters[0]) => Promise, ExtractError>>); +export {}; +//# sourceMappingURL=pipe.d.ts.map \ No newline at end of file diff --git a/dist/lib/pipe.d.ts.map b/dist/lib/pipe.d.ts.map new file mode 100644 index 0000000..ac503dd --- /dev/null +++ b/dist/lib/pipe.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"pipe.d.ts","sourceRoot":"","sources":["../../lib/pipe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAa,MAAM,EAAW,MAAM,UAAU,CAAC;AAEnE;;;;;GAKG;AACH,KAAK,MAAM,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,KAAK,WAAW,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;AAE3D;;;;GAIG;AACH,KAAK,cAAc,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,MAAM,CAAC,GACvD,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,MAAM,CAAC,EAAE,GAAG,CAAC,GACpC,CAAC,GACD,KAAK,GACP,KAAK,CAAC;AAEV;;;GAGG;AACH,KAAK,YAAY,CAAC,CAAC,SAAS,GAAG,EAAE,IAAI,CAAC,CAAC,MAAM,CAAC,SAAS,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC,GAC5E,CAAC,GACD,KAAK,CAAC;AAEV;;;;GAIG;AACH,KAAK,QAAQ,CAAC,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,IAAI;KACjC,CAAC,IAAI,MAAM,CAAC,GAAG,CAAC,SAAS,GAAG,GACzB,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,GACtC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GACf,KAAK,GACP,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC,GACxC,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,GAClC,KAAK;CACV,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,IAAI,GACf,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,UAErD,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,KACnC,CAAC,CACF,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KACrB,OAAO,CACV,MAAM,CAAC,cAAc,CAAC,CAAC,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC,CAAC,CAAC,CAC5E,CAaA,CAAC"} \ No newline at end of file diff --git a/dist/lib/pipe.js b/dist/lib/pipe.js new file mode 100644 index 0000000..7049a44 --- /dev/null +++ b/dist/lib/pipe.js @@ -0,0 +1,43 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +import { isFailure } from "./result"; +/** + * The `pipe` function composes multiple functions, handling both synchronous and asynchronous processes. + * @template T - The tuple of functions. + * @template ExtractError - The error type extracted from the tuple of functions. + * @param {...Pipeline>} 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 = (...fns) => { + return (arg) => __awaiter(void 0, void 0, void 0, function* () { + let result = yield fns[0](arg); + for (const fn of fns.slice(1)) { + if (isFailure(result)) + break; + result = yield fn(result.value); + } + return result; + }); +}; +//# sourceMappingURL=pipe.js.map \ No newline at end of file diff --git a/dist/lib/pipe.js.map b/dist/lib/pipe.js.map new file mode 100644 index 0000000..0064239 --- /dev/null +++ b/dist/lib/pipe.js.map @@ -0,0 +1 @@ +{"version":3,"file":"pipe.js","sourceRoot":"","sources":["../../lib/pipe.ts"],"names":[],"mappings":";;;;;;;;;AAAA,OAAO,EAAe,SAAS,EAAmB,MAAM,UAAU,CAAC;AA4CnE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,MAAM,IAAI,GAAG,CAGlB,GAAG,GAAiC,EAKnC,EAAE;IACH,OAAO,CACL,GAAwB,EAGxB,EAAE;QACF,IAAI,MAAM,GAAiC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC7D,KAAK,MAAM,EAAE,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC;YAC9B,IAAI,SAAS,CAAC,MAAM,CAAC;gBAAE,MAAM;YAC7B,MAAM,GAAG,MAAM,EAAE,CAAE,MAAuB,CAAC,KAAK,CAAC,CAAC;QACpD,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC,CAAA,CAAC;AACJ,CAAC,CAAC"} \ No newline at end of file diff --git a/dist/lib/result.d.ts b/dist/lib/result.d.ts new file mode 100644 index 0000000..531e900 --- /dev/null +++ b/dist/lib/result.d.ts @@ -0,0 +1,101 @@ +/** + * Represents a successful result. + * @template T - The type of the value. + */ +export declare class Success { + readonly value: T; + readonly isSuccess = true; + readonly isFailure = false; + /** + * Creates an instance of Success. + * @param {T} value - The value of the successful result. + */ + constructor(value: T); +} +/** + * Represents a failed result. + * @template E - The type of the error. + */ +export declare class Failure { + readonly error: E; + readonly isSuccess = false; + readonly isFailure = true; + /** + * Creates an instance of Failure. + * @param {E} error - The error of the failed result. + */ + constructor(error: E); +} +/** + * Type alias for a result which can be either a Success or a Failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + */ +export type Result = Success | Failure; +/** + * Type alias for an asynchronous result. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + */ +export type AsyncResult = Result | Promise>; +/** + * Checks if the given result is a Success. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Success} - True if the result is a Success, otherwise false. + */ +export declare const isSuccess: (result: Result) => result is Success; +/** + * Checks if the given result is a Failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Failure} - True if the result is a Failure, otherwise false. + */ +export declare const isFailure: (result: Result) => result is Failure; +/** + * Creates a success result. + * @template T - The type of the value. + * @param {T} value - The value of the success result. + * @returns {Result} - The success result. + */ +export declare const success: (value: T) => Result; +/** + * Creates a failure result. + * @template E - The type of the error. + * @param {E} error - The error of the failure result. + * @returns {Result} - The failure result. + */ +export declare const failure: (error: E) => Result; +/** + * Wraps a value in a success result. + * @template T - The type of the value. + * @param {T} value - The value to wrap. + * @returns {Result} - The success result. + */ +export declare const of: (value: T) => Result; +/** + * Matches a result to a function based on whether it is a success or a failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @template RS - The return type of the success handler. + * @template RF - The return type of the failure handler. + * @param {Object} handlers - An object containing the success and failure handlers. + * @param {(value: T) => RS | Promise} handlers.onSuccess - The success handler. + * @param {(error: E) => RF | Promise} handlers.onFailure - The failure handler. + * @returns {(result: Result) => Promise} - A function that takes a result and returns a promise that resolves to the result of the appropriate handler. + */ +export declare const match: ({ onSuccess, onFailure, }: { + onSuccess: (value: T) => RS | Promise; + onFailure: (error: E) => RF | Promise; +}) => (result: Result) => Promise; +/** + * Converts a promise to a result. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Promise} promise - The promise to convert. + * @returns {Promise>} - A promise that resolves to a result. + */ +export declare const fromPromise: (promise: Promise) => Promise>; +//# sourceMappingURL=result.d.ts.map \ No newline at end of file diff --git a/dist/lib/result.d.ts.map b/dist/lib/result.d.ts.map new file mode 100644 index 0000000..960812a --- /dev/null +++ b/dist/lib/result.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"result.d.ts","sourceRoot":"","sources":["../../lib/result.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,OAAO,CAAC,CAAC;aAOQ,KAAK,EAAE,CAAC;IANpC,QAAQ,CAAC,SAAS,QAAQ;IAC1B,QAAQ,CAAC,SAAS,SAAS;IAC3B;;;OAGG;gBACyB,KAAK,EAAE,CAAC;CACrC;AAED;;;GAGG;AACH,qBAAa,OAAO,CAAC,CAAC;aAOQ,KAAK,EAAE,CAAC;IANpC,QAAQ,CAAC,SAAS,SAAS;IAC3B,QAAQ,CAAC,SAAS,QAAQ;IAC1B;;;OAGG;gBACyB,KAAK,EAAE,CAAC;CACrC;AAED;;;;GAIG;AACH,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEnD;;;;GAIG;AACH,MAAM,MAAM,WAAW,CAAC,CAAC,EAAE,CAAC,IAAI,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;AAErE;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CACxD,CAAC;AAEnB;;;;;;GAMG;AACH,eAAO,MAAM,SAAS,GAAI,CAAC,EAAE,CAAC,UAAU,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,MAAM,IAAI,OAAO,CAAC,CAAC,CACxD,CAAC;AAEnB;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAuB,CAAC;AAE7E;;;;;GAKG;AACH,eAAO,MAAM,OAAO,GAAI,CAAC,SAAS,CAAC,KAAG,MAAM,CAAC,KAAK,EAAE,CAAC,CAAuB,CAAC;AAE7E;;;;;GAKG;AACH,eAAO,MAAM,EAAE,GAAI,CAAC,SAAS,CAAC,KAAG,MAAM,CAAC,CAAC,EAAE,KAAK,CAAmB,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,KAAK,GACf,CAAC,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,6BAGV;IACD,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;IAC1C,SAAS,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,EAAE,CAAC,CAAC;CAC3C,cACc,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,KAAG,OAAO,CAAC,EAAE,GAAG,EAAE,CAC0B,CAAC;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,WAAW,GAAU,CAAC,EAAE,CAAC,SAAS,KAAK,mBACzC,OAAO,CAAC,CAAC,CAAC,KAClB,OAAO,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAOtB,CAAC"} \ No newline at end of file diff --git a/dist/lib/result.js b/dist/lib/result.js new file mode 100644 index 0000000..4691e55 --- /dev/null +++ b/dist/lib/result.js @@ -0,0 +1,105 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +/** + * Represents a successful result. + * @template T - The type of the value. + */ +export class Success { + /** + * Creates an instance of Success. + * @param {T} value - The value of the successful result. + */ + constructor(value) { + this.value = value; + this.isSuccess = true; + this.isFailure = false; + } +} +/** + * Represents a failed result. + * @template E - The type of the error. + */ +export class Failure { + /** + * Creates an instance of Failure. + * @param {E} error - The error of the failed result. + */ + constructor(error) { + this.error = error; + this.isSuccess = false; + this.isFailure = true; + } +} +/** + * Checks if the given result is a Success. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Success} - True if the result is a Success, otherwise false. + */ +export const isSuccess = (result) => result.isSuccess; +/** + * Checks if the given result is a Failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Result} result - The result to check. + * @returns {result is Failure} - True if the result is a Failure, otherwise false. + */ +export const isFailure = (result) => result.isFailure; +/** + * Creates a success result. + * @template T - The type of the value. + * @param {T} value - The value of the success result. + * @returns {Result} - The success result. + */ +export const success = (value) => new Success(value); +/** + * Creates a failure result. + * @template E - The type of the error. + * @param {E} error - The error of the failure result. + * @returns {Result} - The failure result. + */ +export const failure = (error) => new Failure(error); +/** + * Wraps a value in a success result. + * @template T - The type of the value. + * @param {T} value - The value to wrap. + * @returns {Result} - The success result. + */ +export const of = (value) => success(value); +/** + * Matches a result to a function based on whether it is a success or a failure. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @template RS - The return type of the success handler. + * @template RF - The return type of the failure handler. + * @param {Object} handlers - An object containing the success and failure handlers. + * @param {(value: T) => RS | Promise} handlers.onSuccess - The success handler. + * @param {(error: E) => RF | Promise} handlers.onFailure - The failure handler. + * @returns {(result: Result) => Promise} - A function that takes a result and returns a promise that resolves to the result of the appropriate handler. + */ +export const match = ({ onSuccess, onFailure, }) => (result) => __awaiter(void 0, void 0, void 0, function* () { return isSuccess(result) ? onSuccess(result.value) : onFailure(result.error); }); +/** + * Converts a promise to a result. + * @template T - The type of the value in case of success. + * @template E - The type of the error in case of failure. + * @param {Promise} promise - The promise to convert. + * @returns {Promise>} - A promise that resolves to a result. + */ +export const fromPromise = (promise) => __awaiter(void 0, void 0, void 0, function* () { + try { + const value = yield promise; + return success(value); + } + catch (error) { + return failure(error); + } +}); +//# sourceMappingURL=result.js.map \ No newline at end of file diff --git a/dist/lib/result.js.map b/dist/lib/result.js.map new file mode 100644 index 0000000..2bad27c --- /dev/null +++ b/dist/lib/result.js.map @@ -0,0 +1 @@ +{"version":3,"file":"result.js","sourceRoot":"","sources":["../../lib/result.ts"],"names":[],"mappings":";;;;;;;;;AAAA;;;GAGG;AACH,MAAM,OAAO,OAAO;IAGlB;;;OAGG;IACH,YAA4B,KAAQ;QAAR,UAAK,GAAL,KAAK,CAAG;QAN3B,cAAS,GAAG,IAAI,CAAC;QACjB,cAAS,GAAG,KAAK,CAAC;IAKY,CAAC;CACzC;AAED;;;GAGG;AACH,MAAM,OAAO,OAAO;IAGlB;;;OAGG;IACH,YAA4B,KAAQ;QAAR,UAAK,GAAL,KAAK,CAAG;QAN3B,cAAS,GAAG,KAAK,CAAC;QAClB,cAAS,GAAG,IAAI,CAAC;IAKa,CAAC;CACzC;AAgBD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAO,MAAoB,EAAwB,EAAE,CAC5E,MAAM,CAAC,SAAS,CAAC;AAEnB;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAO,MAAoB,EAAwB,EAAE,CAC5E,MAAM,CAAC,SAAS,CAAC;AAEnB;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAI,KAAQ,EAAoB,EAAE,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,OAAO,GAAG,CAAI,KAAQ,EAAoB,EAAE,CAAC,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC;AAE7E;;;;;GAKG;AACH,MAAM,CAAC,MAAM,EAAE,GAAG,CAAI,KAAQ,EAAoB,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAEpE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,KAAK,GAChB,CAAe,EACb,SAAS,EACT,SAAS,GAIV,EAAE,EAAE,CACL,CAAO,MAAoB,EAAoB,EAAE,kDAC/C,OAAA,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA,GAAA,CAAC;AAE1E;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CACzB,OAAmB,EACI,EAAE;IACzB,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC;QAC5B,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,OAAO,CAAC,KAAU,CAAC,CAAC;IAC7B,CAAC;AACH,CAAC,CAAA,CAAC"} \ No newline at end of file diff --git a/dist/src/index.d.ts b/dist/src/index.d.ts new file mode 100644 index 0000000..51043cc --- /dev/null +++ b/dist/src/index.d.ts @@ -0,0 +1,3 @@ +export * from "../lib/pipe"; +export * from "../lib/result"; +//# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/dist/src/index.d.ts.map b/dist/src/index.d.ts.map new file mode 100644 index 0000000..cdcf7f4 --- /dev/null +++ b/dist/src/index.d.ts.map @@ -0,0 +1 @@ +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"} \ No newline at end of file diff --git a/dist/src/index.js b/dist/src/index.js new file mode 100644 index 0000000..35ea301 --- /dev/null +++ b/dist/src/index.js @@ -0,0 +1,3 @@ +export * from "../lib/pipe"; +export * from "../lib/result"; +//# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/dist/src/index.js.map b/dist/src/index.js.map new file mode 100644 index 0000000..d099c36 --- /dev/null +++ b/dist/src/index.js.map @@ -0,0 +1 @@ +{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC"} \ No newline at end of file diff --git a/src/pipe.ts b/lib/pipe.ts similarity index 100% rename from src/pipe.ts rename to lib/pipe.ts diff --git a/src/result.ts b/lib/result.ts similarity index 100% rename from src/result.ts rename to lib/result.ts diff --git a/package.json b/package.json index c440247..14e054e 100644 --- a/package.json +++ b/package.json @@ -1,21 +1,33 @@ { "name": "ts-pipeline", - "version": "1.0.0", - "description": "A minimal library that provides a functional pipeline mechanism in typescript", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "version": "0.1.0", + "description": "A minimal dependency-free library for composing pipelines in TypeScript.", + "main": "dist/index.js", + "types": "dist/index.d.ts", "type": "module", "files": [ "dist" ], "scripts": { "build": "tsc", - "prepare": "pnpm run build", + "prepublishOnly": "pnpm run build", "test": "vitest run", "test:watch": "vitest" }, "author": "konkon", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/username/my-pipeline-ts.git" + }, + "keywords": [ + "pipeline", + "typescript", + "functional-programming" + ], + "bugs": { + "url": "https://github.com/username/my-pipeline-ts/issues" + }, "devDependencies": { "@types/node": "^20.14.11", "ts-node": "^10.9.2", diff --git a/src/index.ts b/src/index.ts index 275cbcd..438b220 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from "./pipe"; -export * from "./result"; +export * from "../lib/pipe"; +export * from "../lib/result"; diff --git a/tests/pipe.test.ts b/tests/pipe.test.ts index d50803f..44be945 100644 --- a/tests/pipe.test.ts +++ b/tests/pipe.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vitest"; -import { pipe } from "../src/pipe"; -import { failure, match, Result, success } from "../src/result"; +import { pipe } from "../lib/pipe"; +import { failure, match, Result, success } from "../lib/result"; // Define interfaces interface Person { diff --git a/tests/result.test.ts b/tests/result.test.ts index 06e3fee..722900b 100644 --- a/tests/result.test.ts +++ b/tests/result.test.ts @@ -6,7 +6,7 @@ import { isSuccess, match, success, -} from "../src/result"; +} from "../lib/result"; describe("Result Type Functions", () => { it("should create a success result", () => { diff --git a/tsconfig.json b/tsconfig.json index 266e599..d2698a9 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,6 +10,6 @@ "declarationMap": true, "sourceMap": true }, - "include": ["src/**/*"], + "include": ["src/**/*", "lib/**/*"], "exclude": ["tests/**/*"] } From a3d4833376313f7863d8277292617770b966f154 Mon Sep 17 00:00:00 2001 From: konkon Date: Mon, 22 Jul 2024 22:32:19 +0900 Subject: [PATCH 4/4] :bug: add gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 82d30cc..585d6d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +dist/ \ No newline at end of file