Skip to content

Instantly share code, notes, and snippets.

@lnhrdt
Last active August 5, 2023 19:53
Show Gist options
  • Save lnhrdt/9971054160c7045520b8cf453210d9b8 to your computer and use it in GitHub Desktop.
Save lnhrdt/9971054160c7045520b8cf453210d9b8 to your computer and use it in GitHub Desktop.
An example of a request validator pattern using Arrow
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