171 lines
5.8 KiB
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, *]]()
|
|
}
|