diff --git a/modules/api/src/main/scala/gs/predicate/v0/api/And.scala b/modules/api/src/main/scala/gs/predicate/v0/api/And.scala index 6dc4fcb..c8fcf12 100644 --- a/modules/api/src/main/scala/gs/predicate/v0/api/And.scala +++ b/modules/api/src/main/scala/gs/predicate/v0/api/And.scala @@ -2,7 +2,6 @@ package gs.predicate.v0.api import cats.Applicative import cats.syntax.all.* -import gs.predicate.v0.api.Predicate.Result.forall import gs.uuid.v0.UUID /** Implements logical AND. @@ -18,7 +17,7 @@ final class And[F[_]: Applicative, -A]( /** @inheritDocs */ override def eval(input: A): F[Predicate.Result] = - ps.map(_.eval(input)).sequence.map(_.forall()) + ps.map(_.eval(input)).sequence.map(_.allMatch()) object And: diff --git a/modules/api/src/main/scala/gs/predicate/v0/api/Or.scala b/modules/api/src/main/scala/gs/predicate/v0/api/Or.scala index 0596bff..7e7b2c3 100644 --- a/modules/api/src/main/scala/gs/predicate/v0/api/Or.scala +++ b/modules/api/src/main/scala/gs/predicate/v0/api/Or.scala @@ -2,7 +2,6 @@ package gs.predicate.v0.api import cats.Applicative import cats.syntax.all.* -import gs.predicate.v0.api.Predicate.Result.forany import gs.uuid.v0.UUID /** Implements logical OR. @@ -18,7 +17,7 @@ final class Or[F[_]: Applicative, -A]( /** @inheritDocs */ override def eval(input: A): F[Predicate.Result] = - ps.map(_.eval(input)).sequence.map(_.forany()) + ps.map(_.eval(input)).sequence.map(_.anyMatch()) object Or: diff --git a/modules/api/src/main/scala/gs/predicate/v0/api/Predicate.scala b/modules/api/src/main/scala/gs/predicate/v0/api/Predicate.scala index cd39d05..98c9bc5 100644 --- a/modules/api/src/main/scala/gs/predicate/v0/api/Predicate.scala +++ b/modules/api/src/main/scala/gs/predicate/v0/api/Predicate.scala @@ -1,5 +1,6 @@ package gs.predicate.v0.api +import cats.Applicative import gs.uuid.v0.UUID /** A _Predicate_ is some function that accepts any input and emits some @@ -48,6 +49,12 @@ trait Predicate[F[_], -A]: object Predicate: + given CanEqual[Predicate[?, ?], Predicate[?, ?]] = CanEqual.derived + + def alwaysTrue[F[_]: Applicative]: Predicate[F, Any] = True[F] + + def alwaysFalse[F[_]: Applicative]: Predicate[F, Any] = False[F] + /** The result of evaluating a [[Predicate]] is a Boolean value where: * * - `true`: The predicate matched the given input. @@ -80,14 +87,15 @@ object Predicate: extension (results: List[Result]) /** @return - * True if all results are true. + * True if all results are true. False if no results are given. */ - def forall(): Result = results.forall(x => x) + def allMatch(): Result = results.nonEmpty && results.forall(x => x) /** @return - * True if any results match. + * True if any results match. False if no results are given. */ - def forany(): Result = results.find(x => x).isDefined + def anyMatch(): Result = + results.nonEmpty && results.find(x => x).isDefined extension (result: Result) /** @return diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/AndTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/AndTests.scala index 957c6c5..a505d50 100644 --- a/modules/api/src/test/scala/gs/predicate/v0/api/AndTests.scala +++ b/modules/api/src/test/scala/gs/predicate/v0/api/AndTests.scala @@ -1,27 +1,28 @@ package gs.predicate.v0.api import cats.effect.IO +import gs.uuid.v0.UUID import support.IOSuite class AndTests extends IOSuite: iotest("should return true if all are true") { - val and = And(True[IO], True[IO], True[IO]) + val id = UUID.v7() + val and = And(True[IO], True[IO], True[IO]) + val and2 = And(id, True[IO], True[IO], True[IO]) - for result <- and.eval(()) - yield assertEquals(result.unwrap(), true) + for + result <- and.eval(()) + result2 <- and2.eval(()) + yield + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) } iotest("should return false if any are false") { - val and = And(True[IO], False[IO], True[IO]) - - for result <- and.eval(()) - yield assertEquals(result.unwrap(), false) - } - - iotest("should return false for an empty list") { - val and = And.empty[IO] - val and2 = And[IO, Any]() + val id = UUID.v7() + val and = And(True[IO], False[IO], True[IO]) + val and2 = And(id, True[IO], False[IO], True[IO]) for result <- and.eval(()) @@ -30,3 +31,33 @@ class AndTests extends IOSuite: assertEquals(result.unwrap(), false) assertEquals(result2.unwrap(), false) } + + iotest("should return false for an empty list") { + val and = And.empty[IO] + val and2 = And[IO, Any]() + val and3 = And[IO, Any](UUID.v7()) + + for + result <- and.eval(()) + result2 <- and2.eval(()) + result3 <- and3.eval(()) + yield + assertEquals(result.unwrap(), false) + assertEquals(result2.unwrap(), false) + assertEquals(result3.unwrap(), false) + } + + iotest("should return the underlying predicate for a singular entry") { + val p = True[IO] + val and = And(p) + val and2 = And(UUID.v7(), p) + + for + result <- and.eval(()) + result2 <- and2.eval(()) + yield + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) + assertEquals(p, and) + assertEquals(p, and2) + } diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/FalseTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/FalseTests.scala new file mode 100644 index 0000000..f502468 --- /dev/null +++ b/modules/api/src/test/scala/gs/predicate/v0/api/FalseTests.scala @@ -0,0 +1,35 @@ +package gs.predicate.v0.api + +import cats.effect.IO +import gs.uuid.v0.UUID +import support.IOSuite + +class FalseTests extends IOSuite: + + iotest("should return false") { + val predicate = False[IO] + + for result <- predicate.eval(()) + yield assertEquals(result.unwrap(), false) + } + + iotest("should provide a unique identifier") { + val id = UUID.v7() + val predicate = False[IO](id) + + for result <- predicate.eval(()) + yield + assertEquals(result.unwrap(), false) + assertEquals(predicate.id, id) + assertEquals(predicate.hashCode(), id.hashCode()) + assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}") + } + + test("should support equality") { + val predicate = False[IO] + val p2 = False[IO](predicate.id) + val p3 = False[IO] + assertEquals(predicate, predicate) + assertEquals(predicate, p2) + assertNotEquals(predicate, p3) + } diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala index fa769e0..d5ff2f6 100644 --- a/modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala +++ b/modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala @@ -1,27 +1,28 @@ package gs.predicate.v0.api import cats.effect.IO +import gs.uuid.v0.UUID import support.IOSuite class OrTests extends IOSuite: iotest("should return true if any are true") { - val or = Or(False[IO], True[IO], False[IO]) + val id = UUID.v7() + val or = Or(False[IO], True[IO], False[IO]) + val or2 = Or(id, False[IO], True[IO], False[IO]) - for result <- or.eval(()) - yield assertEquals(result.unwrap(), true) + for + result <- or.eval(()) + result2 <- or2.eval(()) + yield + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) } iotest("should return false if all are false") { - val or = Or(False[IO], False[IO], False[IO]) - - for result <- or.eval(()) - yield assertEquals(result.unwrap(), false) - } - - iotest("should return false for an empty list") { - val or = Or.empty[IO] - val or2 = Or[IO, Any]() + val id = UUID.v7() + val or = Or(False[IO], False[IO], False[IO]) + val or2 = Or(id, False[IO], False[IO], False[IO]) for result <- or.eval(()) @@ -30,3 +31,33 @@ class OrTests extends IOSuite: assertEquals(result.unwrap(), false) assertEquals(result2.unwrap(), false) } + + iotest("should return false for an empty list") { + val or = Or.empty[IO] + val or2 = Or[IO, Any]() + val or3 = Or[IO, Any](UUID.v7()) + + for + result <- or.eval(()) + result2 <- or2.eval(()) + result3 <- or3.eval(()) + yield + assertEquals(result.unwrap(), false) + assertEquals(result2.unwrap(), false) + assertEquals(result3.unwrap(), false) + } + + iotest("should return the underlying predicate for a singular entry") { + val p = True[IO] + val or = Or(p) + val or2 = Or(UUID.v7(), p) + + for + result <- or.eval(()) + result2 <- or2.eval(()) + yield + assertEquals(result.unwrap(), true) + assertEquals(result2.unwrap(), true) + assertEquals(p, or) + assertEquals(p, or2) + } diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/PredicateResultTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/PredicateResultTests.scala new file mode 100644 index 0000000..d39dd4e --- /dev/null +++ b/modules/api/src/test/scala/gs/predicate/v0/api/PredicateResultTests.scala @@ -0,0 +1,82 @@ +package gs.predicate.v0.api + +import Predicate.Result +import gs.predicate.v0.api.Predicate.Result.allMatch +import munit.FunSuite + +class PredicateResultTests extends FunSuite: + + test("should represent matched and missed") { + val r1 = Result.matched() + val r2 = Result.missed() + assertEquals(r1.unwrap(), true) + assertEquals(r1.isMatch, true) + assertEquals(r1.isMiss, false) + assertEquals(r2.unwrap(), false) + assertEquals(r2.isMatch, false) + assertEquals(r2.isMiss, true) + } + + test("should instantiate given some Boolean value") { + val r1 = Result(true) + val r2 = Result(false) + assertEquals(r1.unwrap(), true) + assertEquals(r1.isMatch, true) + assertEquals(r1.isMiss, false) + assertEquals(r2.unwrap(), false) + assertEquals(r2.isMatch, false) + assertEquals(r2.isMiss, true) + } + + test("should support logical AND") { + val r1 = Result.matched() + val r2 = Result.missed() + val r3 = Result.matched() + val r4 = Result.matched() + val r5 = Result.missed() + assertEquals(r1.and(r3).and(r4), Result.matched()) + assertEquals(r1.and(r2).and(r4), Result.missed()) + assertEquals(r5.and(r1).and(r4), Result.missed()) + } + + test("should support logical OR") { + val r1 = Result.matched() + val r2 = Result.missed() + val r3 = Result.matched() + val r4 = Result.matched() + val r5 = Result.missed() + assertEquals(r1.or(r3).or(r4), Result.matched()) + assertEquals(r1.or(r2).or(r4), Result.matched()) + assertEquals(r5.or(r1).or(r4), Result.matched()) + assertEquals(r2.or(r5), Result.missed()) + } + + test("should support all (true if all results are true)") { + val r1 = Result.matched() + val r2 = Result.missed() + val r3 = Result.matched() + val r4 = Result.matched() + val r5 = Result.missed() + val l1 = List(r1, r3, r4) + val l2 = List(r1, r2, r3, r4, r5) + val l3 = List() + assertEquals(l1.allMatch(), Result.matched()) + assertEquals(l2.allMatch(), Result.missed()) + assertEquals(l3.allMatch(), Result.missed()) + } + + test("should support any (true if any results are true)") { + val r1 = Result.matched() + val r2 = Result.missed() + val r3 = Result.matched() + val r4 = Result.matched() + val r5 = Result.missed() + val l1 = List(r1, r3, r4) + val l2 = List(r1, r2, r3, r4, r5) + val l3 = List(r2, r5) + val l4 = List.empty[Result] + assertEquals(l1.anyMatch(), Result.matched()) + assertEquals(l2.anyMatch(), Result.matched()) + assertEquals(l3.anyMatch(), Result.missed()) + assertEquals(l4.anyMatch(), Result.missed()) + } diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/PredicateTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/PredicateTests.scala new file mode 100644 index 0000000..4d656f8 --- /dev/null +++ b/modules/api/src/test/scala/gs/predicate/v0/api/PredicateTests.scala @@ -0,0 +1,17 @@ +package gs.predicate.v0.api + +import munit.FunSuite + +class PredicateTests extends FunSuite: + + test("should not equal non-predicate objects") { + val x = "other" + val p = Predicate.alwaysTrue[cats.Id] + assertEquals(p.equals(x), false) + assertNotEquals(p, null) + } + + test("should provide a default toString implementation") { + val p = Predicate.alwaysFalse[cats.Id] + assertEquals(p.toString(), s"predicate:${p.id.withoutDashes()}") + } diff --git a/modules/api/src/test/scala/gs/predicate/v0/api/TrueTests.scala b/modules/api/src/test/scala/gs/predicate/v0/api/TrueTests.scala new file mode 100644 index 0000000..0ed014c --- /dev/null +++ b/modules/api/src/test/scala/gs/predicate/v0/api/TrueTests.scala @@ -0,0 +1,35 @@ +package gs.predicate.v0.api + +import cats.effect.IO +import gs.uuid.v0.UUID +import support.IOSuite + +class TrueTests extends IOSuite: + + iotest("should return true") { + val predicate = True[IO] + + for result <- predicate.eval(()) + yield assertEquals(result.unwrap(), true) + } + + iotest("should provide a unique identifier") { + val id = UUID.v7() + val predicate = True[IO](id) + + for result <- predicate.eval(()) + yield + assertEquals(result.unwrap(), true) + assertEquals(predicate.id, id) + assertEquals(predicate.hashCode(), id.hashCode()) + assertEquals(predicate.toString(), s"predicate:${id.withoutDashes()}") + } + + test("should support equality") { + val predicate = True[IO] + val p2 = True[IO](predicate.id) + val p3 = True[IO] + assertEquals(predicate, predicate) + assertEquals(predicate, p2) + assertNotEquals(predicate, p3) + }