Skip to content

Commit 90ce2fc

Browse files
authored
Merge pull request #5 from richecr/feat/added_validation_in_either_result
Feat/added validation in either and result
2 parents a14796b + b60df76 commit 90ce2fc

File tree

6 files changed

+279
-1
lines changed

6 files changed

+279
-1
lines changed

docs/either/index.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,24 @@ const result2 = calculate(10, 2)
9898
.map(n => n + 1)
9999
.mapLeft(e => console.log(`Error: ${e}`)) // Prints "Error: Result is too small"
100100
.unwrapOr(0);
101+
```
102+
103+
### `validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R>`
104+
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`.
105+
106+
```ts
107+
import { Left, Right } from 'holo-fn/either';
108+
109+
const result1 = new Right<string, number>(25).validate((n) => n >= 18, 'Must be 18+');
110+
console.log(result1.unwrapOr(0)); // 25
111+
112+
const result2 = new Right<string, number>(15).validate((n) => n >= 18, 'Must be 18+');
113+
console.log(result2.isLeft()); // true
114+
console.log(result2.unwrapOr(0)); // 0
115+
116+
const result3 = new Left<string, number>('Already failed').validate((n) => n >= 18, 'Must be 18+');
117+
console.log(result3.isLeft()); // true (keeps original error)
101118

102-
console.log(result2); // 0
103119
```
104120

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

388404
---
389405

406+
### `validate`
407+
408+
Curried version of `validate` for `Either`. This allows filtering/validating values in a functional pipeline with custom error values.
409+
410+
```ts
411+
import { right, validate, unwrapOr } from 'holo-fn/either';
412+
413+
const validateAge = (age: number) =>
414+
pipe(
415+
right<string, number>(age),
416+
validate((x: number) => x >= 0, 'Age cannot be negative'),
417+
validate((x: number) => x <= 150, 'Age too high'),
418+
validate((x: number) => x >= 18, 'Must be 18+'),
419+
unwrapOr(0)
420+
);
421+
422+
console.log(validateAge(25)); // 25
423+
console.log(validateAge(15)); // 0 (fails validation)
424+
```
425+
426+
**Common use cases:**
427+
428+
```ts
429+
import { right, tryCatch, validate } from 'holo-fn/either';
430+
431+
// Validate email format
432+
const validateEmail = (email: string) =>
433+
pipe(
434+
right<string, string>(email),
435+
validate((s: string) => s.length > 0, 'Email is required'),
436+
validate((s: string) => s.includes('@'), 'Must contain @'),
437+
validate((s: string) => s.includes('.'), 'Invalid domain')
438+
);
439+
440+
console.log(validateEmail('[email protected]').unwrapOr('Invalid email'));
441+
442+
// Parse and validate numbers
443+
const parsePositive = (input: string) =>
444+
pipe(
445+
tryCatch(
446+
() => parseInt(input, 10),
447+
() => 'Invalid number'
448+
),
449+
validate((n: number) => !isNaN(n), 'Not a number'),
450+
validate((n: number) => n > 0, 'Must be positive')
451+
);
452+
453+
console.log(parsePositive('42').unwrapOr(0));
454+
455+
// Validate with structured errors
456+
type ValidationError = { code: string; message: string };
457+
const validateUser = (age: number) =>
458+
pipe(
459+
right<ValidationError, number>(age),
460+
validate((x: number) => x >= 18, { code: 'AGE_ERROR', message: 'Must be 18+' })
461+
);
462+
463+
console.log(validateUser(20));
464+
console.log(validateUser(15));
465+
```
466+
467+
---
468+
390469
### `unwrapOr`
391470

392471
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`.

docs/result/index.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,22 @@ const result2 = new Err<number, string>("Error")
5757
console.log(result2); // 0
5858
```
5959

