Improved and/or, scalafmt, more tests

This commit is contained in:
Pat Garrity 2025-11-03 09:14:15 -06:00
parent 24fee4b4be
commit c114da203e
Signed by: pfm
GPG key ID: 5CA5D21BAB7F3A76
8 changed files with 183 additions and 115 deletions

View file

@ -88,4 +88,3 @@ lazy val api = project
Deps.Gs.Uuid Deps.Gs.Uuid
) )
) )

View file

@ -1,29 +1,42 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.syntax.all.*
import cats.Applicative import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate.Result.forall import gs.predicate.v0.api.Predicate.Result.forall
import gs.uuid.v0.UUID 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]( final class And[F[_]: Applicative, -A](
val id: UUID, val id: UUID,
private val ps: List[Predicate[F, A]] private val ps: List[Predicate[F, A]]
) extends Predicate[F, A]: ) extends Predicate[F, A]:
/** @inheritDocs */
/** @inheritDocs
*/
override def eval(input: A): F[Predicate.Result] = override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.forall()) ps.map(_.eval(input)).sequence.map(_.forall())
object And: object And:
def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): And[F, A] = def empty[F[_]: Applicative]: Predicate[F, Any] = False[F]
new And(UUID.v7(), ps.toList)
def apply[F[_]: Applicative, A](id: UUID, ps: Predicate[F, A]*): And[F, A] = def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] =
new And(id, ps.toList) 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 end And

View file

@ -1,16 +1,16 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.Applicative import cats.Applicative
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** /** Always returns a miss.
* Always returns a miss.
*/ */
final class False[F[_]: Applicative]( final class False[F[_]: Applicative](
val id: UUID val id: UUID
) extends Predicate[F, Any]: ) extends Predicate[F, Any]:
/** @inheritDocs */
/** @inheritDocs
*/
override def eval(input: Any): F[Predicate.Result] = override def eval(input: Any): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.missed()) Applicative[F].pure(Predicate.Result.missed())

View file

@ -1,29 +1,42 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.syntax.all.*
import cats.Applicative import cats.Applicative
import cats.syntax.all.*
import gs.predicate.v0.api.Predicate.Result.forany import gs.predicate.v0.api.Predicate.Result.forany
import gs.uuid.v0.UUID 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]( final class Or[F[_]: Applicative, -A](
val id: UUID, val id: UUID,
private val ps: List[Predicate[F, A]] private val ps: List[Predicate[F, A]]
) extends Predicate[F, A]: ) extends Predicate[F, A]:
/** @inheritDocs */
/** @inheritDocs
*/
override def eval(input: A): F[Predicate.Result] = override def eval(input: A): F[Predicate.Result] =
ps.map(_.eval(input)).sequence.map(_.forany()) ps.map(_.eval(input)).sequence.map(_.forany())
object Or: object Or:
def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Or[F, A] = def empty[F[_]: Applicative]: Predicate[F, Any] = False[F]
new Or(UUID.v7(), ps.toList)
def apply[F[_]: Applicative, A](id: UUID, ps: Predicate[F, A]*): Or[F, A] = def apply[F[_]: Applicative, A](ps: Predicate[F, A]*): Predicate[F, A] =
new Or(id, ps.toList) 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 end Or

View file

