Skip to content

Commit

Permalink
Refactor Chain class and add $op function
Browse files Browse the repository at this point in the history
  • Loading branch information
shtse8 committed Mar 17, 2024
1 parent 6814b76 commit feb4c8c
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 36 deletions.
49 changes: 41 additions & 8 deletions src/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,58 @@
* @example
* chain(5).pipe(add, 5).pipe(multiply, 2).unwrap() // returns 20
*/
export class Chain<T> {
constructor(private value: T) { }
export class Chain<I, O> {
constructor(private initialValue: I, private ops: ((value: any) => any)[] = []) { }

pipe<U, Args extends unknown[]>(fn: (value: T, ...args: Args) => U, ...args: Args): Chain<U> {
return new Chain(fn(this.value, ...args));
pipe<U>(fn: (value: O) => U): Chain<I, U> {
return new Chain<I, U>(this.initialValue, [...this.ops, fn]);
}

unwrap(): T {
return this.value;
value(): O {
return this.ops.reduce((acc, fn) => fn(acc), this.initialValue) as unknown as O;
}
}

/**
* Converts a function into a format suitable for use with the `Chain.pipe` method.
* This allows for the inclusion of additional parameters to the function prior to its
* execution in the chain. `$op` effectively curries the provided function, splitting
* its application into two steps: first, `$op` is called with the function to adapt,
* returning a new function. This returned function is then called with the additional
* arguments needed for the adapted function, and it returns yet another function that
* expects the current value in the chain as its input. This final function is suitable
* for use with `Chain.pipe`, as it matches the expected signature by taking a single
* argument—the current value in the chain—and applying the original function to it,
* along with the pre-specified additional arguments.
*
* @param fn The function to be converted. This function can take multiple parameters,
* with the first parameter intended to be the current value in the chain, and the
* rest being additional arguments provided during the subsequent call.
* @returns A function that, when called with additional arguments, returns a new
* function designed for the `Chain.pipe` method. This new function takes the current
* value in the chain as its sole argument and applies the original function with the
* specified additional arguments.
*
* @example
* // Assuming `add` and `multiply` are defined as:
* // const add = (x, y) => x + y;
* // const multiply = (x, y) => x * y;
*
* // Correct usage in a chain
* chain(5).pipe($op(add)(5)).pipe($op(multiply)(2)).value(); // returns 20
*/
export function $op<T, Args extends unknown[], U>(fn: (value: T, ...args: Args) => U) {
return (...args: Args) => (value: T) => fn(value, ...args);
}


/**
* Chains a value to be used in a pipeline.
* @param value value to chain
* @returns a chain of the value
* @example
* chain(5).pipe(add, 5).pipe(multiply, 2).unwrap() // returns 20
*/
export function chain<T>(value: T): Chain<T> {
return new Chain(value);
export function chain<T>(value: T): Chain<T, T> {
return new Chain<T, T>(value);
}
98 changes: 70 additions & 28 deletions tests/chain.test.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,76 @@
import { describe, test, it, expect } from 'bun:test'
import x from '../src/index'

describe('chain', () => {
// test using basic array operations
test("Chains array operations", () => {
const arr = [1, 2, 3, 4, 5];
const expected = [2, 4, 6, 8, 10];
const result = x.chain(arr)
.pipe(x.map, x => x * 2)
.unwrap();
expect(result).toEqual(expected);
import { $op, chain } from '../src/index'

// Define a simple function for testing
const add = (x: number, y: number): number => x + y;

describe('$op function', () => {
it('correctly curries a function with provided arguments', () => {
// Curry the `add` function with $op
const curriedAdd = $op(add);

// Prepare the curried function with one argument (5)
const addFive = curriedAdd(5);

// Now `addFive` should be a function that expects another number and adds 5 to it
const result = addFive(10); // This should be equivalent to `add(10, 5)`

// Verify the result is as expected
expect(result).toBe(15);
});

// test using basic string operations
test("Chains string operations", () => {
const str = "Hello world";
const expected = "HELLO WORLD";
const result = x.chain(str)
.pipe(x.upperCase)
.unwrap();
expect(result).toBe(expected);
it('returns a function that correctly applies all arguments to the original function', () => {
// Define a more complex function for testing
const subtract = (x: number, y: number, z: number): number => x - y - z;

// Curry the `subtract` function with $op
const curriedSubtract = $op(subtract);

// Prepare the curried function with two arguments (10, 5)
const subtractTenAndFive = curriedSubtract(10, 5);

// Now `subtractTenAndFive` should be a function that expects another number,
// subtracts 10 and then 5 from it
const result = subtractTenAndFive(20); // This should be equivalent to `subtract(20, 10, 5)`

// Verify the result is as expected
expect(result).toBe(5);
});
});

describe('Chain class', () => {
it('initializes with a value and unwraps it correctly', () => {
const initialValue = 10;
const result = chain(initialValue).value();

expect(result).toBe(initialValue);
});

it('applies a single operation to the initial value', () => {
const initialValue = 5;
const addFive = (x: number) => x + 5;
const result = chain(initialValue).pipe(addFive).value();

expect(result).toBe(10);
});

it('chains multiple operations and applies them in order', () => {
const initialValue = 5;
const addFive = (x: number) => x + 5;
const multiplyByTwo = (x: number) => x * 2;
const result = chain(initialValue).pipe(addFive).pipe(multiplyByTwo).value();

// Expected order of operations: (5 + 5) * 2
expect(result).toBe(20);
});

it('handles operations that change the type of the value', () => {
const initialValue = 5;
const toString = (x: number) => x.toString();
const addHello = (x: string) => `Hello ${x}`;
const result = chain(initialValue).pipe(toString).pipe(addHello).value();

// test using basic object operations
test("Chains object operations", () => {
const obj = { a: 1, b: 2, c: 3 };
const expected = { a: 2, b: 4, c: 6 };
const result = x.chain(obj)
.pipe(x.mapValues, x => x * 2)
.unwrap();
expect(result).toEqual(expected);
// Expected result is a string transformation of the initial value
expect(result).toBe("Hello 5");
});
})
});

0 comments on commit feb4c8c

Please sign in to comment.