60+
### `validate(predicate: (value: T) => boolean, error: E): Result<T, E>`
61+
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`.
62+
63+
```ts
64+
import { Ok, Err } from "holo-fn/result";
65+
66+
const result1 = new Ok(25).validate((n) => n >= 18, 'Must be 18+');
67+
console.log(result1.unwrapOr(0)); // 25
68+
69+
const result2 = new Ok(15).validate((n) => n >= 18, 'Must be 18+');
70+
console.log(result2.isErr()); // true
71+
72+
const result3 = new Err<number, string>('Already failed').validate((n) => n >= 18, 'Must be 18+');
73+
console.log(result3.isErr()); // true (keeps original error)
74+
```
75+
6076
### `unwrapOr(defaultValue: T): T`
6177
Returns the value of `Ok`, or the default value for `Err`.
6278

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

278294
---
279295

296+
### `validate`
297+
298+
Curried version of `validate` for `Result`. This allows filtering/validating values in a functional pipeline with custom error messages.
299+
300+
```ts
301+
import { ok, validate, unwrapOr } from 'holo-fn/result';
302+
303+
const validateAge = (age: number) =>
304+
pipe(
305+
ok<number, string>(age),
306+
validate((x) => x >= 0, 'Age cannot be negative'),
307+
validate((x) => x <= 150, 'Age too high'),
308+
validate((x) => x >= 18, 'Must be 18+'),
309+
unwrapOr(0)
310+
);
311+
312+
console.log(validateAge(25)); // 25
313+
console.log(validateAge(15)); // 0 (fails validation)
314+
```
315+
316+
**Common use cases:**
317+
318+
```ts
319+
// Validate email format
320+
const validateEmail = (email: string) =>
321+
pipe(
322+
ok<string, string>(email),
323+
validate((s) => s.length > 0, 'Email is required'),
324+
validate((s) => s.includes('@'), 'Must contain @'),
325+
validate((s) => s.includes('.'), 'Invalid domain')
326+
);
327+
328+
console.log(validateEmail('[email protected]').unwrapOr('Invalid'));
329+
330+
// Parse and validate numbers
331+
const parsePositive = (input: string) =>
332+
pipe(
333+
fromThrowable(
334+
() => parseInt(input, 10),
335+
() => 'Invalid number'
336+
),
337+
validate((n) => !isNaN(n), 'Not a number'),
338+
validate((n) => n > 0, 'Must be positive')
339+
);
340+
341+
console.log(parsePositive('42').unwrapOr(0)); // 42
342+
console.log(parsePositive('-5').unwrapOr(0));
343+
344+
// Validate objects
345+
type User = { name: string; age: number };
346+
const validateUser = (user: User) =>
347+
pipe(
348+
ok<User, string>(user),
349+
validate((u) => u.name.length > 0, 'Name required'),
350+
validate((u) => u.age >= 18, 'Must be adult')
351+
);
352+
353+
console.log(validateUser({ name: 'Alice', age: 30 }).unwrapOr({ name: '', age: 0 }));
354+
console.log(validateUser({ name: '', age: 16 }).unwrapOr({ name: '', age: 0 }));
355+
```
356+
357+
---
358+
280359
### `unwrapOr`
281360

282361
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`.

src/either/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Either<L, R> {
55
map<U>(fn: (value: R) => U): Either<L, U>;
66
mapLeft<M>(fn: (err: L) => M): Either<M, R>;
77
chain<U>(fn: (value: R) => Either<L, U>): Either<L, U>;
8+
validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R>;
89
unwrapOr(defaultValue: R): R;
910
match<T>(cases: { left: (left: L) => T; right: (right: R) => T }): T;
1011
equals(other: Either<L, R>): boolean;
@@ -35,6 +36,10 @@ export class Right<L, R> implements Either<L, R> {
3536
return fn(this.value);
3637
}
3738

39+
validate(predicate: (value: R) => boolean, leftValue: L): Either<L, R> {
40+
return predicate(this.value) ? this : new Left<L, R>(leftValue);
41+
}
42+
3843
unwrapOr(_: R): R {
3944
return this.value;
4045
}
@@ -75,6 +80,10 @@ export class Left<L, R = never> implements Either<L, R> {
7580
return new Left<L, U>(this.value);
7681
}
7782

83+
validate(_predicate: (value: R) => boolean, _leftValue: L): Either<L, R> {
84+
return this;
85+
}
86+
7887
unwrapOr(defaultValue: R): R {
7988
return defaultValue;
8089
}
@@ -136,6 +145,12 @@ export const chain =
136145
return either.chain(fn);
137146
};
138147

