Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion docs/either/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,24 @@ const result2 = calculate(10, 2)
.map(n => n + 1)
.mapLeft(e => console.log(`Error: ${e}`)) // Prints "Error: Result is too small"
.unwrapOr(0);
```

### `validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R>`
Validates the `Right` value based on a predicate. If the predicate returns `true`, keeps the value. If it returns `false`, converts to `Left` with the provided error. Does nothing for `Left`.

```ts
import { Left, Right } from 'holo-fn/either';

const result1 = new Right<string, number>(25).validate((n) => n >= 18, 'Must be 18+');
console.log(result1.unwrapOr(0)); // 25

const result2 = new Right<string, number>(15).validate((n) => n >= 18, 'Must be 18+');
console.log(result2.isLeft()); // true
console.log(result2.unwrapOr(0)); // 0

const result3 = new Left<string, number>('Already failed').validate((n) => n >= 18, 'Must be 18+');
console.log(result3.isLeft()); // true (keeps original error)

console.log(result2); // 0
```

### `unwrapOr(defaultValue: R): R`
Expand Down Expand Up @@ -387,6 +403,69 @@ console.log(result); // 10

---

### `validate`

Curried version of `validate` for `Either`. This allows filtering/validating values in a functional pipeline with custom error values.

```ts
import { right, validate, unwrapOr } from 'holo-fn/either';

const validateAge = (age: number) =>
pipe(
right<string, number>(age),
validate((x: number) => x >= 0, 'Age cannot be negative'),
validate((x: number) => x <= 150, 'Age too high'),
validate((x: number) => x >= 18, 'Must be 18+'),
unwrapOr(0)
);

console.log(validateAge(25)); // 25
console.log(validateAge(15)); // 0 (fails validation)
```

**Common use cases:**

```ts
import { right, tryCatch, validate } from 'holo-fn/either';

// Validate email format
const validateEmail = (email: string) =>
pipe(
right<string, string>(email),
validate((s: string) => s.length > 0, 'Email is required'),
validate((s: string) => s.includes('@'), 'Must contain @'),
validate((s: string) => s.includes('.'), 'Invalid domain')
);

console.log(validateEmail('[email protected]').unwrapOr('Invalid email'));

// Parse and validate numbers
const parsePositive = (input: string) =>
pipe(
tryCatch(
() => parseInt(input, 10),
() => 'Invalid number'
),
validate((n: number) => !isNaN(n), 'Not a number'),
validate((n: number) => n > 0, 'Must be positive')
);

console.log(parsePositive('42').unwrapOr(0));

// Validate with structured errors
type ValidationError = { code: string; message: string };
const validateUser = (age: number) =>
pipe(
right<ValidationError, number>(age),
validate((x: number) => x >= 18, { code: 'AGE_ERROR', message: 'Must be 18+' })
);

console.log(validateUser(20));
console.log(validateUser(15));
```

---

### `unwrapOr`

Curried version of `unwrapOr` for `Either`. This provides a cleaner way to unwrap the value in a `Either`, returning a default value if it's `Left`.
Expand Down
79 changes: 79 additions & 0 deletions docs/result/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,22 @@ const result2 = new Err<number, string>("Error")
console.log(result2); // 0
```

### `validate(predicate: (value: T) => boolean, error: E): Result<T, E>`
Validates the `Ok` value based on a predicate. If the predicate returns `true`, keeps the value. If it returns `false`, converts to `Err` with the provided error. Does nothing for `Err`.

```ts
import { Ok, Err } from "holo-fn/result";

const result1 = new Ok(25).validate((n) => n >= 18, 'Must be 18+');
console.log(result1.unwrapOr(0)); // 25

const result2 = new Ok(15).validate((n) => n >= 18, 'Must be 18+');
console.log(result2.isErr()); // true

const result3 = new Err<number, string>('Already failed').validate((n) => n >= 18, 'Must be 18+');
console.log(result3.isErr()); // true (keeps original error)
```

