rsult 1.4.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +7 -6
- package/readme.md +161 -5
- package/rust/option.rs +2822 -0
- package/rust/result.rs +2207 -0
- package/src/lib.ts +3 -1
- package/src/option-async.test.ts +410 -0
- package/src/option-async.ts +467 -0
- package/src/option.test.ts +101 -0
- package/src/option.ts +480 -266
- package/src/result-async.test.ts +485 -0
- package/src/result-async.ts +635 -0
- package/src/result.test.ts +36 -0
- package/src/result.ts +418 -340
- package/src/types.test.ts +409 -0
- package/dist/lib.d.ts +0 -2
- package/dist/lib.js +0 -19
- package/dist/lib.js.map +0 -1
- package/dist/option.d.ts +0 -307
- package/dist/option.js +0 -195
- package/dist/option.js.map +0 -1
- package/dist/result.d.ts +0 -410
- package/dist/result.js +0 -231
- package/dist/result.js.map +0 -1
package/src/lib.ts
CHANGED
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
import { OptionAsync } from './option-async';
|
|
2
|
+
import { Option, Some, None } from './option';
|
|
3
|
+
|
|
4
|
+
describe('OptionAsync', () => {
|
|
5
|
+
describe('Constructors', () => {
|
|
6
|
+
it('some() creates OptionAsync with value', async () => {
|
|
7
|
+
const option = await OptionAsync.some(42);
|
|
8
|
+
expect(option.is_some()).toBe(true);
|
|
9
|
+
expect(option.unwrap()).toBe(42);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('none() creates empty OptionAsync', async () => {
|
|
13
|
+
const option = await OptionAsync.none<number>();
|
|
14
|
+
expect(option.is_none()).toBe(true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('fromOption() wraps sync Option', async () => {
|
|
18
|
+
const option = await OptionAsync.fromOption(Some(42));
|
|
19
|
+
expect(option.unwrap()).toBe(42);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('fromOption() wraps Promise<Option>', async () => {
|
|
23
|
+
const option = await OptionAsync.fromOption(Promise.resolve(Some(42)));
|
|
24
|
+
expect(option.unwrap()).toBe(42);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('fromPromise() converts resolved promise to Some', async () => {
|
|
28
|
+
const option = await OptionAsync.fromPromise(Promise.resolve(42));
|
|
29
|
+
expect(option.is_some()).toBe(true);
|
|
30
|
+
expect(option.unwrap()).toBe(42);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('fromPromise() converts rejected promise to None', async () => {
|
|
34
|
+
const option = await OptionAsync.fromPromise(Promise.reject(new Error('failed')));
|
|
35
|
+
expect(option.is_none()).toBe(true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('fromNullable() returns Some for non-null', async () => {
|
|
39
|
+
const option = await OptionAsync.fromNullable(42);
|
|
40
|
+
expect(option.is_some()).toBe(true);
|
|
41
|
+
expect(option.unwrap()).toBe(42);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('fromNullable() returns None for null', async () => {
|
|
45
|
+
const option = await OptionAsync.fromNullable(null);
|
|
46
|
+
expect(option.is_none()).toBe(true);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('fromNullable() returns None for undefined', async () => {
|
|
50
|
+
const option = await OptionAsync.fromNullable(undefined);
|
|
51
|
+
expect(option.is_none()).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('try() catches errors and returns None', async () => {
|
|
55
|
+
const option = await OptionAsync.try(async () => {
|
|
56
|
+
throw new Error('failed');
|
|
57
|
+
});
|
|
58
|
+
expect(option.is_none()).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('try() returns Some on success', async () => {
|
|
62
|
+
const option = await OptionAsync.try(async () => 42);
|
|
63
|
+
expect(option.is_some()).toBe(true);
|
|
64
|
+
expect(option.unwrap()).toBe(42);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('Transformations', () => {
|
|
69
|
+
it('map() transforms Some value with sync function', async () => {
|
|
70
|
+
const option = await OptionAsync.some(5).map(x => x * 2);
|
|
71
|
+
expect(option.unwrap()).toBe(10);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('map() transforms Some value with async function', async () => {
|
|
75
|
+
const option = await OptionAsync.some(5).map(async x => x * 2);
|
|
76
|
+
expect(option.unwrap()).toBe(10);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('map() skips transformation on None', async () => {
|
|
80
|
+
const option = await OptionAsync.none<number>().map(x => x * 2);
|
|
81
|
+
expect(option.is_none()).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('mapOr() returns mapped value on Some', async () => {
|
|
85
|
+
const value = await OptionAsync.some(5).mapOr(0, x => x * 2);
|
|
86
|
+
expect(value).toBe(10);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('mapOr() returns default on None', async () => {
|
|
90
|
+
const value = await OptionAsync.none<number>().mapOr(0, x => x * 2);
|
|
91
|
+
expect(value).toBe(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('mapOrElse() computes default on None', async () => {
|
|
95
|
+
const value = await OptionAsync.none<number>().mapOrElse(
|
|
96
|
+
() => 99,
|
|
97
|
+
x => x * 2
|
|
98
|
+
);
|
|
99
|
+
expect(value).toBe(99);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe('Chaining', () => {
|
|
104
|
+
it('andThen() chains sync Option', async () => {
|
|
105
|
+
const option = await OptionAsync.some(5)
|
|
106
|
+
.andThen(x => Some(x * 2));
|
|
107
|
+
expect(option.unwrap()).toBe(10);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('andThen() chains OptionAsync', async () => {
|
|
111
|
+
const option = await OptionAsync.some(5)
|
|
112
|
+
.andThen(x => OptionAsync.some(x * 2));
|
|
113
|
+
expect(option.unwrap()).toBe(10);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('andThen() propagates None', async () => {
|
|
117
|
+
const option = await OptionAsync.some(5)
|
|
118
|
+
.andThen(x => None<number>());
|
|
119
|
+
expect(option.is_none()).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('andThen() skips on initial None', async () => {
|
|
123
|
+
let called = false;
|
|
124
|
+
const option = await OptionAsync.none<number>()
|
|
125
|
+
.andThen(x => {
|
|
126
|
+
called = true;
|
|
127
|
+
return Some(x * 2);
|
|
128
|
+
});
|
|
129
|
+
expect(called).toBe(false);
|
|
130
|
+
expect(option.is_none()).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('orElse() provides fallback on None', async () => {
|
|
134
|
+
const option = await OptionAsync.none<number>()
|
|
135
|
+
.orElse(() => Some(42));
|
|
136
|
+
expect(option.is_some()).toBe(true);
|
|
137
|
+
expect(option.unwrap()).toBe(42);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('orElse() skips on Some', async () => {
|
|
141
|
+
let called = false;
|
|
142
|
+
const option = await OptionAsync.some(42)
|
|
143
|
+
.orElse(() => {
|
|
144
|
+
called = true;
|
|
145
|
+
return Some(0);
|
|
146
|
+
});
|
|
147
|
+
expect(called).toBe(false);
|
|
148
|
+
expect(option.unwrap()).toBe(42);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('and() returns other on Some', async () => {
|
|
152
|
+
const option = await OptionAsync.some(1).and(OptionAsync.some('hello'));
|
|
153
|
+
expect(option.unwrap()).toBe('hello');
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('and() returns None on initial None', async () => {
|
|
157
|
+
const option = await OptionAsync.none<number>()
|
|
158
|
+
.and(OptionAsync.some('hello'));
|
|
159
|
+
expect(option.is_none()).toBe(true);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('or() returns self on Some', async () => {
|
|
163
|
+
const option = await OptionAsync.some(42).or(OptionAsync.some(0));
|
|
164
|
+
expect(option.unwrap()).toBe(42);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('or() returns other on None', async () => {
|
|
168
|
+
const option = await OptionAsync.none<number>().or(OptionAsync.some(0));
|
|
169
|
+
expect(option.unwrap()).toBe(0);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe('Filtering', () => {
|
|
174
|
+
it('filter() keeps matching values', async () => {
|
|
175
|
+
const option = await OptionAsync.some(10).filter(x => x > 5);
|
|
176
|
+
expect(option.is_some()).toBe(true);
|
|
177
|
+
expect(option.unwrap()).toBe(10);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('filter() removes non-matching values', async () => {
|
|
181
|
+
const option = await OptionAsync.some(3).filter(x => x > 5);
|
|
182
|
+
expect(option.is_none()).toBe(true);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('filter() skips on None', async () => {
|
|
186
|
+
const option = await OptionAsync.none<number>().filter(x => x > 5);
|
|
187
|
+
expect(option.is_none()).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('filter() supports async predicate', async () => {
|
|
191
|
+
const option = await OptionAsync.some(10).filter(async x => x > 5);
|
|
192
|
+
expect(option.is_some()).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe('Unwrapping', () => {
|
|
197
|
+
it('expect() returns value on Some', async () => {
|
|
198
|
+
const value = await OptionAsync.some(42).expect('should not fail');
|
|
199
|
+
expect(value).toBe(42);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('expect() throws on None', async () => {
|
|
203
|
+
await expect(OptionAsync.none().expect('custom message'))
|
|
204
|
+
.rejects.toThrow('custom message');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('unwrap() returns value on Some', async () => {
|
|
208
|
+
const value = await OptionAsync.some(42).unwrap();
|
|
209
|
+
expect(value).toBe(42);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it('unwrap() throws on None', async () => {
|
|
213
|
+
await expect(OptionAsync.none().unwrap()).rejects.toThrow();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('unwrapOr() returns value on Some', async () => {
|
|
217
|
+
const value = await OptionAsync.some(42).unwrapOr(0);
|
|
218
|
+
expect(value).toBe(42);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('unwrapOr() returns default on None', async () => {
|
|
222
|
+
const value = await OptionAsync.none<number>().unwrapOr(0);
|
|
223
|
+
expect(value).toBe(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('unwrapOrElse() computes default on None', async () => {
|
|
227
|
+
const value = await OptionAsync.none<number>().unwrapOrElse(() => 99);
|
|
228
|
+
expect(value).toBe(99);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
describe('Inspection', () => {
|
|
233
|
+
it('inspect() calls function on Some', async () => {
|
|
234
|
+
let inspected: number | null = null;
|
|
235
|
+
await OptionAsync.some(42).inspect(x => { inspected = x; });
|
|
236
|
+
expect(inspected).toBe(42);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('inspect() skips on None', async () => {
|
|
240
|
+
let called = false;
|
|
241
|
+
await OptionAsync.none<number>().inspect(() => { called = true; });
|
|
242
|
+
expect(called).toBe(false);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
describe('Conversion', () => {
|
|
247
|
+
it('flatten() unwraps nested Option', async () => {
|
|
248
|
+
const nested = OptionAsync.some(Some(42));
|
|
249
|
+
const flattened = await nested.flatten();
|
|
250
|
+
expect(flattened.unwrap()).toBe(42);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('flatten() propagates outer None', async () => {
|
|
254
|
+
const nested = OptionAsync.none<Option<number>>();
|
|
255
|
+
const flattened = await nested.flatten();
|
|
256
|
+
expect(flattened.is_none()).toBe(true);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('flatten() propagates inner None', async () => {
|
|
260
|
+
const nested = OptionAsync.some(None<number>());
|
|
261
|
+
const flattened = await nested.flatten();
|
|
262
|
+
expect(flattened.is_none()).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('zip() combines two Some values', async () => {
|
|
266
|
+
const zipped = await OptionAsync.some(1).zip(OptionAsync.some('a'));
|
|
267
|
+
expect(zipped.unwrap()).toEqual([1, 'a']);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('zip() returns None if first is None', async () => {
|
|
271
|
+
const zipped = await OptionAsync.none<number>().zip(OptionAsync.some('a'));
|
|
272
|
+
expect(zipped.is_none()).toBe(true);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it('zip() returns None if second is None', async () => {
|
|
276
|
+
const zipped = await OptionAsync.some(1).zip(OptionAsync.none<string>());
|
|
277
|
+
expect(zipped.is_none()).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('zipWith() applies function to zipped values', async () => {
|
|
281
|
+
const result = await OptionAsync.some(2)
|
|
282
|
+
.zipWith(OptionAsync.some(3), (a, b) => a + b);
|
|
283
|
+
expect(result.unwrap()).toBe(5);
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
describe('Pattern Matching', () => {
|
|
288
|
+
it('match() calls Some handler on Some', async () => {
|
|
289
|
+
const result = await OptionAsync.some(42).match({
|
|
290
|
+
Some: v => `value: ${v}`,
|
|
291
|
+
None: () => 'empty',
|
|
292
|
+
});
|
|
293
|
+
expect(result).toBe('value: 42');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('match() calls None handler on None', async () => {
|
|
297
|
+
const result = await OptionAsync.none().match({
|
|
298
|
+
Some: v => `value: ${v}`,
|
|
299
|
+
None: () => 'empty',
|
|
300
|
+
});
|
|
301
|
+
expect(result).toBe('empty');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('match() supports async handlers', async () => {
|
|
305
|
+
const result = await OptionAsync.some(42).match({
|
|
306
|
+
Some: async v => `value: ${v}`,
|
|
307
|
+
None: async () => 'empty',
|
|
308
|
+
});
|
|
309
|
+
expect(result).toBe('value: 42');
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
describe('Static Combinators', () => {
|
|
314
|
+
it('all() combines all Some options', async () => {
|
|
315
|
+
const option = await OptionAsync.all([
|
|
316
|
+
OptionAsync.some(1),
|
|
317
|
+
OptionAsync.some(2),
|
|
318
|
+
OptionAsync.some(3),
|
|
319
|
+
]);
|
|
320
|
+
expect(option.is_some()).toBe(true);
|
|
321
|
+
expect(option.unwrap()).toEqual([1, 2, 3]);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it('all() returns None if any is None', async () => {
|
|
325
|
+
const option = await OptionAsync.all([
|
|
326
|
+
OptionAsync.some(1),
|
|
327
|
+
OptionAsync.none<number>(),
|
|
328
|
+
OptionAsync.some(3),
|
|
329
|
+
]);
|
|
330
|
+
expect(option.is_none()).toBe(true);
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('Type Checking', () => {
|
|
335
|
+
it('isSome() returns true for Some', async () => {
|
|
336
|
+
expect(await OptionAsync.some(42).isSome()).toBe(true);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('isSome() returns false for None', async () => {
|
|
340
|
+
expect(await OptionAsync.none().isSome()).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('isNone() returns true for None', async () => {
|
|
344
|
+
expect(await OptionAsync.none().isNone()).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('isNone() returns false for Some', async () => {
|
|
348
|
+
expect(await OptionAsync.some(42).isNone()).toBe(false);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
describe('toAsync() extension', () => {
|
|
353
|
+
it('Some.toAsync() returns OptionAsync', async () => {
|
|
354
|
+
const option = await Some(42).toAsync();
|
|
355
|
+
expect(option.is_some()).toBe(true);
|
|
356
|
+
expect(option.unwrap()).toBe(42);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('None.toAsync() returns OptionAsync', async () => {
|
|
360
|
+
const option = await None().toAsync();
|
|
361
|
+
expect(option.is_none()).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
describe('Complex Async Chains', () => {
|
|
367
|
+
it('handles realistic optional data flow', async () => {
|
|
368
|
+
const findUser = (id: number) => OptionAsync.try(async () => {
|
|
369
|
+
if (id === 1) return { id: 1, name: 'Alice', profileId: 100 };
|
|
370
|
+
throw new Error('not found');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
const findProfile = (profileId: number) => OptionAsync.try(async () => {
|
|
374
|
+
return { id: profileId, bio: 'Hello!' };
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const result = await findUser(1)
|
|
378
|
+
.andThen(user => findProfile(user.profileId)
|
|
379
|
+
.map(profile => ({ ...user, profile }))
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
expect(result.is_some()).toBe(true);
|
|
383
|
+
expect(result.unwrap()).toEqual({
|
|
384
|
+
id: 1,
|
|
385
|
+
name: 'Alice',
|
|
386
|
+
profileId: 100,
|
|
387
|
+
profile: { id: 100, bio: 'Hello!' },
|
|
388
|
+
});
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('handles None propagation', async () => {
|
|
392
|
+
const result = await OptionAsync.some(5)
|
|
393
|
+
.andThen(() => OptionAsync.none<number>())
|
|
394
|
+
.map(x => x * 2);
|
|
395
|
+
|
|
396
|
+
expect(result.is_none()).toBe(true);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
it('handles fallback chains', async () => {
|
|
400
|
+
const primary = () => OptionAsync.none<string>();
|
|
401
|
+
const secondary = () => OptionAsync.none<string>();
|
|
402
|
+
const fallback = () => OptionAsync.some('default');
|
|
403
|
+
|
|
404
|
+
const result = await primary()
|
|
405
|
+
.orElse(() => secondary())
|
|
406
|
+
.orElse(() => fallback());
|
|
407
|
+
|
|
408
|
+
expect(result.unwrap()).toBe('default');
|
|
409
|
+
});
|
|
410
|
+
});
|