148+
export const validate =
149+
<L, R>(predicate: (value: R) => boolean, leftValue: L) =>
150+
(either: Either<L, R>): Either<L, R> => {
151+
return either.validate(predicate, leftValue);
152+
};
153+
139154
export const unwrapOr =
140155
<L, R>(defaultValue: R) =>
141156
(either: Either<L, R>): R => {

src/result/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface Result<T, E> {
55
map<U>(fn: (value: T) => U): Result<U, E>;
66
mapErr<F>(fn: (err: E) => F): Result<T, F>;
77
chain<U>(fn: (value: T) => Result<U, E>): Result<U, E>;
8+
validate(predicate: (value: T) => boolean, error: E): Result<T, E>;
89
unwrapOr(defaultValue: T): T;
910
match<U>(cases: { ok: (value: T) => U; err: (err: E) => U }): U;
1011
equals(other: Result<T, E>): boolean;
@@ -35,6 +36,10 @@ export class Ok<T, E> implements Result<T, E> {
3536
return fn(this.value);
3637
}
3738

39+
validate(predicate: (value: T) => boolean, error: E): Result<T, E> {
40+
return predicate(this.value) ? this : new Err<T, E>(error);
41+
}
42+
3843
unwrapOr(_: T): T {
3944
return this.value;
4045
}
@@ -75,6 +80,10 @@ export class Err<T, E> implements Result<T, E> {
7580
return new Err<U, E>(this.error);
7681
}
7782

83+
validate(_predicate: (value: T) => boolean, _error: E): Result<T, E> {
84+
return this;
85+
}
86+
7887
unwrapOr(defaultValue: T): T {
7988
return defaultValue;
8089
}
@@ -142,6 +151,12 @@ export const chain =
142151
return result.chain(fn);
143152
};
144153

154+
export const validate =
155+
<T, E>(predicate: (value: T) => boolean, error: E) =>
156+
(result: Result<T, E>): Result<T, E> => {
157+
return result.validate(predicate, error);
158+
};
159+
145160
export const unwrapOr =
146161
<T, E>(defaultValue: T) =>
147162
(result: Result<T, E>): T => {

tests/either.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
right,
1515
tryCatch,
1616
unwrapOr,
17+
validate,
1718
} from '../src/either';
1819

1920
describe('Either', () => {
@@ -59,6 +60,31 @@ describe('Either', () => {
5960
expect(result.unwrapOr(0)).toBe(0);
6061
});
6162

63+
it('Right.validate should keep value when predicate returns true', () => {
64+
const result = new Right<string, number>(25).validate((x) => x >= 18, 'Must be 18+');
65+
expect(result.isRight()).toBe(true);
66+
expect(result.unwrapOr(0)).toBe(25);
67+
});
68+
69+
it('Right.validate should convert to Left when predicate returns false', () => {
70+
const result = new Right<string, number>(15).validate((x) => x >= 18, 'Must be 18+');
71+
expect(result.isLeft()).toBe(true);
72+
expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
73+
});
74+
75+
it('Right.validate should support chaining multiple validations', () => {
76+
const validateAge = (age: number) =>
77+
right<string, number>(age)
78+
.validate((x) => x >= 0, 'Age cannot be negative')
79+
.validate((x) => x <= 150, 'Age too high')
80+
.validate((x) => x >= 18, 'Must be 18+');
81+
82+
expect(validateAge(25).unwrapOr(0)).toBe(25);
83+
expect(validateAge(15).match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
84+
expect(validateAge(-5).match({ left: (e) => e, right: (_) => '' })).toBe('Age cannot be negative');
85+
expect(validateAge(200).match({ left: (e) => e, right: (_) => '' })).toBe('Age too high');
86+
});
87+
6288
it('Right.unwrapOr returns the value', () => {
6389
const result = new Right(42);
6490
expect(result.unwrapOr(0)).toBe(42);
@@ -249,6 +275,24 @@ describe('Either - Curried Helpers', () => {
249275
expect(result).toBe(0);
250276
});
251277

278+
it('Left.validate should not run predicate (short-circuit)', () => {
279+
const result = right<string, number>(15)
280+
.validate((x) => x >= 18, 'Must be 18+')
281+
.validate((x) => x <= 100, 'Too old');
282+
283+
expect(result.match({ left: (e) => e, right: (_) => '' })).toBe('Must be 18+');
284+
});
285+
286+
it('curried validate should work with pipe', () => {
287+
const result = pipe(
288+
right<string, number>(42),
289+
validate((x: number) => x > 0, 'Must be positive'),
290+
validate((x: number) => x % 2 === 0, 'Must be even'),
291+
unwrapOr(0)
292+
);
293+
expect(result).toBe(42);
294+
});
295+
252296
it('should unwrapOr with default value using curried unwrapOr', () => {
253297
const result = pipe(new Right(10), unwrapOr(42));
254298
expect(result).toBe(10);

0 commit comments

Comments
 (0)