Skip to content

Commit

Permalink
Adding sample models. Refactored tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
vickumar1981 committed Jan 14, 2019
1 parent 057c493 commit 8bc94cf
Show file tree
Hide file tree
Showing 8 changed files with 209 additions and 72 deletions.
4 changes: 2 additions & 2 deletions build.sbt
Expand Up @@ -42,6 +42,6 @@ coverageExcludedPackages := "<empty>"

coverageEnabled in(Test, compile) := true
coverageEnabled in(Compile, compile) := false
coverageMinimum := 0
coverageFailOnMinimum := false
coverageMinimum := 100
coverageFailOnMinimum := true
scalastyleFailOnWarning := true
@@ -0,0 +1,50 @@
package com.github.vickumar1981.svalidate.util;

import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

public class Validation<T> {

private List<T> exceptions;

private Validation(List<T> errors) {
this.exceptions = errors;
}

public List<T> errors() {
return exceptions;
}

public Validation<T> append(Validation<T> other) {
this.exceptions.addAll(other.errors());
return this;
}

public Validation<T> andThen(Supplier<Validation<T>> other) {
if (isSuccess()) {
return other.get();
}
return this;
}

public boolean isSuccess() {
return exceptions.isEmpty();
}

public boolean isFailure() {
return !isSuccess();
}

public static <A> Validation fail(A ...value) {
return new Validation<>(
Arrays.stream(value).collect(Collectors.toList())
);
}

public static <A> Validation success() {
return new Validation<>(Collections.emptyList());
}
}
@@ -0,0 +1,24 @@
package com.github.vickumar1981.svalidate.util;

import java.util.Optional;
import java.util.function.Function;

public class ValidationSyntax {
public static <T> Function<Boolean, Validation<T>> orElse(T ...errors) {
return (cond) -> {
if (!cond) {
return Validation.fail(errors);
} else {
return Validation.success();
}
};
}

public static <A> Function<Optional<A>, Validation<A>> errorIfEmpty(A ...errors) {
return (validatable) -> validatable.map(v -> Validation.success()).orElse(Validation.fail(errors));
}

public static <A> Function<Optional<A>, Validation<A>> errorIfDefined(A ...errors) {
return (validatable) -> validatable.map(v -> Validation.fail(errors)).orElse(Validation.success());
}
}
@@ -0,0 +1,26 @@
package com.github.vickumar1981.svalidate.util.example.model;

import com.github.vickumar1981.svalidate.util.Validation;

import static com.github.vickumar1981.svalidate.util.ValidationSyntax.orElse;

public class Address implements Validatable<String> {
private String street;
private String city;
private String state;
private String zipCode;

public Address(String street, String city, String state, String zipCode) {
this.street = street;
this.city = city;
this.state = state;
this.zipCode = zipCode;
}

public Validation<String> validate() {
return orElse("Zip code must be 5 digits").apply(zipCode.matches("\\d{5}")).append(
orElse("State abbr must be 2 letters").apply(state.matches("[A-Z]{2}"))
);
}

}
@@ -0,0 +1,7 @@
package com.github.vickumar1981.svalidate.util.example.model;

import com.github.vickumar1981.svalidate.util.Validation;

public interface Validatable<T> {
Validation<T> validate();
}
@@ -1,72 +1,8 @@
package fixtures
package com.github.vickumar1981.svalidate.example

import com.github.javafaker.Faker
import com.github.vickumar1981.svalidate.{Validatable, ValidatableWith, Validation}

import scala.util.Random

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

case class BlankObject()

implicit object BlankObjectValidator extends Validatable[BlankObject]
implicit object BlankObjectValidatorWith extends ValidatableWith[BlankObject, String]

def validAddress: Address = Address(
faker.address.streetAddress,
faker.address.city,
faker.address.stateAbbr,
faker.address.zipCode.take(zipCodeLength))

def validPerson: Person = Person(
faker.name.firstName,
faker.name.lastName,
true,
Some(validAddress),
Some(Random.alphanumeric.filter(_.isDigit).take(phoneNumberLength).mkString))

