I have a couple questions around validation+normal...
# arrow
l
I have a couple questions around validation+normalization patterns using Arrow. In our project we have a server that validates request objects submitted to an API using a homegrown validation library. This homegrown validation library has a Kotlin DSL that allows us to define validation rules alongside normalization rules. Something like this:
Copy code
// 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?
r
Hi @leonhardt, the DSL looks good and intuitive. There is a incubating module in the Arrow org that attempts to encapsulate a pattern similar to yours and how you validate types. https://github.com/arrow-kt/arrow-exact It's not a full fledge validation library at the moment but it's in essence a similar use case as the one you have there. A type may need to be constructed but only when its underlying values satisfy certain constrains. Some of these checks can actually be done at compile time with a compiler plugin and a way to symbolically execute the constrains https://arrow-kt.io/ecosystem/analysis/types/ None of these are full solutions to your problem but I think Arrow Exact can benefit from seeing real world examples. When you say
normalization
, 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:
Copy code
@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:
Copy code
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:
Copy code
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")
l
Hey @raulraja thank you so much. I appreciate the time to share some suggestions! We'll take a look at Arrow Exact and Tribune and see how they work for us. I'll make sure to share back.
When you say
normalization
, do you mean the
trim
and
lowercase
calls to change the value once it has been validated?
Forgot 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.
@raulraja a quick update on our experience since the last message. It turns out the thing we were looking for was simply Arrow Optics. The copy builder it provides is all we need for what we've been referring to as "normalization" in our project. It also coincidentally has a nearly identical syntax to what we've been using.
👏 2
We ended up building a very light validation DSL that fits the patterns in codebase but uses Arrow for the heavy lifting. The "normalization" part is now just a passthrough to Arrow Optics copy builder. And the "validation" is just basic use of Arrow's raise DSL. Our latest iteration looks like this and we're thrilled with it so far:
Copy code
@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-L96
Would love your feedback / suggestions if you have any. Otherwise just want to pass along our thanks for taking the time to give us feedback yesterday and maintaining Arrow. 🍻
r
Thank you!, looks great, we have @simon.vergauwen and @Alejandro Serrano Mena to thank for those great DSL in optics 🙌
🙌 2
l
@simon.vergauwen & @Alejandro Serrano Mena everyone on my team is a recent big fan of the Optics DSL. Almost embarrassed we didn't take the time to understand it earlier. It's deceptively simple. Thank you for making something that makes our work so much better! 🍻
🙌 1