Skip to content

Commit 0704f58

Browse files
authored
Merge pull request #2 from webcompere/async-optional
Add async optional
2 parents a08994e + 7d2fdcb commit 0704f58

File tree

6 files changed

+422
-2
lines changed

6 files changed

+422
-2
lines changed

README.md

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -697,8 +697,51 @@ Optional.of('foo').ifPresentOrElse(
697697
698698
### Optional
699699
700-
Optional supports `mapAsync`, `flatMapAsync` and `filterAsync` which take either a synchronous or
701-
`async` version of the `Mapper` or `Predicate` and return a `Promise` of an `Optional`.
700+
For one-off async operations, we could continue to use `Optional`, which supports `mapAsync`, `flatMapAsync` and `filterAsync` which take either a synchronous or
701+
`async` version of the `Mapper` or `Predicate` and returns a `Promise<Optional>` which can be `await`ed
702+
to get the actual optional.
703+
704+
However, for fluent async operations on the `Promise` of an `Optional`, we can use `AsyncOptional`.
705+
706+
We can convert an `Optional` to `AsyncOptional`:
707+
708+
```ts
709+
const asyncOptional = Optional.of('someValue').async();
710+
```
711+
712+
Or we can construct an `AsyncOptional` from a `Promise` of a value:
713+
714+
```ts
715+
const asyncOptional = AsyncOptional.of(functionThatReturnsPromise());
716+
```
717+
718+
Then we can use `map`, `filter`, `flatMap` on the `AsyncOptional` with a mix of synchronous functions,
719+
or functions that return promises.
720+
721+
```ts
722+
const asyncOptional = AsyncOptional.of('someValue')
723+
.map(someAsyncFunction)
724+
.filter((s) => s !== 'error')
725+
.map((s) => `${s}!`)
726+
.filter(asyncLookupFunction);
727+
```
728+
729+
The terminal operations of the `AsyncOptional`, such as `get`, `isPresent`, `isEmpty`, `orElse`, etc
730+
must themselves be awaited, and the `AsyncOptional` can be converted back to an `Optional` by awaiting it:
731+
732+
```ts
733+
const value = await AsyncOptional.of(callAsync())
734+
.filter(someAsyncFilter)
735+
.toOptional(); // now we have a standard optional
736+
```
737+
738+
or
739+
740+
```ts
741+
const value = await AsyncOptional.of(callAsync())
742+
.filter(someAsyncFilter)
743+
.orElseGet(someAsyncSupplierOfNewValue);
744+
```
702745
703746
#### See Also
704747

src/AsyncOptional.test.ts

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import AsyncOptional from './AsyncOptional';
2+
import Optional from './Optional';
3+
4+
describe('AsyncOptional', () => {
5+
describe('construction', () => {
6+
it('can be constructed from a promise', async () => {
7+
expect(await AsyncOptional.of(Promise.resolve('foo')).get()).toBe('foo');
8+
});
9+
10+
it('can be constructed from a value', async () => {
11+
expect(await AsyncOptional.of('foo').get()).toBe('foo');
12+
});
13+
14+
it('can be constructed from a neighbouring optional', async () => {
15+
const asyncOptional = Optional.of('foo').async();
16+
expect(await asyncOptional.get()).toBe('foo');
17+
});
18+
});
19+
20+
describe('getting', () => {
21+
it('an async optional can be get-ed', async () => {
22+
expect(await AsyncOptional.of('foo').get()).toBe('foo');
23+
});
24+
25+
it('an empty async optional can be get-ed', async () => {
26+
expect(await AsyncOptional.empty().get()).toBeUndefined();
27+
});
28+
29+
it('can convert back to a standard optional', async () => {
30+
expect((await AsyncOptional.of('foo').toOptional()).get()).toBe('foo');
31+
});
32+
});
33+
34+
describe('filter', () => {
35+
it('can filter an optional with a sync function', async () => {
36+
const optional = Optional.of(27)
37+
.async()
38+
.filter((val) => val % 2 === 0);
39+
expect(await optional.isPresent()).toBeFalsy();
40+
});
41+
42+
it('can filter an optional with a sync function and check with isEmpty', async () => {
43+
const optional = Optional.of(27)
44+
.async()
45+
.filter((val) => val % 2 === 0);
46+
expect(await optional.isEmpty()).toBeTruthy();
47+
});
48+
49+
it('can filter an optional with an async function', async () => {
50+
const optional = Optional.of(27)
51+
.async()
52+
.filter(async (val) => val % 2 === 0);
53+
expect(await optional.isPresent()).toBeFalsy();
54+
});
55+
});
56+
57+
describe('map', () => {
58+
it('can map an empty optional with a synchronous function', async () => {
59+
const optional = Optional.empty<number>()
60+
.async()
61+
.map((val) => val * 2);
62+
expect(await optional.get()).toBeUndefined();
63+
});
64+
65+
it('can map an optional with a synchronous function', async () => {
66+
const optional = Optional.of(7)
67+
.async()
68+
.map((val) => val * 2);
69+
expect(await optional.get()).toBe(14);
70+
});
71+
72+
it('can map an optional with an asynchronous function', async () => {
73+
const optional = Optional.of(7)
74+
.async()
75+
.map(async (val) => val * 2);
76+
expect(await optional.get()).toBe(14);
77+
});
78+
});
79+
80+
describe('flat mapping', () => {
81+
it('can flatmap with an async function', async () => {
82+
const optional = Optional.of(7)
83+
.async()
84+
.flatMap(async () => Optional.of(14));
85+
expect(await optional.get()).toBe(14);
86+
});
87+
88+
it('can flatmap with a synchronouse function', async () => {
89+
const optional = Optional.of(7)
90+
.async()
91+
.flatMap(() => Optional.of(14));
92+
expect(await optional.get()).toBe(14);
93+
});
94+
});
95+
96+
describe('or else', () => {
97+
it('resolves empty with or else', async () => {
98+
expect(await AsyncOptional.empty<number>().orElse(2)).toBe(2);
99+
});
100+
101+
it('resolves full with or else', async () => {
102+
expect(await AsyncOptional.of(10).orElse(2)).toBe(10);
103+
});
104+
105+
it('resolves empty with or else throw to throw', async () => {
106+
await expect(
107+
AsyncOptional.empty<number>().orElseThrow()
108+
).rejects.toThrow();
109+
});
110+
111+
it('resolves empty with or else throw to throw custom error', async () => {
112+
await expect(
113+
AsyncOptional.empty<number>().orElseThrow(() => new Error('boom'))
114+
).rejects.toThrow('boom');
115+
});
116+
117+
it('resolves full with or else', async () => {
118+
await expect(AsyncOptional.of(3).orElseThrow()).resolves.toBe(3);
119+
});
120+
121+
it('or else gets with a synchronous supplier of a value when there is a value', async () => {
122+
expect(await AsyncOptional.of(10).orElseGet(() => 2)).toBe(10);
123+
});
124+
125+
it('or elses with a synchronous supplier of a value', async () => {
126+
expect(await AsyncOptional.empty<number>().orElseGet(() => 2)).toBe(2);
127+
});
128+
129+
it('or elses with an asynchronous supplier of a value', async () => {
130+
expect(await AsyncOptional.empty<number>().orElseGet(async () => 2)).toBe(
131+
2
132+
);
133+
});
134+
});
135+
136+
describe('if present', () => {
137+
it('if not present does nothing', async () => {
138+
const ifPresent = vi.fn();
139+
await AsyncOptional.empty().ifPresent(ifPresent);
140+
141+
expect(ifPresent).not.toHaveBeenCalled();
142+
});
143+
144+
it('if present does something', async () => {
145+
const ifPresent = vi.fn();
146+
await AsyncOptional.of('foo').ifPresent(ifPresent);
147+
148+
expect(ifPresent).toHaveBeenCalled();
149+
});
150+
151+
it('if not present calls or else', async () => {
152+
const ifPresent = vi.fn();
153+
const orElse = vi.fn();
154+
await AsyncOptional.empty().ifPresentOrElse(ifPresent, orElse);
155+
156+
expect(ifPresent).not.toHaveBeenCalled();
157+
expect(orElse).toHaveBeenCalled();
158+
});
159+
160+
it('if present does something', async () => {
161+
const ifPresent = vi.fn();
162+
const orElse = vi.fn();
163+
await AsyncOptional.of('foo').ifPresentOrElse(ifPresent, orElse);
164+
165+
expect(ifPresent).toHaveBeenCalled();
166+
expect(orElse).not.toHaveBeenCalled();
167+
});
168+
});
169+
});

0 commit comments

Comments
 (0)