Skip to content

Commit

Permalink
Refactoring Validaiton dsl syntax.
Browse files Browse the repository at this point in the history
  • Loading branch information
vickumar1981 committed Jan 13, 2019
1 parent 34bb5a7 commit 9e1bddd
Show file tree
Hide file tree
Showing 4 changed files with 35 additions and 38 deletions.
@@ -1,40 +1,26 @@
package com.github.vickumar1981.svalidate

trait ValidationDsl[T] {
case class ConditionAndValidation(condition: Boolean,
validation: () => ValidationResult[T])
private def collapseErrors(errors: Seq[T]): ValidationResult[T] =
if (errors.isEmpty) Validation.success else ValidationFailure(errors)

case class ConditionAndError(condition: Boolean, error: T)

private def throwErrorsIfConditionsAreMet(conditions: ConditionAndError*): ValidationResult[T] =
ValidationResult(
conditions
.filter(_.condition)
.map(c => c.error)
.toList)

private def performValidationsWithConditions(conditions: ConditionAndValidation*):ValidationResult[T] = {
ValidationResult(conditions.filter(_.condition).flatMap(_.validation().errors))
}

implicit class BoolsToValidation(val cond: Boolean) {
def thenThrow(err: T): ValidationResult[T] =
throwErrorsIfConditionsAreMet(ConditionAndError(cond, err))
implicit class BoolsToValidation(cond: Boolean) {
def thenThrow(errors: T*): ValidationResult[T] =
if (cond) collapseErrors(errors.toSeq) else Validation.success

def thenCheck(validation: => ValidationResult[T]): ValidationResult[T] =
performValidationsWithConditions(
ConditionAndValidation(cond, () => validation))
if (cond) validation else Validation.success
}

implicit class OptionToValidatable[A](validatable: Option[A]) {
def maybeValidate()(implicit validator: ValidatableResult[A, T]): ValidationResult[T] =
validatable.map(v => validator.validate(v)).getOrElse(Validation.success)
validatable.map(validator.validate).getOrElse(Validation.success)

def maybeValidate(validation: A => ValidationResult[T]): ValidationResult[T] =
validatable.map(validation).getOrElse(Validation.success)

def maybeThrow(errMsg: T): ValidationResult[T] =
validatable.map(_ => ValidationResult(errMsg :: Nil)).getOrElse(Validation.success)
def maybeThrow(errors: T*): ValidationResult[T] =
validatable.map(_ => collapseErrors(errors.toSeq)).getOrElse(Validation.success)
}

implicit class OptionToValidatableWith[A, B](validatable: Option[A])
Expand Down
26 changes: 17 additions & 9 deletions src/main/scala/com/github/vickumar1981/svalidate/package.scala
Expand Up @@ -2,22 +2,30 @@ package com.github.vickumar1981

package object svalidate {

case class ValidationResult[T](errors: Seq[T] = Seq.empty) {
// scalastyle:off
def ++(other: ValidationResult[T]): ValidationResult[T] = ValidationResult(errors ++ other.errors)

// scalastyle:on

sealed trait ValidationResult[+T] {
def errors: Seq[T]
def isSuccess: Boolean = errors.isEmpty
def isFailure: Boolean = !isSuccess
}

case class ValidationSuccess[+T]() extends ValidationResult[T] {
override val errors: Seq[T] = Seq.empty
}

case class ValidationFailure[+T](errorList: Seq[T]) extends ValidationResult[T] {
override val errors: Seq[T] = errorList
}

implicit def validationToSeq[T](v: ValidationResult[T]): Seq[T] = v.errors

implicit def seqToValidation[T](seq: Seq[T]): ValidationResult[T] =
if (seq.isEmpty) ValidationSuccess() else ValidationFailure(seq)

type Validation = ValidationResult[String]

object Validation {
//def fail[T](validationError: T): ValidationResult[T] = ValidationResult(validationError :: Nil)
def fail[T](validationErrors: T*): ValidationResult[T] = ValidationResult(validationErrors)
def success[T]: ValidationResult[T] = ValidationResult()
def fail[T](validationErrors: T*): ValidationResult[T] = ValidationFailure[T](validationErrors.toSeq)
def success[T]: ValidationResult[T] = ValidationSuccess[T]()
}

trait ValidatableResult[-A, B] extends ValidationDsl[B] {
Expand Down
10 changes: 5 additions & 5 deletions src/test/scala/TestValidationDsl.scala
Expand Up @@ -15,29 +15,29 @@ class TestValidationDsl extends FlatSpec with Matchers {
}

"validate" should "return a success if no validation errors occur" in {
val result = validPerson.validate
val result = validPerson.validate()
checkSuccess(result)
}

"thenThrow" should "return a single validation result error" in {
val firstNameRequired = validPerson.copy(firstName = "").validate
val lastNameRequired = validPerson.copy(lastName = "").validate
val firstNameRequired = validPerson.copy(firstName = "").validate()
val lastNameRequired = validPerson.copy(lastName = "").validate()
firstNameRequired should be(Validation.fail("First name is required"))
checkFailure(firstNameRequired)
lastNameRequired should be(Validation.fail("Last name is required"))
checkFailure(lastNameRequired)
}

"thenCheck" should "perform validation conditionally" in {
val emptyContactInfo = validPerson.copy(address = None, phone = None).validate
val emptyContactInfo = validPerson.copy(address = None, phone = None).validate()
emptyContactInfo should be(
Validation.fail("Address is required", "Phone # is required"))
checkFailure(emptyContactInfo)

val emptyContactInfoOkay = validPerson.copy(
address = None,
phone = None,
hasContactInfo = false).validate
hasContactInfo = false).validate()
emptyContactInfoOkay should be(Validation.success)
checkSuccess(emptyContactInfoOkay)
}
Expand Down
5 changes: 4 additions & 1 deletion src/test/scala/fixtures/TestData.scala
Expand Up @@ -3,8 +3,11 @@ package fixtures
import com.github.javafaker.Faker
import com.github.vickumar1981.svalidate.{Validatable, Validation}

import scala.util.Random

object TestData {
private final val zipCodeLength = 5
private final val phoneNumberLength = 10
val faker = new Faker()

def validAddress: Address = Address(
Expand All @@ -18,7 +21,7 @@ object TestData {
faker.name.lastName,
true,
Some(validAddress),
Some(faker.phoneNumber.cellPhone.filter(_.isDigit)))
Some(Random.alphanumeric.filter(_.isDigit).take(phoneNumberLength).mkString))

case class Address(street: String,
city: String,
Expand Down

0 comments on commit 9e1bddd

Please sign in to comment.