Rust-style Option and Result Classes for PHP
at main 378 lines 13 kB view raw
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}