tangled
alpha
login
or
join now
ciaran.co.za
/
option-result
0
fork
atom
Rust-style Option and Result Classes for PHP
0
fork
atom
overview
issues
pulls
pipelines
feat(tests): ai generated / human reviewed unit tests
Ciaran
3 months ago
bf5d5121
1959e306
+1292
-3
3 changed files
expand all
collapse all
unified
split
composer.json
tests
OptionTest.php
ResultTest.php
+3
composer.json
···
16
16
],
17
17
"require-dev": {
18
18
"phpunit/phpunit": "^12.4"
19
19
+
},
20
20
+
"scripts": {
21
21
+
"test": "./vendor/bin/phpunit --testdox tests"
19
22
}
20
23
}
+561
-1
tests/OptionTest.php
···
1
1
<?php
2
2
3
3
use PHPUnit\Framework\TestCase;
4
4
+
use Ciarancoza\OptionResult\Option;
5
5
+
use Ciarancoza\OptionResult\Exceptions\UnwrapNoneException;
4
6
5
5
-
class OptionTest extends TestCase {
7
7
+
class OptionTest extends TestCase
8
8
+
{
9
9
+
// =========================================================================
10
10
+
// STATIC CONSTRUCTOR TESTS
11
11
+
// =========================================================================
12
12
+
13
13
+
public function testSomeCreatesOptionWithValue(): void
14
14
+
{
15
15
+
$option = Option::Some("hello");
16
16
+
$this->assertTrue($option->isSome());
17
17
+
$this->assertFalse($option->isNone());
18
18
+
$this->assertEquals("hello", $option->unwrap());
19
19
+
}
20
20
+
21
21
+
public function testSomeCreatesOptionWithNumber(): void
22
22
+
{
23
23
+
$option = Option::Some(42);
24
24
+
$this->assertTrue($option->isSome());
25
25
+
$this->assertFalse($option->isNone());
26
26
+
$this->assertEquals(42, $option->unwrap());
27
27
+
}
28
28
+
29
29
+
public function testSomeCanExplicitlyContainNull(): void
30
30
+
{
31
31
+
$option = Option::Some(null);
32
32
+
$this->assertTrue($option->isSome());
33
33
+
$this->assertFalse($option->isNone());
34
34
+
$this->assertNull($option->unwrap());
35
35
+
}
36
36
+
37
37
+
public function testNoneCreatesEmptyOption(): void
38
38
+
{
39
39
+
$option = Option::None();
40
40
+
$this->assertFalse($option->isSome());
41
41
+
$this->assertTrue($option->isNone());
42
42
+
}
43
43
+
44
44
+
public function testSomeWithFalsyValues(): void
45
45
+
{
46
46
+
$falsyValues = [
47
47
+
false,
48
48
+
0,
49
49
+
0.0,
50
50
+
'',
51
51
+
'0',
52
52
+
[],
53
53
+
];
54
54
+
55
55
+
foreach ($falsyValues as $falsyValue) {
56
56
+
$option = Option::Some($falsyValue);
57
57
+
$this->assertTrue($option->isSome());
58
58
+
$this->assertFalse($option->isNone());
59
59
+
$this->assertSame($falsyValue, $option->unwrap());
60
60
+
}
61
61
+
}
62
62
+
63
63
+
// =========================================================================
64
64
+
// STATE CHECKING TESTS
65
65
+
// =========================================================================
66
66
+
67
67
+
public function testIsSomeReturnsTrueForSomeValue(): void
68
68
+
{
69
69
+
$option = Option::Some("value");
70
70
+
$this->assertTrue($option->isSome());
71
71
+
}
72
72
+
73
73
+
public function testIsSomeReturnsFalseForNone(): void
74
74
+
{
75
75
+
$option = Option::None();
76
76
+
$this->assertFalse($option->isSome());
77
77
+
}
78
78
+
79
79
+
public function testIsNoneReturnsFalseForSomeValue(): void
80
80
+
{
81
81
+
$option = Option::Some("value");
82
82
+
$this->assertFalse($option->isNone());
83
83
+
}
84
84
+
85
85
+
public function testIsNoneReturnsTrueForNone(): void
86
86
+
{
87
87
+
$option = Option::None();
88
88
+
$this->assertTrue($option->isNone());
89
89
+
}
90
90
+
91
91
+
// =========================================================================
92
92
+
// VALUE EXTRACTION TESTS - unwrap()
93
93
+
// =========================================================================
94
94
+
95
95
+
public function testUnwrapReturnsValueFromSome(): void
96
96
+
{
97
97
+
$option = Option::Some("hello");
98
98
+
$this->assertEquals("hello", $option->unwrap());
99
99
+
}
100
100
+
101
101
+
public function testUnwrapThrowsExceptionOnNone(): void
102
102
+
{
103
103
+
$this->expectException(UnwrapNoneException::class);
104
104
+
105
105
+
$option = Option::None();
106
106
+
$option->unwrap();
107
107
+
}
108
108
+
109
109
+
public function testUnwrapReturnsNullFromSomeNull(): void
110
110
+
{
111
111
+
$option = Option::Some(null);
112
112
+
$this->assertNull($option->unwrap());
113
113
+
}
114
114
+
115
115
+
public function testUnwrapReturnsComplexValues(): void
116
116
+
{
117
117
+
$complexValues = [
118
118
+
['key' => 'value', 'nested' => ['data']],
119
119
+
(object)['property' => 'value'],
120
120
+
fn($x) => $x * 2,
121
121
+
];
122
122
+
123
123
+
foreach ($complexValues as $complexValue) {
124
124
+
$option = Option::Some($complexValue);
125
125
+
$this->assertSame($complexValue, $option->unwrap());
126
126
+
}
127
127
+
}
128
128
+
129
129
+
// =========================================================================
130
130
+
// VALUE EXTRACTION TESTS - unwrapOr()
131
131
+
// =========================================================================
132
132
+
133
133
+
public function testUnwrapOrReturnsValueFromSome(): void
134
134
+
{
135
135
+
$option = Option::Some("hello");
136
136
+
$result = $option->unwrapOr("default");
137
137
+
$this->assertEquals("hello", $result);
138
138
+
}
139
139
+
140
140
+
public function testUnwrapOrReturnsDefaultFromNone(): void
141
141
+
{
142
142
+
$option = Option::None();
143
143
+
$result = $option->unwrapOr("default");
144
144
+
$this->assertEquals("default", $result);
145
145
+
}
146
146
+
147
147
+
public function testUnwrapOrWithNullValue(): void
148
148
+
{
149
149
+
// Some(null) should return null, not the default
150
150
+
$option = Option::Some(null);
151
151
+
$result = $option->unwrapOr("default");
152
152
+
$this->assertNull($result);
153
153
+
}
154
154
+
155
155
+
public function testUnwrapOrWithFalsyDefaults(): void
156
156
+
{
157
157
+
$option = Option::None();
158
158
+
159
159
+
$this->assertFalse($option->unwrapOr(false));
160
160
+
$this->assertEquals(0, $option->unwrapOr(0));
161
161
+
$this->assertEquals('', $option->unwrapOr(''));
162
162
+
$this->assertEquals([], $option->unwrapOr([]));
163
163
+
}
164
164
+
165
165
+
public function testUnwrapOrWithComplexDefaults(): void
166
166
+
{
167
167
+
$option = Option::None();
168
168
+
$defaultObject = (object)['default' => true];
169
169
+
$defaultArray = ['default' => 'value'];
170
170
+
171
171
+
$this->assertSame($defaultObject, $option->unwrapOr($defaultObject));
172
172
+
$this->assertSame($defaultArray, $option->unwrapOr($defaultArray));
173
173
+
}
174
174
+
175
175
+
// =========================================================================
176
176
+
// TRANSFORMATION TESTS - map()
177
177
+
// =========================================================================
178
178
+
179
179
+
public function testMapTransformsValueInSome(): void
180
180
+
{
181
181
+
$option = Option::Some(5);
182
182
+
$doubled = $option->map(fn($x) => $x * 2);
183
183
+
184
184
+
$this->assertTrue($doubled->isSome());
185
185
+
$this->assertEquals(10, $doubled->unwrap());
186
186
+
}
187
187
+
188
188
+
public function testMapOnNoneRetrunsNone(): void
189
189
+
{
190
190
+
$option = Option::None();
191
191
+
$result = $option->map(fn($x) => $x * 2);
192
192
+
193
193
+
$this->assertTrue($result->isNone());
194
194
+
$this->assertFalse($result->isSome());
195
195
+
}
196
196
+
197
197
+
public function testMapDoesNotExecuteCallbackOnNone(): void
198
198
+
{
199
199
+
$callbackExecuted = false;
200
200
+
201
201
+
$option = Option::None();
202
202
+
$option->map(function($x) use (&$callbackExecuted) {
203
203
+
$callbackExecuted = true;
204
204
+
return $x * 2;
205
205
+
});
206
206
+
207
207
+
$this->assertFalse($callbackExecuted, 'Callback should not execute on None');
208
208
+
}
209
209
+
210
210
+
public function testMapStringTransformation(): void
211
211
+
{
212
212
+
$option = Option::Some("hello");
213
213
+
$upper = $option->map(fn($s) => strtoupper($s));
214
214
+
215
215
+
$this->assertTrue($upper->isSome());
216
216
+
$this->assertEquals("HELLO", $upper->unwrap());
217
217
+
}
218
218
+
219
219
+
public function testMapWithTypeChanges(): void
220
220
+
{
221
221
+
$option = Option::Some("hello");
222
222
+
$length = $option->map(fn($s) => strlen($s));
223
223
+
224
224
+
$this->assertTrue($length->isSome());
225
225
+
$this->assertEquals(5, $length->unwrap());
226
226
+
227
227
+
$bool = $length->map(fn($n) => $n > 3);
228
228
+
$this->assertTrue($bool->isSome());
229
229
+
$this->assertTrue($bool->unwrap());
230
230
+
}
231
231
+
232
232
+
public function testMapCanReturnNull(): void
233
233
+
{
234
234
+
$option = Option::Some("test");
235
235
+
$nullResult = $option->map(fn($s) => null);
236
236
+
237
237
+
$this->assertTrue($nullResult->isSome());
238
238
+
$this->assertNull($nullResult->unwrap());
239
239
+
}
240
240
+
241
241
+
public function testMapCanReturnFalsyValues(): void
242
242
+
{
243
243
+
$option = Option::Some("test");
244
244
+
245
245
+
$falseResult = $option->map(fn($s) => false);
246
246
+
$this->assertTrue($falseResult->isSome());
247
247
+
$this->assertFalse($falseResult->unwrap());
248
248
+
249
249
+
$zeroResult = $option->map(fn($s) => 0);
250
250
+
$this->assertTrue($zeroResult->isSome());
251
251
+
$this->assertEquals(0, $zeroResult->unwrap());
252
252
+
253
253
+
$emptyResult = $option->map(fn($s) => '');
254
254
+
$this->assertTrue($emptyResult->isSome());
255
255
+
$this->assertEquals('', $emptyResult->unwrap());
256
256
+
}
257
257
+
258
258
+
// =========================================================================
259
259
+
// METHOD CHAINING TESTS
260
260
+
// =========================================================================
261
261
+
262
262
+
public function testBasicChaining(): void
263
263
+
{
264
264
+
$result = Option::Some("hello")
265
265
+
->map(fn($s) => strtoupper($s)) // "HELLO"
266
266
+
->map(fn($s) => strlen($s)) // 5
267
267
+
->map(fn($n) => $n > 3); // true
268
268
+
269
269
+
$this->assertTrue($result->isSome());
270
270
+
$this->assertTrue($result->unwrap());
271
271
+
}
272
272
+
273
273
+
public function testChainingWithNoneSkipsAllTransformations(): void
274
274
+
{
275
275
+
$callbackCount = 0;
276
276
+
277
277
+
$result = Option::None()
278
278
+
->map(function($s) use (&$callbackCount) {
279
279
+
$callbackCount++;
280
280
+
return strtoupper($s);
281
281
+
})
282
282
+
->map(function($s) use (&$callbackCount) {
283
283
+
$callbackCount++;
284
284
+
return strlen($s);
285
285
+
})
286
286
+
->unwrapOr(0);
287
287
+
288
288
+
$this->assertEquals(0, $result);
289
289
+
$this->assertEquals(0, $callbackCount, 'No callbacks should execute on None');
290
290
+
}
291
291
+
292
292
+
public function testLongChainWithMixedTransformations(): void
293
293
+
{
294
294
+
$result = Option::Some(" hello world ")
295
295
+
->map(fn($s) => trim($s)) // "hello world"
296
296
+
->map(fn($s) => explode(' ', $s)) // ["hello", "world"]
297
297
+
->map(fn($arr) => count($arr)) // 2
298
298
+
->map(fn($n) => $n * 10) // 20
299
299
+
->map(fn($n) => $n > 15); // true
300
300
+
301
301
+
$this->assertTrue($result->isSome());
302
302
+
$this->assertTrue($result->unwrap());
303
303
+
}
304
304
+
305
305
+
public function testChainingWithUnwrapOr(): void
306
306
+
{
307
307
+
$someResult = Option::Some("hello")
308
308
+
->map(fn($s) => strtoupper($s))
309
309
+
->map(fn($s) => strlen($s))
310
310
+
->unwrapOr(0);
311
311
+
312
312
+
$this->assertEquals(5, $someResult);
313
313
+
314
314
+
$noneResult = Option::None()
315
315
+
->map(fn($s) => strtoupper($s))
316
316
+
->map(fn($s) => strlen($s))
317
317
+
->unwrapOr(0);
318
318
+
319
319
+
$this->assertEquals(0, $noneResult);
320
320
+
}
321
321
+
322
322
+
// =========================================================================
323
323
+
// EDGE CASES AND ERROR SCENARIOS
324
324
+
// =========================================================================
325
325
+
326
326
+
public function testMapWithExceptionInCallback(): void
327
327
+
{
328
328
+
$this->expectException(RuntimeException::class);
329
329
+
$this->expectExceptionMessage("Test exception");
330
330
+
331
331
+
$option = Option::Some("test");
332
332
+
$option->map(function($s) {
333
333
+
throw new RuntimeException("Test exception");
334
334
+
});
335
335
+
}
336
336
+
337
337
+
public function testMapWithExceptionOnNoneDoesNotThrow(): void
338
338
+
{
339
339
+
// Exception should not be thrown because callback is not executed on None
340
340
+
$option = Option::None();
341
341
+
$result = $option->map(function($s) {
342
342
+
throw new RuntimeException("This should not execute");
343
343
+
});
344
344
+
345
345
+
$this->assertTrue($result->isNone());
346
346
+
}
347
347
+
348
348
+
public function testMultipleUnwrapCallsOnSame(): void
349
349
+
{
350
350
+
$option = Option::Some("test");
351
351
+
352
352
+
// Multiple unwrap calls should return the same value
353
353
+
$this->assertEquals("test", $option->unwrap());
354
354
+
$this->assertEquals("test", $option->unwrap());
355
355
+
$this->assertEquals("test", $option->unwrapOr("default"));
356
356
+
}
357
357
+
358
358
+
public function testMultipleMapCallsOnSame(): void
359
359
+
{
360
360
+
$option = Option::Some(5);
361
361
+
362
362
+
// Multiple map calls should create independent new Options
363
363
+
$doubled = $option->map(fn($x) => $x * 2);
364
364
+
$tripled = $option->map(fn($x) => $x * 3);
365
365
+
366
366
+
$this->assertEquals(5, $option->unwrap());
367
367
+
$this->assertEquals(10, $doubled->unwrap());
368
368
+
$this->assertEquals(15, $tripled->unwrap());
369
369
+
}
370
370
+
371
371
+
// =========================================================================
372
372
+
// REAL-WORLD SCENARIO TESTS (Based on USAGE.md examples)
373
373
+
// =========================================================================
374
374
+
375
375
+
public function testUserFindingScenario(): void
376
376
+
{
377
377
+
// Simulate the findUser example from USAGE.md
378
378
+
$findUser = function(int $id): Option {
379
379
+
$users = [
380
380
+
123 => (object)['id' => 123, 'name' => 'john doe'],
381
381
+
456 => (object)['id' => 456, 'name' => 'jane smith'],
382
382
+
];
383
383
+
384
384
+
return isset($users[$id]) ? Option::Some($users[$id]) : Option::None();
385
385
+
};
386
386
+
387
387
+
$getUserName = function(int $userId) use ($findUser): string {
388
388
+
return $findUser($userId)
389
389
+
->map(fn($user) => $user->name)
390
390
+
->map(fn($name) => ucfirst($name))
391
391
+
->unwrapOr('Unknown User');
392
392
+
};
393
393
+
394
394
+
$this->assertEquals('John doe', $getUserName(123));
395
395
+
$this->assertEquals('Jane smith', $getUserName(456));
396
396
+
$this->assertEquals('Unknown User', $getUserName(999));
397
397
+
}
398
398
+
399
399
+
public function testEmailChainScenario(): void
400
400
+
{
401
401
+
// Simulate the findUserEmail example from USAGE.md
402
402
+
$users = [
403
403
+
123 => (object)[
404
404
+
'id' => 123,
405
405
+
'profile' => (object)['email' => 'JOHN@EXAMPLE.COM']
406
406
+
],
407
407
+
456 => (object)[
408
408
+
'id' => 456,
409
409
+
'profile' => null
410
410
+
],
411
411
+
];
412
412
+
413
413
+
$findUser = fn(int $id): Option =>
414
414
+
isset($users[$id]) ? Option::Some($users[$id]) : Option::None();
415
415
+
416
416
+
$findUserEmail = function(int $userId) use ($findUser): Option {
417
417
+
return $findUser($userId)
418
418
+
->map(fn($user) => $user->profile)
419
419
+
->map(fn($profile) => $profile ? $profile->email : null)
420
420
+
->map(fn($email) => $email ? strtolower($email) : null);
421
421
+
};
422
422
+
423
423
+
// User 123 has email
424
424
+
$email1 = $findUserEmail(123)->unwrapOr('no-email@example.com');
425
425
+
$this->assertEquals('john@example.com', $email1);
426
426
+
427
427
+
// User 456 has null profile - the map chain continues but returns null
428
428
+
$email2 = $findUserEmail(456)->unwrapOr('no-email@example.com');
429
429
+
$this->assertNull($email2); // Option contains Some(null), so unwrapOr returns null
430
430
+
431
431
+
// Non-existent user - this should return the default
432
432
+
$email3 = $findUserEmail(999)->unwrapOr('no-email@example.com');
433
433
+
$this->assertEquals('no-email@example.com', $email3);
434
434
+
}
435
435
+
436
436
+
public function testComplexDataProcessingPipeline(): void
437
437
+
{
438
438
+
$processUserData = function(array $rawData): Option {
439
439
+
return Option::Some($rawData)
440
440
+
->map(function($data) {
441
441
+
// Validate required fields
442
442
+
if (empty($data['name']) || empty($data['email'])) {
443
443
+
return null;
444
444
+
}
445
445
+
return $data;
446
446
+
})
447
447
+
->map(function($data) {
448
448
+
if ($data === null) return null;
449
449
+
// Normalize data
450
450
+
return [
451
451
+
'name' => trim(ucwords(strtolower($data['name']))),
452
452
+
'email' => trim(strtolower($data['email'])),
453
453
+
'age' => isset($data['age']) ? (int)$data['age'] : null,
454
454
+
];
455
455
+
})
456
456
+
->map(function($data) {
457
457
+
if ($data === null) return null;
458
458
+
// Add computed fields
459
459
+
$data['initials'] = strtoupper(substr($data['name'], 0, 1));
460
460
+
$data['domain'] = explode('@', $data['email'])[1] ?? '';
461
461
+
return $data;
462
462
+
});
463
463
+
};
464
464
+
465
465
+
// Valid data
466
466
+
$validResult = $processUserData([
467
467
+
'name' => ' JOHN DOE ',
468
468
+
'email' => ' John.Doe@EXAMPLE.COM ',
469
469
+
'age' => '25'
470
470
+
])->unwrapOr([]);
471
471
+
472
472
+
$expected = [
473
473
+
'name' => 'John Doe',
474
474
+
'email' => 'john.doe@example.com',
475
475
+
'age' => 25,
476
476
+
'initials' => 'J',
477
477
+
'domain' => 'example.com'
478
478
+
];
479
479
+
480
480
+
$this->assertEquals($expected, $validResult);
481
481
+
482
482
+
// Invalid data (missing email) - Option will contain Some(null)
483
483
+
$invalidResult = $processUserData([
484
484
+
'name' => 'John Doe',
485
485
+
// missing email
486
486
+
])->unwrapOr(['error' => 'Invalid data']);
487
487
+
488
488
+
// Since the Option contains Some(null), unwrapOr returns null, not the default
489
489
+
$this->assertNull($invalidResult);
490
490
+
}
491
491
+
492
492
+
// =========================================================================
493
493
+
// TYPE SAFETY AND CONSISTENCY TESTS
494
494
+
// =========================================================================
495
495
+
496
496
+
public function testConsistentBehaviorAcrossDataTypes(): void
497
497
+
{
498
498
+
$testValues = [
499
499
+
'string' => 'hello',
500
500
+
'integer' => 42,
501
501
+
'float' => 3.14,
502
502
+
'boolean_true' => true,
503
503
+
'boolean_false' => false,
504
504
+
'null' => null,
505
505
+
'array' => ['key' => 'value'],
506
506
+
'object' => (object)['prop' => 'value'],
507
507
+
];
508
508
+
509
509
+
foreach ($testValues as $type => $value) {
510
510
+
$option = Option::Some($value);
511
511
+
512
512
+
$this->assertTrue($option->isSome(), "isSome() should be true for {$type}");
513
513
+
$this->assertFalse($option->isNone(), "isNone() should be false for {$type}");
514
514
+
$this->assertSame($value, $option->unwrap(), "unwrap() should return original value for {$type}");
515
515
+
$this->assertSame($value, $option->unwrapOr('default'), "unwrapOr() should return original value for {$type}");
516
516
+
}
517
517
+
}
518
518
+
519
519
+
public function testMethodReturnTypes(): void
520
520
+
{
521
521
+
$some = Option::Some("test");
522
522
+
$none = Option::None();
523
523
+
524
524
+
// State methods should return boolean
525
525
+
$this->assertIsBool($some->isSome());
526
526
+
$this->assertIsBool($some->isNone());
527
527
+
$this->assertIsBool($none->isSome());
528
528
+
$this->assertIsBool($none->isNone());
529
529
+
530
530
+
// Map should return Option instance
531
531
+
$mapped = $some->map(fn($x) => $x);
532
532
+
$this->assertInstanceOf(Option::class, $mapped);
533
533
+
534
534
+
$mappedNone = $none->map(fn($x) => $x);
535
535
+
$this->assertInstanceOf(Option::class, $mappedNone);
536
536
+
}
537
537
+
538
538
+
// =========================================================================
539
539
+
// PERFORMANCE AND MEMORY TESTS
540
540
+
// =========================================================================
541
541
+
542
542
+
public function testLargeDataHandling(): void
543
543
+
{
544
544
+
$largeArray = range(1, 10000);
545
545
+
$option = Option::Some($largeArray);
546
546
+
547
547
+
$this->assertTrue($option->isSome());
548
548
+
$this->assertEquals(10000, count($option->unwrap()));
549
549
+
550
550
+
$summed = $option->map(fn($arr) => array_sum($arr));
551
551
+
$this->assertEquals(50005000, $summed->unwrap()); // Sum of 1 to 10000
552
552
+
}
553
553
+
554
554
+
public function testDeepChaining(): void
555
555
+
{
556
556
+
$option = Option::Some(1);
557
557
+
558
558
+
// Create a deep chain of 100 transformations
559
559
+
for ($i = 0; $i < 100; $i++) {
560
560
+
$option = $option->map(fn($x) => $x + 1);
561
561
+
}
562
562
+
563
563
+
$this->assertTrue($option->isSome());
564
564
+
$this->assertEquals(101, $option->unwrap());
565
565
+
}
6
566
}
+728
-2
tests/ResultTest.php
···
1
1
<?php
2
2
3
3
+
declare(strict_types=1);
4
4
+
5
5
+
require_once __DIR__ . '/../vendor/autoload.php';
6
6
+
3
7
use PHPUnit\Framework\TestCase;
8
8
+
use Ciarancoza\OptionResult\Result;
9
9
+
use Ciarancoza\OptionResult\Option;
10
10
+
use Ciarancoza\OptionResult\Exceptions\UnwrapErrException;
11
11
+
use Ciarancoza\OptionResult\Exceptions\UnwrapOkException;
4
12
5
5
-
class ResultTest extends TestCase {
6
6
-
}
13
13
+
class ResultTest extends TestCase
14
14
+
{
15
15
+
// ==============================================
16
16
+
// Static Constructor Tests
17
17
+
// ==============================================
18
18
+
19
19
+
public function testOkConstructor(): void
20
20
+
{
21
21
+
$result = Result::Ok("success");
22
22
+
$this->assertTrue($result->isOk());
23
23
+
$this->assertFalse($result->isErr());
24
24
+
$this->assertEquals("success", $result->unwrap());
25
25
+
}
26
26
+
27
27
+
public function testOkConstructorWithNumber(): void
28
28
+
{
29
29
+
$result = Result::Ok(42);
30
30
+
$this->assertTrue($result->isOk());
31
31
+
$this->assertEquals(42, $result->unwrap());
32
32
+
}
33
33
+
34
34
+
public function testOkConstructorWithArray(): void
35
35
+
{
36
36
+
$data = ['key' => 'value'];
37
37
+
$result = Result::Ok($data);
38
38
+
$this->assertTrue($result->isOk());
39
39
+
$this->assertEquals($data, $result->unwrap());
40
40
+
}
41
41
+
42
42
+
public function testOkConstructorWithObject(): void
43
43
+
{
44
44
+
$obj = (object) ['prop' => 'value'];
45
45
+
$result = Result::Ok($obj);
46
46
+
$this->assertTrue($result->isOk());
47
47
+
$this->assertEquals($obj, $result->unwrap());
48
48
+
}
49
49
+
50
50
+
public function testErrConstructor(): void
51
51
+
{
52
52
+
$result = Result::Err("failure");
53
53
+
$this->assertFalse($result->isOk());
54
54
+
$this->assertTrue($result->isErr());
55
55
+
$this->assertEquals("failure", $result->unwrapErr());
56
56
+
}
57
57
+
58
58
+
public function testErrConstructorWithNumber(): void
59
59
+
{
60
60
+
$result = Result::Err(404);
61
61
+
$this->assertTrue($result->isErr());
62
62
+
$this->assertEquals(404, $result->unwrapErr());
63
63
+
}
64
64
+
65
65
+
public function testErrConstructorWithArray(): void
66
66
+
{
67
67
+
$error = ['code' => 500, 'message' => 'Internal Error'];
68
68
+
$result = Result::Err($error);
69
69
+
$this->assertTrue($result->isErr());
70
70
+
$this->assertEquals($error, $result->unwrapErr());
71
71
+
}
72
72
+
73
73
+
// ==============================================
74
74
+
// Edge Cases with Falsy Values
75
75
+
// ==============================================
76
76
+
77
77
+
public function testOkWithNull(): void
78
78
+
{
79
79
+
$result = Result::Ok(null);
80
80
+
$this->assertTrue($result->isOk());
81
81
+
$this->assertFalse($result->isErr());
82
82
+
$this->assertNull($result->unwrap());
83
83
+
}
84
84
+
85
85
+
public function testOkWithFalse(): void
86
86
+
{
87
87
+
$result = Result::Ok(false);
88
88
+
$this->assertTrue($result->isOk());
89
89
+
$this->assertFalse($result->unwrap());
90
90
+
}
91
91
+
92
92
+
public function testOkWithZero(): void
93
93
+
{
94
94
+
$result = Result::Ok(0);
95
95
+
$this->assertTrue($result->isOk());
96
96
+
$this->assertEquals(0, $result->unwrap());
97
97
+
}
98
98
+
99
99
+
public function testOkWithEmptyString(): void
100
100
+
{
101
101
+
$result = Result::Ok("");
102
102
+
$this->assertTrue($result->isOk());
103
103
+
$this->assertEquals("", $result->unwrap());
104
104
+
}
105
105
+
106
106
+
public function testOkWithEmptyArray(): void
107
107
+
{
108
108
+
$result = Result::Ok([]);
109
109
+
$this->assertTrue($result->isOk());
110
110
+
$this->assertEquals([], $result->unwrap());
111
111
+
}
112
112
+
113
113
+
public function testErrWithNull(): void
114
114
+
{
115
115
+
$result = Result::Err(null);
116
116
+
$this->assertTrue($result->isErr());
117
117
+
$this->assertNull($result->unwrapErr());
118
118
+
}
119
119
+
120
120
+
public function testErrWithFalse(): void
121
121
+
{
122
122
+
$result = Result::Err(false);
123
123
+
$this->assertTrue($result->isErr());
124
124
+
$this->assertFalse($result->unwrapErr());
125
125
+
}
126
126
+
127
127
+
public function testErrWithZero(): void
128
128
+
{
129
129
+
$result = Result::Err(0);
130
130
+
$this->assertTrue($result->isErr());
131
131
+
$this->assertEquals(0, $result->unwrapErr());
132
132
+
}
133
133
+
134
134
+
public function testErrWithEmptyString(): void
135
135
+
{
136
136
+
$result = Result::Err("");
137
137
+
$this->assertTrue($result->isErr());
138
138
+
$this->assertEquals("", $result->unwrapErr());
139
139
+
}
140
140
+
141
141
+
// ==============================================
142
142
+
// State Checking Tests
143
143
+
// ==============================================
144
144
+
145
145
+
public function testIsOkAndIsErr(): void
146
146
+
{
147
147
+
$testCases = [
148
148
+
['value' => "success", 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
149
149
+
['value' => 42, 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
150
150
+
['value' => null, 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
151
151
+
['value' => false, 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
152
152
+
['value' => "", 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
153
153
+
['value' => 0, 'isOk' => true, 'expectedIsOk' => true, 'expectedIsErr' => false],
154
154
+
['value' => "error", 'isOk' => false, 'expectedIsOk' => false, 'expectedIsErr' => true],
155
155
+
['value' => 500, 'isOk' => false, 'expectedIsOk' => false, 'expectedIsErr' => true],
156
156
+
['value' => null, 'isOk' => false, 'expectedIsOk' => false, 'expectedIsErr' => true],
157
157
+
['value' => false, 'isOk' => false, 'expectedIsOk' => false, 'expectedIsErr' => true],
158
158
+
];
159
159
+
160
160
+
foreach ($testCases as $case) {
161
161
+
$result = $case['isOk'] ? Result::Ok($case['value']) : Result::Err($case['value']);
162
162
+
$this->assertEquals($case['expectedIsOk'], $result->isOk());
163
163
+
$this->assertEquals($case['expectedIsErr'], $result->isErr());
164
164
+
}
165
165
+
}
166
166
+
167
167
+
// ==============================================
168
168
+
// Value Extraction Tests
169
169
+
// ==============================================
170
170
+
171
171
+
public function testUnwrapOnOk(): void
172
172
+
{
173
173
+
$result = Result::Ok("data");
174
174
+
$this->assertEquals("data", $result->unwrap());
175
175
+
}
176
176
+
177
177
+
public function testUnwrapOnErr(): void
178
178
+
{
179
179
+
$this->expectException(UnwrapErrException::class);
180
180
+
$result = Result::Err("error");
181
181
+
$result->unwrap();
182
182
+
}
183
183
+
184
184
+
public function testUnwrapErrOnOk(): void
185
185
+
{
186
186
+
$this->expectException(UnwrapOkException::class);
187
187
+
$result = Result::Ok("data");
188
188
+
$result->unwrapErr();
189
189
+
}
190
190
+
191
191
+
public function testUnwrapErrOnErr(): void
192
192
+
{
193
193
+
$result = Result::Err("error message");
194
194
+
$this->assertEquals("error message", $result->unwrapErr());
195
195
+
}
196
196
+
197
197
+
public function testUnwrapOr(): void
198
198
+
{
199
199
+
$testCases = [
200
200
+
['result' => Result::Ok("data"), 'default' => "default", 'expected' => "data"],
201
201
+
['result' => Result::Err("error"), 'default' => "default", 'expected' => "default"],
202
202
+
['result' => Result::Ok(null), 'default' => "default", 'expected' => null],
203
203
+
['result' => Result::Ok(false), 'default' => "default", 'expected' => false],
204
204
+
['result' => Result::Ok(0), 'default' => "default", 'expected' => 0],
205
205
+
['result' => Result::Ok(""), 'default' => "default", 'expected' => ""],
206
206
+
['result' => Result::Err("error"), 'default' => null, 'expected' => null],
207
207
+
['result' => Result::Err("error"), 'default' => false, 'expected' => false],
208
208
+
['result' => Result::Err("error"), 'default' => 0, 'expected' => 0],
209
209
+
];
210
210
+
211
211
+
foreach ($testCases as $case) {
212
212
+
$this->assertEquals($case['expected'], $case['result']->unwrapOr($case['default']));
213
213
+
}
214
214
+
}
215
215
+
216
216
+
// ==============================================
217
217
+
// Option Conversion Tests
218
218
+
// ==============================================
219
219
+
220
220
+
public function testGetOkOnOkResult(): void
221
221
+
{
222
222
+
$result = Result::Ok("data");
223
223
+
$option = $result->getOk();
224
224
+
225
225
+
$this->assertInstanceOf(Option::class, $option);
226
226
+
$this->assertTrue($option->isSome());
227
227
+
$this->assertEquals("data", $option->unwrap());
228
228
+
}
229
229
+
230
230
+
public function testGetOkOnErrResult(): void
231
231
+
{
232
232
+
$result = Result::Err("error");
233
233
+
$option = $result->getOk();
234
234
+
235
235
+
$this->assertInstanceOf(Option::class, $option);
236
236
+
$this->assertTrue($option->isNone());
237
237
+
}
238
238
+
239
239
+
public function testGetErrOnOkResult(): void
240
240
+
{
241
241
+
$result = Result::Ok("data");
242
242
+
$option = $result->getErr();
243
243
+
244
244
+
$this->assertInstanceOf(Option::class, $option);
245
245
+
$this->assertTrue($option->isNone());
246
246
+
}
247
247
+
248
248
+
public function testGetErrOnErrResult(): void
249
249
+
{
250
250
+
$result = Result::Err("error");
251
251
+
$option = $result->getErr();
252
252
+
253
253
+
$this->assertInstanceOf(Option::class, $option);
254
254
+
$this->assertTrue($option->isSome());
255
255
+
$this->assertEquals("error", $option->unwrap());
256
256
+
}
257
257
+
258
258
+
public function testGetOkWithFalsyValues(): void
259
259
+
{
260
260
+
$testCases = [null, false, 0, "", []];
261
261
+
262
262
+
foreach ($testCases as $value) {
263
263
+
$result = Result::Ok($value);
264
264
+
$option = $result->getOk();
265
265
+
266
266
+
$this->assertTrue($option->isSome());
267
267
+
$this->assertEquals($value, $option->unwrap());
268
268
+
}
269
269
+
}
270
270
+
271
271
+
public function testGetErrWithFalsyValues(): void
272
272
+
{
273
273
+
$testCases = [null, false, 0, "", []];
274
274
+
275
275
+
foreach ($testCases as $value) {
276
276
+
$result = Result::Err($value);
277
277
+
$option = $result->getErr();
278
278
+
279
279
+
$this->assertTrue($option->isSome());
280
280
+
$this->assertEquals($value, $option->unwrap());
281
281
+
}
282
282
+
}
283
283
+
284
284
+
// ==============================================
285
285
+
// Map Transformation Tests
286
286
+
// ==============================================
287
287
+
288
288
+
public function testMapOnOkResult(): void
289
289
+
{
290
290
+
$result = Result::Ok(5);
291
291
+
$mapped = $result->map(fn($x) => $x * 2);
292
292
+
293
293
+
$this->assertTrue($mapped->isOk());
294
294
+
$this->assertEquals(10, $mapped->unwrap());
295
295
+
}
296
296
+
297
297
+
public function testMapOnErrResult(): void
298
298
+
{
299
299
+
$result = Result::Err("database error");
300
300
+
$mapped = $result->map(fn($x) => $x * 2);
301
301
+
302
302
+
$this->assertTrue($mapped->isErr());
303
303
+
$this->assertEquals("database error", $mapped->unwrapErr());
304
304
+
}
305
305
+
306
306
+
public function testMapChaining(): void
307
307
+
{
308
308
+
$result = Result::Ok("hello")
309
309
+
->map(fn($s) => strtoupper($s)) // Result::Ok("HELLO")
310
310
+
->map(fn($s) => strlen($s)) // Result::Ok(5)
311
311
+
->map(fn($n) => $n > 3); // Result::Ok(true)
312
312
+
313
313
+
$this->assertTrue($result->isOk());
314
314
+
$this->assertTrue($result->unwrap());
315
315
+
}
316
316
+
317
317
+
public function testMapChainingWithError(): void
318
318
+
{
319
319
+
$result = Result::Err("connection failed")
320
320
+
->map(fn($s) => strtoupper($s)) // Result::Err("connection failed")
321
321
+
->map(fn($s) => strlen($s)) // Result::Err("connection failed")
322
322
+
->map(fn($n) => $n > 3); // Result::Err("connection failed")
323
323
+
324
324
+
$this->assertTrue($result->isErr());
325
325
+
$this->assertEquals("connection failed", $result->unwrapErr());
326
326
+
}
327
327
+
328
328
+
public function testMapWithFalsyValues(): void
329
329
+
{
330
330
+
// Test that falsy values are properly transformed
331
331
+
$testCases = [
332
332
+
[null, fn($x) => "null", "null"],
333
333
+
[false, fn($x) => "false", "false"],
334
334
+
[0, fn($x) => $x + 1, 1],
335
335
+
["", fn($x) => "empty", "empty"],
336
336
+
[[], fn($x) => count($x), 0],
337
337
+
];
338
338
+
339
339
+
foreach ($testCases as [$input, $mapper, $expected]) {
340
340
+
$result = Result::Ok($input)->map($mapper);
341
341
+
$this->assertTrue($result->isOk());
342
342
+
$this->assertEquals($expected, $result->unwrap());
343
343
+
}
344
344
+
}
345
345
+
346
346
+
public function testMapWithComplexTransformations(): void
347
347
+
{
348
348
+
$data = ['name' => 'john', 'age' => 30];
349
349
+
$result = Result::Ok($data)
350
350
+
->map(fn($user) => $user['name'])
351
351
+
->map(fn($name) => ucfirst($name))
352
352
+
->map(fn($name) => "Hello, {$name}!");
353
353
+
354
354
+
$this->assertTrue($result->isOk());
355
355
+
$this->assertEquals("Hello, John!", $result->unwrap());
356
356
+
}
357
357
+
358
358
+
// ==============================================
359
359
+
// MapErr Transformation Tests
360
360
+
// ==============================================
361
361
+
362
362
+
public function testMapErrOnErrResult(): void
363
363
+
{
364
364
+
$result = Result::Err("database error");
365
365
+
$mapped = $result->mapErr(fn($e) => "Error: " . $e);
366
366
+
367
367
+
$this->assertTrue($mapped->isErr());
368
368
+
$this->assertEquals("Error: database error", $mapped->unwrapErr());
369
369
+
}
370
370
+
371
371
+
public function testMapErrOnOkResult(): void
372
372
+
{
373
373
+
$result = Result::Ok(5);
374
374
+
$mapped = $result->mapErr(fn($e) => "Error: " . $e);
375
375
+
376
376
+
$this->assertTrue($mapped->isOk());
377
377
+
$this->assertEquals(5, $mapped->unwrap());
378
378
+
}
379
379
+
380
380
+
public function testMapErrChaining(): void
381
381
+
{
382
382
+
$result = Result::Err("connection")
383
383
+
->mapErr(fn($e) => $e . " failed") // "connection failed"
384
384
+
->mapErr(fn($e) => "Error: " . $e) // "Error: connection failed"
385
385
+
->mapErr(fn($e) => strtoupper($e)); // "ERROR: CONNECTION FAILED"
386
386
+
387
387
+
$this->assertTrue($result->isErr());
388
388
+
$this->assertEquals("ERROR: CONNECTION FAILED", $result->unwrapErr());
389
389
+
}
390
390
+
391
391
+
public function testMapErrWithFalsyValues(): void
392
392
+
{
393
393
+
$testCases = [
394
394
+
[null, fn($x) => "null error", "null error"],
395
395
+
[false, fn($x) => "false error", "false error"],
396
396
+
[0, fn($x) => "zero error", "zero error"],
397
397
+
["", fn($x) => "empty error", "empty error"],
398
398
+
];
399
399
+
400
400
+
foreach ($testCases as [$input, $mapper, $expected]) {
401
401
+
$result = Result::Err($input)->mapErr($mapper);
402
402
+
$this->assertTrue($result->isErr());
403
403
+
$this->assertEquals($expected, $result->unwrapErr());
404
404
+
}
405
405
+
}
406
406
+
407
407
+
public function testMixedMapAndMapErr(): void
408
408
+
{
409
409
+
// Test that map and mapErr can be used together
410
410
+
$okResult = Result::Ok("success")
411
411
+
->map(fn($s) => strtoupper($s))
412
412
+
->mapErr(fn($e) => "Error: " . $e);
413
413
+
414
414
+
$this->assertTrue($okResult->isOk());
415
415
+
$this->assertEquals("SUCCESS", $okResult->unwrap());
416
416
+
417
417
+
$errResult = Result::Err("failure")
418
418
+
->map(fn($s) => strtoupper($s))
419
419
+
->mapErr(fn($e) => "Error: " . $e);
420
420
+
421
421
+
$this->assertTrue($errResult->isErr());
422
422
+
$this->assertEquals("Error: failure", $errResult->unwrapErr());
423
423
+
}
424
424
+
425
425
+
// ==============================================
426
426
+
// Exception Testing
427
427
+
// ==============================================
428
428
+
429
429
+
public function testUnwrapExceptionOnErr(): void
430
430
+
{
431
431
+
$this->expectException(UnwrapErrException::class);
432
432
+
$result = Result::Err("something failed");
433
433
+
$result->unwrap();
434
434
+
}
435
435
+
436
436
+
public function testUnwrapErrExceptionOnOk(): void
437
437
+
{
438
438
+
$this->expectException(UnwrapOkException::class);
439
439
+
$result = Result::Ok("success");
440
440
+
$result->unwrapErr();
441
441
+
}
442
442
+
443
443
+
public function testExceptionWithFalsyErrorValues(): void
444
444
+
{
445
445
+
$falsyValues = [null, false, 0, "", []];
446
446
+
447
447
+
foreach ($falsyValues as $value) {
448
448
+
try {
449
449
+
$result = Result::Err($value);
450
450
+
$result->unwrap();
451
451
+
$this->fail("Expected UnwrapErrException was not thrown for value: " . var_export($value, true));
452
452
+
} catch (UnwrapErrException $e) {
453
453
+
$this->assertInstanceOf(UnwrapErrException::class, $e);
454
454
+
}
455
455
+
}
456
456
+
}
457
457
+
458
458
+
public function testExceptionWithFalsyOkValues(): void
459
459
+
{
460
460
+
$falsyValues = [null, false, 0, "", []];
461
461
+
462
462
+
foreach ($falsyValues as $value) {
463
463
+
try {
464
464
+
$result = Result::Ok($value);
465
465
+
$result->unwrapErr();
466
466
+
$this->fail("Expected UnwrapOkException was not thrown for value: " . var_export($value, true));
467
467
+
} catch (UnwrapOkException $e) {
468
468
+
$this->assertInstanceOf(UnwrapOkException::class, $e);
469
469
+
}
470
470
+
}
471
471
+
}
472
472
+
473
473
+
// ==============================================
474
474
+
// Real-World Scenario Tests
475
475
+
// ==============================================
476
476
+
477
477
+
public function testApiResponseScenario(): void
478
478
+
{
479
479
+
// Simulate successful API response processing
480
480
+
$successResponse = ['user' => ['id' => 123, 'name' => 'john doe', 'email' => 'JOHN@EXAMPLE.COM']];
481
481
+
482
482
+
$result = Result::Ok($successResponse)
483
483
+
->map(fn($data) => $data['user'])
484
484
+
->map(fn($user) => [
485
485
+
'id' => $user['id'],
486
486
+
'name' => ucfirst($user['name']),
487
487
+
'email' => strtolower($user['email'])
488
488
+
])
489
489
+
->unwrapOr(['error' => 'User not found']);
490
490
+
491
491
+
$expected = [
492
492
+
'id' => 123,
493
493
+
'name' => 'John doe',
494
494
+
'email' => 'john@example.com'
495
495
+
];
496
496
+
497
497
+
$this->assertEquals($expected, $result);
498
498
+
}
499
499
+
500
500
+
public function testApiErrorScenario(): void
501
501
+
{
502
502
+
// Simulate failed API response processing
503
503
+
$result = Result::Err("API request failed: 404")
504
504
+
->map(fn($data) => $data['user'])
505
505
+
->map(fn($user) => [
506
506
+
'id' => $user['id'],
507
507
+
'name' => ucfirst($user['name']),
508
508
+
'email' => strtolower($user['email'])
509
509
+
])
510
510
+
->mapErr(fn($error) => "Failed to process user: " . $error)
511
511
+
->unwrapOr(['error' => 'User not found']);
512
512
+
513
513
+
$this->assertEquals(['error' => 'User not found'], $result);
514
514
+
}
515
515
+
516
516
+
public function testValidationChainScenario(): void
517
517
+
{
518
518
+
// Test a validation chain scenario
519
519
+
$processInput = function(string $input) {
520
520
+
if (empty(trim($input))) {
521
521
+
return Result::Err("Input cannot be empty");
522
522
+
}
523
523
+
524
524
+
$trimmed = trim($input);
525
525
+
if (strlen($trimmed) > 10) {
526
526
+
return Result::Err("Input too long");
527
527
+
}
528
528
+
529
529
+
return Result::Ok(strtoupper($trimmed));
530
530
+
};
531
531
+
532
532
+
// Test successful validation
533
533
+
$result1 = $processInput(" hello ");
534
534
+
$this->assertTrue($result1->isOk());
535
535
+
$this->assertEquals("HELLO", $result1->unwrap());
536
536
+
537
537
+
// Test empty input
538
538
+
$result2 = $processInput(" ");
539
539
+
$this->assertTrue($result2->isErr());
540
540
+
$this->assertEquals("Input cannot be empty", $result2->unwrapErr());
541
541
+
542
542
+
// Test too long input
543
543
+
$result3 = $processInput("this is way too long");
544
544
+
$this->assertTrue($result3->isErr());
545
545
+
$this->assertEquals("Input too long", $result3->unwrapErr());
546
546
+
}
547
547
+
548
548
+
public function testDataPipelineScenario(): void
549
549
+
{
550
550
+
// Test a complex data transformation pipeline
551
551
+
$data = [
552
552
+
'users' => [
553
553
+
['name' => ' alice ', 'score' => 85],
554
554
+
['name' => 'bob', 'score' => 92],
555
555
+
['name' => '', 'score' => 78]
556
556
+
]
557
557
+
];
558
558
+
559
559
+
$result = Result::Ok($data)
560
560
+
->map(fn($d) => $d['users'])
561
561
+
->map(fn($users) => array_filter($users, fn($u) => !empty(trim($u['name']))))
562
562
+
->map(fn($users) => array_map(fn($u) => [
563
563
+
'name' => ucfirst(trim($u['name'])),
564
564
+
'score' => $u['score'],
565
565
+
'grade' => $u['score'] >= 90 ? 'A' : ($u['score'] >= 80 ? 'B' : 'C')
566
566
+
], $users));
567
567
+
568
568
+
$this->assertTrue($result->isOk());
569
569
+
$users = $result->unwrap();
570
570
+
$this->assertCount(2, $users); // Empty name user filtered out
571
571
+
$this->assertEquals('Alice', $users[0]['name']);
572
572
+
$this->assertEquals('B', $users[0]['grade']);
573
573
+
$this->assertEquals('Bob', $users[1]['name']);
574
574
+
$this->assertEquals('A', $users[1]['grade']);
575
575
+
}
576
576
+
577
577
+
// ==============================================
578
578
+
// Error Propagation Tests
579
579
+
// ==============================================
580
580
+
581
581
+
public function testErrorPropagationThroughLongChain(): void
582
582
+
{
583
583
+
$result = Result::Err("initial error")
584
584
+
->map(fn($x) => $x * 2)
585
585
+
->map(fn($x) => $x + 10)
586
586
+
->map(fn($x) => "Result: " . $x)
587
587
+
->mapErr(fn($e) => "Enhanced: " . $e)
588
588
+
->map(fn($x) => strtoupper($x))
589
589
+
->mapErr(fn($e) => $e . " (final)");
590
590
+
591
591
+
$this->assertTrue($result->isErr());
592
592
+
$this->assertEquals("Enhanced: initial error (final)", $result->unwrapErr());
593
593
+
}
594
594
+
595
595
+
public function testSuccessPropagationThroughLongChain(): void
596
596
+
{
597
597
+
$result = Result::Ok(5)
598
598
+
->map(fn($x) => $x * 2) // 10
599
599
+
->map(fn($x) => $x + 10) // 20
600
600
+
->map(fn($x) => "Result: " . $x) // "Result: 20"
601
601
+
->mapErr(fn($e) => "Enhanced: " . $e) // No effect on Ok
602
602
+
->map(fn($x) => strtoupper($x)) // "RESULT: 20"
603
603
+
->mapErr(fn($e) => $e . " (final)"); // No effect on Ok
604
604
+
605
605
+
$this->assertTrue($result->isOk());
606
606
+
$this->assertEquals("RESULT: 20", $result->unwrap());
607
607
+
}
608
608
+
609
609
+
// ==============================================
610
610
+
// Type Safety and Identity Tests
611
611
+
// ==============================================
612
612
+
613
613
+
public function testResultIdentity(): void
614
614
+
{
615
615
+
$result1 = Result::Ok("data");
616
616
+
$result2 = Result::Ok("data");
617
617
+
$result3 = Result::Err("error");
618
618
+
619
619
+
// Different instances with same data should not be identical
620
620
+
$this->assertNotSame($result1, $result2);
621
621
+
622
622
+
// But their values should be equal
623
623
+
$this->assertEquals($result1->unwrap(), $result2->unwrap());
624
624
+
625
625
+
// Different types should behave differently
626
626
+
$this->assertNotEquals($result1->isOk(), $result3->isOk());
627
627
+
}
628
628
+
629
629
+
public function testResultWithDifferentTypes(): void
630
630
+
{
631
631
+
$stringResult = Result::Ok("string");
632
632
+
$numberResult = Result::Ok(123);
633
633
+
$arrayResult = Result::Ok(['key' => 'value']);
634
634
+
$objectResult = Result::Ok((object)['prop' => 'value']);
635
635
+
636
636
+
$this->assertIsString($stringResult->unwrap());
637
637
+
$this->assertIsInt($numberResult->unwrap());
638
638
+
$this->assertIsArray($arrayResult->unwrap());
639
639
+
$this->assertIsObject($objectResult->unwrap());
640
640
+
}
641
641
+
642
642
+
// ==============================================
643
643
+
// Integration with unwrapOr Tests
644
644
+
// ==============================================
645
645
+
646
646
+
public function testComplexChainWithUnwrapOr(): void
647
647
+
{
648
648
+
// Test a complete chain ending with unwrapOr for safe extraction
649
649
+
$processData = function($input) {
650
650
+
return Result::Ok($input)
651
651
+
->map(fn($data) => is_array($data) ? $data : [])
652
652
+
->map(fn($arr) => array_filter($arr, fn($x) => $x > 0))
653
653
+
->map(fn($arr) => array_sum($arr))
654
654
+
->unwrapOr(0);
655
655
+
};
656
656
+
657
657
+
$this->assertEquals(15, $processData([1, 2, 3, 4, 5]));
658
658
+
$this->assertEquals(9, $processData([1, -2, 3, -4, 5]));
659
659
+
$this->assertEquals(0, $processData([]));
660
660
+
$this->assertEquals(0, $processData("not an array"));
661
661
+
662
662
+
// Test with error case
663
663
+
$errorResult = Result::Err("processing failed")
664
664
+
->map(fn($x) => $x * 2)
665
665
+
->unwrapOr("default value");
666
666
+
667
667
+
$this->assertEquals("default value", $errorResult);
668
668
+
}
669
669
+
670
670
+
public function testUnwrapOrWithComplexDefaults(): void
671
671
+
{
672
672
+
$complexDefault = ['status' => 'error', 'data' => null, 'message' => 'Operation failed'];
673
673
+
674
674
+
$result = Result::Err("network timeout")
675
675
+
->map(fn($data) => json_decode($data, true))
676
676
+
->unwrapOr($complexDefault);
677
677
+
678
678
+
$this->assertEquals($complexDefault, $result);
679
679
+
}
680
680
+
681
681
+
// ==============================================
682
682
+
// Additional Edge Case Tests
683
683
+
// ==============================================
684
684
+
685
685
+
public function testResultMethodsPreserveTypeWithCallbacks(): void
686
686
+
{
687
687
+
// Test that transformations preserve the Result type through the chain
688
688
+
$result = Result::Ok(['count' => 5])
689
689
+
->map(fn($data) => $data['count'])
690
690
+
->map(fn($count) => $count * 2)
691
691
+
->map(fn($doubled) => $doubled > 5 ? "high" : "low");
692
692
+
693
693
+
$this->assertInstanceOf(Result::class, $result);
694
694
+
$this->assertTrue($result->isOk());
695
695
+
$this->assertEquals("high", $result->unwrap());
696
696
+
}
697
697
+
698
698
+
public function testChainedMethodsWithNullCallbackResults(): void
699
699
+
{
700
700
+
// Test behavior when callbacks return null
701
701
+
$result = Result::Ok("input")
702
702
+
->map(fn($s) => null) // Callback returns null
703
703
+
->map(fn($n) => "processed");
704
704
+
705
705
+
$this->assertTrue($result->isOk());
706
706
+
$this->assertEquals("processed", $result->unwrap());
707
707
+
}
708
708
+
709
709
+
public function testGetMethodsReturnCorrectOptionTypes(): void
710
710
+
{
711
711
+
$okResult = Result::Ok("success");
712
712
+
$errResult = Result::Err("failure");
713
713
+
714
714
+
// Test getOk returns Some for Ok and None for Err
715
715
+
$okOption = $okResult->getOk();
716
716
+
$this->assertTrue($okOption->isSome());
717
717
+
$this->assertFalse($okOption->isNone());
718
718
+
719
719
+
$errOkOption = $errResult->getOk();
720
720
+
$this->assertFalse($errOkOption->isSome());
721
721
+
$this->assertTrue($errOkOption->isNone());
722
722
+
723
723
+
// Test getErr returns None for Ok and Some for Err
724
724
+
$okErrOption = $okResult->getErr();
725
725
+
$this->assertFalse($okErrOption->isSome());
726
726
+
$this->assertTrue($okErrOption->isNone());
727
727
+
728
728
+
$errOption = $errResult->getErr();
729
729
+
$this->assertTrue($errOption->isSome());
730
730
+
$this->assertFalse($errOption->isNone());
731
731
+
}
732
732
+
}