Skip to content

Commit 39a68a9

Browse files
authored
Merge pull request #8 from GregOnNet/feat/result-combine
feat(result): support combining multiple results
2 parents 6e27ccb + 1b3fda2 commit 39a68a9

File tree

5 files changed

+152
-14
lines changed

5 files changed

+152
-14
lines changed

package-lock.json

+41-2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
},
4848
"devDependencies": {
4949
"@babel/preset-typescript": "7.16.7",
50+
"@jest/expect-utils": "^28.0.0-alpha.0",
5051
"@types/jest": "27.4.1",
5152
"cpy-cli": "4.1.0",
5253
"jest": "27.5.1",

src/result.ts

+45
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,55 @@ import {
2121
Some,
2222
} from './utilities';
2323

24+
/**
25+
* Allows to extract the Value of the given Result-Type
26+
* e.g. ResultValueOf<Result<string>> => string
27+
*/
28+
export type ResultValueOf<T> = T extends Result<infer TResultValue>
29+
? TResultValue
30+
: unknown;
31+
2432
/**
2533
* Represents a successful or failed operation
2634
*/
2735
export class Result<TValue = Unit, TError = string> {
36+
/**
37+
* Combines several results (and any error messages) into a single result.
38+
* The returned result will be a failure if any of the input results are failures.
39+
*
40+
* @param results The Results to be combined.
41+
* @returns A Result that is a success when all the input results are also successes.
42+
*/
43+
static combine<T extends Record<string, Result<unknown>>>(
44+
results: T
45+
): Result<{ [K in keyof T]: ResultValueOf<T[K]> }> {
46+
const resultEntries = Object.entries(results);
47+
48+
const failedResults = resultEntries.filter(
49+
([, result]) => result.isFailure
50+
);
51+
const succeededResults = resultEntries.filter(
52+
([, result]) => result.isSuccess
53+
);
54+
55+
if (failedResults.length === 0) {
56+
const values = succeededResults.reduce((resultValues, [key, result]) => {
57+
resultValues[key] = result.getValueOrThrow();
58+
return resultValues;
59+
}, {} as { [key: string]: unknown });
60+
61+
return Result.success(
62+
values as Some<{ [K in keyof T]: ResultValueOf<T[K]> }>
63+
);
64+
}
65+
66+
const errorMessages = failedResults
67+
.map(([, result]) => result.getErrorOrThrow())
68+
.join(', ');
69+
70+
return Result.failure(errorMessages);
71+
}
72+
2873
/**
2974
* Creates a new successful Result with a string error type
3075
* and Unit value type

test/expectExtensions.ts

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Maybe } from '@/src/maybe';
22
import { Result } from '@/src/result';
33
import { FunctionOfT, isDefined, isFunction } from '@/src/utilities';
4+
import { equals } from '@jest/expect-utils';
45

56
expect.extend({
67
toHaveNoValue<TValue>(received: Maybe<TValue>): MatchResponse {
@@ -25,7 +26,7 @@ expect.extend({
2526

2627
const value = received.getValueOrThrow();
2728

28-
return value === expectedValue
29+
return equals(value, expectedValue)
2930
? r(
3031
true,
3132
`expected [${received}] to have a value [${expectedValue}], but it has a value of [${value}]`
@@ -66,17 +67,15 @@ expect.extend({
6667

6768
const value = received.getValueOrThrow();
6869

69-
if (expectedValue === value) {
70-
return r(
71-
true,
72-
`expected [${received}] to be successful but not have value [${expectedValue}] but it did`
73-
);
74-
}
75-
76-
return r(
77-
false,
78-
`expected [${received}] to have value [${expectedValue}], but found value [${value}]`
79-
);
70+
return equals(expectedValue, value)
71+
? r(
72+
true,
73+
`expected [${received}] to be successful but not have value [${expectedValue}] but it did`
74+
)
75+
: r(
76+
false,
77+
`expected [${received}] to have value [${expectedValue}], but found value [${value}]`
78+
);
8079
},
8180
toFail<TValue, TError>(received: Result<TValue, TError>) {
8281
return received.isSuccess

test/result/combine.spec.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Result } from '@/src/result';
2+
3+
describe('Result', () => {
4+
describe('combine', () => {
5+
test('fails if one result fails', () => {
6+
const success = Result.success(1);
7+
const failure = Result.failure('1st Error');
8+
9+
const result = Result.combine({ success, failure });
10+
11+
expect(result).toFailWith('1st Error');
12+
});
13+
14+
test('succeeds if one results succeed', () => {
15+
const success = Result.success(1);
16+
17+
const result = Result.combine({ success });
18+
19+
expect(result).toSucceed();
20+
});
21+
22+
test('succeeds if all results succeed', () => {
23+
const success_1 = Result.success(1);
24+
const success_2 = Result.success(2);
25+
26+
const result = Result.combine({ success_1, success_2 });
27+
28+
expect(result).toSucceed();
29+
});
30+
31+
test('yields all result values on success', () => {
32+
const success_1 = Result.success(1);
33+
const success_2 = Result.success({ name: 'Arthur' });
34+
35+
const result = Result.combine({ success_1, success_2 });
36+
37+
expect(result).toSucceedWith({
38+
success_1: 1,
39+
success_2: { name: 'Arthur' },
40+
});
41+
});
42+
43+
test('concatenates error messages', () => {
44+
const failure_1_message = '1st Error';
45+
const failure_1 = Result.failure(failure_1_message);
46+
const failure_2_message = '2nd Error';
47+
const failure_2 = Result.failure(failure_2_message);
48+
49+
const result = Result.combine({ failure_1, failure_2 });
50+
51+
expect(result).toFailWith(`${failure_1_message}, ${failure_2_message}`);
52+
});
53+
});
54+
});

0 commit comments

Comments
 (0)