Skip to content

Commit 5567f43

Browse files
authored
Merge pull request #35 from GregOnNet/feature/combine-in-order
feature: combineInOrderAsync
2 parents 25b47dc + f7f9e12 commit 5567f43

5 files changed

Lines changed: 217 additions & 16 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010
},
1111

1212
"editor.codeActionsOnSave": {
13-
"source.fixAll": true,
14-
"source.organizeImports": true
13+
"source.fixAll": "explicit",
14+
"source.organizeImports": "explicit"
1515
},
1616

1717
"editor.defaultFormatter": "esbenp.prettier-vscode",

src/result.ts

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ import {
2323
} from './utilities.js';
2424

2525
/**
26-
* Allows to extract the Value of the given Result-Type
27-
* e.g. ResultValueOf<Result<string>> => string
26+
* Allows to extract the Value of the given `Result` or `ResultAsync`
27+
*
28+
* @example ResultValue<Result<string>> => string
29+
* @example ResultValue<ResultAsync<string>> => string
2830
*/
29-
export type ResultValueOf<T> = T extends Result<infer TResultValue>
31+
export type ResultValue<T> = T extends Result<infer TResultValue>
3032
? TResultValue
31-
: unknown;
33+
: T extends ResultAsync<infer TResultAsyncValue>
34+
? TResultAsyncValue
35+
: never;
36+
37+
export type ResultRecord<TResultRecord> = {
38+
[K in keyof TResultRecord]: ResultValue<TResultRecord[K]>;
39+
};
3240