def mkContactList: List[String] = (1 to Random.nextInt(maxContacts))
.toList.map { _ => faker.internet().emailAddress() }

def validContactInfo: Contacts = Contacts(
Some(mkContactList),
Some(mkContactList))

def invalidContactAndSettings: (Contacts, ContactSettings) = {
val generateTrueFalse = () => Random.nextInt(maxContacts) % 2
val hasContacts = (x: Int) => if (x == 1) Some(true) else Some(false)
val hasFacebook = hasContacts(generateTrueFalse())
val hasTwitter = hasContacts(generateTrueFalse())
val contacts = Contacts(
if (hasFacebook.getOrElse(true)) None else Some(mkContactList),
if (hasTwitter.getOrElse(true)) None else Some(mkContactList)
)
(contacts, ContactSettings(hasFacebook, hasTwitter))
}

case class Contacts(facebook: Option[List[String]] = None, twitter: Option[List[String]] = None)
case class ContactSettings(hasFacebookContacts: Option[Boolean] = Some(true),
hasTwitterContacts: Option[Boolean] = Some(true))

implicit object ContactInfoValidator extends ValidatableWith[Contacts, ContactSettings] {
override def validateWith(value: Contacts, contactSettings: ContactSettings): Validation = {
contactSettings.hasFacebookContacts.maybeValidate {
contacts =>
(contacts andThen { value.facebook errorIfEmpty "Facebook contacts are required" }) ++
(contacts orElse { value.facebook errorIfDefined "Facebook contacts must be empty"})
} ++
contactSettings.hasTwitterContacts.maybeValidate {
contacts =>
(contacts andThen { value.twitter errorIfEmpty "Twitter contacts are required" }) ++
(contacts orElse { value.twitter errorIfDefined "Twitter contacts must be empty" })
}
}
}

package object model {
case class Address(street: String,
city: String,
state: String,
Expand All @@ -80,7 +16,9 @@ object TestData {

implicit object AddressValidator extends Validatable[Address] {
override def validate(value: Address): Validation = {
(value.zipCode.matches("\\d{5}") orElse "Zip code must be 5 digits") ++
(value.street.nonEmpty orElse "Street addr. is required") ++
(value.city.nonEmpty orElse "City is required") ++
(value.zipCode.matches("\\d{5}") orElse "Zip code must be 5 digits") ++
(value.state.matches("[A-Z]{2}") orElse "State abbr must be 2 letters")
}
}
Expand All @@ -103,4 +41,23 @@ object TestData {
}
}
}

case class Contacts(facebook: Option[List[String]] = None, twitter: Option[List[String]] = None)
case class ContactSettings(hasFacebookContacts: Option[Boolean] = Some(true),
hasTwitterContacts: Option[Boolean] = Some(true))

implicit object ContactInfoValidator extends ValidatableWith[Contacts, ContactSettings] {
override def validateWith(value: Contacts, contactSettings: ContactSettings): Validation = {
contactSettings.hasFacebookContacts.maybeValidate {
contacts =>
(contacts andThen { value.facebook errorIfEmpty "Facebook contacts are required" }) ++
(contacts orElse { value.facebook errorIfDefined "Facebook contacts must be empty"})
} ++
contactSettings.hasTwitterContacts.maybeValidate {
contacts =>
(contacts andThen { value.twitter errorIfEmpty "Twitter contacts are required" }) ++
(contacts orElse { value.twitter errorIfDefined "Twitter contacts must be empty" })
}
}
}
}
49 changes: 49 additions & 0 deletions src/test/scala/TestFixtures.scala
@@ -0,0 +1,49 @@
import com.github.javafaker.Faker
import com.github.vickumar1981.svalidate.{Validatable, ValidatableWith}
import com.github.vickumar1981.svalidate.example.model.{Address, ContactSettings, Contacts, Person}

import scala.util.Random

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

case class BlankObject()

implicit object BlankObjectValidator extends Validatable[BlankObject]
implicit object BlankObjectValidatorWith extends ValidatableWith[BlankObject, String]

def validAddress: Address = Address(
faker.address.streetAddress,
faker.address.city,
faker.address.stateAbbr,
faker.address.zipCode.take(zipCodeLength))

