diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3631d15 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Sergio Cabrera Durán + +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. \ No newline at end of file diff --git a/README.md b/README.md index db00a96..04f2be5 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,7 @@ Comparison | same as | === Comparison | not same as | !== Logical | and | && Logical | or | \|\| +Containment | contained in (alias: in) | in # Notes * It's not necessary to provide callbacks for *execute* method, it will return a boolean instead as *assert* does. @@ -85,10 +86,15 @@ $ phpunit * Increase the number of unit tests to prevent bad contexts or bad formatted rules from being executed. # To do -* Add more operators (in, for example). +* Improve the interpreted method response. # Changelist * Allow aliases ("is equal to" can be written as "is" and "is not equal to" as "is not"/"isn't"). * Context values may permit callable functions too. * Added the strict comparison operators (same as, not same as). * It can be interesting to implement a kind of *dump* method to show the interpreted rule. +* Added the "in" operator. +* Context accepts array values. + +# License +MIT \ No newline at end of file diff --git a/src/Context.php b/src/Context.php index 5a71a93..f48269f 100644 --- a/src/Context.php +++ b/src/Context.php @@ -71,7 +71,9 @@ private function processValue($value) $value = is_callable($value) ? $value() : $value; - if (is_string($value)) { + if (is_array($value)) { + $value = '['.implode(',', array_map(function($e) {return is_string($e) ? '"'.$e.'"' : $e;}, $value)).']'; + } elseif (is_string($value)) { $value = '"'.$value.'"'; } elseif (is_bool($value)) { $value = $value ? 'true' : 'false'; diff --git a/src/Evaluator/Evaluator.php b/src/Evaluator/Evaluator.php index 150e149..065e30b 100644 --- a/src/Evaluator/Evaluator.php +++ b/src/Evaluator/Evaluator.php @@ -17,13 +17,14 @@ class Evaluator */ public function assert($rules, $context) { - foreach ($rules->get() as $rule) { - if (!(new Rule($this->prepare($rule, $context)))->isTrue()) { - return false; - } - } - - return true; + return array_product( + array_map( + function($rule) { + return (new Rule($rule))->isTrue(); + }, + $this->interpret($rules, $context) + ) + ); } /** @@ -33,13 +34,14 @@ public function assert($rules, $context) */ public function valid($rules, $context) { - foreach ($rules->get() as $rule) { - if (!(new Rule($this->prepare($rule, $context)))->isValid()) { - return false; - } - } - - return true; + return array_product( + array_map( + function($rule) { + return (new Rule($rule))->isValid(); + }, + $this->interpret($rules, $context) + ) + ); } /** @@ -49,13 +51,22 @@ public function valid($rules, $context) */ public function interpret($rules, $context) { - $interpretations = []; - - foreach ($rules->get() as $rule) { - $interpretations[] = $this->prepare($rule, $context); - } + return $this->build($rules, $context); + } - return $interpretations; + /** + * @param RuleCollection $rules + * @param Context $context + * @return string[] + */ + private function build($rules, $context) + { + return array_map( + function($rule) use ($context) { + return $this->prepare($rule, $context); + }, + $rules->get() + ); } /** @@ -65,16 +76,8 @@ public function interpret($rules, $context) */ private function prepare($rule, $context) { - $replacements = array_merge( - (new ComparisonOperator())->getAll(), - (new LogicalOperator())->getAll(), - $context->get() - ); - - foreach ($replacements as $search => $replace) { - $rule = str_replace($search, $replace, $rule); - } + $replacements = array_merge((new ComparisonOperator())->all(), (new LogicalOperator())->all(), $context->get()); - return $rule; + return str_replace(array_keys($replacements), array_values($replacements), $rule); } } diff --git a/src/Operator/ComparisonOperator.php b/src/Operator/ComparisonOperator.php index 33ee4da..846ea34 100644 --- a/src/Operator/ComparisonOperator.php +++ b/src/Operator/ComparisonOperator.php @@ -7,7 +7,7 @@ class ComparisonOperator /** * @return array */ - public function getAll() + public function all() { return [ ' is greater than ' => ' > ', @@ -22,6 +22,8 @@ public function getAll() ' isn\'t ' => ' != ', ' isn"t ' => ' != ', ' is ' => ' == ', + ' contained in ' => ' in ', + ' in ' => ' in ', ]; } } \ No newline at end of file diff --git a/src/Operator/LogicalOperator.php b/src/Operator/LogicalOperator.php index fc8ebad..4edb11d 100644 --- a/src/Operator/LogicalOperator.php +++ b/src/Operator/LogicalOperator.php @@ -7,7 +7,7 @@ class LogicalOperator /** * @return array */ - public function getAll() + public function all() { return [ ' and ' => ' && ', diff --git a/src/Test/RulingEvaluationTest.php b/src/Test/RulingEvaluationTest.php index b3c1763..935eda1 100644 --- a/src/Test/RulingEvaluationTest.php +++ b/src/Test/RulingEvaluationTest.php @@ -46,38 +46,29 @@ public function shouldReturnAnStringWhenRuleDoesNotAssertAndFailCallbackIsNotSet } /** - * @dataProvider getData - * @param array $context - * @param string|string[] $rules - * @param callable $successCallback - * @param callable $failCallback * @test */ - public function itShouldReturnTheExpectedCallback($context, $rules, $successCallback = null, $failCallback = null) + public function itShouldReturnACorrectInterpretation() { $this->assertSame( - $successCallback ? $successCallback() : $failCallback(), + ['(true === false && 1 < 2) || 3 <= 4'], $this->ruling - ->given($context) - ->when($rules) - ->then($successCallback) - ->otherwise($failCallback) - ->execute() + ->given(['a' => true, 'b' => 2, 'c' => 4]) + ->when('(:a same as false and 1 < :b) or 3 is less or equal to 4') + ->interpret() ); } /** + * @dataProvider getData + * @param array $context + * @param string|string[] $rules + * @param bool $expectation * @test */ - public function itShouldReturnACorrectInterpretation() + public function itShouldReturnTheExpectedCallback($context, $rules, $expectation) { - $this->assertSame( - ['(true === false && 1 < 2) || 3 <= 4'], - $this->ruling - ->given(['a' => true, 'b' => 2, 'c' => 4]) - ->when('(:a same as false and 1 < :b) or 3 is less or equal to 4') - ->interpret() - ); + $this->assertSame($expectation, $this->ruling->given($context)->when($rules)->execute()); } /** @@ -109,23 +100,22 @@ public function simpleContextAndSimpleRule() [ ['something' => 10], ':something is greater than 5 and :something is less than 15', - function () {return 'It works!';} + true ], [ ['something' => 2.3], ':something is greater than 1.5 and :something is less than 3.2', - function () {return true;} + true ], [ ['something' => 'fideuŕ'], ':something is equal to "fideuá" and :something isn\'t "croissant"', - null, - function () {return false;} + false ], [ ['something' => 'fideuŕ'], ':something is equal to "fideuŕ" and :something is not equal to "croissant"', - function () {return true;} + true ], ]; } @@ -139,7 +129,7 @@ public function multipleContext() [ ['something' => 10, 'somehow' => 'Joe'], ':something is greater than 5 and :something is less than 15 and :somehow is equal to "Joe"', - function () {return 'It works!';} + true ], ]; } @@ -153,7 +143,27 @@ public function multipleRule() [ ['something' => 'fricandó'], [':something is equal to "fricandó"', ':something is not equal to "fideuŕ"'], - function () {return true;} + true + ], + [ + ['something' => 'fricandó'], + [':something is not equal to "fricandó"', ':something is equal to "fideuŕ"'], + false + ], + [ + ['something' => 3], + [':something is equal to 3', ':something is equal to 4'], + false + ], + [ + ['something' => 'fricandó'], + [':something is not equal to "fricandó"', ':something is equal to "fricandó"'], + false + ], + [ + ['something' => 8], + [':something is less or equal to 10', ':something is greater than 6'], + true ], ]; } @@ -167,13 +177,12 @@ public function parenthesis() [ ['something' => 'fideuŕ'], '(:something is equal to "fideuŕ" and :something is not equal to "croissant") or :something is equal to "fideuŕ"', - function () {return true;} + true ], [ ['something' => 'tortilla de patatas'], '(:something is equal to "tortilla de patatas" and :something is equal to "antananaribo") or :something is equal to "madalenas"', - null, - function () {return false;} + false ], ]; } @@ -187,8 +196,7 @@ public function encodings() [ ['something' => 'fideuŕ'], ':something is equal to "fideuá" and :something is not equal to "croissant"', - null, - function(){return false;} + false ], ]; } @@ -202,8 +210,7 @@ public function caseSensitivity() [ ['something' => 'fideua'], ':something is equal to "FIDEUA" and :something is not equal to "croissant"', - null, - function(){return false;} + false ] ]; } @@ -217,7 +224,42 @@ public function operators() [ ['something' => 'gazpacho'], ':something is "gazpacho" and :something is not "salmorejo"', - function(){return true;}, + true + ], + [ + ['something' => ['a', 'b', 'c']], + '\'a\' in :something', + true + ], + [ + ['something' => ['1', '2', '3']], + '\'1\' in :something', + true + ], + [ + ['something' => ['1', '2', '3']], + '1 in :something', + false + ], + [ + ['something' => [1, 2, 3]], + '\'1\' in :something', + false + ], + [ + ['something' => [1, 2, 3]], + '"1" in :something', + false + ], + [ + ['something' => [1, 2, 3]], + '1 in :something', + true + ], + [ + ['something' => [1.13, 2, 3]], + '1.13 in :something', + true ] ]; } @@ -231,13 +273,12 @@ public function callableContextValue() [ ['purchase' => function(){return 'gazpacho';}, 'price' => function(){return 40;}], ':purchase is "gazpacho" and :price is greater than 50', - null, - function(){return false;} + false ], [ ['logged' => function(){return true;}, 'name' => function(){return 'foo';}], ':logged is true and :name is "foo"', - function(){return 'It\'s him!';} + true ], ]; } @@ -248,27 +289,27 @@ public function stricts() [ ['pretty' => 1, 'likes_acdc' => function(){return true;}], ':pretty same as 1 and :likes_acdc is true', - function(){return true;} + true ], [ ['pretty' => false, 'likes_acdc' => function(){return true;}], ':pretty not same as true or :likes_acdc same as true', - function(){return true;} + true ], [ ['pretty' => true, 'likes_acdc' => function(){return false;}], ':pretty same as true and :likes_acdc same as false', - function(){return true;} + true ], [ ['pretty' => '1'], ':pretty same as "1"', - function(){return true;} + true ], [ ['pretty' => 1], ':pretty same as 1', - function(){return true;} + true ], ]; } @@ -279,29 +320,27 @@ public function notStricts() [ ['pretty' => 1, 'likes_acdc' => function(){return true;}], ':pretty is true and :likes_acdc is not true', - null, - function(){return 'Shook me all night long is a masterpice honey.';} + false ], [ ['logged' => function(){return 'true';}, 'name' => function(){return 'foo';}], ':logged is "true" and :name is "foo"', - function(){return 'It\'s him!';} + true ], [ ['logged' => function(){return 1;}, 'name' => function(){return 'foo';}], ':logged is true and :name is "foo"', - function(){return 'It\'s him!';} + true ], [ ['pretty' => 0, 'likes_acdc' => function(){return true;}], ':pretty is not true and :likes_acdc is not true', - null, - function(){return 'Shook me all night long is a masterpice honey.';} + false ], [ ['logged' => function(){return 'false';}, 'name' => function(){return 'foo';}], ':logged isn\'t "true" and :name is "foo"', - function(){return 'It\'s him!';} + true ], ]; } @@ -312,98 +351,128 @@ public function expectations() [ ['something' => null], ':something is null', - function(){return 'Yep!';} + true ], [ ['something' => 'null'], ':something is not null', - function(){return 'Yep!';} + true ], [ ['something' => true], ':something is true', - function(){return 'Yep!';} + true ], [ ['something' => false], ':something is false', - function(){return 'Yep!';} + true ], [ ['something' => 'true'], ':something is "true"', - function(){return 'Yep!';} + true ], [ ['something' => 'false'], ':something is "false"', - function(){return 'Yep!';} + true ], [ ['something' => 1], ':something is true', - function(){return 'Yep!';} + true ], [ ['something' => 0], ':something is false', - function(){return 'Yep!';} + true ], [ ['something' => 1.1], ':something is less than 1.2', - function(){return 'Yep!';} + true ], [ ['something' => 1.3], ':something is greater or equal to 1.2', - function(){return 'Yep!';} + true ], [ ['something' => 'The Rolling Stones'], ':something is "The Rolling Stones"', - function(){return 'Yep!';} + true ], [ ['something' => 'The Rolling Stones'], ':something is \'The Rolling Stones\'', - function(){return 'Yep!';} + true ], [ ['something' => 'The Rolling Stones'], ":something is 'The Rolling Stones'", - function(){return 'Yep!';} + true ], [ ['something' => 'The Cardigans'], ':something is not "The Cure"', - function(){return 'Yep!';} + true ], [ ['something' => 'The Cardigans'], ':something is not \'The Cure\'', - function(){return 'Yep!';} + true ], [ ['something' => 'The Cardigans'], ":something is not 'The Cure'", - function(){return 'Yep!';} + true ], [ ['something' => 'Pink Floyd'], ':something isn"t "Deep Purple"', - function(){return 'Yep!';} + true ], [ ['something' => 'Pink Floyd'], ':something isn\'t \'Deep Purple\'', - function(){return 'Yep!';} + true ], [ ['something' => 'Pink Floyd'], ":something isn't 'Deep Purple'", - function(){return 'Yep!';} + true + ], + [ + ['number' => 'Pink Floyd'], + ":number in ['Deep Purple','Pink Floyd']", + true + ], + [ + ['number' => 3], + ":number contained in [1,2,3,4]", + true + ], + [ + ['number' => '34'], + ":number contained in [1,2,'34',3,4]", + true + ], + [ + ['numbers' => [1, 2, 3]], + "3 in :numbers", + true ], + [ + ['strings' => ['the', 'rolling', 'stones']], + "'rolling' in :strings", + true + ], + [ + ['strings' => ['the', 'rolling', 'stones']], + "'potato' in :strings", + false + ] ]; } } \ No newline at end of file