leonhardt
08/03/2023, 6:37 PM// an example of our homegrown validation library
object RequestValidator : Validator<Request>({
Request::emailAddress transform { trim() }
Request::emailAddress transform { lowercase() }
Request::emailAddress must {
satisfy { isNotBlank() } otherwise "Email address is required."
satisfy { isEmailAddress() } otherwise "Email address is invalid."
}
})
If validations fail it returns an Either.Left of errors corresponding to their field. If they succeed it returns the transformed/normalized object. This pattern has been working great for us because for us validation and normalization of requests always go together and this approach is simple for us.
However the homegrown validation library is quite complicated. It's the only part of our codebase that uses reflection and when we develop a new need from the validation library it's a bit of a chore to remember how everything works and get working in that module. We'd much prefer to gut it and use an open source validation library that's more feature-full and supported by the community. However when we've looked at the options we've only found libraries that manage the validation concerns and haven't found anything that manages the normalization concerns, either together or as a standalone library.
Today I happened to reread the fantastically simple validations page in Arrow's docs. I wish we could swap out our approach for this, but we'd need to figure out how we handle the normalization concerns too. So I thought I'd ask here:
• Is there something in the Arrow library that I'm missing that can handle our normalization/transformation concerns of request objects?
• Is there another approach to validation+normalization of requests in general that anyone has seen work well that we should consider?raulraja
08/03/2023, 10:41 PMnormalization
, do you mean the trim
and lowercase
calls to change the value once it has been validated?
If your type was model with Arrow Exact that may look like:
@JvmInline
value class Request private constructor(val email: String) {
companion object : Exact<String, Request> by Exact({
ensure(it.isNotBlank())
ensure(it.isEmailAddress())
Request(it.trim().lowercase())
})
}
which enables syntax:
Request.from(email) //Either<Error, Request>
Request.fromOrNull(email) // Request?
Request.fromOrThrow(email) // Request or throws ExactException
There is also this great library https://github.com/sksamuel/tribune which has a similar scope in terms of validating types and also has a richer DSL for transformations:
val isbnParser =
Parser.fromNullableString()
.notNullOrBlank { "ISBN must be provided" }
.map { it.replace("-", "") } // remove dashes
.length({ it == 10 || it == 13 }) { "Valid ISBNs have length 10 or 13" }
.filter({ it.length == 10 || it.startsWith("9") }, { "13 Digit ISBNs must start with 9" })
.map { Isbn(it) }
isbnParser.parse("9783161484100")
leonhardt
08/03/2023, 10:56 PMleonhardt
08/05/2023, 7:39 PMWhen you sayForgot to answer your question here. Yes, that's how we've been using that term in our project. In our project we're just trying to clean up input from web users., do you mean thenormalization
andtrim
calls to change the value once it has been validated?lowercase
leonhardt
08/05/2023, 7:40 PMleonhardt
08/05/2023, 7:40 PM@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.") }
}
}
It's definitely tailored to our project's patterns, but in case it's a useful example for you or others I thought I'd throw it in a gist.
https://gist.github.com/lnhrdt/9971054160c7045520b8cf453210d9b8#file-request_validator_example-kt-L83-L96leonhardt
08/05/2023, 7:42 PMraulraja
08/05/2023, 7:46 PMleonhardt
08/05/2023, 7:51 PM