From 580a02b78eaa4382c6ca4ddadbcd074700d07ea7 Mon Sep 17 00:00:00 2001
From: Herberto Graca <herberto.graca@lendable.co.uk>
Date: Wed, 26 Jul 2023 21:58:12 +0200
Subject: [PATCH 1/2] Create a And expression

This will allow for complex nested expressions, like ANDs inside ORs.
---
 src/Expression/Boolean/Andx.php             |  56 +++++++++++
 tests/Unit/Expressions/Boolean/AndxTest.php | 105 ++++++++++++++++++++
 2 files changed, 161 insertions(+)
 create mode 100644 src/Expression/Boolean/Andx.php
 create mode 100644 tests/Unit/Expressions/Boolean/AndxTest.php

diff --git a/src/Expression/Boolean/Andx.php b/src/Expression/Boolean/Andx.php
new file mode 100644
index 00000000..a4a8c329
--- /dev/null
+++ b/src/Expression/Boolean/Andx.php
@@ -0,0 +1,56 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Arkitect\Expression\Boolean;
+
+use Arkitect\Analyzer\ClassDescription;
+use Arkitect\Expression\Description;
+use Arkitect\Expression\Expression;
+use Arkitect\Rules\Violation;
+use Arkitect\Rules\ViolationMessage;
+use Arkitect\Rules\Violations;
+
+final class Andx implements Expression
+{
+    /** @var Expression[] */
+    private $expressions;
+
+    public function __construct(Expression ...$expressions)
+    {
+        $this->expressions = $expressions;
+    }
+
+    public function describe(ClassDescription $theClass, string $because): Description
+    {
+        $expressionsDescriptions = [];
+        foreach ($this->expressions as $expression) {
+            $expressionsDescriptions[] = $expression->describe($theClass, '')->toString();
+        }
+
+        return new Description(
+            'all expressions must be true ('.implode(', ', $expressionsDescriptions).')',
+            $because
+        );
+    }
+
+    public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
+    {
+        foreach ($this->expressions as $expression) {
+            $newViolations = new Violations();
+            $expression->evaluate($theClass, $newViolations, $because);
+            if (0 !== $newViolations->count()) {
+                $violations->add(Violation::create(
+                    $theClass->getFQCN(),
+                    ViolationMessage::withDescription(
+                        $this->describe($theClass, $because),
+                        "The class '".$theClass->getFQCN()."' violated the expression "
+                        .$expression->describe($theClass, '')->toString()
+                    )
+                ));
+
+                return;
+            }
+        }
+    }
+}
diff --git a/tests/Unit/Expressions/Boolean/AndxTest.php b/tests/Unit/Expressions/Boolean/AndxTest.php
new file mode 100644
index 00000000..1af18c05
--- /dev/null
+++ b/tests/Unit/Expressions/Boolean/AndxTest.php
@@ -0,0 +1,105 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Arkitect\Tests\Unit\Expressions\Boolean;
+
+use Arkitect\Analyzer\ClassDescription;
+use Arkitect\Analyzer\FullyQualifiedClassName;
+use Arkitect\Expression\Boolean\Andx;
+use Arkitect\Expression\ForClasses\Extend;
+use Arkitect\Expression\ForClasses\Implement;
+use Arkitect\Rules\Violations;
+use PHPUnit\Framework\TestCase;
+
+class AndxTest extends TestCase
+{
+    public function test_it_should_pass_the_rule(): void
+    {
+        $interface = 'interface';
+        $class = 'SomeClass';
+        $classDescription = new ClassDescription(
+            FullyQualifiedClassName::fromString('HappyIsland'),
+            [],
+            [FullyQualifiedClassName::fromString($interface)],
+            FullyQualifiedClassName::fromString($class),
+            false,
+            false,
+            false,
+            false,
+            false
+        );
+        $implementConstraint = new Implement($interface);
+        $extendsConstraint = new Extend($class);
+        $andConstraint = new Andx($implementConstraint, $extendsConstraint);
+
+        $because = 'reasons';
+        $violations = new Violations();
+        $andConstraint->evaluate($classDescription, $violations, $because);
+
+        self::assertEquals(0, $violations->count());
+    }
+
+    public function test_it_should_pass_the_rule_when_and_is_empty(): void
+    {
+        $interface = 'interface';
+        $class = 'SomeClass';
+        $classDescription = new ClassDescription(
+            FullyQualifiedClassName::fromString('HappyIsland'),
+            [],
+            [FullyQualifiedClassName::fromString($interface)],
+            FullyQualifiedClassName::fromString($class),
+            false,
+            false,
+            false,
+            false,
+            false
+        );
+        $andConstraint = new Andx();
+
+        $because = 'reasons';
+        $violations = new Violations();
+        $andConstraint->evaluate($classDescription, $violations, $because);
+
+        self::assertEquals(0, $violations->count());
+    }
+
+    public function test_it_should_not_pass_the_rule(): void
+    {
+        $interface = 'SomeInterface';
+        $class = 'SomeClass';
+
+        $classDescription = new ClassDescription(
+            FullyQualifiedClassName::fromString('HappyIsland'),
+            [],
+            [FullyQualifiedClassName::fromString($interface)],
+            null,
+            false,
+            false,
+            false,
+            false,
+            false
+        );
+
+        $implementConstraint = new Implement($interface);
+        $extendsConstraint = new Extend($class);
+        $andConstraint = new Andx($implementConstraint, $extendsConstraint);
+
+        $because = 'reasons';
+        $violationError = $andConstraint->describe($classDescription, $because)->toString();
+
+        $violations = new Violations();
+        $andConstraint->evaluate($classDescription, $violations, $because);
+        self::assertNotEquals(0, $violations->count());
+
+        $this->assertEquals(
+            'all expressions must be true (should implement SomeInterface, should extend SomeClass) because reasons',
+            $violationError
+        );
+        $this->assertEquals(
+            "The class 'HappyIsland' violated the expression should extend SomeClass, but "
+            .'all expressions must be true (should implement SomeInterface, should extend SomeClass) because reasons',
+            $violations->get(0)->getError()
+        );
+    }
+}

