Skip to content
Browse files

Initial commit

  • Loading branch information...
0 parents commit 4b345444960013b6c701c08404b0d587f723a02c @bobthecow committed
Showing with 2,139 additions and 0 deletions.
  1. +3 −0 .gitmodules
  2. +19 −0 LICENSE
  3. +59 −0 README.markdown
  4. +15 −0 phpunit.xml.dist
  5. +38 −0 src/Ruler/Context.php
  6. +40 −0 src/Ruler/Operator/ComparisonOperator.php
  7. +34 −0 src/Ruler/Operator/EqualTo.php
  8. +34 −0 src/Ruler/Operator/GreaterThan.php
  9. +34 −0 src/Ruler/Operator/GreaterThanOrEqualTo.php
  10. +34 −0 src/Ruler/Operator/LessThan.php
  11. +34 −0 src/Ruler/Operator/LessThanOrEqualTo.php
  12. +44 −0 src/Ruler/Operator/LogicalAnd.php
  13. +79 −0 src/Ruler/Operator/LogicalNot.php
  14. +52 −0 src/Ruler/Operator/LogicalOperator.php
  15. +44 −0 src/Ruler/Operator/LogicalOr.php
  16. +47 −0 src/Ruler/Operator/LogicalXor.php
  17. +34 −0 src/Ruler/Operator/NotEqualTo.php
  18. +31 −0 src/Ruler/Proposition.php
  19. +72 −0 src/Ruler/Rule.php
  20. +139 −0 src/Ruler/RuleBuilder.php
  21. +60 −0 src/Ruler/RuleSet.php
  22. +81 −0 src/Ruler/Value.php
  23. +166 −0 src/Ruler/Variable.php
  24. +31 −0 tests/Ruler/Test/ContextTest.php
  25. +25 −0 tests/Ruler/Test/Fixtures/CallbackProposition.php
  26. +14 −0 tests/Ruler/Test/Fixtures/FalseProposition.php
  27. +14 −0 tests/Ruler/Test/Fixtures/TrueProposition.php
  28. +39 −0 tests/Ruler/Test/Operator/EqualToTest.php
  29. +42 −0 tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php
  30. +39 −0 tests/Ruler/Test/Operator/GreaterThanTest.php
  31. +42 −0 tests/Ruler/Test/Operator/LessThanOrEqualToTest.php
  32. +39 −0 tests/Ruler/Test/Operator/LessThanTest.php
  33. +57 −0 tests/Ruler/Test/Operator/LogicalAndTest.php
  34. +61 −0 tests/Ruler/Test/Operator/LogicalNotTest.php
  35. +57 −0 tests/Ruler/Test/Operator/LogicalOrTest.php
  36. +60 −0 tests/Ruler/Test/Operator/LogicalXorTest.php
  37. +42 −0 tests/Ruler/Test/Operator/NotEqualToTest.php
  38. +86 −0 tests/Ruler/Test/RuleBuilderTest.php
  39. +47 −0 tests/Ruler/Test/RuleSetTest.php
  40. +73 −0 tests/Ruler/Test/RuleTest.php
  41. +43 −0 tests/Ruler/Test/ValueTest.php
  42. +114 −0 tests/Ruler/Test/VariableTest.php
  43. +20 −0 tests/bootstrap.php
  44. +1 −0 vendor/pimple
