embracing-nondeterminism-code/src/test/scala/green/thisfieldwas/embracingnondeterminism/data/ValidatedSpec.scala

171 lines
5.8 KiB
Scala

package green.thisfieldwas.embracingnondeterminism.data
import green.thisfieldwas.embracingnondeterminism.control.ApplicativeLaws
import green.thisfieldwas.embracingnondeterminism.util.{LiftedGen, _}
import org.scalacheck.Arbitrary.arbitrary
import org.scalacheck.{Arbitrary, Gen}
import org.scalatest.Inside
import org.scalatest.matchers.should.Matchers
import org.scalatest.wordspec.AnyWordSpec
class ValidatedSpec extends AnyWordSpec with Matchers with Inside {
"Validated" can {
"isValid" when {
"it's the desired case" in {
Valid("banana").isValid shouldBe true
}
"it's the undesired case" in {
Invalid("watermelon").isValid shouldBe false
}
}
"isInvalid" when {
"it's the desired case" in {
Valid("banana").isInvalid shouldBe false
}
"it's the undesired case" in {
Invalid("watermelon").isInvalid shouldBe true
}
}
"valid" when {
"it's the desired case" in {
Valid("banana").valid shouldBe "banana"
}
"it's the undesired case" in {
val e = the[NoSuchElementException].thrownBy(Invalid("watermelon").valid)
e.getMessage shouldBe "Invalid.valid"
}
}
"invalid" when {
"it's the desired case" in {
val e = the[NoSuchElementException].thrownBy(Valid("banana").invalid)
e.getMessage shouldBe "Valid.invalid"
}
"it's the undesired case" in {
Invalid("watermelon").invalid shouldBe "watermelon"
}
}
}
"Validated Applicative" can {
import green.thisfieldwas.embracingnondeterminism.syntax.applicative._
import green.thisfieldwas.embracingnondeterminism.syntax.validated._
case class User(username: String, email: String, password: String)
def validateUsername(username: String): ValidatedNec[String, String] =
if (username.isEmpty) {
"Username can't be blank".invalidNec[String]
} else {
username.validNec[String]
}
def validateEmail(email: String): ValidatedNec[String, String] =
if (!email.contains('@')) {
"Email does not appear to be valid".invalidNec[String]
} else {
email.validNec[String]
}
def validatePassword(password: String): ValidatedNec[String, String] =
List(
Option.cond(password.length < 8)("Password must have at least 8 characters"),
Option.cond(!password.exists(_.isLower))("Password must contain at least one lowercase character"),
Option.cond(!password.exists(_.isUpper))("Password must contain at least one uppercase character"),
Option.cond(!password.exists(_.isDigit))("Password must contain at least one digit"),
Option.cond(!password.exists("`~!@#$%^&*()-_=+[]{}\\|;:'\",.<>/?".contains(_)))(
"Password must contain at least one symbol"
),
).foldLeft(password.validNec[String]) { (validated, errorOption) =>
errorOption.fold(validated) { message =>
Invalid(validated.fold(_.cons(message))(_ => NonEmptyChain(message)))
}
}
def validateUser(username: String, email: String, password: String): Validated[NonEmptyChain[String], User] =
User.curried
.pure[Validated[NonEmptyChain[String], *]]
.ap(validateUsername(username))
.ap(validateEmail(email))
.ap(validatePassword(password))
"validate a User" when {
"all arguments are valid" in {
val validatedUser = validateUser(
username = "commander.keen",
email = "commander.keen@vorticonexterminator.net",
password = "m4rti@an$Rul3",
)
inside(validatedUser) { case Valid(user) =>
user shouldBe User(
username = "commander.keen",
email = "commander.keen@vorticonexterminator.net",
password = "m4rti@an$Rul3",
)
}
}
"one argument is invalid" in {
val validatedUser = validateUser(
username = "commander.keen",
email = "commander.keen|xterminator.net",
password = "m4rti@an$Rul3",
)
inside(validatedUser) { case Invalid(reasons) =>
(reasons.toSeq should contain).only(
"Email does not appear to be valid"
)
}
}
"no arguments are valid" in {
val validatedUser = validateUser(
username = "",
email = "bananaphone",
password = "\n\t\r\r\r",
)
inside(validatedUser) { case Invalid(reasons) =>
(reasons.toSeq should contain).theSameElementsAs(
Seq(
"Username can't be blank",
"Email does not appear to be valid",
"Password must have at least 8 characters",
"Password must contain at least one lowercase character",
"Password must contain at least one uppercase character",
"Password must contain at least one digit",
"Password must contain at least one symbol",
)
)
}
}
}
}
}
class ValidatedLaws extends Laws with FunctorLaws with ApplicativeLaws {
import green.thisfieldwas.embracingnondeterminism.data.LiftedGens._
implicit def validatedNecLiftedGen[E: Arbitrary]: LiftedGen[ValidatedNec[E, *]] = new LiftedGen[ValidatedNec[E, *]] {
/** Given a generator, lift its output into `F`.
*
* @param gen
* The generator whose outputs to lift.
* @tparam A
* The type output by the generator.
* @return
* A generator containing the lifted output.
*/
override def lift[A](gen: Gen[A]): Gen[ValidatedNec[E, A]] =
Gen.lzy(
Gen.oneOf(
arbitrary[E].lift[NonEmptyChain].map(Invalid(_)),
gen.map(Valid(_)),
)
)
}
checkFunctorLaws[ValidatedNec[Exception, *]]()
checkApplicativeLaws[ValidatedNec[Exception, *]]()
}