3341
/**
3442
* Represents a successful Result operation.
@@ -55,9 +63,9 @@ export class Result<TValue = Unit, TError = string> {
5563
* @param results The Results to be combined.
5664
* @returns A Result that is a success when all the input results are also successes.
5765
*/
58-
static combine<T extends Record<string, Result<unknown>>>(
59-
results: T
60-
): Result<{ [K in keyof T]: ResultValueOf<T[K]> }> {
66+
static combine<TResultRecord extends Record<string, Result<unknown>>>(
67+
results: TResultRecord
68+
): Result<ResultRecord<TResultRecord>> {
6169
const resultEntries = Object.entries(results);
6270

6371
const failedResults = resultEntries.filter(
@@ -74,7 +82,9 @@ export class Result<TValue = Unit, TError = string> {
7482
}, {} as { [key: string]: unknown });
7583

7684
return Result.success(
77-
values as Some<{ [K in keyof T]: ResultValueOf<T[K]> }>
85+
values as Some<{
86+
[K in keyof TResultRecord]: ResultValue<TResultRecord[K]>;
87+
}>
7888
);
7989
}
8090

@@ -85,6 +95,12 @@ export class Result<TValue = Unit, TError = string> {
8595
return Result.failure(errorMessages);
8696
}
8797

98+
static combineInOrderAsync<
99+
TResultRecord extends Record<string, Result<unknown> | ResultAsync<unknown>>
100+
>(record: TResultRecord): ResultAsync<ResultRecord<TResultRecord>> {
101+
return ResultAsync.combineInOrder(record);
102+
}
103+
88104
/**
89105
* Creates a new successful Result with a string error type
90106
* and Unit value type
@@ -692,7 +708,7 @@ export class Result<TValue = Unit, TError = string> {
692708
/**
693709
* Maps the value successful Result to a new async value wrapped in a ResultAsync
694710
* @param projection a function given the value of the current Result which returns a Promise of some value
695-
711+
696712
* @returns
697713
*/
698714
mapAsync<TNewValue>(
@@ -706,7 +722,7 @@ export class Result<TValue = Unit, TError = string> {
706722
/**
707723
* Maps the error of a failed Result to a new async value wrapped in a ResultAsync
708724
* @param projection a function given the error of the current Result which returns a Promise of some value
709-
725+
710726
* @returns
711727
*/
712728
mapFailureAsync(
@@ -733,7 +749,7 @@ export class Result<TValue = Unit, TError = string> {
733749
/**
734750
* Maps a successful Result to a new ResultAsync
735751
* @param projection
736-
752+
737753
*/
738754
bindAsync<TNewValue>(
739755
projection: FunctionOfTtoK<TValue, Promise<Result<TNewValue, TError>>>
@@ -748,7 +764,7 @@ export class Result<TValue = Unit, TError = string> {
748764
/**
749765
* Maps a successful Result to a new ResultAsync
750766
* @param projection
751-
767+
752768
* @returns
753769
*/
754770
bindAsync<TNewValue>(
@@ -856,7 +872,7 @@ export class Result<TValue = Unit, TError = string> {
856872
/**
857873
* Executes an async action if the Result succeeded
858874
* @param action a function given the Result's value returns a Promise
859-
875+
860876
* @returns a ResultAsync
861877
*/
862878
tapAsync(action: AsyncActionOfT<TValue>): ResultAsync<TValue, TError> {

src/resultAsync.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Result } from './result.js';
1+
import { Result, type ResultRecord } from './result.js';
22
import { Unit } from './unit.js';
33
import {
44
Action,
@@ -23,6 +23,50 @@ import {
2323
* Represents and asynchronous Result that could succeed with a value or fail with an error
2424
*/
2525
export class ResultAsync<TValue = Unit, TError = string> {
26+
/**
27+
* Combines several results (and any error messages) into a single result.
28+
* The returned result will be a failure if any of the input results are failures.
29+
*
30+
* Asynchronous operations are executed one after another.
31+
*
32+
* @param results The Results to be combined.
33+
* @returns A Result that is a success when all the input results are also successes.
34+
*/
35+
static combineInOrder<
36+
TResultRecord extends Record<string, Result<unknown> | ResultAsync<unknown>>
37+
>(results: TResultRecord): ResultAsync<ResultRecord<TResultRecord>> {
38+
const entries = Object.entries(results);
39+
40+
const promiseResult = entries
41+
.reduce(
42+
async (accumulator, [key, result]) => {
43+
const sink = await accumulator;
44+
45+
const resolvedResult =
46+
result instanceof Result ? result : await result.toPromise();
47+
48+
resolvedResult.isSuccess
49+
? (sink.values[key] = resolvedResult.getValueOrThrow())
50+
: sink.errors.push(resolvedResult.getErrorOrThrow());
51+
52+
return sink;
53+
},
54+
Promise.resolve({
55+
errors: [],
56+
values: {},
57+
} as { errors: string[]; values: { [key: string]: unknown } })
58+
)
59+
.then((valuesAndErrors) =>
60+
valuesAndErrors.errors.length
61+
? Result.failure(valuesAndErrors.errors.join(', '))
62+
: Result.success(valuesAndErrors.values)
63+
);
64+
65+
return ResultAsync.from(
66+
promiseResult as Promise<Result<ResultRecord<TResultRecord>>>
67+
);
68+
}
69+
2670
/**
2771
* Creates a new ResultAsync from the given Result
2872
* @param value a successful or failed Result
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { Result } from '@/src/result';
2+
3+
describe('Result', () => {
4+
describe('combineInOrderAsync', () => {
5+
test('fails if one result fails', async () => {
6+
const success = Result.success(1);
7+
const failure = Result.failure('1st Error');
8+
9+
const sut = await Result.combineInOrderAsync({
10+
success,
11+
failure,
12+
}).toPromise();
13+
14+
expect(sut).toFailWith('1st Error');
15+
});
16+
17+
test('succeeds if one results succeed', async () => {
18+
const success = Result.success(1);
19+
20+
const sut = await Result.combineInOrderAsync({ success }).toPromise();
21+
22+
expect(sut).toSucceed();
23+
});
24+
25+
test('concatenates error messages', async () => {
26+
const failure_1_message = '1st Error';
27+
const failure_1 = Result.failure(failure_1_message);
28+
const failure_2_message = '2nd Error';
29+
const failure_2 = Result.failure(failure_2_message);
30+
31+
const sut = await Result.combineInOrderAsync({
32+
failure_1,
33+
failure_2,
34+
}).toPromise();
35+
36+
expect(sut).toFailWith(`${failure_1_message}, ${failure_2_message}`);
37+
});
38+
});
39+
});
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Result } from '@/src/result';
2+
import { ResultAsync } from '@/src/resultAsync';
3+
4+
describe('ResultAsync', () => {
5+
describe('combineInOrder', () => {
6+
it('succeeds with one successful ResultAsync', async () => {
7+
const sut = await ResultAsync.combineInOrder({
8+
success: ResultAsync.success('✅'),
9+
}).toPromise();
10+
11+
expect(sut).toSucceedWith({
12+
success: '✅',
13+
});
14+
});
15+
16+
it('succeeds with two successful ResultAsync', async () => {
17+
const sut = await ResultAsync.combineInOrder({
18+
first: ResultAsync.success('✅'),
19+
second: ResultAsync.success('✅'),
20+
}).toPromise();
21+
22+
expect(sut).toSucceedWith({
23+
first: '✅',
24+
second: '✅',
25+
});
26+
});
27+
28+
it('fails with one failed ResultAsync', async () => {
29+
const sut = await ResultAsync.combineInOrder({
30+
failure: ResultAsync.failure('💥'),
31+
}).toPromise();
32+
33+
expect(sut).toFailWith('💥');
34+
});
35+
36+
it('fails with one successful and one failed ResultAsync', async () => {
37+
const sut = await ResultAsync.combineInOrder({
38+
success: ResultAsync.success('✅'),
39+
failure: ResultAsync.failure('💥'),
40+
}).toPromise();
41+
42+
expect(sut).toFailWith('💥');
43+
});
44+
45+
it('succeeds with one successful Result', async () => {
46+
const sut = await ResultAsync.combineInOrder({
47+
success: Result.success('✅'),
48+
}).toPromise();
49+
50+
expect(sut).toSucceedWith({
51+
success: '✅',
52+
});
53+
});
54+
55+
it('succeeds with two successful Results', async () => {
56+
const sut = await ResultAsync.combineInOrder({
57+
first: Result.success('✅'),
58+
second: Result.success('✅'),
59+
}).toPromise();
60+
61+
expect(sut).toSucceedWith({
62+
first: '✅',
63+
second: '✅',
64+
});
65+
});
66+
67+
it('fails with one failed Result', async () => {
68+
const sut = await ResultAsync.combineInOrder({
69+
failure: Result.failure('💥'),
70+
}).toPromise();
71+
72+
expect(sut).toFailWith('💥');
73+
});
74+
75+
it('fails with one successful and one failed Result', async () => {
76+
const sut = await ResultAsync.combineInOrder({
77+
success: Result.success('✅'),
78+
failure: Result.failure('💥'),
79+
}).toPromise();
80+
81+
expect(sut).toFailWith('💥');
82+
});
83+
84+
it('fails with mixed types when one fails', async () => {
85+
const sut = await ResultAsync.combineInOrder({
86+
sut: Result.success('✅'),
87+
sutAsync: ResultAsync.failure('💥'),
88+
}).toPromise();
89+
90+
expect(sut).toFailWith('💥');
91+
});
92+
93+
it('concatenates failures', async () => {
94+
const sut = await ResultAsync.combineInOrder({
95+
sut: Result.failure('💥'),
96+
sutAsync: ResultAsync.failure('💥'),
97+
}).toPromise();
98+
99+
expect(sut).toFailWith('💥, 💥');
100+
});
101+
});
102+
});

0 commit comments

Comments
 (0)