Skip to content

Instantly share code, notes, and snippets.

@raulraja
Created March 5, 2020 17:51
Show Gist options
  • Save raulraja/b7844cea081dab96aca95ee22201ca7c to your computer and use it in GitHub Desktop.
Save raulraja/b7844cea081dab96aca95ee22201ca7c to your computer and use it in GitHub Desktop.
Validation rules abstracting strategies with operations as extension functions
package arrow.rules.test
import arrow.Kind
import arrow.core.Either
import arrow.core.EitherPartialOf
import arrow.core.Nel
import arrow.core.NonEmptyList
import arrow.core.Validated
import arrow.core.ValidatedPartialOf
import arrow.core.extensions.either.applicativeError.applicativeError
import arrow.core.extensions.nonemptylist.semigroup.semigroup
import arrow.core.extensions.validated.applicativeError.applicativeError
import arrow.core.nel
import arrow.typeclasses.ApplicativeError
/**
* A generic rules class that abstracts over validation strategies
*/
sealed class Rules<F, E>(A: ApplicativeError<F, Nel<E>>) : ApplicativeError<F, Nel<E>> by A {
/**
* Accumulates errors thanks to validated and non empty list
*/
class ErrorAccumulationStrategy<E> :
Rules<ValidatedPartialOf<Nel<E>>, E>(Validated.applicativeError(NonEmptyList.semigroup()))
/**
* Fails fast thanks to Either
*/
class FailFastStrategy<E> :
Rules<EitherPartialOf<Nel<E>>, E>(Either.applicativeError())
/**
* DSL
*/
companion object {
fun <E> failFast(): FailFastStrategy<E> = FailFastStrategy()
fun <E> accumulateErrors(): ErrorAccumulationStrategy<E> = ErrorAccumulationStrategy()
}
}
/**
* User defined model errors
*/
sealed class ValidationError(val msg: String) {
data class DoesNotContain(val value: String) : ValidationError("Did not contain $value")
data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value")
data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email")
}
/**
* Arbitrary rules can be defined anywhere outside the Rules algebra
*/
fun <F> Rules<F, ValidationError>.contains(value: String, needle: String): Kind<F, String> =
if (value.contains(needle, false)) just(value)
else raiseError(ValidationError.DoesNotContain(needle).nel())
/**
* Arbitrary rules can be defined anywhere outside the Rules algebra
*/
fun <F> Rules<F, ValidationError>.maxLength(value: String, maxLength: Int): Kind<F, String> =
if (value.length <= maxLength) just(value)
else raiseError(ValidationError.MaxLength(maxLength).nel())
data class Email(val value: String)
/**
* Some rules that use the applicative syntax to validate and gather errors
*/
fun <F> Rules<F, ValidationError>.validateEmail(value: String): Kind<F, Email> =
mapN(contains(value, "@"), maxLength(value, 250)) {
Email(value)
}.handleErrorWith { raiseError(ValidationError.NotAnEmail(it).nel()) }
/**
* Proof the same code works polymorphically
*/
fun main() {
val accResult =
Rules.accumulateErrors<ValidationError>().run {
tupledN(
validateEmail("nowhere.com"),
validateEmail("nowheretoolong${(0..251).map { "g" }}")
)
}
val failFastResult = Rules.failFast<ValidationError>().run {
tupledN(
validateEmail("nowhere.com"),
validateEmail("nowheretoolong${(0..251).map { "g" }}")
)
}
println(accResult)
//Invalid(e=NonEmptyList(all=[NotAnEmail(reasons=NonEmptyList(all=[DoesNotContain(value=@)])), NotAnEmail(reasons=NonEmptyList(all=[DoesNotContain(value=@), MaxLength(value=250)]))]))
println(failFastResult)
//Left(a=NonEmptyList(all=[NotAnEmail(reasons=NonEmptyList(all=[DoesNotContain(value=@)]))]))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment