From e461a0d9470e82d46b7f485117a606ce900cb1a0 Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Mon, 25 Nov 2019 18:41:43 -0600 Subject: [PATCH 1/6] Providing Pipe Function w/multiple entry arguments --- composer.json | 1 + src/Functional/Functional.php | 5 +++ src/Functional/Pipe.php | 45 +++++++++++++++++++++++++++ tests/Functional/PipeTest.php | 57 +++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+) create mode 100644 src/Functional/Pipe.php create mode 100644 tests/Functional/PipeTest.php diff --git a/composer.json b/composer.json index d0a8aa52..c42ae2af 100644 --- a/composer.json +++ b/composer.json @@ -84,6 +84,7 @@ "src/Functional/PartialRight.php", "src/Functional/Partition.php", "src/Functional/Pick.php", + "src/Functional/Pipe.php", "src/Functional/Pluck.php", "src/Functional/Poll.php", "src/Functional/Product.php", diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index 17ae6211..f8dae429 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -309,6 +309,11 @@ final class Functional */ const pick = '\Functional\pick'; + /** + * @see \Functional\pipe + */ + const pipe = '\Functional\pipe'; + /** * @see \Functional\pluck */ diff --git a/src/Functional/Pipe.php b/src/Functional/Pipe.php new file mode 100644 index 00000000..e488999f --- /dev/null +++ b/src/Functional/Pipe.php @@ -0,0 +1,45 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +namespace Functional; + +/** + * Provides a callable that applies the functions passed as arguments from left + * to right, first function is able to admit several arguments at once + * @link https://github.com/lstrojny/functional-php/issues/141 + * @param callable[] ...$functions functions to be composed + * @return callable + */ +function pipe(...$functions): callable +{ + return function () use ($functions) { + $fun_args = \func_get_args(); + $entryFunction = \array_shift($functions); + return \array_reduce( + $functions, + function ($prev, $current_fun) { + return \call_user_func($current_fun, $prev); + }, + \call_user_func_array($entryFunction, $fun_args) + ); + }; +} diff --git a/tests/Functional/PipeTest.php b/tests/Functional/PipeTest.php new file mode 100644 index 00000000..0ee8219f --- /dev/null +++ b/tests/Functional/PipeTest.php @@ -0,0 +1,57 @@ + + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +namespace Functional\Tests; + +use function Functional\pipe; + +class PipeTest extends AbstractTestCase +{ + public function testPipeFunction() + { + $closure1 = $this->prophesize(CustomTestClosure::class); + $closure1->__invoke('o', 'n', 'e')->willReturn('one'); + $closure2 = $this->prophesize(CustomTestClosure::class); + $closure2->__invoke('one')->willReturn('one, two'); + $closure3 = $this->prophesize(CustomTestClosure::class); + $closure3->__invoke('one, two')->willReturn('one, two, three'); + + $result = pipe( + $closure1->reveal(), + $closure2->reveal(), + $closure3->reveal() + )('o', 'n', 'e'); + + $this->assertEquals('one, two, three', $result); + + $closure1->checkProphecyMethodsPredictions(); + $closure2->checkProphecyMethodsPredictions(); + $closure3->checkProphecyMethodsPredictions(); + } +} + +class CustomTestClosure +{ + public function __invoke() + { + } +} From 302f217099a44945bb818148a86e618ad105eb40 Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Fri, 31 Jul 2020 23:11:34 -0500 Subject: [PATCH 2/6] using mock objects for PipeTest --- src/Functional/Pipe.php | 10 ++++----- tests/Functional/PipeTest.php | 39 ++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/Functional/Pipe.php b/src/Functional/Pipe.php index e488999f..24706313 100644 --- a/src/Functional/Pipe.php +++ b/src/Functional/Pipe.php @@ -1,6 +1,6 @@ + * Copyright (C) 2019, 2020 by Jesus Franco Martinez * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -32,14 +32,14 @@ function pipe(...$functions): callable { return function () use ($functions) { - $fun_args = \func_get_args(); + $funArgs = \func_get_args(); $entryFunction = \array_shift($functions); return \array_reduce( $functions, - function ($prev, $current_fun) { - return \call_user_func($current_fun, $prev); + function ($prev, $currentFun) { + return \call_user_func($currentFun, $prev); }, - \call_user_func_array($entryFunction, $fun_args) + \call_user_func_array($entryFunction, $funArgs) ); }; } diff --git a/tests/Functional/PipeTest.php b/tests/Functional/PipeTest.php index 0ee8219f..51dc26b1 100644 --- a/tests/Functional/PipeTest.php +++ b/tests/Functional/PipeTest.php @@ -22,30 +22,45 @@ */ namespace Functional\Tests; +use Apantle\FunPHP\Test\CustomClosure; use function Functional\pipe; class PipeTest extends AbstractTestCase { + /** @group tzkmx */ public function testPipeFunction() { - $closure1 = $this->prophesize(CustomTestClosure::class); - $closure1->__invoke('o', 'n', 'e')->willReturn('one'); - $closure2 = $this->prophesize(CustomTestClosure::class); - $closure2->__invoke('one')->willReturn('one, two'); - $closure3 = $this->prophesize(CustomTestClosure::class); - $closure3->__invoke('one, two')->willReturn('one, two, three'); + $mockFirst = $this->getClosureMock(1, ['o', 'n', 'e'], 'one'); + $mockSecond = $this->getClosureMock(1, ['one'], 'one, two'); + $mockThird = $this->getClosureMock(1, ['one, two'], 'one, two, three'); $result = pipe( - $closure1->reveal(), - $closure2->reveal(), - $closure3->reveal() + $mockFirst, + $mockSecond, + $mockThird )('o', 'n', 'e'); $this->assertEquals('one, two, three', $result); + } + + private function getClosureMock( + int $invocations, + array $expectedArguments, + $mustReturnValue + ) { + $mock = $this->getMockBuilder(CustomTestClosure::class) + ->getMock(); + + $argsArray = []; + for ($index = 0; $index < count($expectedArguments); $index++) { + $argsArray[] = $this->equalTo($expectedArguments[$index]); + } - $closure1->checkProphecyMethodsPredictions(); - $closure2->checkProphecyMethodsPredictions(); - $closure3->checkProphecyMethodsPredictions(); + $mock->expects($this->exactly($invocations)) + ->method('__invoke') + ->withConsecutive($argsArray) + ->willReturn($mustReturnValue); + return $mock; } } From 5161029632de79875e484490ae9a466537bcfc16 Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Sat, 1 Aug 2020 00:18:48 -0500 Subject: [PATCH 3/6] dropping functional implementation for Functor - iterates with a for loop through next functions --- src/Functional/Pipe.php | 59 ++++++++++++++++++++++++++++------- tests/Functional/PipeTest.php | 37 ++++++++++++++++++++-- 2 files changed, 81 insertions(+), 15 deletions(-) diff --git a/src/Functional/Pipe.php b/src/Functional/Pipe.php index 24706313..1801224e 100644 --- a/src/Functional/Pipe.php +++ b/src/Functional/Pipe.php @@ -1,4 +1,5 @@ * @@ -20,26 +21,60 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + namespace Functional; +use Functional\Exceptions\InvalidArgumentException; + /** - * Provides a callable that applies the functions passed as arguments from left - * to right, first function is able to admit several arguments at once + * Provides a functor that applies the functions passed at construction + * from left to right, first function is able to admit several arguments + * at once. + * * @link https://github.com/lstrojny/functional-php/issues/141 * @param callable[] ...$functions functions to be composed * @return callable */ function pipe(...$functions): callable { - return function () use ($functions) { + return new Pipe($functions); +} + +class Pipe +{ + /** @var callable[] */ + protected $callables; + + protected $carry; + + protected $pipeLength = 0; + + public function __construct(array $functions) + { + $this->pipeLength = count($functions); + if ($this->pipeLength < 2) { + throw new InvalidArgumentException( + 'You should pass at least 2 functions or functors to build a pipe' + ); + } + foreach ($functions as $index => $callable) { + InvalidArgumentException::assertCallback($callable, 'pipe', $index + 1); + $this->callables[] = $callable; + } + } + + public function __invoke() + { $funArgs = \func_get_args(); - $entryFunction = \array_shift($functions); - return \array_reduce( - $functions, - function ($prev, $currentFun) { - return \call_user_func($currentFun, $prev); - }, - \call_user_func_array($entryFunction, $funArgs) - ); - }; + $this->carry = \call_user_func_array($this->callables[0], $funArgs); + + for ($index = 1; $index < $this->pipeLength; $index++) { + $this->carry = \call_user_func( + $this->callables[$index], + $this->carry + ); + } + + return $this->carry; + } } diff --git a/tests/Functional/PipeTest.php b/tests/Functional/PipeTest.php index 51dc26b1..e5590e40 100644 --- a/tests/Functional/PipeTest.php +++ b/tests/Functional/PipeTest.php @@ -1,6 +1,6 @@ + * Copyright (C) 2019, 2020 by Jesus Franco Martinez * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,11 @@ */ namespace Functional\Tests; -use Apantle\FunPHP\Test\CustomClosure; +use Functional\Exceptions\InvalidArgumentException; use function Functional\pipe; class PipeTest extends AbstractTestCase { - /** @group tzkmx */ public function testPipeFunction() { $mockFirst = $this->getClosureMock(1, ['o', 'n', 'e'], 'one'); @@ -43,6 +42,38 @@ public function testPipeFunction() $this->assertEquals('one, two, three', $result); } + public function testShouldNotAcceptSingleFunction() + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('You should pass at least 2 functions or functors to build a pipe'); + pipe('strval')(); + } + + public function testExceptionNotCallable($maybeFun1, $maybeFun2, $expectedException) + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage($expectedException); + pipe($maybeFun1, $maybeFun2)(); + } + + public function notQuiteFunctionsProvider() + { + return [ + [ + 'strval', + '__not', + 'pipe() expects parameter 2 to be a valid callback, ' . + 'function \'__not\' not found or invalid function name' + ], + [ + 'runabout', + 'intval', + 'pipe() expects parameter 1 to be a valid callback, ' . + 'function \'runabout\' not found or invalid function name' + ] + ]; + } + private function getClosureMock( int $invocations, array $expectedArguments, From e0a8eb1881d100ed71bcf689a80184172dd033d7 Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Sat, 1 Aug 2020 00:56:18 -0500 Subject: [PATCH 4/6] fixed Pipe Tests --- src/Functional/Functional.php | 2 +- tests/Functional/FunctionalTest.php | 2 +- tests/Functional/PipeTest.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Functional/Functional.php b/src/Functional/Functional.php index 90cb25e7..5dbb8a29 100644 --- a/src/Functional/Functional.php +++ b/src/Functional/Functional.php @@ -14,7 +14,7 @@ final class Functional { /** - * @see \Function\ary + * @see \Functional\ary */ const ary = '\Functional\ary'; diff --git a/tests/Functional/FunctionalTest.php b/tests/Functional/FunctionalTest.php index 85b9bfda..5f548ad7 100644 --- a/tests/Functional/FunctionalTest.php +++ b/tests/Functional/FunctionalTest.php @@ -24,7 +24,7 @@ public function testAllDefinedConstantsAreValidCallables() $functions = $functionalClass->getConstants(); foreach ($functions as $function) { - $this->assertInternalType('callable', $function); + $this->assertIsCallable($function); } } diff --git a/tests/Functional/PipeTest.php b/tests/Functional/PipeTest.php index e5590e40..fbf30347 100644 --- a/tests/Functional/PipeTest.php +++ b/tests/Functional/PipeTest.php @@ -49,6 +49,7 @@ public function testShouldNotAcceptSingleFunction() pipe('strval')(); } + /** @dataProvider notQuiteFunctionsProvider */ public function testExceptionNotCallable($maybeFun1, $maybeFun2, $expectedException) { $this->expectException(InvalidArgumentException::class); From 0a9726d7f3a29075bda3f3d6f0ebe71b3f5cfdda Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Sat, 1 Aug 2020 01:03:58 -0500 Subject: [PATCH 5/6] fixing style nagging --- tests/Functional/PipeTest.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Functional/PipeTest.php b/tests/Functional/PipeTest.php index fbf30347..8c299cff 100644 --- a/tests/Functional/PipeTest.php +++ b/tests/Functional/PipeTest.php @@ -1,4 +1,5 @@ * @@ -20,9 +21,11 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ + namespace Functional\Tests; use Functional\Exceptions\InvalidArgumentException; + use function Functional\pipe; class PipeTest extends AbstractTestCase From 6f9ac1aa5a61d08f33fa3383fb07ea48f3e680ed Mon Sep 17 00:00:00 2001 From: "Jesus E. Franco Martinez" Date: Sat, 1 Aug 2020 01:08:54 -0500 Subject: [PATCH 6/6] prefix count function with a " --- src/Functional/Pipe.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Functional/Pipe.php b/src/Functional/Pipe.php index 1801224e..3bf13f12 100644 --- a/src/Functional/Pipe.php +++ b/src/Functional/Pipe.php @@ -51,7 +51,7 @@ class Pipe public function __construct(array $functions) { - $this->pipeLength = count($functions); + $this->pipeLength = \count($functions); if ($this->pipeLength < 2) { throw new InvalidArgumentException( 'You should pass at least 2 functions or functors to build a pipe'