3 .gitmodules
@@ -0,0 +1,3 @@
+[submodule "vendor/pimple"]
+ path = vendor/pimple
+ url = https://github.com/fabpot/Pimple.git
19 LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2011 OpenSky Project Inc
+
+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.
59 README.markdown
@@ -0,0 +1,59 @@
+Ruler
+=====
+
+Ruler is a simple stateless production rules engine for PHP 5.3.
+
+Ruler uses a pretty straightforward DSL provided by the RuleBuilder:
+
+``` php
+<?php
+
+$rb = new RuleBuilder();
+$rule = $rb->create(
+ $rb->logicalOr(
+ $rb['minNumPeople']->lessThanOrEqualTo($rb['actualNumPeople']),
+ $rb['maxNumPeople']->greaterThanOrEqualTo($rb['actualNumPeople'])
+ ),
+ function() {
+ echo 'YAY!';
+ }
+);
+
+$context = new Context(array(
+ 'minNumPeople' => 5,
+ 'maxNumPeople' => 25,
+ 'actualNumPeople' => function() {
+ return 6;
+ },
+));
+
+$rule->execute($context);
+
+```
+
+Of course, if you're not into the whole brevity thing, you can use it without a RuleBuilder:
+
+``` php
+<?php
+
+$actualNumPeople = new Variable('actualNumPeople');
+$rule = new Rule(
+ new Operator\LogicalAnd(
+ new Operator\LessThanOrEqualTo(new Variable('minNumPeople'), $actualNumPeople),
+ new Operator\GreaterThanOrEqualTo(new Variable('maxNumPeople'), $actualNumPeople)
+ ),
+ function() {
+ echo 'YAY!';
+ }
+);
+
+$context = new Context(array(
+ 'minNumPeople' => 5,
+ 'maxNumPeople' => 25,
+ 'actualNumPeople' => function() {
+ return 6;
+ },
+));
+
+$rule->execute($context)
+```
15 phpunit.xml.dist
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<phpunit bootstrap="./tests/bootstrap.php" colors="true">
+ <testsuites>
+ <testsuite name="Ruler Test Suite">
+ <directory suffix="Test.php">./tests/Ruler/Test/</directory>
+ </testsuite>
+ </testsuites>
+
+ <filter>
+ <whitelist>
+ <directory suffix=".php">./src/Ruler/</directory>
+ </whitelist>
+ </filter>
+</phpunit>
38 src/Ruler/Context.php
@@ -0,0 +1,38 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+/**
+ * Ruler Context.
+ *
+ * The Context contains facts with which to evaluate a Rule or other Proposition.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ */
+class Context extends \Pimple
+{
+
+ /**
+ * Context constructor.
+ *
+ * Optionally, bootstrap the context by passing an array of fact names and
+ * values.
+ *
+ * @param array $values (default: array())
+ */
+ public function __construct(array $values = array())
+ {
+ foreach ($values as $key => $val) {
+ $this[$key] = $val;
+ }
+ }
+}
40 src/Ruler/Operator/ComparisonOperator.php
@@ -0,0 +1,40 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Proposition;
+use Ruler\Variable;
+
+/**
+ * Abstract Comparison Operator class.
+ *
+ * @abstract
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @implements Proposition
+ */
+abstract class ComparisonOperator implements Proposition
+{
+ protected $left;
+ protected $right;
+
+ /**
+ * Comparison Operator constrcutor.
+ *
+ * @param Variable $left
+ * @param Variable $right
+ */
+ public function __construct(Variable $left, Variable $right)
+ {
+ $this->left = $left;
+ $this->right = $right;
+ }
+}
34 src/Ruler/Operator/EqualTo.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * An EqualTo comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class EqualTo extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the given variables are equal in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->equalTo($this->right->prepareValue($context));
+ }
+}
34 src/Ruler/Operator/GreaterThan.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A GreaterThan comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class GreaterThan extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the left variable is greater than the right in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->greaterThan($this->right->prepareValue($context));
+ }
+}
34 src/Ruler/Operator/GreaterThanOrEqualTo.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A GreaterThan comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class GreaterThanOrEqualTo extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the left variable is greater than or equal to the right in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->lessThan($this->right->prepareValue($context)) === false;
+ }
+}
34 src/Ruler/Operator/LessThan.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A LessThan comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class LessThan extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the left variable is less than the right in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->lessThan($this->right->prepareValue($context));
+ }
+}
34 src/Ruler/Operator/LessThanOrEqualTo.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A LessThanOrEqualTo comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class LessThanOrEqualTo extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the left variable is less than or equal to the right in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->greaterThan($this->right->prepareValue($context)) === false;
+ }
+}
44 src/Ruler/Operator/LogicalAnd.php
@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A logical AND operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends LogicalOperator
+ */
+class LogicalAnd extends LogicalOperator
+{
+ /**
+ * Evaluate whether all child Propositions evaluate to true given the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ if (empty($this->propositions)) {
+ throw new \LogicException('Logical And requires at least one proposition');
+ }
+
+ foreach ($this->propositions as $prop) {
+ if ($prop->evaluate($context) === false) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+}
79 src/Ruler/Operator/LogicalNot.php
@@ -0,0 +1,79 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+use Ruler\Proposition;
+
+/**
+ * A logical NOT operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends LogicalOperator
+ */
+class LogicalNot extends LogicalOperator
+{
+ protected $proposition;
+
+ /**
+ * Logical NOT constructor
+ *
+ * Logical NOT is unable to process multiple child Propositions, so passing an array with
+ * more than one Proposition will result in a LogicException.
+ *
+ * @param array $props Child Proposition (default:null)
+ * @throws LogicException
+ */
+ public function __construct(array $props = null)
+ {
+ if ($props !== null) {
+ if (count($props) != 1) {
+ throw new \LogicException('Logical Not requires exactly one proposition');
+ }
+
+ $this->proposition = array_pop($props);
+ }
+ }
+
+ /**
+ * Set the child Proposition.
+ *
+ * Logical NOT is unable to process multiple child Propositions, so calling addProposition
+ * if a Proposition has already been set will result in a LogicException.
+ *
+ * @param Proposition $prop
+ * @throws LogicException
+ */
+ public function addProposition(Proposition $prop)
+ {
+ if (isset($this->proposition)) {
+ throw new \LogicException('Logical Not requires exactly one proposition');
+ }
+
+ $this->proposition = $prop;
+ }
+
+ /**
+ * Evaluate whether the child Proposition evaluates to false given the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ if (!isset($this->proposition)) {
+ throw new \LogicException('Logical Not requires exactly one proposition');
+ }
+
+ return !$this->proposition->evaluate($context);
+ }
+}
52 src/Ruler/Operator/LogicalOperator.php
@@ -0,0 +1,52 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Proposition;
+
+/**
+ * Abstract Logical Operator class.
+ *
+ * Logical Operators represent propositional operations: AND, OR, NOT and XOR.
+ *
+ * @abstract
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @implements Proposition
+ */
+abstract class LogicalOperator implements Proposition
+{
+ protected $propositions = array();
+
+ /**
+ * Logical Operator constructor.
+ *
+ * @param array $props Initial Propositions to add to the Operator (default: null)
+ */
+ public function __construct(array $props = null)
+ {
+ if ($props !== null) {
+ foreach ($props as $prop) {
+ $this->addProposition($prop);
+ }
+ }
+ }
+
+ /**
+ * Add a Proposition to the Operator.
+ *
+ * @param Proposition $prop
+ */
+ public function addProposition(Proposition $prop)
+ {
+ $this->propositions[] = $prop;
+ }
+}
44 src/Ruler/Operator/LogicalOr.php
@@ -0,0 +1,44 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A logical OR operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends LogicalOperator
+ */
+class LogicalOr extends LogicalOperator
+{
+ /**
+ * Evaluate whether any child Proposition evaluates to true given the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ if (empty($this->propositions)) {
+ throw new \LogicException('Logical Or requires at least one proposition');
+ }
+
+ foreach ($this->propositions as $prop) {
+ if ($prop->evaluate($context) === true) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
47 src/Ruler/Operator/LogicalXor.php
@@ -0,0 +1,47 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A logical XOR operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends LogicalOperator
+ */
+class LogicalXor extends LogicalOperator
+{
+ /**
+ * Evaluate whether exactly one child Proposition evaluates to true given the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ if (empty($this->propositions)) {
+ throw new \LogicException('Logical Xor requires at least one proposition');
+ }
+
+ $true = 0;
+ foreach ($this->propositions as $prop) {
+ if ($prop->evaluate($context) === true) {
+ if (++$true > 1) {
+ return false;
+ }
+ }
+ }
+
+ return $true === 1;
+ }
+}
34 src/Ruler/Operator/NotEqualTo.php
@@ -0,0 +1,34 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler\Operator;
+
+use Ruler\Context;
+
+/**
+ * A NotEqualTo comparison operator.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @extends ComparisonOperator
+ */
+class NotEqualTo extends ComparisonOperator
+{
+ /**
+ * Evaluate whether the given variables are not equal in the current Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->left->prepareValue($context)->equalTo($this->right->prepareValue($context)) === false;
+ }
+}
31 src/Ruler/Proposition.php
@@ -0,0 +1,31 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+use Ruler\Context;
+
+/**
+ * The Proposition interface represents a propositional statement.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ */
+interface Proposition
+{
+
+ /**
+ * Evaluate the Proposition with the given Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context);
+}
72 src/Ruler/Rule.php
@@ -0,0 +1,72 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+use Ruler\Proposition;
+use Ruler\Context;
+
+/**
+ * Rule class.
+ *
+ * A Rule is a conditional Proposition with an (optional) action which is
+ * executed upon successful evaluation.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @implements Proposition
+ */
+class Rule implements Proposition
+{
+ protected $condition;
+ protected $action;
+
+ /**
+ * Rule constructor.
+ *
+ * @param Proposition $condition
+ * @param callback $action (default: null)
+ */
+ public function __construct(Proposition $condition, $action = null)
+ {
+ $this->condition = $condition;
+ $this->action = $action;
+ }
+
+ /**
+ * Evaluate the Rule with the given Context.
+ *
+ * @param Context $context
+ * @return boolean
+ */
+ public function evaluate(Context $context)
+ {
+ return $this->condition->evaluate($context);
+ }
+
+ /**
+ * Execute the Rule with the given Context.
+ *
+ * The Rule will be evaluated, and if successful, will execute its
+ * $action callback.
+ *
+ * @param Context $context
+ */
+ public function execute(Context $context)
+ {
+ if ($this->evaluate($context) && isset($this->action)) {
+ if (!is_callable($this->action)) {
+ throw new \LogicException('Rule actions must be callable.');
+ }
+
+ call_user_func($this->action);
+ }
+ }
+}
139 src/Ruler/RuleBuilder.php
@@ -0,0 +1,139 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+use Ruler\Proposition;
+use Ruler\Rule;
+use Ruler\Operator;
+use Ruler\Variable;
+
+/**
+ * RuleBuilder.
+ *
+ * The RuleBuilder provides a DSL and fluent interface for constructing
+ * Rules.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ * @implements ArrayAccess
+ */
+class RuleBuilder implements \ArrayAccess
+{
+ private $variables;
+
+ /**
+ * RuleBuilder constructor.
+ */
+ public function __construct()
+ {
+ $this->variables = array();
+ }
+
+ /**
+ * Create a Rule with the given propositional condition.
+ *
+ * @param Proposition $condition
+ * @param callback $action (default: null)
+ * @return Rule
+ */
+ public function create(Proposition $condition, $action = null)
+ {
+ return new Rule($condition, $action);
+ }
+
+ /**
+ * Create a logical AND operator proposition.
+ *
+ * @param Proposition $prop One or more Propositions
+ * @return Operator\LogicalAnd
+ */
+ public function logicalAnd(Proposition $prop)
+ {
+ return new Operator\LogicalAnd(func_get_args());
+ }
+
+ /**
+ * Create a logical OR operator proposition.
+ *
+ * @param Proposition $prop One or more Propositions
+ * @return Operator\LogicalOr
+ */
+ public function logicalOr(Proposition $prop)
+ {
+ return new Operator\LogicalOr(func_get_args());
+ }
+
+ /**
+ * Create a logical NOT operator proposition.
+ *
+ * @param Proposition $prop Exactly one Proposition
+ * @return Operator\LogicalNot
+ */
+ public function logicalNot(Proposition $prop)
+ {
+ return new Operator\LogicalNot(func_get_args());
+ }
+
+ /**
+ * Create a logical XOR operator proposition.
+ *
+ * @param Proposition $prop One or more Propositions
+ * @return Operator\LogicalXor
+ */
+ public function logicalXor(Proposition $prop)
+ {
+ return new Operator\LogicalXor(func_get_args());
+ }
+
+ /**
+ * Check whether a Variable is already set.
+ *
+ * @param string $name The Variable name
+ * @return boolean
+ */
+ public function offsetExists($name) {
+ return isset($this->variables[$name]);
+ }
+
+ /**
+ * Retrieve a Variable by name.
+ *
+ * @param string $name The Variable name
+ * @return Variable
+ */
+ public function offsetGet($name) {
+ if (!isset($this->variables[$name])) {
+ $this->variables[$name] = new Variable($name);
+ }
+
+ return $this->variables[$name];
+ }
+
+ /**
+ * Set the default value of a Variable.
+ *
+ * @param string $name The Variable name
+ * @param mixed $value The Variable default value
+ * @return Variable
+ */
+ public function offsetSet($name, $value) {
+ $this->offsetGet($name)->setValue($value);
+ }
+
+ /**
+ * Remove a defined Variable from the RuleBuilder.
+ *
+ * @param string $name The Variable name
+ */
+ public function offsetUnset($name) {
+ unset($this->variables[$name]);
+ }
+}
60 src/Ruler/RuleSet.php
@@ -0,0 +1,60 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+use Ruler\Context;
+
+/**
+ * A Ruler RuleSet.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ */
+class RuleSet
+{
+ protected $rules = array();
+
+ /**
+ * RuleSet constructor.
+ *
+ * @param array $rules Rules to add to RuleSet
+ */
+ public function __construct(array $rules = array())
+ {
+ foreach ($rules as $rule) {
+ $this->addRule($rule);
+ }
+ }
+
+ /**
+ * Add a Rule to the RuleSet.
+ *
+ * Adding duplicate Rules to the RuleSet will have no effect.
+ *
+ * @param Rule $rule
+ */
+ public function addRule(Rule $rule)
+ {
+ $this->rules[spl_object_hash($rule)] = $rule;
+ }
+
+ /**
+ * Execute all Rules in the RuleSet.
+ *
+ * @param Context $context
+ */
+ public function executeRules(Context $context)
+ {
+ foreach ($this->rules as $rule) {
+ $rule->execute($context);
+ }
+ }
+}
81 src/Ruler/Value.php
@@ -0,0 +1,81 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+/**
+ * A Ruler Value.
+ *
+ * A Value represents a comparable terminal value. Variables and Comparison Operators
+ * are resolved to Values by applying the current Context and the default Variable value.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ */
+class Value
+{
+ protected $value;
+
+ /**
+ * Value constructor.
+ *
+ * A Value object is immutable, and is used by Variables for comparing their default
+ * values or facts from the current Context.
+ *
+ * @param mixed $value
+ */
+ public function __construct($value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Return the value.
+ *
+ * @return mixed
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Equal To comparison.
+ *
+ * @param Value $value
+ * @return boolean
+ */
+ public function equalTo(Value $value)
+ {
+ return $this->value == $value->getValue();
+ }
+
+ /**
+ * Greater Than comparison.
+ *
+ * @param Value $value
+ * @return boolean
+ */
+ public function greaterThan(Value $value)
+ {
+ return $this->value > $value->getValue();
+ }
+
+ /**
+ * Less Than comparison.
+ *
+ * @param Value $value
+ * @return boolean
+ */
+ public function lessThan(Value $value)
+ {
+ return $this->value < $value->getValue();
+ }
+}
166 src/Ruler/Variable.php
@@ -0,0 +1,166 @@
+<?php
+
+/*
+ * This file is part of the Ruler package, an OpenSky project.
+ *
+ * (c) 2011 OpenSky Project Inc
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Ruler;
+
+use Ruler\Operator;
+use Ruler\Context;
+
+/**
+ * A propositional Variable.
+ *
+ * Variables are placeholders in Propositions and Comparison Operators. During
+ * evaluation, they are replaced with terminal Values, either from the Variable
+ * default or from the current Context.
+ *
+ * @author Justin Hileman <justin@shopopensky.com>
+ */
+class Variable
+{
+ private $name;
+ private $value;
+
+ /**
+ * Variable class constructor.
+ *
+ * @param string $name Variable name (default: null)
+ * @param mixed $value Default Variable value (default: null)
+ */
+ public function __construct($name = null, $value = null)
+ {
+ $this->name = $name;
+ $this->value = $value;
+ }
+
+ /**
+ * Return the Variable name.
+ *
+ * @return string Variable name
+ */
+ public function getName()
+ {
+ return $this->name;
+ }
+
+ /**
+ * Set the default Variable value.
+ *
+ * @param mixed $value
+ */
+ public function setValue($value)
+ {
+ $this->value = $value;
+ }
+
+ /**
+ * Get the default Variable value.
+ *
+ * @return mixed Variable value
+ */
+ public function getValue()
+ {
+ return $this->value;
+ }
+
+ /**
+ * Prepare a Value for this Variable given the current Context.
+ *
+ * @param Context $context
+ * @return Value
+ */
+ public function prepareValue(Context $context)
+ {
+ if (isset($this->name) && isset($context[$this->name])) {
+ $value = $context[$this->name];
+ } else {
+ $value = $this->value;
+ }
+
+ return ($value instanceof Value) ? $value : new Value($value);
+ }
+
+ /**
+ * Fluent interface helper to create a GreaterThan comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\GreaterThan
+ */
+ public function greaterThan($variable)
+ {
+ return new Operator\GreaterThan($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Fluent interface helper to create a GreaterThanOrEqualTo comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\GreaterThanOrEqualTo
+ */
+ public function greaterThanOrEqualTo($variable)
+ {
+ return new Operator\GreaterThanOrEqualTo($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Fluent interface helper to create a LessThan comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\LessThan
+ */
+ public function lessThan($variable)
+ {
+ return new Operator\LessThan($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Fluent interface helper to create a LessThanOrEqualTo comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\LessThanOrEqualTo
+ */
+ public function lessThanOrEqualTo($variable)
+ {
+ return new Operator\LessThanOrEqualTo($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Fluent interface helper to create a EqualTo comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\EqualTo
+ */
+ public function equalTo($variable)
+ {
+ return new Operator\EqualTo($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Fluent interface helper to create a NotEqualTo comparison operator.
+ *
+ * @param mixed $variable Right side of comparison operator
+ * @return Operator\NotEqualTo
+ */
+ public function notEqualTo($variable)
+ {
+ return new Operator\NotEqualTo($this, $this->asVariable($variable));
+ }
+
+ /**
+ * Private helper to retrieve a Variable instance for the given $variable.
+ *
+ * @param mixed $variable
+ * @return Variable
+ */
+ private function asVariable($variable)
+ {
+ return ($variable instanceof Variable) ? $variable : new Variable(null, $variable);
+ }
+}
31 tests/Ruler/Test/ContextTest.php
@@ -0,0 +1,31 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\Context;
+
+class ContextTest extends \PHPUnit_Framework_TestCase
+{
+ public function testConstructor()
+ {
+ $facts = array(
+ 'name' => 'Mint Chip',
+ 'type' => 'Ice Cream',
+ 'delicious' => function() {
+ return true;
+ }
+ );
+
+ $context = new Context($facts);
+
+ $this->assertTrue(isset($context['name']));
+ $this->assertEquals('Mint Chip', $context['name']);
+
+ $this->assertTrue(isset($context['type']));
+ $this->assertEquals('Ice Cream', $context['type']);
+
+ $this->assertTrue(isset($context['delicious']));
+ $this->assertTrue($context['delicious']);
+ }
+}
+
25 tests/Ruler/Test/Fixtures/CallbackProposition.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Ruler\Test\Fixtures;
+
+use Ruler\Proposition;
+use Ruler\Context;
+
+class CallbackProposition implements Proposition
+{
+ private $callback;
+
+ public function __construct($callback)
+ {
+ if (!is_callable($callback)) {
+ throw new \InvalidArgumentException('CallbackProposition expects a callable argument');
+ }
+
+ $this->callback = $callback;
+ }
+
+ public function evaluate(Context $context)
+ {
+ return call_user_func($this->callback, $context);
+ }
+}
14 tests/Ruler/Test/Fixtures/FalseProposition.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Ruler\Test\Fixtures;
+
+use Ruler\Proposition;
+use Ruler\Context;
+
+class FalseProposition implements Proposition
+{
+ public function evaluate(Context $context)
+ {
+ return false;
+ }
+}
14 tests/Ruler/Test/Fixtures/TrueProposition.php
@@ -0,0 +1,14 @@
+<?php
+
+namespace Ruler\Test\Fixtures;
+
+use Ruler\Proposition;
+use Ruler\Context;
+
+class TrueProposition implements Proposition
+{
+ public function evaluate(Context $context)
+ {
+ return true;
+ }
+}
39 tests/Ruler/Test/Operator/EqualToTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class EqualToTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\EqualTo($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\EqualTo($varA, $varB);
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 3;
+ };
+ $this->assertTrue($op->evaluate($context));
+ }
+}
42 tests/Ruler/Test/Operator/GreaterThanOrEqualToTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class GreaterThanOrEqualToTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\GreaterThanOrEqualTo($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\GreaterThanOrEqualTo($varA, $varB);
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 3;
+ };
+ $this->assertTrue($op->evaluate($context));
+
+ $context['4'] = 3;
+ $this->assertTrue($op->evaluate($context));
+ }
+}
39 tests/Ruler/Test/Operator/GreaterThanTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class GreaterThanTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\GreaterThan($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\GreaterThan($varA, $varB);
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 0;
+ };
+ $this->assertTrue($op->evaluate($context));
+ }
+}
42 tests/Ruler/Test/Operator/LessThanOrEqualToTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class LessThanOrEqualToTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\GreaterThanOrEqualTo($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\GreaterThanOrEqualTo($varA, $varB);
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 3;
+ };
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertFalse($op->evaluate($context));
+ }
+}
39 tests/Ruler/Test/Operator/LessThanTest.php
@@ -0,0 +1,39 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class LessThanTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\LessThan($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\LessThan($varA, $varB);
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 1;
+ };
+ $this->assertFalse($op->evaluate($context));
+ }
+}
57 tests/Ruler/Test/Operator/LogicalAndTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\FalseProposition;
+
+class LogicalAndTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $true = new TrueProposition();
+
+ $op = new Operator\LogicalAnd(array($true));
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op);
+ }
+
+ public function testConstructor()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalAnd(array($true, $false));
+ $this->assertFalse($op->evaluate($context));
+ }
+
+ public function testAddPropositionAndEvaluate()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalAnd();
+
+ $op->addProposition($true);
+ $this->assertTrue($op->evaluate($context));
+
+ $op->addProposition($true);
+ $this->assertTrue($op->evaluate($context));
+
+ $op->addProposition($false);
+ $this->assertFalse($op->evaluate($context));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testExecutingALogicalAndWithoutPropositionsThrowsAnException()
+ {
+ $op = new Operator\LogicalAnd();
+ $op->evaluate(new Context());
+ }
+}
61 tests/Ruler/Test/Operator/LogicalNotTest.php
@@ -0,0 +1,61 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\FalseProposition;
+
+class LogicalNotTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $true = new TrueProposition();
+
+ $op = new Operator\LogicalNot(array($true));
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op);
+ }
+
+ public function testConstructor()
+ {
+ $op = new Operator\LogicalNot(array(new FalseProposition()));
+ $this->assertTrue($op->evaluate(new Context()));
+ }
+
+ public function testAddPropositionAndEvaluate()
+ {
+ $op = new Operator\LogicalNot();
+
+ $op->addProposition(new TrueProposition());
+ $this->assertFalse($op->evaluate(new Context()));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testExecutingALogicalNotWithoutPropositionsThrowsAnException()
+ {
+ $op = new Operator\LogicalNot();
+ $op->evaluate(new Context());
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testInstantiatingALogicalNotWithTooManyArgumentsThrowsAnException()
+ {
+ $op = new Operator\LogicalNot(array(new TrueProposition(), new FalseProposition()));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testAddingASecondPropositionToLogicalNotThrowsAnException()
+ {
+ $op = new Operator\LogicalNot();
+ $op->addProposition(new TrueProposition());
+ $op->addProposition(new TrueProposition());
+ }
+}
57 tests/Ruler/Test/Operator/LogicalOrTest.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\FalseProposition;
+
+class LogicalOrTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $true = new TrueProposition();
+
+ $op = new Operator\LogicalOr(array($true));
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op);
+ }
+
+ public function testConstructor()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalOr(array($true, $false));
+ $this->assertTrue($op->evaluate($context));
+ }
+
+ public function testAddPropositionAndEvaluate()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalOr();
+
+ $op->addProposition($false);
+ $this->assertFalse($op->evaluate($context));
+
+ $op->addProposition($false);
+ $this->assertFalse($op->evaluate($context));
+
+ $op->addProposition($true);
+ $this->assertTrue($op->evaluate($context));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testExecutingALogicalOrWithoutPropositionsThrowsAnException()
+ {
+ $op = new Operator\LogicalOr();
+ $op->evaluate(new Context());
+ }
+}
60 tests/Ruler/Test/Operator/LogicalXorTest.php
@@ -0,0 +1,60 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\FalseProposition;
+
+class LogicalXorTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $true = new TrueProposition();
+
+ $op = new Operator\LogicalXor(array($true));
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $op);
+ }
+
+ public function testConstructor()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalXor(array($true, $false));
+ $this->assertTrue($op->evaluate($context));
+ }
+
+ public function testAddPropositionAndEvaluate()
+ {
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+ $context = new Context();
+
+ $op = new Operator\LogicalXor();
+
+ $op->addProposition($false);
+ $this->assertFalse($op->evaluate($context));
+
+ $op->addProposition($false);
+ $this->assertFalse($op->evaluate($context));
+
+ $op->addProposition($true);
+ $this->assertTrue($op->evaluate($context));
+
+ $op->addProposition($true);
+ $this->assertFalse($op->evaluate($context));
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testExecutingALogicalXorWithoutPropositionsThrowsAnException()
+ {
+ $op = new Operator\LogicalXor();
+ $op->evaluate(new Context());
+ }
+}
42 tests/Ruler/Test/Operator/NotEqualToTest.php
@@ -0,0 +1,42 @@
+<?php
+
+namespace Ruler\Test\Operator;
+
+use Ruler\Operator;
+use Ruler\Context;
+use Ruler\Variable;
+
+class NotEqualToTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+
+ $op = new Operator\NotEqualTo($varA, $varB);
+ $this->assertInstanceOf('Ruler\Proposition', $op);
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $op);
+ }
+
+ public function testConstructorAndEvaluation()
+ {
+ $varA = new Variable('a', 1);
+ $varB = new Variable('b', 2);
+ $context = new Context();
+
+ $op = new Operator\NotEqualTo($varA, $varB);
+ $this->assertTrue($op->evaluate($context));
+
+ $context['a'] = 2;
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 3;
+ $context['b'] = function() {
+ return 3;
+ };
+ $this->assertFalse($op->evaluate($context));
+
+ $context['a'] = 1;
+ $this->assertTrue($op->evaluate($context));
+ }
+}
86 tests/Ruler/Test/RuleBuilderTest.php
@@ -0,0 +1,86 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\RuleBuilder;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\FalseProposition;
+
+class RuleBuilderTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $rb = new RuleBuilder();
+ $this->assertInstanceOf('Ruler\RuleBuilder', $rb);
+ $this->assertInstanceOf('ArrayAccess', $rb);
+ }
+
+ public function testManipulateVariablesViaArrayAccess()
+ {
+ $name = 'alpha';
+ $rb = new RuleBuilder();
+
+ $this->assertFalse(isset($rb[$name]));
+
+ $var = $rb[$name];
+ $this->assertTrue(isset($rb[$name]));
+
+ $this->assertInstanceOf('Ruler\Variable', $var);
+ $this->assertEquals($name, $var->getName());
+
+ $this->assertSame($var, $rb[$name]);
+ $this->assertNull($var->getValue());
+
+ $rb[$name] = 'eeesh.';
+ $this->assertEquals('eeesh.', $var->getValue());
+
+ unset($rb[$name]);
+ $this->assertFalse(isset($rb[$name]));
+ $this->assertNotSame($var, $rb[$name]);
+ }
+
+ public function testLogicalOperatorGeneration()
+ {
+ $rb = new RuleBuilder();
+ $context = new Context();
+
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+
+ $this->assertInstanceOf('Ruler\Operator\LogicalOperator', $rb->logicalAnd($true, $false));
+ $this->assertInstanceOf('Ruler\Operator\LogicalAnd', $rb->logicalAnd($true, $false));
+ $this->assertFalse($rb->logicalAnd($true, $false)->evaluate($context));
+
+ $this->assertInstanceOf('Ruler\Operator\LogicalOr', $rb->logicalOr($true, $false));
+ $this->assertTrue($rb->logicalOr($true, $false)->evaluate($context));
+
+ $this->assertInstanceOf('Ruler\Operator\LogicalNot', $rb->logicalNot($true));
+ $this->assertFalse($rb->logicalNot($true)->evaluate($context));
+
+ $this->assertInstanceOf('Ruler\Operator\LogicalXor', $rb->logicalXor($true, $false));
+ $this->assertTrue($rb->logicalXor($true, $false)->evaluate($context));
+ }
+
+ public function testRuleCreation()
+ {
+ $rb = new RuleBuilder();
+ $context = new Context();
+
+ $true = new TrueProposition();
+ $false = new FalseProposition();
+
+ $this->assertInstanceOf('Ruler\Rule', $rb->create($true));
+ $this->assertTrue($rb->create($true)->evaluate($context));
+ $this->assertFalse($rb->create($false)->evaluate($context));
+
+ $executed = false;
+ $rule = $rb->create($true, function() use (&$executed) {
+ $executed = true;
+ });
+
+ $this->assertFalse($executed);
+ $rule->execute($context);
+ $this->assertTrue($executed);
+ }
+}
47 tests/Ruler/Test/RuleSetTest.php
@@ -0,0 +1,47 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\RuleSet;
+use Ruler\Rule;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+
+class RuleSetTest extends \PHPUnit_Framework_TestCase
+{
+ public function testRulesetCreationUpdateAndExecution()
+ {
+ $context = new Context();
+ $true = new TrueProposition();
+
+ $executedActionA = false;
+ $ruleA = new Rule($true, function() use (&$executedActionA) {
+ $executedActionA = true;
+ });
+
+ $executedActionB = false;
+ $ruleB = new Rule($true, function() use (&$executedActionB) {
+ $executedActionB = true;
+ });
+
+ $executedActionC = false;
+ $ruleC = new Rule($true, function() use (&$executedActionC) {
+ $executedActionC = true;
+ });
+
+ $ruleset = new RuleSet(array($ruleA));
+
+ $ruleset->executeRules($context);
+
+ $this->assertTrue($executedActionA);
+ $this->assertFalse($executedActionB);
+ $this->assertFalse($executedActionC);
+
+ $ruleset->addRule($ruleC);
+ $ruleset->executeRules($context);
+
+ $this->assertTrue($executedActionA);
+ $this->assertFalse($executedActionB);
+ $this->assertTrue($executedActionC);
+ }
+}
73 tests/Ruler/Test/RuleTest.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\Rule;
+use Ruler\Proposition;
+use Ruler\Context;
+use Ruler\Test\Fixtures\TrueProposition;
+use Ruler\Test\Fixtures\CallbackProposition;
+
+class RuleTest extends \PHPUnit_Framework_TestCase
+{
+ public function testInterface()
+ {
+ $rule = new Rule(new TrueProposition());
+ $this->assertInstanceOf('Ruler\Proposition', $rule);
+ }
+
+ public function testConstructorEvaluationAndExecution()
+ {
+ $test = $this;
+ $context = new Context();
+ $executed = false;
+ $actionExecuted = false;
+
+ $ruleOne = new Rule(
+ new CallbackProposition(function ($c) use ($test, $context, &$executed, &$actionExecuted) {
+ $test->assertSame($c, $context);
+ $executed = true;
+ return false;
+ }),
+ function() use ($test, &$actionExecuted) {
+ $actionExecuted = true;
+ }
+ );
+
+ $this->assertFalse($ruleOne->evaluate($context));
+ $this->assertTrue($executed);
+
+ $ruleOne->execute($context);
+ $this->assertFalse($actionExecuted);
+
+ $executed = false;
+ $actionExecuted = false;
+
+ $ruleTwo = new Rule(
+ new CallbackProposition(function ($c) use ($test, $context, &$executed, &$actionExecuted) {
+ $test->assertSame($c, $context);
+ $executed = true;
+ return true;
+ }),
+ function() use ($test, &$actionExecuted) {
+ $actionExecuted = true;
+ }
+ );
+
+ $this->assertTrue($ruleTwo->evaluate($context));
+ $this->assertTrue($executed);
+
+ $ruleTwo->execute($context);
+ $this->assertTrue($actionExecuted);
+ }
+
+ /**
+ * @expectedException LogicException
+ */
+ public function testNonCallableActionsWillThrowAnException()
+ {
+ $context = new Context();
+ $rule = new Rule(new TrueProposition(), 'this is not callable');
+ $rule->execute($context);
+ }
+}
43 tests/Ruler/Test/ValueTest.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\Value;
+
+class ValueTest extends \PHPUnit_Framework_TestCase
+{
+ public function testConstructor()
+ {
+ $valueString = 'technologic';
+ $value = new Value($valueString);
+ $this->assertEquals($valueString, $value->getValue());
+ }
+
+ /**
+ * @dataProvider getRelativeValues
+ */
+ public function testGreaterThanEqualToAndLessThan($a, $b, $gt, $eq, $lt)
+ {
+ $valA = new Value($a);
+ $valB = new Value($b);
+
+ $this->assertEquals($gt, $valA->greaterThan($valB));
+ $this->assertEquals($lt, $valA->lessThan($valB));
+ $this->assertEquals($eq, $valA->equalTo($valB));
+ }
+
+ public function getRelativeValues()
+ {
+ return array(
+ array(1, 2, false, false, true),
+ array(2, 1, true, false, false),
+ array(1, 1, false, true, false),
+ array('a', 'b', false, false, true),
+ array(
+ new \DateTime('-5 days'),
+ new \DateTime('+5 days'),
+ false, false, true
+ ),
+ );
+ }
+}
114 tests/Ruler/Test/VariableTest.php
@@ -0,0 +1,114 @@
+<?php
+
+namespace Ruler\Test;
+
+use Ruler\Variable;
+use Ruler\Context;
+use Ruler\Value;
+
+class VariableTest extends \PHPUnit_Framework_TestCase
+{
+ public function testConstructor()
+ {
+ $name = 'evil';
+ $var = new Variable($name);
+ $this->assertEquals($name, $var->getName());
+ $this->assertNull($var->getValue());
+ }
+
+ public function testGetSetValue()
+ {
+ $values = explode(', ', 'Plug it, play it, burn it, rip it, drag and drop it, zip, unzip it');
+
+ $variable = new Variable('technologic');
+ foreach ($values as $valueString) {
+ $variable->setValue($valueString);
+ $this->assertEquals($valueString, $variable->getValue());
+ }
+ }
+
+ public function testPrepareValue()
+ {
+ $values = array(
+ 'one' => 'Foo',
+ 'two' => 'BAR',
+ 'three' => function() {
+ return 'baz';
+ }
+ );
+
+ $context = new Context($values);
+
+ $varA = new Variable('four', 'qux');
+ $this->assertInstanceOf('Ruler\Value', $varA->prepareValue($context));
+ $this->assertEquals(
+ 'qux',
+ $varA->prepareValue($context)->getValue(),
+ "Variables should return the default value default if it's missing from the context."
+ );
+
+ $varB = new Variable('one', 'FAIL');
+ $this->assertEquals(
+ 'Foo',
+ $varB->prepareValue($context)->getValue()
+ );
+
+ $varC = new Variable('three', 'FAIL');
+ $this->assertEquals(
+ 'baz',
+ $varC->prepareValue($context)->getValue()
+ );
+
+ $varD = new Variable(null, 'qux');
+ $this->assertInstanceOf('Ruler\Value', $varD->prepareValue($context));
+ $this->assertEquals(
+ 'qux',
+ $varD->prepareValue($context)->getValue(),
+ "Anonymous variables don't require a name to prepare value"
+ );
+ }
+
+ public function testFluentInterfaceHelpersAndAnonymousVariables()
+ {
+ $context = new Context(array(
+ 'a' => 1,
+ 'b' => 2,
+ ));
+
+ $varA = new Variable('a');
+ $varB = new Variable('b');
+
+ $this->assertInstanceOf('Ruler\Operator\ComparisonOperator', $varA->greaterThan(0));
+
+ $this->assertInstanceOf('Ruler\Operator\GreaterThan', $varA->greaterThan(0));
+ $this->assertTrue($varA->greaterThan(0)->evaluate($context));
+ $this->assertFalse($varA->greaterThan(2)->evaluate($context));
+
+ $this->assertInstanceOf('Ruler\Operator\GreaterThanOrEqualTo', $varA->greaterThanOrEqualTo(0));
+ $this->assertTrue($varA->greaterThanOrEqualTo(0)->evaluate($context));
+ $this->assertTrue($varA->greaterThanOrEqualTo(1)->evaluate($context));
+ $this->assertFalse($varA->greaterThanOrEqualTo(2)->evaluate($context));
+