Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Form validation kata #2

Merged
merged 3 commits into from
Mar 31, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ Scala training repository used to learn Scala and Functional Programming by solv

### List of katas:

| # | Kata Statement | Pull Request |
|---|----------------|---------------|
| 1 | [Maxibons](https://github.com/Karumi/MaxibonKataJava#-kata-maxibon-for-java-) | [https://github.com/Karumi/ScalaKatas/pull/1](https://github.com/Karumi/ScalaKatas/pull/1) |
| # | Kata Statement | PR | Topic |
|---|----------------|----|-------|
| 1 | [Maxibons](https://github.com/Karumi/MaxibonKataJava#-kata-maxibon-for-java-) | [https://github.com/Karumi/ScalaKatas/pull/1](https://github.com/Karumi/ScalaKatas/pull/1) | Polymorphic programming |
| 2 | [Form validation](https://gist.github.com/pedrovgs/d83fe1f096928715a6f31946e557995a) | [https://github.com/Karumi/ScalaKatas/pull/2](https://github.com/Karumi/ScalaKatas/pull/2) | Validated data type|

### Executing tests:

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ scalacOptions += "-Ypartial-unification"

libraryDependencies += "org.typelevel" %% "cats-core" % "1.5.0"
libraryDependencies += "org.typelevel" %% "cats-effect" % "1.1.0"
libraryDependencies += "eu.timepit" %% "refined" % "0.9.3"
libraryDependencies += "eu.timepit" %% "refined" % "0.9.4"
libraryDependencies += "com.github.julien-truffaut" %% "monocle-core" % "1.5.0"
libraryDependencies += "com.github.julien-truffaut" %% "monocle-macro" % "1.5.0"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.github.pedrovgs.scalakatas.formvalidation

import java.time.LocalDateTime

import cats.data.{NonEmptyList, Validated, ValidatedNel}
import cats.syntax.apply._
import eu.timepit.refined.{W, _}
import eu.timepit.refined.api.{Refined, Validate}
import eu.timepit.refined.collection.NonEmpty
import eu.timepit.refined.string.MatchesRegex

object FormValidator {

type FirstName = Refined[String, NonEmpty]
type LastName = Refined[String, NonEmpty]
type ValidDocumentId = MatchesRegex[W.`"""\\d{8}[a-zA-Z]{1}"""`.T]
type DocumentId = Refined[String, ValidDocumentId]
type ValidPhone = MatchesRegex[W.`"""\\d{9}"""`.T]
type Phone = Refined[String, ValidPhone]
type Email = Refined[String, ValidEmail]

case class ValidEmail()
implicit val emailValidate: Validate.Plain[String, ValidEmail] =
Validate.fromPredicate(e => e.contains("@"), p => s"$p is not a valid email", ValidEmail())

final case class UnsafeForm(
firstName: String,
lastName: String,
birthday: LocalDateTime,
documentId: String,
phone: String,
email: String
)
final case class Form(
firstName: FirstName,
lastName: LastName,
birthday: LocalDateTime,
documentId: DocumentId,
phone: Phone,
email: Email
)
type FormValidationResult[A] = ValidatedNel[FormError, A]

def apply(referenceDate: LocalDateTime, form: UnsafeForm): FormValidationResult[Form] =
(validateFirstName(form.firstName),
validateLastName(form.lastName),
validateBirthday(referenceDate, form.birthday),
validateDocumentId(form.documentId),
validatePhone(form.phone),
validateEmail(form.email)).mapN(Form)

private def validateFirstName(firstName: String): FormValidationResult[FirstName] =
Validated
.fromEither(refineV[NonEmpty](firstName))
.leftMap(_ => NonEmptyList.of(EmptyFirstName(firstName)))

private def validateLastName(lastName: String): FormValidationResult[LastName] =
Validated
.fromEither(refineV[NonEmpty](lastName))
.leftMap(_ => NonEmptyList.of(EmptyLastName(lastName)))

private def validateBirthday(refDate: LocalDateTime, birthday: LocalDateTime): FormValidationResult[LocalDateTime] =
if (birthday.compareTo(refDate.minusYears(18)) <= 0) {
Validated.valid(birthday)
} else {
Validated.invalidNel(UserTooYoung(birthday))
}
private def validateDocumentId(documentId: String): FormValidationResult[DocumentId] =
Validated
.fromEither(refineV[ValidDocumentId](documentId))
.leftMap(_ => NonEmptyList.of(InvalidDocumentId(documentId)))

private def validatePhone(phone: String): FormValidationResult[Phone] =
Validated
.fromEither(refineV[ValidPhone](phone))
.leftMap(_ => NonEmptyList.of(InvalidPhone(phone)))

private def validateEmail(email: String): FormValidationResult[Email] =
Validated
.fromEither(refineV[ValidEmail](email))
.leftMap(_ => NonEmptyList.of(InvalidEmail(email)))

}

sealed trait FormError
case class EmptyFirstName(firstName: String) extends FormError
final case class EmptyLastName(lastName: String) extends FormError
final case class UserTooYoung(birthday: LocalDateTime) extends FormError
final case class InvalidDocumentId(documentId: String) extends FormError
final case class InvalidPhone(phone: String) extends FormError
final case class InvalidEmail(email: String) extends FormError
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.github.pedrovgs.formvalidation
import java.time.LocalDateTime

import com.github.pedrovgs.scalakatas.formvalidation.FormValidator.{FirstName, LastName, UnsafeForm}
import eu.timepit.refined.scalacheck.string._
import org.scalacheck.{Arbitrary, Gen}

trait ArbitraryForms {

implicit val arbitraryForm: Arbitrary[UnsafeForm] = Arbitrary {
for {
firstName <- Arbitrary.arbitrary[FirstName]
lastName <- Arbitrary.arbitrary[LastName]
birthday <- Gen.const(LocalDateTime.MIN)
documentId <- Gen.const("44632508A")
phone <- Gen.const("999673292")
email <- Gen.const("p@k.com")
} yield UnsafeForm(firstName.value, lastName.value, birthday, documentId, phone, email)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.github.pedrovgs.formvalidation
import java.time.LocalDateTime

import cats.data.{NonEmptyList, Validated}
import com.github.pedrovgs.scalakatas.formvalidation.FormValidator.UnsafeForm
import com.github.pedrovgs.scalakatas.formvalidation._
import org.scalatest.prop.PropertyChecks
import org.scalatest.{FlatSpec, Matchers}

class FormValidatorSpec extends FlatSpec with Matchers with PropertyChecks with ArbitraryForms {

it should "indicate all the invalid values in an unsafe form with every field as invalid" in {
val form = UnsafeForm(
firstName = "",
lastName = "",
birthday = LocalDateTime.now(),
documentId = "48632500",
phone = "6799",
email = "pedro"
)

val result = FormValidator(LocalDateTime.now(), form)

result shouldBe Validated.invalid(
NonEmptyList.of(
EmptyFirstName(form.firstName),
EmptyLastName(form.lastName),
UserTooYoung(form.birthday),
InvalidDocumentId(form.documentId),
InvalidPhone(form.phone),
InvalidEmail(form.email)
))
}

it should "indicate just one invalid value in an unsafe form where just one field is invalid" in {
val form = UnsafeForm(
firstName = "Pedro",
lastName = "G贸mez",
birthday = LocalDateTime.MIN,
documentId = "38632509C",
phone = "677673297",
email = "pedro"
)

val result = FormValidator(LocalDateTime.now(), form)

result shouldBe Validated.invalid(
NonEmptyList.of(
InvalidEmail(form.email)
))
}

it should "consider as valid form with evey field as valid" in {
forAll { form: UnsafeForm =>
FormValidator(LocalDateTime.now(), form).isValid shouldBe true
}
}
}