Rust-style Option and Result Classes for PHP
1<?php
2
3declare(strict_types=1);
4
5require_once __DIR__.'/../vendor/autoload.php';
6
7use Ciarancoza\OptionResult\Exceptions\UnwrapErrException;
8use Ciarancoza\OptionResult\Exceptions\UnwrapOkException;
9use Ciarancoza\OptionResult\Result;
10use PHPUnit\Framework\TestCase;
11
12class ResultTest extends TestCase
13{
14 // Unit tests - Adapted from Rust Docs
15
16 public function test_is_ok(): void
17 {
18 $x = Result::Ok(-3);
19 $this->assertSame(true, $x->isOk());
20
21 $x = Result::Err('Some error message');
22 $this->assertSame(false, $x->isOk());
23 }
24
25 public function test_is_err(): void
26 {
27 $x = Result::Ok(-3);
28 $this->assertSame(false, $x->isErr());
29
30 $x = Result::Err('Some error message');
31 $this->assertSame(true, $x->isErr());
32 }
33
34 public function test_unwrap(): void
35 {
36 $x = Result::Ok(2);
37 $this->assertSame(2, $x->unwrap());
38
39 $this->expectException(UnwrapErrException::class);
40 Result::Err('emergency failure')->unwrap();
41 }
42
43 public function test_unwrap_err(): void
44 {
45 $x = Result::Err('emergency failure');
46 $this->assertSame('emergency failure', $x->unwrapErr());
47
48 $this->expectException(UnwrapOkException::class);
49 Result::Ok(2)->unwrapErr();
50 }
51
52 public function test_unwrap_or(): void
53 {
54 $default = 2;
55 $this->assertSame(9, Result::Ok(9)->unwrapOr($default));
56 $this->assertSame($default, Result::Err('error')->unwrapOr($default));
57
58 $this->assertSame(9, Result::Ok(9)->unwrapOr(fn () => $default));
59 $this->assertSame($default, Result::Err('error')->unwrapOr(fn () => $default));
60 }
61
62 public function test_unwrap_or_else(): void
63 {
64 $this->assertSame(2, Result::Ok(2)->unwrapOrElse(fn ($err) => strlen($err)));
65 $this->assertSame(3, Result::Err('foo')->unwrapOrElse(fn ($err) => strlen($err)));
66 }
67
68 public function test_map(): void
69 {
70 $result = Result::Ok(5)->map(fn ($i) => $i * 2);
71 $this->assertTrue($result->isOk());
72 $this->assertSame(10, $result->unwrap());
73
74 $result = Result::Err('parse error')->map(fn ($i) => $i * 2);
75 $this->assertTrue($result->isErr());
76 $this->assertSame('parse error', $result->unwrapErr());
77 }
78
79 public function test_map_err(): void
80 {
81 $stringify = fn ($x) => "error code: $x";
82
83 $result = Result::Ok(2)->mapErr($stringify);
84 $this->assertTrue($result->isOk());
85 $this->assertSame(2, $result->unwrap());
86
87 $result = Result::Err(13)->mapErr($stringify);
88 $this->assertTrue($result->isErr());
89 $this->assertSame('error code: 13', $result->unwrapErr());
90 }
91
92 public function test_map_or(): void
93 {
94 $this->assertSame(3, Result::Ok('foo')->mapOr(42, fn ($v) => strlen($v)));
95 $this->assertSame(42, Result::Err('bar')->mapOr(42, fn ($v) => strlen($v)));
96
97 $this->assertSame(3, Result::Ok('foo')->mapOr(fn () => 42, fn ($v) => strlen($v)));
98 $this->assertSame(42, Result::Err('bar')->mapOr(fn () => 42, fn ($v) => strlen($v)));
99 }
100
101 public function test_map_or_else(): void
102 {
103 $this->markTestIncomplete('TODO');
104 $this->assertSame(3, Result::Ok('foo')->mapOrElse(fn ($err) => strlen($err), fn ($v) => strlen($v)));
105 $this->assertSame(3, Result::Err('bar')->mapOrElse(fn ($err) => strlen($err), fn ($v) => strlen($v)));
106 }
107
108 public function test_get_ok(): void
109 {
110 $option = Result::Ok(2)->getOk();
111 $this->assertTrue($option->isSome());
112 $this->assertSame(2, $option->unwrap());
113
114 $option = Result::Err('Nothing here')->getOk();
115 $this->assertTrue($option->isNone());
116 }
117
118 public function test_get_err(): void
119 {
120 $option = Result::Ok(2)->getErr();
121 $this->assertTrue($option->isNone());
122
123 $option = Result::Err('Nothing here')->getErr();
124 $this->assertTrue($option->isSome());
125 $this->assertSame('Nothing here', $option->unwrap());
126 }
127
128 public function test_expect(): void
129 {
130 $this->assertSame('value', Result::Ok('value')->expect('Testing expect'));
131
132 $this->expectException(UnwrapErrException::class);
133 $this->expectExceptionMessage('Testing expect');
134 Result::Err('error')->expect('Testing expect');
135 }
136
137 public function test_expect_err(): void
138 {
139 $this->assertSame('value', Result::Err('value')->expectErr('Testing expect_err'));
140
141 $this->expectException(UnwrapOkException::class);
142 $this->expectExceptionMessage('Testing expect_err');
143 Result::Ok('error')->expectErr('Testing expect_err');
144 }
145
146 public function test_and(): void
147 {
148 $result = Result::Ok(2)->and(Result::Ok(4));
149 $this->assertTrue($result->isOk());
150 $this->assertSame(4, $result->unwrap());
151
152 $result = Result::Err('error')->and(Result::Ok(2));
153 $this->assertTrue($result->isErr());
154 $this->assertSame('error', $result->unwrapErr());
155
156 // Returns Err case
157 $result = Result::Ok(2)->and(Result::Err('new error'));
158 $this->assertTrue($result->isErr());
159 $this->assertSame('new error', $result->unwrapErr());
160 }
161
162 public function test_and_then(): void
163 {
164 $result = Result::Ok(2)->andThen(fn ($x) => Result::Ok($x * 2));
165 $this->assertTrue($result->isOk());
166 $this->assertSame(4, $result->unwrap());
167
168 $result = Result::Err('error')->andThen(fn ($x) => Result::Ok($x * 2));
169 $this->assertTrue($result->isErr());
170 $this->assertSame('error', $result->unwrapErr());
171
172 // Returns Err case
173 $result = Result::Ok(2)->andThen(fn ($_) => Result::Err('new error'));
174 $this->assertTrue($result->isErr());
175 $this->assertSame('new error', $result->unwrapErr());
176 }
177
178 public function test_try_catch(): void
179 {
180 $this->markTestIncomplete('TODO');
181 $result = Result::tryCatch(fn () => 'success', fn ($e) => 'Error: '.$e->getMessage());
182 $this->assertTrue($result->isOk());
183 $this->assertSame('success', $result->unwrap());
184
185 $result = Result::tryCatch(
186 fn () => throw new Exception('something failed'),
187 fn ($e) => 'Error: '.$e->getMessage()
188 );
189 $this->assertTrue($result->isErr());
190 $this->assertSame('Error: something failed', $result->unwrapErr());
191 }
192
193 public function test_inspect(): void
194 {
195 $this->markTestIncomplete('TODO');
196
197 $inspected = null;
198 $result = Result::Ok(4)->inspect(function ($x) use (&$inspected) {
199 $inspected = $x;
200 });
201
202 $this->assertTrue($result->isOk());
203 $this->assertSame(4, $result->unwrap());
204 $this->assertSame(4, $inspected);
205
206 $inspected = null;
207 $result = Result::Err('error')->inspect(function ($x) use (&$inspected) {
208 $inspected = $x;
209 });
210
211 $this->assertTrue($result->isErr());
212 $this->assertSame('error', $result->unwrapErr());
213 $this->assertNull($inspected); // Should not be called for Err
214 }
215
216 public function test_inspect_err(): void
217 {
218 $this->markTestIncomplete('TODO');
219
220 $inspected = null;
221 $result = Result::Err('error')->inspectErr(function ($e) use (&$inspected) {
222 $inspected = $e;
223 });
224
225 $this->assertTrue($result->isErr());
226 $this->assertSame('error', $result->unwrapErr());
227 $this->assertSame('error', $inspected);
228
229 $inspected = null;
230 $result = Result::Ok(42)->inspectErr(function ($e) use (&$inspected) {
231 $inspected = $e;
232 });
233
234 $this->assertTrue($result->isOk());
235 $this->assertSame(42, $result->unwrap());
236 $this->assertNull($inspected); // Should not be called for Ok
237 }
238
239 public function test_or(): void
240 {
241 $this->markTestIncomplete('TODO');
242
243 $x = Result::Ok(2);
244 $y = Result::Err('late error');
245 $result = $x->or($y);
246 $this->assertTrue($result->isOk());
247 $this->assertSame(2, $result->unwrap());
248
249 $x = Result::Err('early error');
250 $y = Result::Ok(2);
251 $result = $x->or($y);
252 $this->assertTrue($result->isOk());
253 $this->assertSame(2, $result->unwrap());
254
255 $x = Result::Err('not a 2');
256 $y = Result::Err('late error');
257 $result = $x->or($y);
258 $this->assertTrue($result->isErr());
259 $this->assertSame('late error', $result->unwrapErr());
260
261 $x = Result::Ok(2);
262 $y = Result::Ok(100);
263 $result = $x->or($y);
264 $this->assertTrue($result->isOk());
265 $this->assertSame(2, $result->unwrap());
266 }
267
268 // Integration tests
269
270 public function test_core_construction_and_state(): void
271 {
272 $testValues = ['success', 42, false, 0, '', []];
273 foreach ($testValues as $value) {
274 $ok = Result::Ok($value);
275 $this->assertTrue($ok->isOk());
276 $this->assertFalse($ok->isErr());
277 $this->assertSame($value, $ok->unwrap());
278
279 $err = Result::Err($value);
280 $this->assertFalse($err->isOk());
281 $this->assertTrue($err->isErr());
282 $this->assertSame($value, $err->unwrapErr());
283 }
284
285 $this->assertSame(true, Result::Ok()->unwrap());
286
287 $okNoneType = Result::Ok(null);
288 $this->assertTrue($okNoneType->isOk());
289 $this->assertFalse($okNoneType->isErr());
290 $this->assertNull($okNoneType->unwrap());
291 $this->assertEquals('test', $okNoneType->getOk()->unwrapOr('test'));
292
293 $errNoneType = Result::Err(null);
294 $this->assertTrue($errNoneType->isErr());
295 $this->assertFalse($errNoneType->isOk());
296 $this->assertNull($errNoneType->unwrapErr());
297 $this->assertEquals('test', $errNoneType->getErr()->unwrapOr('test'));
298 }
299
300 public function test_option_conversion(): void
301 {
302 $ok = Result::Ok('data');
303 $err = Result::Err('error');
304
305 $this->assertTrue($ok->getOk()->isSome());
306 $this->assertEquals('data', $ok->getOk()->unwrap());
307 $this->assertTrue($err->getOk()->isNone());
308
309 $this->assertTrue($err->getErr()->isSome());
310 $this->assertEquals('error', $err->getErr()->unwrap());
311 $this->assertTrue($ok->getErr()->isNone());
312
313 foreach ([false, 0, '', []] as $value) {
314 $this->assertEquals($value, Result::Ok($value)->getOk()->unwrap());
315 $this->assertEquals($value, Result::Err($value)->getErr()->unwrap());
316 }
317 }
318
319 public function test_api_response_scenario(): void
320 {
321 $successResponse = ['user' => ['id' => 123, 'name' => 'john doe', 'email' => 'JOHN@EXAMPLE.COM']];
322
323 $expected = ['id' => 123, 'name' => 'John doe', 'email' => 'john@example.com'];
324 $this->assertEquals($expected, Result::Ok($successResponse)
325 ->map(fn ($data) => $data['user'])
326 ->map(fn ($user) => [
327 'id' => $user['id'],
328 'name' => ucfirst($user['name']),
329 'email' => strtolower($user['email']),
330 ])->unwrapOr(['error' => 'User not found']));
331
332 $this->assertEquals(['error' => 'User not found'], Result::Err('API request failed: 404')
333 ->map(fn ($data) => $data['user'])
334 ->mapErr(fn ($error) => 'Failed to process user: '.$error)
335 ->unwrapOr(['error' => 'User not found']));
336 }
337
338 public function test_validation_chain_scenario(): void
339 {
340 $processInput = function (string $input) {
341 if (empty(trim($input))) {
342 return Result::Err('Input cannot be empty');
343 }
344 $trimmed = trim($input);
345 if (strlen($trimmed) > 10) {
346 return Result::Err('Input too long');
347 }
348
349 return Result::Ok(strtoupper($trimmed));
350 };
351
352 $this->assertEquals('HELLO', $processInput(' hello ')->unwrap());
353 $this->assertEquals('Input cannot be empty', $processInput(' ')->unwrapErr());
354 $this->assertEquals('Input too long', $processInput('this is way too long')->unwrapErr());
355 }
356
357 public function test_data_pipeline_scenario(): void
358 {
359 $data = ['users' => [
360 ['name' => ' alice ', 'score' => 85],
361 ['name' => 'bob', 'score' => 92],
362 ['name' => '', 'score' => 78],
363 ]];
364
365 $users = Result::Ok($data)
366 ->map(fn ($d) => $d['users'])
367 ->map(fn ($users) => array_filter($users, fn ($u) => ! empty(trim($u['name']))))
368 ->map(fn ($users) => array_map(fn ($u) => [
369 'name' => ucfirst(trim($u['name'])),
370 'score' => $u['score'],
371 'grade' => $u['score'] >= 90 ? 'A' : ($u['score'] >= 80 ? 'B' : 'C'),
372 ], $users))->unwrap();
373
374 $this->assertCount(2, $users);
375 $this->assertEquals(['name' => 'Alice', 'score' => 85, 'grade' => 'B'], $users[0]);
376 $this->assertEquals(['name' => 'Bob', 'score' => 92, 'grade' => 'A'], $users[1]);
377 }
378}