From c114da203e6176ccd5e9f6cd9cc4cf2c2959937a Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Mon, 3 Nov 2025 09:14:15 -0600 Subject: [PATCH] Improved and/or, scalafmt, more tests --- build.sbt | 1 - .../main/scala/gs/predicate/v0/api/And.scala | 31 ++-- .../scala/gs/predicate/v0/api/False.scala | 8 +- .../main/scala/gs/predicate/v0/api/Or.scala | 31 ++-- .../scala/gs/predicate/v0/api/Predicate.scala | 152 +++++++++--------- .../main/scala/gs/predicate/v0/api/True.scala | 8 +- .../scala/gs/predicate/v0/api/AndTests.scala | 35 ++-- .../scala/gs/predicate/v0/api/OrTests.scala | 32 ++++ 8 files changed, 183 insertions(+), 115 deletions(-) create mode 100644 modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala diff --git a/build.sbt b/build.sbt index 5c52768..1f0536c 100644 --- a/build.sbt +++ b/build.sbt @@ -88,4 +88,3 @@ lazy val api = project Deps.Gs.Uuid ) ) - 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 d44fefd..6dc4fcb 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 @@ -1,29 +1,42 @@ package gs.predicate.v0.api -import cats.syntax.all.* import cats.Applicative +import cats.syntax.all.* import gs.predicate.v0.api.Predicate.Result.forall import gs.uuid.v0.UUID -/** - * Implements logical AND. +/** Implements logical AND. * - * @param ps The predicates to evaluate. + * @param ps + * The predicates to evaluate. */ final class And[F[_]: Applicative, -A]( val id: UUID, private val ps: List[Predicate[F, A]] ) extends Predicate[F, A]: - /** @inheritDocs */ + + /** @inheritDocs + */ override def eval(input: A): F[Predicate.Result] = ps.map(_.eval(input)).sequence.map(_.forall()) object And: - def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): And[F, A] = - new And(UUID.v7(), ps.toList) + def empty[F[_]: Applicative]: Predicate[F, Any] = False[F] - def apply[F[_]: Applicative, A](id: UUID, ps: Predicate[F, A]*): And[F, A] = - new And(id, ps.toList) + def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] = + ps.toList match + case Nil => False[F] + case p :: Nil => p + case list => new And(UUID.v7(), list) + + def apply[F[_]: Applicative, A]( + id: UUID, + ps: Predicate[F, A]* + ): Predicate[F, A] = + ps.toList match + case Nil => False[F] + case p :: Nil => p + case list => new And(id, list) end And diff --git a/modules/api/src/main/scala/gs/predicate/v0/api/False.scala b/modules/api/src/main/scala/gs/predicate/v0/api/False.scala index 03a16b2..8a6cb78 100644 --- a/modules/api/src/main/scala/gs/predicate/v0/api/False.scala +++ b/modules/api/src/main/scala/gs/predicate/v0/api/False.scala @@ -1,16 +1,16 @@ package gs.predicate.v0.api import cats.Applicative - import gs.uuid.v0.UUID -/** - * Always returns a miss. +/** Always returns a miss. */ final class False[F[_]: Applicative]( val id: UUID ) extends Predicate[F, Any]: - /** @inheritDocs */ + + /** @inheritDocs + */ override def eval(input: Any): F[Predicate.Result] = Applicative[F].pure(Predicate.Result.missed()) 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 ce72e78..0596bff 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 @@ -1,29 +1,42 @@ package gs.predicate.v0.api -import cats.syntax.all.* import cats.Applicative +import cats.syntax.all.* import gs.predicate.v0.api.Predicate.Result.forany import gs.uuid.v0.UUID -/** - * Implements logical OR. +/** Implements logical OR. * - * @param ps The predicates to evaluate. + * @param ps + * The predicates to evaluate. */ final class Or[F[_]: Applicative, -A]( val id: UUID, private val ps: List[Predicate[F, A]] ) extends Predicate[F, A]: - /** @inheritDocs */ + + /** @inheritDocs + */ override def eval(input: A): F[Predicate.Result] = ps.map(_.eval(input)).sequence.map(_.forany()) object Or: - def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Or[F, A] = - new Or(UUID.v7(), ps.toList) + def empty[F[_]: Applicative]: Predicate[F, Any] = False[F] - def apply[F[_]: Applicative, A](id: UUID, ps: Predicate[F, A]*): Or[F, A] = - new Or(id, ps.toList) + def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] = + ps.toList match + case Nil => False[F] + case p :: Nil => p + case list => new Or(UUID.v7(), list) + + def apply[F[_]: Applicative, A]( + id: UUID, + ps: Predicate[F, A]* + ): Predicate[F, A] = + ps.toList match + case Nil => False[F] + case p :: Nil => p + case list => new Or(id, list) end 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 7bb3a7f..cd39d05 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 @@ -2,119 +2,125 @@ package gs.predicate.v0.api import gs.uuid.v0.UUID -/** - * A _Predicate_ is some function that accepts any input and emits some - * [[Predicate.Result]] (whether the predicate matched). - * - * Predicates evaluate input to see if the predicate matches that input. - */ +/** A _Predicate_ is some function that accepts any input and emits some + * [[Predicate.Result]] (whether the predicate matched). + * + * Predicates evaluate input to see if the predicate matches that input. + */ trait Predicate[F[_], -A]: - /** - * @return The unique identifier of this Predicate. - */ + /** @return + * The unique identifier of this Predicate. + */ def id: UUID - /** - * Evaluate this predicate against the given input. - * - * @param input The input to evaluate this predicate against. - * @return Some [[Predicate.Result]] that describes whether the input matched the predicate. - */ + /** Evaluate this predicate against the given input. + * + * @param input + * The input to evaluate this predicate against. + * @return + * Some [[Predicate.Result]] that describes whether the input matched the + * predicate. + */ def eval(input: A): F[Predicate.Result] - /** - * Predicate equality is based on the _unique identifier_. Two predicates with - * the same ID are considered equal. - * - * @param that The other object. - * @return True if the other object is a predicate with the same ID, false otherwise. - */ - override def equals(that: Any): Boolean = + /** Predicate equality is based on the _unique identifier_. Two predicates + * with the same ID are considered equal. + * + * @param that + * The other object. + * @return + * True if the other object is a predicate with the same ID, false + * otherwise. + */ + override def equals(that: Any): Boolean = that match case p: Predicate[?, ?] => p.id == id - case _ => false + case _ => false - /** - * @return The hash code of the unique identifier. - */ + /** @return + * The hash code of the unique identifier. + */ override def hashCode(): Int = id.hashCode() - /** @inheritDocs */ + /** @inheritDocs + */ override def toString(): String = s"predicate:${id.withoutDashes()}" object Predicate: - /** - * The result of evaluating a [[Predicate]] is a Boolean value where: - * - * - `true`: The predicate matched the given input. - * - `false`: The predicate missed (did not match) the given input. - */ + /** The result of evaluating a [[Predicate]] is a Boolean value where: + * + * - `true`: The predicate matched the given input. + * - `false`: The predicate missed (did not match) the given input. + */ opaque type Result = Boolean object Result: - /** - * @return A result indicating a predicate matched (`true`). - */ + /** @return + * A result indicating a predicate matched (`true`). + */ def matched(): Result = true - /** - * @return A result indicating a predicate missed (`false`). - */ + /** @return + * A result indicating a predicate missed (`false`). + */ def missed(): Result = false - /** - * Instantiate a new [[Predicate.Result]] from the given value. - * - * @param value The underlying `Boolean` value. - * @return The new predicate result. - */ + /** Instantiate a new [[Predicate.Result]] from the given value. + * + * @param value + * The underlying `Boolean` value. + * @return + * The new predicate result. + */ def apply(value: Boolean): Result = value given CanEqual[Result, Result] = CanEqual.derived extension (results: List[Result]) - /** - * @return True if all results are true. - */ + /** @return + * True if all results are true. + */ def forall(): Result = results.forall(x => x) - /** - * @return True if any results match. - */ + /** @return + * True if any results match. + */ def forany(): Result = results.find(x => x).isDefined extension (result: Result) - /** - * @return The underlying value. - */ + /** @return + * The underlying value. + */ def unwrap(): Boolean = result - /** - * Logical AND operation. - * - * @param other The other result. - * @return True if both results match. False otherwise. - */ + /** Logical AND operation. + * + * @param other + * The other result. + * @return + * True if both results match. False otherwise. + */ def and(other: Result): Result = result && other - /** - * Logical OR operation. - * - * @param other The other result. - * @return True if either result matches. False otherwise. - */ + /** Logical OR operation. + * + * @param other + * The other result. + * @return + * True if either result matches. False otherwise. + */ def or(other: Result): Result = result || other - /** - * @return True if this result is a match. False otherwise. - */ + /** @return + * True if this result is a match. False otherwise. + */ def isMatch: Boolean = result - /** - * @return True if this result is a miss. False otherwise. - */ + /** @return + * True if this result is a miss. False otherwise. + */ def isMiss: Boolean = !result end Result diff --git a/modules/api/src/main/scala/gs/predicate/v0/api/True.scala b/modules/api/src/main/scala/gs/predicate/v0/api/True.scala index 7ef55a7..851d29e 100644 --- a/modules/api/src/main/scala/gs/predicate/v0/api/True.scala +++ b/modules/api/src/main/scala/gs/predicate/v0/api/True.scala @@ -1,16 +1,16 @@ package gs.predicate.v0.api import cats.Applicative - import gs.uuid.v0.UUID -/** - * Always returns a match. +/** Always returns a match. */ final class True[F[_]: Applicative]( val id: UUID ) extends Predicate[F, Any]: - /** @inheritDocs */ + + /** @inheritDocs + */ override def eval(input: Any): F[Predicate.Result] = Applicative[F].pure(Predicate.Result.matched()) 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 0fa9532..957c6c5 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,32 @@ package gs.predicate.v0.api +import cats.effect.IO import support.IOSuite -import cats.effect.IO - -import gs.predicate.v0.api.And - class AndTests extends IOSuite: + iotest("should return true if all are true") { - val and: And[IO, Any] = And( - True[IO], True[IO], True[IO] - ) - and.eval(()).map(_.unwrap()) + val and = And(True[IO], True[IO], True[IO]) + + for result <- and.eval(()) + yield assertEquals(result.unwrap(), true) } iotest("should return false if any are false") { - val and: And[IO, Any] = And( - True[IO], False[IO], True[IO] - ) - and.eval(()).map(x => !x.unwrap()) + val and = And(True[IO], False[IO], True[IO]) + + for result <- and.eval(()) + yield assertEquals(result.unwrap(), false) } - iotest("should return true for an empty list") { - val and: And[IO, Any] = And() - and.eval(()).map(_.unwrap()) + iotest("should return false for an empty list") { + val and = And.empty[IO] + val and2 = And[IO, Any]() + + for + result <- and.eval(()) + result2 <- and2.eval(()) + yield + assertEquals(result.unwrap(), false) + assertEquals(result2.unwrap(), false) } 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 new file mode 100644 index 0000000..fa769e0 --- /dev/null +++ b/modules/api/src/test/scala/gs/predicate/v0/api/OrTests.scala @@ -0,0 +1,32 @@ +package gs.predicate.v0.api + +import cats.effect.IO +import support.IOSuite + +class OrTests extends IOSuite: + + iotest("should return true if any are true") { + val or = Or(False[IO], True[IO], False[IO]) + + for result <- or.eval(()) + yield assertEquals(result.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]() + + for + result <- or.eval(()) + result2 <- or2.eval(()) + yield + assertEquals(result.unwrap(), false) + assertEquals(result2.unwrap(), false) + }