def validPerson: Person = Person(
faker.name.firstName,
faker.name.lastName,
true,
Some(validAddress),
Some(Random.alphanumeric.filter(_.isDigit).take(phoneNumberLength).mkString))

def mkContactList: List[String] = (1 to Random.nextInt(maxContacts))
.toList.map { _ => faker.internet().emailAddress() }

def validContactInfo: Contacts = Contacts(
Some(mkContactList),
Some(mkContactList))

def invalidContactAndSettings: (Contacts, ContactSettings) = {
val generateTrueFalse = () => Random.nextInt(maxContacts) % 2
val hasContacts = (x: Int) => if (x == 1) Some(true) else Some(false)
val hasFacebook = hasContacts(generateTrueFalse())
val hasTwitter = hasContacts(generateTrueFalse())
val contacts = Contacts(
if (hasFacebook.getOrElse(true)) None else Some(mkContactList),
if (hasTwitter.getOrElse(true)) None else Some(mkContactList)
)
(contacts, ContactSettings(hasFacebook, hasTwitter))
}
}
30 changes: 27 additions & 3 deletions src/test/scala/TestValidationDsl.scala
@@ -1,8 +1,10 @@

import com.github.vickumar1981.svalidate.{Validation, ValidationDsl, ValidationFailure, ValidationSuccess}
import org.scalatest.{FlatSpec, Matchers}

class TestValidationDsl extends FlatSpec with Matchers {
import fixtures.TestData._
import TestFixtures._
import com.github.vickumar1981.svalidate.example.model._
import com.github.vickumar1981.svalidate.ValidationSyntax._

private def checkFailure(v: Validation) = {
Expand Down Expand Up @@ -44,7 +46,7 @@ class TestValidationDsl extends FlatSpec with Matchers {
checkSuccess(result)
}

"thenThrow" should "return a single validation result error" in {
"orElse" should "return a single validation result error" in {
val firstNameRequired = validPerson.copy(firstName = "").validate()
val lastNameRequired = validPerson.copy(lastName = "").validate()
firstNameRequired should be(Validation.fail("First name is required"))
Expand All @@ -53,7 +55,7 @@ class TestValidationDsl extends FlatSpec with Matchers {
checkFailure(lastNameRequired)
}

"thenCheck" should "perform validation conditionally" in {
"andThen" should "perform validation conditionally" in {
val emptyPersonInfo = validPerson.copy(address = None, phone = None).validate()
emptyPersonInfo should be(
Validation.fail("Address is required", "Phone # is required"))
Expand All @@ -78,6 +80,28 @@ class TestValidationDsl extends FlatSpec with Matchers {
checkFailure(invalidTwitterContacts)
}

"maybeValidate" should "return complementary values for when option is present and when option is empty" in {
val invalidFacebookContacts = validContactInfo.copy(facebook = None).validateWith(ContactSettings())
invalidFacebookContacts should be(Validation.fail("Facebook contacts are required"))
checkFailure(invalidFacebookContacts)

val invalidFacebookContacts2 =
validContactInfo.copy(facebook = Some(mkContactList))
.validateWith(ContactSettings(hasFacebookContacts = Some(false)))
invalidFacebookContacts2 should be(Validation.fail("Facebook contacts must be empty"))
checkFailure(invalidFacebookContacts2)

val invalidTwitterContacts = validContactInfo.copy(twitter = None).validateWith(ContactSettings())
invalidTwitterContacts should be(Validation.fail("Twitter contacts are required"))
checkFailure(invalidTwitterContacts)

val invalidTwitterContacts2 =
validContactInfo.copy(twitter = Some(mkContactList))
.validateWith(ContactSettings(hasTwitterContacts = Some(false)))
invalidTwitterContacts2 should be(Validation.fail("Twitter contacts must be empty"))
checkFailure(invalidTwitterContacts2)
}

"maybeValidateWith" should "return a success for an empty option" in {
val result = validationDsl.testMaybeValidateWith(None, ContactSettings())
checkSuccess(result)
Expand Down

0 comments on commit 8bc94cf

Please sign in to comment.