### `unwrapOr(defaultValue: T): T`
Returns the value of `Ok`, or the default value for `Err`.

Expand Down Expand Up @@ -277,6 +293,69 @@ console.log(result); // 15

---

### `validate`

Curried version of `validate` for `Result`. This allows filtering/validating values in a functional pipeline with custom error messages.

```ts
import { ok, validate, unwrapOr } from 'holo-fn/result';

const validateAge = (age: number) =>
pipe(
ok<number, string>(age),
validate((x) => x >= 0, 'Age cannot be negative'),
validate((x) => x <= 150, 'Age too high'),
validate((x) => x >= 18, 'Must be 18+'),
unwrapOr(0)
);

console.log(validateAge(25)); // 25
console.log(validateAge(15)); // 0 (fails validation)
```

**Common use cases:**

```ts
// Validate email format
const validateEmail = (email: string) =>
pipe(
ok<string, string>(email),
validate((s) => s.length > 0, 'Email is required'),
validate((s) => s.includes('@'), 'Must contain @'),
validate((s) => s.includes('.'), 'Invalid domain')
);

console.log(validateEmail('[email protected]').unwrapOr('Invalid'));

// Parse and validate numbers
const parsePositive = (input: string) =>
pipe(
fromThrowable(
() => parseInt(input, 10),
() => 'Invalid number'
),
validate((n) => !isNaN(n), 'Not a number'),
validate((n) => n > 0, 'Must be positive')
);

console.log(parsePositive('42').unwrapOr(0)); // 42
console.log(parsePositive('-5').unwrapOr(0));

// Validate objects
type User = { name: string; age: number };
const validateUser = (user: User) =>
pipe(
ok<User, string>(user),
validate((u) => u.name.length > 0, 'Name required'),
validate((u) => u.age >= 18, 'Must be adult')
);

console.log(validateUser({ name: 'Alice', age: 30 }).unwrapOr({ name: '', age: 0 }));
console.log(validateUser({ name: '', age: 16 }).unwrapOr({ name: '', age: 0 }));
```

---

### `unwrapOr`

Curried version of `unwrapOr` for `Result`. This provides a cleaner way to unwrap the value in a `Result`, returning a default value if it's `Err`.
Expand Down
15 changes: 15 additions & 0 deletions src/either/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Either<L, R> {
map<U>(fn: (value: R) => U): Either<L, U>;
mapLeft<M>(fn: (err: L) => M): Either<M, R>;
chain<U>(fn: (value: R) => Either<L, U>): Either<L, U>;
validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R>;
unwrapOr(defaultValue: R): R;
match<T>(cases: { left: (left: L) => T; right: (right: R) => T }): T;
equals(other: Either<L, R>): boolean;
Expand Down Expand Up @@ -35,6 +36,10 @@ export class Right<L, R> implements Either<L, R> {
return fn(this.value);
}

validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R> {
return predicate(this.value) ? this : new Left<L, R>(leftValue);
}

unwrapOr(_: R): R {
return this.value;
}
Expand Down Expand Up @@ -75,6 +80,10 @@ export class Left<L, R = never> implements Either<L, R> {
return new Left<L, U>(this.value);
}

validate(_predicate: (value: R) => boolean, _leftValue: L): Either<L, R> {
return this;
}

unwrapOr(defaultValue: R): R {
return defaultValue;
}
Expand Down Expand Up @@ -136,6 +145,12 @@ export const chain =
return either.chain(fn);
};

export const validate =
<L, R>(predicate: (value: R) => boolean, leftValue: L) =>
(either: Either<L, R>): Either<L, R> => {
return either.validate(predicate, leftValue);
};

