diff --git a/.atoum.php b/.atoum.php index 731e15b..bacd141 100644 --- a/.atoum.php +++ b/.atoum.php @@ -1,15 +1,13 @@ addDefaultReport(); - -$coverageField = new atoum\report\fields\runner\coverage\html('Pattern Matching', './reports/'); -$report->addField($coverageField); - -$cloverWriter = new atoum\writers\file('./reports/atoum.coverage.xml'); -$cloverReport = new atoum\reports\asynchronous\clover(); -$cloverReport->addWriter($cloverWriter); -$runner->addReport($cloverReport); +$script->addDefaultReport(); +$clover = new \atoum\atoum\reports\sonar\clover(); +$writer = new \atoum\atoum\writers\file('coverage.xml'); +$clover->addWriter($writer); +$runner->addReport($clover); $runner->addTestsFromDirectory('./tests'); diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..0fa5b72 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,35 @@ +name: pattern-matching CI +on: [push, pull_request] +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: ['7.2', '7.3', '7.4', '8.0'] + name: PHP ${{ matrix.php }} + steps: + - uses: actions/checkout@v1 + - name: Install PHP + uses: shivammathur/setup-php@master + with: + php-version: ${{ matrix.php }} + # install xdebug for code coverage purposes + extensions: xdebug + - name: Validate composer.json and composer.lock + run: composer validate + - name: Get Composer cache directory + id: composer-cache + run: echo "::set-output name=dir::$(composer config cache-files-dir)" + - name: Cache dependencies + uses: actions/cache@v1 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ matrix.php }}-composer- + - name: Install dependencies + run: composer install --prefer-dist --no-progress --no-suggest + - name: Run test suite + run: vendor/bin/atoum --enable-branch-and-path-coverage + - name: Generate codecov coverage report + run: bash <(curl -s https://codecov.io/bash) diff --git a/.gitignore b/.gitignore index d61182c..b0e8fb2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ vendor/ reports/ +composer.lock +*.cache diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php new file mode 100644 index 0000000..67472f7 --- /dev/null +++ b/.php-cs-fixer.php @@ -0,0 +1,16 @@ +exclude(['vendor', 'cache', 'bin']) + ->in(__DIR__); + +$config = new Config; + +return $config + ->setRules([ + '@PSR12' => true + ]) + ->setFinder($finder); diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..6597f2e --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,8 @@ +risky: false +version: 7 +preset: 'psr12' +finder: + exclude: + - 'node_modules' + - 'vendor' + name: '*.php' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 44474f8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: php - -php: - - 5.6 - - 7.0 - - hhvm - - nightly - -matrix: - fast_finish: true - allow_failures: - - php: hhvm - - php: nightly - -install: - - composer install - -script: - - composer test - -after_success: - - wget https://scrutinizer-ci.com/ocular.phar - - php ocular.phar code-coverage:upload --access-token="f9425f56b2c9bb0e5f93eef1c1c53df07dc0c515c2373e7626e2195dce94fe6a" --format=php-clover ./reports/atoum.coverage.xml diff --git a/README.md b/README.md index 82deec3..4ecf3b9 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,20 @@ # Pattern Matching -[![Build Status](https://travis-ci.org/functional-php/pattern-matching.svg)](https://travis-ci.org/functional-php/pattern-matching) -[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/functional-php/pattern-matching/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/functional-php/pattern-matching/?branch=master) +[![pattern-matching CI](https://github.com/ace411/pattern-matching/actions/workflows/php.yml/badge.svg?branch=master)](https://github.com/ace411/pattern-matching/actions/workflows/php.yml) +[![StyleCI](https://github.styleci.io/repos/341440518/shield?branch=master)](https://github.styleci.io/repos/341440518?branch=master) [![Code Coverage](https://scrutinizer-ci.com/g/functional-php/pattern-matching/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/functional-php/pattern-matching/?branch=master) -[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/functional-php/pattern-matching.svg)](http://isitmaintained.com/project/functional-php/pattern-matching "Average time to resolve an issue") -[![Percentage of issues still open](http://isitmaintained.com/badge/open/functional-php/pattern-matching.svg)](http://isitmaintained.com/project/functional-php/pattern-matching "Percentage of issues still open") +[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/functional-php/pattern-matching.svg)](http://isitmaintained.com/project/functional-php/pattern-matching 'Average time to resolve an issue') +[![Percentage of issues still open](http://isitmaintained.com/badge/open/functional-php/pattern-matching.svg)](http://isitmaintained.com/project/functional-php/pattern-matching 'Percentage of issues still open') [![Chat on Gitter](https://img.shields.io/gitter/room/gitterHQ/gitter.svg)](https://gitter.im/functional-php) Pattern matching is the process of checking a series of token against a pattern. It is different from pattern recognition as the match needs to be exact. The process does not only match as a switch statement does, it also assigns the value -a bit like the ``list`` construct in PHP, a process called **destructuring**. +a bit like the `list` construct in PHP, a process called **destructuring**. Most functional languages implement it as a core feature. Here is are some small examples in Haskell: -``` haskell +```haskell fact :: (Integral a) => a -> a fact 0 = 1 @@ -74,7 +74,7 @@ You can also use the `match` function if you want to have a beefed up version of use FunctionalPHP\PatternMatching as m; function factorial($n) { - return m\match($n, [ + return m\pmatch($n, [ '0' => 1, 'n' => function($n) use(&$fact) { return $n * factorial($n - 1); @@ -82,7 +82,7 @@ function factorial($n) { ]); } -echo m\match([1, 2, ['a', 'b'], true], [ +echo m\pmatch([1, 2, ['a', 'b'], true], [ '"toto"' => 'first', '[a, [b, c], d]' => 'second', '[a, _, (x:xs), c]' => 'third', @@ -94,7 +94,7 @@ echo m\match([1, 2, ['a', 'b'], true], [ If you are just interested in destructuring your values, there is also a helper for that: -``` php +```php use FunctionalPHP\PatternMatching as m; @@ -112,21 +112,21 @@ print_r(m\extract([1, 2, ['a', 'b'], true], '[a, _, (x:xs), c]')); Here is a quick recap of the available patterns: -| Name | Format | Example | -|---------------|-----------------------------------|---------------------------------| -| Constant | Any scalar value (int, float, string, boolean) | ``1.0``, ``42``, "test" | -| Variable | ``identifier`` | ``a``, ``name``, ``anything`` | -| Array | ``[, ..., ]`` | ``[]``, ``[a]``, ``[a, b, c]`` | -| Cons | ``(identifier:list-identifier)`` | ``(x:xs)``, ``(x:y:z:xs)`` | -| Wildcard | ``_`` | ``_`` | -| As | ``identifier@()`` | ``all@(x:xs)`` | +| Name | Format | Example | +| -------- | ---------------------------------------------- | ------------------------ | +| Constant | Any scalar value (int, float, string, boolean) | `1.0`, `42`, "test" | +| Variable | `identifier` | `a`, `name`, `anything` | +| Array | `[, ..., ]` | `[]`, `[a]`, `[a, b, c]` | +| Cons | `(identifier:list-identifier)` | `(x:xs)`, `(x:y:z:xs)` | +| Wildcard | `_` | `_` | +| As | `identifier@()` | `all@(x:xs)` | ## Testing You can run the test suite for the library using: composer test - + A test report will be available in the `reports` directory. ## Contributing diff --git a/composer.json b/composer.json index 8b77364..32aff9f 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,12 @@ { "name": "functional-php/pattern-matching", "description": "Pattern matching for PHP with automatic destructuring.", - "keywords": ["functional", "pattern", "pattern matching", "destructuring"], + "keywords": [ + "functional", + "pattern", + "pattern matching", + "destructuring" + ], "license": "BSD-3-Clause", "authors": [ { @@ -13,10 +18,9 @@ "php": ">=5.6.0" }, "require-dev": { - "atoum/atoum": "*" - }, - "scripts": { - "test": "./vendor/bin/atoum --enable-branch-and-path-coverage" + "atoum/atoum": "^3 || ^4", + "atoum/reports-extension": "^3 || ^4", + "friendsofphp/php-cs-fixer": "~2 || ~3" }, "autoload": { "psr-4": { @@ -25,5 +29,9 @@ "files": [ "src/functions.php" ] + }, + "scripts": { + "ci:fix": "./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.php --diff --verbose --rules=@PSR12", + "test": "./vendor/bin/atoum --enable-branch-and-path-coverage" } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 3746288..0000000 --- a/composer.lock +++ /dev/null @@ -1,101 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "d84cf2c3d5dded177df804715a555340", - "content-hash": "948c1d8f127468aca01c2ad3db0cced0", - "packages": [], - "packages-dev": [ - { - "name": "atoum/atoum", - "version": "2.8.2", - "source": { - "type": "git", - "url": "https://github.com/atoum/atoum.git", - "reference": "4d0136b21185eea5fc2ee638f77b291e6c537100" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/atoum/atoum/zipball/4d0136b21185eea5fc2ee638f77b291e6c537100", - "reference": "4d0136b21185eea5fc2ee638f77b291e6c537100", - "shasum": "" - }, - "require": { - "ext-hash": "*", - "ext-json": "*", - "ext-session": "*", - "ext-tokenizer": "*", - "ext-xml": "*", - "php": ">=5.3.3" - }, - "replace": { - "mageekguy/atoum": "*" - }, - "suggest": { - "atoum/stubs": "Provides IDE support (like autocompletion) for atoum", - "ext-mbstring": "Provides support for UTF-8 strings" - }, - "bin": [ - "bin/atoum" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev" - } - }, - "autoload": { - "classmap": [ - "classes/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Frédéric Hardy", - "email": "frederic.hardy@atoum.org", - "homepage": "http://blog.mageekbox.net" - }, - { - "name": "François Dussert", - "email": "francois.dussert@atoum.org" - }, - { - "name": "Gérald Croes", - "email": "gerald.croes@atoum.org" - }, - { - "name": "Julien Bianchi", - "email": "julien.bianchi@atoum.org" - }, - { - "name": "Ludovic Fleury", - "email": "ludovic.fleury@atoum.org" - } - ], - "description": "Simple modern and intuitive unit testing framework for PHP 5.3+", - "homepage": "http://www.atoum.org", - "keywords": [ - "TDD", - "atoum", - "test", - "unit testing" - ], - "time": "2016-08-12 13:45:10" - } - ], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.6.0" - }, - "platform-dev": [] -} diff --git a/src/Parser.php b/src/Parser.php index 28f2be7..9f6bf2a 100644 --- a/src/Parser.php +++ b/src/Parser.php @@ -47,8 +47,10 @@ protected function _parseArray($value, $pattern) { $patterns = $this->_split(',', '[', ']', substr($pattern, 1, -1)); - if(count($patterns) === 0) { - return count($value) === 0 ? [] : false; + if ($value instanceof \Countable || is_array($value)) { + if (count($patterns) === 0) { + return count($value) === 0 ? [] : false; + } } return $this->_recurse($value, $patterns); @@ -59,7 +61,7 @@ protected function _parseCons($value, $pattern) $patterns = $this->_split(':', '(', ')', substr($pattern, 1, -1)); $last_pattern = array_pop($patterns); - if(! is_array($value)) { + if (! is_array($value)) { return false; } @@ -86,18 +88,18 @@ public function parse($pattern, $value) { $pattern = trim($pattern); - if(is_numeric($pattern)) { + if (is_numeric($pattern)) { return $this->_parseNumericConstant($value, $pattern); } // a true value will mean that no regex matched // a false value will mean that at least one regex matched but the pattern didn't // anything else is the result of the pattern matching - $result = array_reduce(array_keys($this->rules), function($current, $regex) use($value, $pattern) { + $result = array_reduce(array_keys($this->rules), function ($current, $regex) use ($value, $pattern) { return $this->_updateParsingResult($value, $pattern, $regex, $current); }, true); - if($result === true) { + if ($result === true) { $this->_invalidPattern($pattern); } @@ -106,7 +108,7 @@ public function parse($pattern, $value) protected function _updateParsingResult($value, $pattern, $regex, $current) { - if(is_bool($current) && preg_match($regex, $pattern)) { + if (is_bool($current) && preg_match($regex, $pattern)) { $current = call_user_func_array([$this, $this->rules[$regex]], [$value, $pattern]); } @@ -117,7 +119,7 @@ protected function _split($delimiter, $start, $stop, $pattern) { $result = split_enclosed($delimiter, $start, $stop, $pattern); - if($result === false) { + if ($result === false) { $this->_invalidPattern($pattern); } @@ -126,23 +128,23 @@ protected function _split($delimiter, $start, $stop, $pattern) protected function _recurse($value, $patterns) { - if(! is_array($value) || count($patterns) > count($value)) { + if (! is_array($value) || count($patterns) > count($value)) { return false; } - return array_reduce($patterns, function($results, $p) use(&$value) { + return array_reduce($patterns, function ($results, $p) use (&$value) { return $this->_mergeResults($this->parse($p, array_shift($value)), $results); }, []); } protected function _mergeResults($new, $current) { - if($new === false || $current === false) { + if ($new === false || $current === false) { return false; } $common = array_intersect_key($current, $new); - if(count($common) > 0) { + if (count($common) > 0) { throw new \RuntimeException(sprintf('Non unique identifiers: "%s".', implode(', ', array_keys($common)))); } @@ -153,4 +155,4 @@ protected function _invalidPattern($pattern) { throw new \RuntimeException(sprintf('Invalid pattern "%s".', $pattern)); } -} \ No newline at end of file +} diff --git a/src/functions.php b/src/functions.php index a80e132..1c826ce 100644 --- a/src/functions.php +++ b/src/functions.php @@ -15,7 +15,7 @@ */ function extract($pattern, $value = null) { - $function = function($value) use($pattern) { + $function = function ($value) use ($pattern) { return (new Parser())->parse($pattern, $value); }; @@ -31,18 +31,22 @@ function extract($pattern, $value = null) * @param mixed $value * @return array|mixed|callable */ -function match(array $patterns, $value = null) +function pmatch(array $patterns, $value = null) { - $function = function($value) use($patterns) { + $function = function ($value) use ($patterns) { $parser = new Parser(); - foreach($patterns as $pattern => $callback) { + foreach ($patterns as $pattern => $callback) { $match = $parser->parse($pattern, $value); - if($match !== false) { - return is_callable($callback) ? - call_user_func_array($callback, $match) : - $callback; + if ($match !== false) { + try { + return is_callable($callback) ? + call_user_func_array($callback, array_values($match)) : + $callback; + } catch (\Throwable $exp) { + return $callback; + } } } @@ -66,12 +70,12 @@ function match(array $patterns, $value = null) */ function func(array $patterns) { - $array_patterns = array_combine(array_map(function($k) { - return '['.implode(', ', explode(' ', $k)).']'; + $array_patterns = array_combine(array_map(function ($k) { + return '[' . implode(', ', explode(' ', $k)) . ']'; }, array_keys($patterns)), array_values($patterns)); - return function() use($array_patterns) { - return match($array_patterns, func_get_args()); + return function () use ($array_patterns) { + return pmatch($array_patterns, func_get_args()); }; } @@ -101,20 +105,20 @@ function split_enclosed($delimiter, $open, $close, $string) { $string = trim($string); - if(strlen($string) === 0) { + if (strlen($string) === 0) { return []; } $results = []; $buffer = ''; $depth = 0; - foreach(str_split($string) as $c) { - if($c === ' ') { + foreach (str_split($string) as $c) { + if ($c === ' ') { continue; } - if($c === $delimiter && $depth === 0) { - if(strlen($buffer) === 0) { + if ($c === $delimiter && $depth === 0) { + if (strlen($buffer) === 0) { return false; } @@ -123,9 +127,9 @@ function split_enclosed($delimiter, $open, $close, $string) continue; } - if($c === $open) { + if ($c === $open) { ++$depth; - } else if($c === $close) { + } elseif ($c === $close) { --$depth; } @@ -134,4 +138,3 @@ function split_enclosed($delimiter, $open, $close, $string) return strlen($buffer) === 0 ? false : array_merge($results, [$buffer]); } - diff --git a/tests/PHPFunctional/PatternMatching/Parser.php b/tests/PHPFunctional/PatternMatching/Parser.php index 5e91a87..e6a7e36 100644 --- a/tests/PHPFunctional/PatternMatching/Parser.php +++ b/tests/PHPFunctional/PatternMatching/Parser.php @@ -46,7 +46,7 @@ public function noMatchingPatternDataProvider() /** @dataProvider invalidPatternProvider */ public function testInvalidPattern($pattern) { - $this->exception(function() use($pattern) { + $this->exception(function () use ($pattern) { $this->newTestedInstance->parse($pattern, ''); })->isInstanceOf('\RuntimeException') ->message->contains('Invalid pattern'); @@ -62,11 +62,11 @@ public function invalidPatternProvider() ['[a, b]@(x:xs)'], ['(x:xs)@[c, d]'], // ['[a, b]@[c, d]'], ['(x:xs)@(x:xs)'], ]; } - + /** @dataProvider nonUniquePatternProvider */ public function testNonUniquePattern($pattern) { - $this->exception(function() use($pattern) { + $this->exception(function () use ($pattern) { $this->newTestedInstance->parse($pattern, [1, 2, 3, 4]); })->isInstanceOf('\RuntimeException') ->message->contains('Non unique identifiers'); @@ -80,7 +80,7 @@ public function nonUniquePatternProvider() ['all@(all:xs)'], ['all@(x:all)'], ['all@(all:all)'], ]; } - + /** @dataProvider constantDataProvider */ public function testConstant($value, $pattern) { @@ -102,12 +102,7 @@ public function constantDataProvider() ['test test', '"test test"'], ['test test', "'test test'"], [true, 'true'], [true, 'True'], [true, 'TRUE'], - [True, 'true'], [True, 'True'], [True, 'TRUE'], - [TRUE, 'true'], [TRUE, 'True'], [TRUE, 'TRUE'], - [false, 'false'], [false, 'False'], [false, 'FALSE'], - [False, 'false'], [False, 'False'], [False, 'FALSE'], - [FALSE, 'false'], [FALSE, 'False'], [FALSE, 'FALSE'], ]; } @@ -185,7 +180,7 @@ public function consDataProvider() [ [1], '(x:_)', [1] ], ]; } - + /** @dataProvider asDataProvider */ public function testAs($value, $pattern, $expected) { @@ -208,4 +203,3 @@ public function asDataProvider() ]; } } - diff --git a/tests/PHPFunctional/PatternMatching/functions.php b/tests/PHPFunctional/PatternMatching/functions.php index 7186377..be604a1 100644 --- a/tests/PHPFunctional/PatternMatching/functions.php +++ b/tests/PHPFunctional/PatternMatching/functions.php @@ -5,7 +5,6 @@ use atoum; use FunctionalPHP\PatternMatching as M; - class stdClass extends atoum { /** @dataProvider splitEnclosedDataProvider */ @@ -45,14 +44,19 @@ public function splitEnclosedDataProvider() public function testNoPatterns() { - $this->exception(function() { M\match([], 'some value'); }) + $this->exception(function () { + M\pmatch([], 'some value'); + }) ->hasMessage('Non-exhaustive patterns.') ->isInstanceOf('\RuntimeException'); } public function testNoMatch() { - $this->exception(function() { M\match(['"other text"' => function() {}], 'some value'); }) + $this->exception(function () { + M\pmatch(['"other text"' => function () { + }], 'some value'); + }) ->hasMessage('Non-exhaustive patterns.') ->isInstanceOf('\RuntimeException'); } @@ -60,9 +64,11 @@ public function testNoMatch() /** @dataProvider matchDataProvider */ public function testMatch($value, $pattern, $expected) { - $function = function() { return func_get_args(); }; + $function = function () { + return func_get_args(); + }; - $this->variable(M\match([$pattern => $function], $value))->isIdenticalTo($expected); + $this->variable(M\pmatch([$pattern => $function], $value))->isIdenticalTo($expected); } public function matchDataProvider() @@ -76,13 +82,13 @@ public function matchDataProvider() /** @dataProvider matchDataProvider */ public function testConst($value, $pattern, $expected) { - $this->variable(M\match([$pattern => $expected], $value))->isIdenticalTo($expected); + $this->variable(M\pmatch([$pattern => $expected], $value))->isIdenticalTo($expected); } /** @dataProvider matchDataProvider */ public function testCurryConst($value, $pattern, $expected) { - $curryied = M\match([$pattern => $expected]); + $curryied = M\pmatch([$pattern => $expected]); $this->variable($curryied)->isCallable(); $this->variable($curryied($value))->isIdenticalTo($expected);