Akkurate 0.7.0 Help

Use external sources

Data validation can sometimes rely on external sources, a database, an HTTP API, even a file. Akkurate comes with a mechanic of contextual validation to allow using those external sources during validation.

Contextual validation

Remember the code example about Twitter for conditional constraints? We were using the following global variable to query our database:

val userDao = object : UserDao { override fun existsByUsername(username: String): Boolean = TODO() }

However, instead of using a global variable, you're probably storing your DAO instance inside a container and injecting it wherever you need.

Akkurate allows you to pass a context to your validator in addition to the validated object. This is done by using Validator<ContextType, ValueType> to instantiate the validator.

Let's reuse our Twitter example and adapt it with this new feature:

// We remove the global variable and only keep the interface. interface UserDao { fun existsByUsername(username: String): Boolean } @Validate data class UserUpdate(val username: String) val validateUser = Validator<UserDao, UserUpdate> { userDao -> // The context is passed as a parameter ^^^^^^^ val (isValidUsername) = username.hasLengthGreaterThanOrEqualTo(5) if (isValidUsername) { username.constrain { // Use the context wherever you want. !userDao.existsByUsername(it) } otherwise { "This username is already taken" } } }

The context type is passed first, then the value type; while the context value is available as the first parameter of the lambda.

Now, when calling our validator, we have to provide an instance of UserDao before the value to validate:

val someUserDao: UserDao = TODO() val someUserUpdate: UserUpdate = TODO() validateUser(someUserDao, someUserUpdate)

Keep the same context across multiple validations

You don't need to provide the context for each validation. Especially when you want to provide the context higher in the code hierarchy and let some more specific code provide the object to validate.

You can solve this by currying your validator:

val validateUserWithSomeDao = validateUser(someUserDao) val userUpdate1: UserUpdate = TODO() val userUpdate2: UserUpdate = TODO() validateUserWithSomeDao(userUpdate1) validateUserWithSomeDao(userUpdate2)

Use multiple contextual objects

When you need to pass multiple objects as a context, create a data class to wrap them:

interface TweetDao data class Context(val userDao: UserDao, val tweetDao: TweetDao) Validator<Context, Any> { context -> context.userDao // UserDao context.tweetDao // TweetDao }

Use destructuring to write more concise code:

Validator<Context, Any> { (userDao, tweetDao) -> userDao // UserDao tweetDao // TweetDao }

Suspendable validation

If your external source is asynchronous, like an HTTP API, you have to make your validator suspendable by calling Validator.suspendable.

Let's reuse the contextual validation example based on Twitter and replace the UserDao interface by UserApi:

interface UserApi { suspend fun existsByUsername(username: String): Boolean }

To be able to call the suspendable method existsByUsername, we have to make our validator suspendable:

val validateUser = Validator.suspendable<UserApi, UserUpdate> { api -> username.constrain { !api.existsByUsername(it) } otherwise { "This username is already taken" } }

Note that suspendable validators can only be called from suspendable functions:

suspend fun main() { validateUser(someUserApi, someUserUpdate) }
Last modified: 27 February 2024