From bc4e5b9b4e5e5005d45e1283495f1143dd6bdf37 Mon Sep 17 00:00:00 2001 From: Fabio Ivona Date: Sun, 10 Oct 2021 00:16:21 +0200 Subject: [PATCH] implemented pipe closure with $next as the last parameter --- src/Exceptions/PipeException.php | 18 +++++ src/Expectation.php | 39 +++++----- src/Support/ExpectationPipeline.php | 114 ++++++++++++++++++++++++++++ src/Support/Pipeline.php | 71 ----------------- tests/Features/Expect/pipe.php | 90 ++++++++++++---------- 5 files changed, 202 insertions(+), 130 deletions(-) create mode 100644 src/Exceptions/PipeException.php create mode 100644 src/Support/ExpectationPipeline.php delete mode 100644 src/Support/Pipeline.php diff --git a/src/Exceptions/PipeException.php b/src/Exceptions/PipeException.php new file mode 100644 index 00000000..c41e221e --- /dev/null +++ b/src/Exceptions/PipeException.php @@ -0,0 +1,18 @@ +value) ? $this->value : iterator_to_array($this->value); - $keys = array_keys($value); - $values = array_values($value); + $value = is_array($this->value) ? $this->value : iterator_to_array($this->value); + $keys = array_keys($value); + $values = array_values($value); $callbacksCount = count($callbacks); $index = 0; while (count($callbacks) < count($values)) { $callbacks[] = $callbacks[$index]; - $index = $index < count($values) - 1 ? $index + 1 : 0; + $index = $index < count($values) - 1 ? $index + 1 : 0; } if ($callbacksCount > count($values)) { @@ -168,7 +170,7 @@ final class Expectation * * @template TMatchSubject of array-key * - * @param callable(): TMatchSubject|TMatchSubject $subject + * @param callable(): TMatchSubject|TMatchSubject $subject * @param array): mixed)|TValue> $expressions */ public function match($subject, array $expressions): Expectation @@ -260,28 +262,23 @@ final class Expectation return new HigherOrderExpectation($this, $this->value->$method(...$parameters)); } - Pipeline::send(...$parameters) + ExpectationPipeline::for($method, $this->getExpectationClosure($method)) + ->send(...$parameters) ->through($this->pipes($method, $this, Expectation::class)) - ->finally(function ($parameters) use ($method): void { - $this->callExpectation($method, $parameters); - }); + ->run(); return $this; } - /** - * @param array $parameters - */ - private function callExpectation(string $name, array $parameters): void + private function getExpectationClosure(string $name): Closure { if (method_exists($this->coreExpectation, $name)) { - //@phpstan-ignore-next-line - $this->coreExpectation->{$name}(...$parameters); - } else { - if (self::hasExtend($name)) { - $this->__extendsCall($name, $parameters); - } + return Closure::fromCallable([$this->coreExpectation, $name]); + } elseif (self::hasExtend($name)) { + return self::$extends[$name]; } + + throw PipeException::expectationNotFound($name); } private function hasExpectation(string $name): bool diff --git a/src/Support/ExpectationPipeline.php b/src/Support/ExpectationPipeline.php new file mode 100644 index 00000000..bef4bdc6 --- /dev/null +++ b/src/Support/ExpectationPipeline.php @@ -0,0 +1,114 @@ + */ + private $pipes = []; + + /** @var array */ + private $passable; + + /** @var Closure */ + private $expectationClosure; + + /** @var string */ + private $expectationName; + + public function __construct(string $expectationName, Closure $expectationClosure) + { + $this->expectationClosure = $expectationClosure; + $this->expectationName = $expectationName; + } + + public static function for(string $expectationName, Closure $expectationClosure): self + { + return new self($expectationName, $expectationClosure); + } + + /** + * @param array $passable + */ + public function send(...$passable): self + { + $this->passable = $passable; + return $this; + } + + /** + * @param array $pipes + */ + public function through(array $pipes): self + { + $this->pipes = $pipes; + + return $this; + } + + public function run(): void + { + $pipeline = array_reduce( + array_reverse($this->pipes), + $this->carry(), + $this->expectationClosure + ); + + $pipeline(...$this->passable); + } + + public function carry(): Closure + { + return function ($stack, $pipe): Closure { + return function (...$passable) use ($stack, $pipe) { + $this->checkOptionalParametersBecomeRequired($pipe); + + $passable = $this->preparePassable($passable); + + $passable[] = $stack; + + return $pipe(...$passable); + }; + }; + } + + + private function preparePassable(array $passable): array + { + $reflection = new ReflectionFunction($this->expectationClosure); + + $requiredParametersCount = $reflection->getNumberOfParameters(); + + + if (count($passable) < $requiredParametersCount) { + foreach ($reflection->getParameters() as $index => $parameter) { + if (!isset($passable[$index])) { + $passable[$index] = $parameter->getDefaultValue(); + } + } + } + + return $passable; + } + + private function checkOptionalParametersBecomeRequired($pipe) + { + $reflection = new ReflectionFunction($pipe); + + foreach ($reflection->getParameters() as $parameter) { + if ($parameter->isDefaultValueAvailable()) { + /* + * TODO add pipeline blame in the exception message and a stronger clarification like + * “You’re attempting to pipe ‘toBe’, but haven’t added the $actual parameter to your pipe handler” + */ + throw PipeException::optionalParmetersShouldBecomeRequired($this->expectationName); + } + } + } +} diff --git a/src/Support/Pipeline.php b/src/Support/Pipeline.php deleted file mode 100644 index 4501f278..00000000 --- a/src/Support/Pipeline.php +++ /dev/null @@ -1,71 +0,0 @@ - */ - private $pipes = []; - - /** @var array */ - private $passable; - - /** - * @param array $passable - */ - public function __construct(...$passable) - { - $this->passable = $passable; - } - - /** - * @param array $passable - */ - public static function send(...$passable): self - { - return new self(...$passable); - } - - /** - * @param array $pipes - */ - public function through(array $pipes): self - { - $this->pipes = $pipes; - - return $this; - } - - public function finally(Closure $finalClosure): void - { - $pipeline = array_reduce( - array_reverse($this->pipes), - $this->carry(), - $this->prepareFinalClosure($finalClosure) - ); - - $pipeline(...$this->passable); - } - - public function carry(): Closure - { - return function ($stack, $pipe): Closure { - return function (...$passable) use ($stack, $pipe) { - $passable[] = $stack; - - return $pipe(...$passable); - }; - }; - } - - private function prepareFinalClosure(Closure $finalClosure): Closure - { - return function (...$passable) use ($finalClosure) { - return $finalClosure($passable); - }; - } -} diff --git a/tests/Features/Expect/pipe.php b/tests/Features/Expect/pipe.php index 1d864f9c..d21ea5ed 100644 --- a/tests/Features/Expect/pipe.php +++ b/tests/Features/Expect/pipe.php @@ -1,6 +1,7 @@ runCount = [ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]; $this->appliedCount = [ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]; } } @@ -65,7 +66,7 @@ class State $state = new State(); /* - * Asserts two Characters are the same + * Overrides toBe to assert two Characters are the same */ expect()->pipe('toBe', function ($expected, $next) use ($state) { $state->runCount['character']++; @@ -82,7 +83,7 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { }); /* - * Asserts two Numbers are the same + * Overrides toBe to assert two Numbers are the same */ expect()->intercept('toBe', Number::class, function ($expected) use ($state) { $state->runCount['number']++; @@ -91,7 +92,7 @@ expect()->intercept('toBe', Number::class, function ($expected) use ($state) { }); /* - * Asserts all integers are allowed if value is an '*' + * Overrides toBe to assert all integers are allowed if value is an '*' */ expect()->intercept('toBe', function ($value) { return $value === '*'; @@ -102,7 +103,7 @@ expect()->intercept('toBe', function ($value) { }); /* - * Asserts two Symbols are the same + * Overrides toBe to assert two Symbols are the same */ expect()->pipe('toBe', function ($expected, $next) use ($state) { $state->runCount['symbol']++; @@ -118,6 +119,13 @@ expect()->pipe('toBe', function ($expected, $next) use ($state) { $next($expected); }); +expect()->intercept('toHaveProperty', function ($value) { + return $value instanceof Symbol && $value->value == '£'; +}, function (string $propertyName, $propertyValue = null) { + assertEquals("£", $this->value->value); +}); + + test('pipe is applied and can stop pipeline', function () use ($state) { $letter = new Character('A'); @@ -126,18 +134,19 @@ test('pipe is applied and can stop pipeline', function () use ($state) { expect($letter)->toBe(new Character('A')) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]) ->appliedCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]); -}); +}) +; test('pipe is run and can let the pipeline keep going', function () use ($state) { $state->reset(); @@ -145,16 +154,16 @@ test('pipe is run and can let the pipeline keep going', function () use ($state) expect(3)->toBe(3) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 1, + 'character' => 1, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 1, ]) ->appliedCount->toMatchArray([ - 'character' => 0, - 'number' => 0, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 0, + 'wildcard' => 0, + 'symbol' => 0, ]); }); @@ -177,16 +186,16 @@ test('intercept stops the pipeline', function () use ($state) { expect($number)->toBe(new Number(1)) ->and($state) ->runCount->toMatchArray([ - 'character' => 1, - 'number' => 1, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 1, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, ]) ->appliedCount->toMatchArray([ - 'character' => 0, - 'number' => 1, - 'wildcard' => 0, - 'symbol' => 0, + 'character' => 0, + 'number' => 1, + 'wildcard' => 0, + 'symbol' => 0, ]); }); @@ -207,3 +216,8 @@ test('intercept can be filtered with a closure', function () use ($state) { ->runCount->toHaveKey('wildcard', 1) ->appliedCount->toHaveKey('wildcard', 1); }); + +test('intercept can handle default values', function(){ + expect(new Symbol("£"))->toHaveProperty('value'); + expect(new Symbol("£"))->toHaveProperty('value', '£'); +})->only();