Domain Model Validation In Kotlin: Part 4

In the previous article of this series, we tackled the most complex types of validations by exploring different ways we can use context in our rules. We have come a long way, but we only used our code isolated in unit tests. We would like to see how our code would fit in an application.
One of the simplest ways of doing this is to create a CLI (command-line interface) that interacts with our code. The CLI will ask for the user to input the values of the properties needed to address an email: from, to, cc and bcc. Then it prints the execution result with a mock message instead of sending an email.

Of the variants explored in the previous article, we will go forward with examples from the context receivers solution. Still, you can find branches with the other options: params, partially applied functions, and generic receiver.

Here is a quick recap of the model class that we want to validate and instantiate from CLI:

Dealing with exceptions

Writing a JVM application that doesn’t have to deal with exceptions is a chimaera, but the next best thing is isolating the code that deals with exceptional behaviour.

To achieve this, we will introduce a new data type: Either. The type signature is very similar to Validated:

Ignoring the functions that we can apply over them, it seems to be just a renaming: Validated to Either, Invalid to Left, Valid to Right. When choosing between the two data types, the rule of thumb is that we’re working with Either when we fail fast and go with Validated when we need to accumulate validation errors. But consider the naming, too: if it’s a validation Validated might declare the intent better, even when we have a single rule!
In the following code samples, we will return Either when there is a potential exception and Validated when we apply validation rules.

We will start with an example of reading the property file that contains the values for two of our validation rules: allowedSenders and receiveEmailConsents. A file like this one:

We are now ready to write the code for reading this file, but instead of letting potential exceptions through, we will return an Either that explicitly states that our function returns either an error or a pair of values. The return type is a mouthful, but we’ll see that it’s easier to deal with it than to type it — that’s one of the reasons why, a lot of times, we let the compiler infer it.

  • (1) we define this function as an extension function over any non-nullable type to have access to this (in the context of this::class) and get a classpath resource
  • (5) use is an extension function over AutoClosable that executes a block of code and then closes the resource
  • (6) apply is a scope function that invokes a function with the invoking instance as the receiver, equivalent to val properties = Properties(); properties.load(inputStream)
  • (8, 11) asList is an extension function we created for convenience to read a comma-separated string as a list of strings (we have to love extension functions):
  • (13) we return the Pair of valid properties from the use block
  • (14) if everything goes well (no exception thrown), we map the pair to Right
  • (16) if there’s an exception, we return a Left;we also need to extend a bit our error hierarchy:

The is even a convenience function to convert exception throwing code to Either without having to explicitly map it: Either.catch. We use this to further refactor our readProperties function:

We will use this technique of wrapping potential exception throwing code in Either.catch and converting it to Either data type to keep the rest of our code base exception free.

We learned a lot from just a few lines of code, and we’re just getting started.

Composing Validated and Either

Let’s recall from the previous article that we need to implement these two interfaces required to validate our model:

Arrow provides us with an elegant way of dealing with both Either and Validated data types: either computation blocks. Inside either blocks, we have thebind extension function on Either and Validated, and bind short-circuits the either block if things go wrong (bind is applied over aLeft or an Invalid). Once we see it in action, in just a moment, this will become obvious.

We will read and validate both string lists from the properties file.

  • (1) our function is suspended to be able to use either blocks; there is the alternative of using either.eager if we’re not in a suspended context
  • (3) we start an either block, this enables us to use the bind function on Either or Validated
  • (4 —5) readProperties has an ApplicationError on the left, but to be able to compose easily with Validated<ApplicationErrors, T> (accumulated errors) we will map the single error to a list of errors using mapLeft
  • (5) bind short-circuits our either computation block; if readProperties returns:
    Right then the pair of expected values is destructured into allowedSenderProperties and receiveEmailConsentProperties
    Leftthen the computation ends (hence the short circuit), and the block evaluates to Left<ApplicationErrors>
    – the same applies to Validated, mapping Valid to Right and Invalid to Left
  • (7— 12) we use our validate function to get valid allowedSenders and consents (9) then map it to a Pair, then destructure into validatedAllowedSender and validatedConsents variables
  • (12) we use bind again, but this time on a Validated
  • (14–15) we can provide simple implementations to AllowedSenders and ReceiveEmailConsents, backed by the lists we just read from our property file

We can already identify two reusable patterns from this implementation: composing single errors with multiple errors (5) — this will be easier if we consider a single error as a list of errors with a single element — and in-place parallel validations (7–12). Let’s create some functions to help us reuse this: leftNel, and validated returning tuples.

Using these functions we can simplify our code:

  • (5) we replaced mapLeft { it.nel() } with leftNel()
  • (7–10) our new validate overload maps our valid values to a pair, and we can destructure it; this is what we meant by in-place parallel validation

We are now able to use our appConfig function to read the externalized configuration. We also have our domain ready from the previous articles.
Let’s create a CLI that will allow us to read our input and test our validations,
using the same techniques of isolating exceptions with try-catch-to-either (12–17) and either computation blocks (3):

  • (2) the return type of readInput looks intimidating, but we can see in our main function below that we can deal with it nicely
  • (4) we wrap all the input strings into a Tuple4: we take some shortcuts in our proof of concept; a new data class would make the code more readable
  • (12) the potential exception-throwing code is isolated in the readStringfunction

Putting everything together

Our main wraps everything together: reads the configuration, sets the context for validations, reads the input and maps it to an EmailRoute. We are already familiar with all these techniques from this and the previous articles.

  • (1) we are not able to use type inference for the generic types of either because main returns Unit, thus we specify them explicitly
  • (4–5) set the validation context — we use the context receivers solution from the previous article
  • (6) destructure Tuple4 into the corresponding variables
  • (7) create a validated EmailRoute and then bind the validated value to theemailRoute variable
  • (9) we confirm that emailRoute is valid by printing it
  • (12) our either block ends here, and we apply the getOrHandle function to provide a way of dealing with errors: we print them

In the GitHub source code, I added a loop that executes the program repeatedly to make experimenting easier. Play around to test different failing validation rules, invalid property files, or invalid inputs.
Here is a potential output of our application:

We finally reached the end of our journey:

  • In the first part, we used inline classes to validate our simple data types in a type-safe way
  • We continued in the second part by validating multiple properties and lists of values and accumulating the errors
  • In the third part, we explored multiple ways of dealing with a validation context without using by relying solely on the compiler (without dependency injection frameworks)
  • And in this final part, we integrated our model into a demo CLI application that deals with exceptions and validation failures in a unitary and elegant way.

I hope that, at least partially, I managed to reason and explain the techniques applied in the example solution, not only list them as a recipe. Some ideas are opinionated, especially the naming, and I’m open to suggestions and debates. Thus, feel free to comment, and we can start a conversation.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store