-
-
Save lnhrdt/9971054160c7045520b8cf453210d9b8 to your computer and use it in GitHub Desktop.
An example of a request validator pattern using Arrow
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import arrow.core.Either | |
import arrow.core.NonEmptyList | |
import arrow.core.raise.either | |
import arrow.core.toNonEmptyListOrNull | |
import arrow.optics.Copy | |
import arrow.optics.copy | |
import arrow.optics.optics | |
import javax.mail.internet.InternetAddress | |
typealias RequestError = Pair<String, String> | |
typealias RequestErrors = NonEmptyList<RequestError> | |
class RequestValidator<T> { | |
private var normalization: Copy<T>.() -> Unit = {} | |
private var validation: Validation<T> = Validation() | |
operator fun invoke(request: T): Either<RequestErrors, T> { | |
val normalizedRequest = request.copy(normalization) | |
return validation(normalizedRequest) | |
} | |
fun normalize(normalization: Copy<T>.() -> Unit) { | |
this.normalization = normalization | |
} | |
fun validate(init: Validation<T>.() -> Unit) { | |
validation.init() | |
} | |
class Validation<T> { | |
private val steps = mutableListOf<Step<T>>() | |
private data class Step<T>( | |
val condition: T.() -> Boolean, | |
val requestError: T.() -> RequestError, | |
) | |
class StepBuilder<T>(val condition: T.() -> Boolean) | |
fun ensure(condition: T.() -> Boolean) = StepBuilder(condition) | |
infix fun StepBuilder<T>.orError(requestError: T.() -> RequestError) { | |
steps.add(Step(condition = condition, requestError = requestError)) | |
} | |
operator fun invoke(request: T): Either<RequestErrors, T> { | |
val requestErrors: RequestErrors? = steps | |
.filter { step -> !step.condition(request) } | |
.map { validation -> validation.requestError(request) } | |
.distinctBy { it.first } | |
.toNonEmptyListOrNull() | |
return either { | |
when (requestErrors) { | |
null -> request | |
else -> raise(requestErrors) | |
} | |
} | |
} | |
} | |
companion object { | |
fun <T> requestValidator(init: RequestValidator<T>.() -> Unit): RequestValidator<T> { | |
val requestValidator = RequestValidator<T>() | |
requestValidator.init() | |
return requestValidator | |
} | |
} | |
} | |
fun String.isEmailAddress(): Boolean = Either.catch { InternetAddress(this).validate() }.fold({ false }, { true }) | |
@optics | |
data class ExampleRequest( | |
val firstName: String, | |
val lastName: String, | |
val emailAddress: String, | |
) { | |
companion object | |
} | |
val exampleRequestValidator: RequestValidator<ExampleRequest> = requestValidator { | |
normalize { | |
ExampleRequest.firstName transform { it.trim() } | |
ExampleRequest.lastName transform { it.trim() } | |
ExampleRequest.emailAddress transform { it.trim() } | |
ExampleRequest.emailAddress transform { it.lowercase() } | |
} | |
validate { | |
ensure { firstName.isNotBlank() } orError { RequestError("firstName", "First name is required.") } | |
ensure { lastName.isNotBlank() } orError { RequestError("lastName", "Last name is required.") } | |
ensure { emailAddress.isNotBlank() } orError { RequestError("emailAddress", "Email address is required.") } | |
ensure { emailAddress.isEmailAddress() } orError { RequestError("emailAddress", "Email address is invalid.") } | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment