From 060d6fbb2519f193b1a68739140b45ffe3d11fee Mon Sep 17 00:00:00 2001
From: Herberto Graca <herberto.graca@lendable.co.uk>
Date: Wed, 26 Jul 2023 21:31:48 +0200
Subject: [PATCH] Create a Not expression

This will reduce the amount of expressions needed
and allow for complex expressions (within an OR
or an AND) to be negated.
---
 src/Expression/Boolean/Not.php             | 42 +++++++++++++++
 tests/Unit/Expressions/Boolean/NotTest.php | 59 ++++++++++++++++++++++
 2 files changed, 101 insertions(+)
 create mode 100644 src/Expression/Boolean/Not.php
 create mode 100644 tests/Unit/Expressions/Boolean/NotTest.php

diff --git a/src/Expression/Boolean/Not.php b/src/Expression/Boolean/Not.php
new file mode 100644
index 00000000..3db76654
--- /dev/null
+++ b/src/Expression/Boolean/Not.php
@@ -0,0 +1,42 @@
+<?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 Not implements Expression
+{
+    /** @var Expression */
+    private $expression;
+
+    public function __construct(Expression $expression)
+    {
+        $this->expression = $expression;
+    }
+
+    public function describe(ClassDescription $theClass, string $because): Description
+    {
+        return new Description('must NOT ('.$this->expression->describe($theClass, '')->toString().')', $because);
+    }
+
+    public function evaluate(ClassDescription $theClass, Violations $violations, string $because): void
+    {
+        $newViolations = new Violations();
+        $this->expression->evaluate($theClass, $newViolations, $because);
+        if (0 !== $newViolations->count()) {
+            return;
+        }
+
+        $violations->add(Violation::create(
+            $theClass->getFQCN(),
+            ViolationMessage::selfExplanatory($this->describe($theClass, $because))
+        ));
+    }
+}
diff --git a/tests/Unit/Expressions/Boolean/NotTest.php b/tests/Unit/Expressions/Boolean/NotTest.php
new file mode 100644
index 00000000..ac844d8a
--- /dev/null
+++ b/tests/Unit/Expressions/Boolean/NotTest.php
@@ -0,0 +1,59 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Arkitect\Tests\Unit\Expressions\Boolean;
+
+use Arkitect\Analyzer\ClassDescription;
+use Arkitect\Analyzer\FullyQualifiedClassName;
+use Arkitect\Expression\Boolean\Not;
+use Arkitect\Expression\ForClasses\IsInterface;
+use Arkitect\Rules\Violations;
+use PHPUnit\Framework\TestCase;
+
+class NotTest extends TestCase
+{
+    public function test_it_should_return_violation_error(): void
+    {
+        $isNotInterface = new Not(new IsInterface());
+        $classDescription = new ClassDescription(
+            FullyQualifiedClassName::fromString('HappyIsland'),
+            [],
+            [],
+            null,
+            false,
+            false,
+            true,
+            false,
+            false
+        );
+        $because = 'we want to add this rule for our software';
+        $violationError = $isNotInterface->describe($classDescription, $because)->toString();
+
+        $violations = new Violations();
+        $isNotInterface->evaluate($classDescription, $violations, $because);
+        self::assertNotEquals(0, $violations->count());
+
+        $this->assertEquals('must NOT (HappyIsland should be an interface) because we want to add this rule for our software', $violationError);
+    }
+
+    public function test_it_should_return_true_if_is_not_interface(): void
+    {
+        $isNotInterface = new Not(new IsInterface());
+        $classDescription = new ClassDescription(
+            FullyQualifiedClassName::fromString('HappyIsland'),
+            [],
+            [],
+            null,
+            false,
+            false,
+            false,
+            false,
+            false
+        );
+        $because = 'we want to add this rule for our software';
+        $violations = new Violations();
+        $isNotInterface->evaluate($classDescription, $violations, $because);
+        self::assertEquals(0, $violations->count());
+    }
+}