@ -2,119 +2,125 @@ package gs.predicate.v0.api
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** /** A _Predicate_ is some function that accepts any input and emits some
* A _Predicate_ is some function that accepts any input and emits some * [[Predicate.Result]] (whether the predicate matched).
* [[Predicate.Result]] (whether the predicate matched). *
* * Predicates evaluate input to see if the predicate matches that input.
* Predicates evaluate input to see if the predicate matches that input. */
*/
trait Predicate[F[_], -A]: trait Predicate[F[_], -A]:
/** /** @return
* @return The unique identifier of this Predicate. * The unique identifier of this Predicate.
*/ */
def id: UUID def id: UUID
/** /** Evaluate this predicate against the given input.
* Evaluate this predicate against the given input. *
* * @param input
* @param input The input to evaluate this predicate against. * The input to evaluate this predicate against.
* @return Some [[Predicate.Result]] that describes whether the input matched the predicate. * @return
*/ * Some [[Predicate.Result]] that describes whether the input matched the
* predicate.
*/
def eval(input: A): F[Predicate.Result] def eval(input: A): F[Predicate.Result]
/** /** Predicate equality is based on the _unique identifier_. Two predicates
* Predicate equality is based on the _unique identifier_. Two predicates with * with the same ID are considered equal.
* the same ID are considered equal. *
* * @param that
* @param that The other object. * The other object.
* @return True if the other object is a predicate with the same ID, false otherwise. * @return
*/ * True if the other object is a predicate with the same ID, false
* otherwise.
*/
override def equals(that: Any): Boolean = override def equals(that: Any): Boolean =
that match that match
case p: Predicate[?, ?] => p.id == id case p: Predicate[?, ?] => p.id == id
case _ => false case _ => false
/** /** @return
* @return The hash code of the unique identifier. * The hash code of the unique identifier.
*/ */
override def hashCode(): Int = id.hashCode() override def hashCode(): Int = id.hashCode()
/** @inheritDocs */ /** @inheritDocs
*/
override def toString(): String = s"predicate:${id.withoutDashes()}" override def toString(): String = s"predicate:${id.withoutDashes()}"
object Predicate: object Predicate:
/** /** The result of evaluating a [[Predicate]] is a Boolean value where:
* The result of evaluating a [[Predicate]] is a Boolean value where: *
* * - `true`: The predicate matched the given input.
* - `true`: The predicate matched the given input. * - `false`: The predicate missed (did not match) the given input.
* - `false`: The predicate missed (did not match) the given input. */
*/
opaque type Result = Boolean opaque type Result = Boolean
object Result: object Result:
/** /** @return
* @return A result indicating a predicate matched (`true`). * A result indicating a predicate matched (`true`).
*/ */
def matched(): Result = true def matched(): Result = true
/** /** @return
* @return A result indicating a predicate missed (`false`). * A result indicating a predicate missed (`false`).
*/ */
def missed(): Result = false def missed(): Result = false
/** /** Instantiate a new [[Predicate.Result]] from the given value.
* Instantiate a new [[Predicate.Result]] from the given value. *
* * @param value
* @param value The underlying `Boolean` value. * The underlying `Boolean` value.
* @return The new predicate result. * @return
*/ * The new predicate result.
*/
def apply(value: Boolean): Result = value def apply(value: Boolean): Result = value
given CanEqual[Result, Result] = CanEqual.derived given CanEqual[Result, Result] = CanEqual.derived
extension (results: List[Result]) extension (results: List[Result])
/** /** @return
* @return True if all results are true. * True if all results are true.
*/ */
def forall(): Result = results.forall(x => x) def forall(): Result = results.forall(x => x)
/** /** @return
* @return True if any results match. * True if any results match.
*/ */
def forany(): Result = results.find(x => x).isDefined def forany(): Result = results.find(x => x).isDefined
extension (result: Result) extension (result: Result)
/** /** @return
* @return The underlying value. * The underlying value.
*/ */
def unwrap(): Boolean = result def unwrap(): Boolean = result
/** /** Logical AND operation.
* Logical AND operation. *
* * @param other
* @param other The other result. * The other result.
* @return True if both results match. False otherwise. * @return
*/ * True if both results match. False otherwise.
*/
def and(other: Result): Result = result && other def and(other: Result): Result = result && other
/** /** Logical OR operation.
* Logical OR operation. *
* * @param other
* @param other The other result. * The other result.
* @return True if either result matches. False otherwise. * @return
*/ * True if either result matches. False otherwise.
*/
def or(other: Result): Result = result || other def or(other: Result): Result = result || other
/** /** @return
* @return True if this result is a match. False otherwise. * True if this result is a match. False otherwise.
*/ */
def isMatch: Boolean = result def isMatch: Boolean = result
/** /** @return
* @return True if this result is a miss. False otherwise. * True if this result is a miss. False otherwise.
*/ */
def isMiss: Boolean = !result def isMiss: Boolean = !result
end Result end Result

View file

@ -1,16 +1,16 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.Applicative import cats.Applicative
import gs.uuid.v0.UUID import gs.uuid.v0.UUID
/** /** Always returns a match.
* Always returns a match.
*/ */
final class True[F[_]: Applicative]( final class True[F[_]: Applicative](
val id: UUID val id: UUID
) extends Predicate[F, Any]: ) extends Predicate[F, Any]:
/** @inheritDocs */
/** @inheritDocs
*/
override def eval(input: Any): F[Predicate.Result] = override def eval(input: Any): F[Predicate.Result] =
Applicative[F].pure(Predicate.Result.matched()) Applicative[F].pure(Predicate.Result.matched())

View file

@ -1,27 +1,32 @@
package gs.predicate.v0.api package gs.predicate.v0.api
import cats.effect.IO
import support.IOSuite import support.IOSuite
import cats.effect.IO
import gs.predicate.v0.api.And
class AndTests extends IOSuite: class AndTests extends IOSuite:
iotest("should return true if all are true") { iotest("should return true if all are true") {
val and: And[IO, Any] = And( val and = And(True[IO], True[IO], True[IO])
True[IO], True[IO], True[IO]
) for result <- and.eval(())
and.eval(()).map(_.unwrap()) yield assertEquals(result.unwrap(), true)
} }
iotest("should return false if any are false") { iotest("should return false if any are false") {
val and: And[IO, Any] = And( val and = And(True[IO], False[IO], True[IO])
True[IO], False[IO], True[IO]
) for result <- and.eval(())
and.eval(()).map(x => !x.unwrap()) yield assertEquals(result.unwrap(), false)
} }
iotest("should return true for an empty list") { iotest("should return false for an empty list") {
val and: And[IO, Any] = And() val and = And.empty[IO]
and.eval(()).map(_.unwrap()) val and2 = And[IO, Any]()
for
result <- and.eval(())
result2 <- and2.eval(())
yield
assertEquals(result.unwrap(), false)
assertEquals(result2.unwrap(), false)
} }

View file

@ -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)
}