export const unwrapOr =
<L, R>(defaultValue: R) =>
(either: Either<L, R>): R => {
Expand Down
15 changes: 15 additions & 0 deletions src/result/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Result<T, E> {
map<U>(fn: (value: T) => U): Result<U, E>;
mapErr<F>(fn: (err: E) => F): Result<T, F>;
chain<U>(fn: (value: T) => Result<U, E>): Result<U, E>;
validate(predicate: (value: T) => boolean, error: E): Result<T, E>;
unwrapOr(defaultValue: T): T;
match<U>(cases: { ok: (value: T) => U; err: (err: E) => U }): U;
equals(other: Result<T, E>): boolean;
Expand Down Expand Up @@ -35,6 +36,10 @@ export class Ok<T, E> implements Result<T, E> {
return fn(this.value);
}

validate(predicate: (value: T) => boolean, error: E): Result<T, E> {
return predicate(this.value) ? this : new Err<T, E>(error);
}

unwrapOr(_: T): T {
return this.value;
}
Expand Down Expand Up @@ -75,6 +80,10 @@ export class Err<T, E> implements Result<T, E> {
return new Err<U, E>(this.error);
}

validate(_predicate: (value: T) => boolean, _error: E): Result<T, E> {
return this;
}

unwrapOr(defaultValue: T): T {
return defaultValue;
}
Expand Down Expand Up @@ -142,6 +151,12 @@ export const chain =
return result.chain(fn);
};

export const validate =
<T, E>(predicate: (value: T) => boolean, error: E) =>
(result: Result<T, E>): Result<T, E> => {
return result.validate(predicate, error);
};

export const unwrapOr =
<T, E>(defaultValue: T) =>
(result: Result<T, E>): T => {
Expand Down
44 changes: 44 additions & 0 deletions tests/either.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
right,
tryCatch,
unwrapOr,
validate,
} from '../src/either';

describe('Either', () => {
Expand Down Expand Up @@ -59,6 +60,31 @@ describe('Either', () => {
expect(result.unwrapOr(0)).toBe(0);
});

it('Right.validate should keep value when predicate returns true', () => {
const result = new Right<string, number>(25).validate((x) => x >= 18, 'Must be 18+');
expect(result.isRight()).toBe(true);
expect(result.unwrapOr(0)).toBe(25);
});

it('Right.validate should convert to Left when predicate returns false', () => {
const result = new Right<string, number>(15).validate((x) => x >= 18, 'Must be 18+');
expect(result.isLeft()).toBe(true);
expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
});

it('Right.validate should support chaining multiple validations', () => {
const validateAge = (age: number) =>
right<string, number>(age)
.validate((x) => x >= 0, 'Age cannot be negative')
.validate((x) => x <= 150, 'Age too high')
.validate((x) => x >= 18, 'Must be 18+');

expect(validateAge(25).unwrapOr(0)).toBe(25);
expect(validateAge(15).match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
expect(validateAge(-5).match({ left: (e) => e, right: (_) => '' })).toBe('Age cannot be negative');
expect(validateAge(200).match({ left: (e) => e, right: (_) => '' })).toBe('Age too high');
});

it('Right.unwrapOr returns the value', () => {
const result = new Right(42);
expect(result.unwrapOr(0)).toBe(42);
Expand Down Expand Up @@ -249,6 +275,24 @@ describe('Either - Curried Helpers', () => {
expect(result).toBe(0);
});

it('Left.validate should not run predicate (short-circuit)', () => {
const result = right<string, number>(15)
.validate((x) => x >= 18, 'Must be 18+')
.validate((x) => x <= 100, 'Too old');

expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
});

it('curried validate should work with pipe', () => {
const result = pipe(
right<string, number>(42),
validate((x: number) => x > 0, 'Must be positive'),
validate((x: number) => x % 2 === 0, 'Must be even'),
unwrapOr(0)
);
expect(result).toBe(42);
});

it('should unwrapOr with default value using curried unwrapOr', () => {
const result = pipe(new Right(10), unwrapOr(42));
expect(result).toBe(10);
Expand Down
Loading