From a0c29bfc9dfe5d6947576195bc0430e7ee03047f Mon Sep 17 00:00:00 2001
From: Herberto Graca <herberto.graca@gmail.com>
Date: Sun, 10 Sep 2023 13:06:55 +0200
Subject: [PATCH 2/2] fixup!

---
 src/Expression/Boolean/Andx.php             | 12 ++++-----
 tests/Unit/Expressions/Boolean/AndxTest.php | 28 ++++++++++++++++++---
 2 files changed, 31 insertions(+), 9 deletions(-)

diff --git a/src/Expression/Boolean/Andx.php b/src/Expression/Boolean/Andx.php
index a4a8c329..5f4cb222 100644
--- a/src/Expression/Boolean/Andx.php
+++ b/src/Expression/Boolean/Andx.php
@@ -25,13 +25,13 @@ public function describe(ClassDescription $theClass, string $because): Descripti
     {
         $expressionsDescriptions = [];
         foreach ($this->expressions as $expression) {
-            $expressionsDescriptions[] = $expression->describe($theClass, '')->toString();
+            $expressionsDescriptions[] = $expression->describe($theClass, $because)->toString();
         }
+        $expressionsDescriptionsString = "(\n"
+            .implode("\nAND\n", array_unique(array_map('trim', $expressionsDescriptions)))
+            ."\n)";
 
-        return new Description(
-            'all expressions must be true ('.implode(', ', $expressionsDescriptions).')',
-            $because
-        );
+        return new Description($expressionsDescriptionsString, $because);
     }
 
     public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
@@ -44,7 +44,7 @@ public function evaluate(ClassDescription $theClass, Violations $violations, str
                     $theClass->getFQCN(),
                     ViolationMessage::withDescription(
                         $this->describe($theClass, $because),
-                        "The class '".$theClass->getFQCN()."' violated the expression "
+                        "The class '".$theClass->getFQCN()."' violated the expression\n"
                         .$expression->describe($theClass, '')->toString()
                     )
                 ));
diff --git a/tests/Unit/Expressions/Boolean/AndxTest.php b/tests/Unit/Expressions/Boolean/AndxTest.php
index 1af18c05..31ace709 100644
--- a/tests/Unit/Expressions/Boolean/AndxTest.php
+++ b/tests/Unit/Expressions/Boolean/AndxTest.php
@@ -5,6 +5,7 @@
 namespace Arkitect\Tests\Unit\Expressions\Boolean;
 
 use Arkitect\Analyzer\ClassDescription;
+use Arkitect\Analyzer\ClassDescriptionBuilder;
 use Arkitect\Analyzer\FullyQualifiedClassName;
 use Arkitect\Expression\Boolean\Andx;
 use Arkitect\Expression\ForClasses\Extend;
@@ -14,6 +15,21 @@
 
 class AndxTest extends TestCase
 {
+    public function test_it_should_return_no_violation_when_empty(): void
+    {
+        $and = new Andx();
+
+        $classDescription = (new ClassDescriptionBuilder())
+            ->setClassName('My\Class')
+            ->setExtends('My\BaseClass', 10)
+            ->build();
+
+        $violations = new Violations();
+        $and->evaluate($classDescription, $violations, 'because');
+
+        self::assertEquals(0, $violations->count());
+    }
+
     public function test_it_should_pass_the_rule(): void
     {
         $interface = 'interface';
@@ -93,12 +109,18 @@ public function test_it_should_not_pass_the_rule(): void
         self::assertNotEquals(0, $violations->count());
 
         $this->assertEquals(
-            'all expressions must be true (should implement SomeInterface, should extend SomeClass) because reasons',
+            "(\nshould implement SomeInterface because reasons\nAND\nshould extend SomeClass because reasons\n) because reasons",
             $violationError
         );
         $this->assertEquals(
-            "The class 'HappyIsland' violated the expression should extend SomeClass, but "
-            .'all expressions must be true (should implement SomeInterface, should extend SomeClass) because reasons',
+            "The class 'HappyIsland' violated the expression\n"
+            ."should extend SomeClass\n"
+            ."from the rule\n"
+            ."(\n"
+            ."should implement SomeInterface because reasons\n"
+            ."AND\n"
+            ."should extend SomeClass because reasons\n"
+            .') because reasons',
             $violations->get(0)->getError()
         );
     }