MultiErrorKResult
Typealias to represent KResult types that have a List type on the Failure side
Validation
MultiErrorKResult instances have validate and errors extension functions. They can be used for very easy, type-safe and side-effect-free object validation:
import io.kresult.core.KResult
import io.kresult.core.MultiErrorKResult
import io.kresult.core.errors
import io.kresult.core.validate
import io.kotest.matchers.shouldBe
data class User(val name: String, val age: Int, val active: Boolean)
enum class ValidationError(val message: String) {
InvalidName("Name must not be empty"),
IllegalAge("User must be over 18 years old"),
Inactive("User must active"),
}
fun test() {
val user: MultiErrorKResult<ValidationError, User> =
KResult.Success(User(name = "Max", age = 17, active = false))
val result: MultiErrorKResult<ValidationError, User> = user
.validate(ValidationError.InvalidName) { u ->
u.name.isNotBlank()
}
.validate(ValidationError.IllegalAge) { u ->
u.age >= 18
}
.validate(ValidationError.Inactive) { u ->
u.active
}
result.isFailure() shouldBe true
result.errors().size shouldBe 2
result.errors()[0] shouldBe ValidationError.IllegalAge
result.errors()[1] shouldBe ValidationError.Inactive
}
Be careful with flatMap-ing validation results
If you need to flatMap a validation result (which is either a Success or a FailureWithValue), make sure that there are no successive validate calls. In the failure case of a flatMap operation, we lose the type information of FailureWithValue.value and therefore need to return a plain Failure object. This, will cause all successive validate calls to be ignored without being evaluated, as we have nothing to apply the predicate function to. While such scenarios compile, having validations that are never applied should be, at least semantically, considered bugs.
import io.kresult.core.KResult
import io.kresult.core.MultiErrorKResult
import io.kresult.core.flatMap
import io.kresult.core.validate
import io.kotest.matchers.shouldBe
fun test() {
val num: MultiErrorKResult<String, Int> = KResult.Success(7)
val result = num
.validate("Must be greater zero") {
it 0
}
.flatMap {
// while this compiles...
if (it % 3 == 0) {
KResult.Success(it)
} else {
KResult.Failure(listOf("Must be modulus of 3"))
}
}
.validate("Must be even") {
// this will never be called, if the flatMap before evaluates to Failure
it % 2 == 0
}
// we would expect a second error: "Must be even" here, but the last validation was never called
result shouldBe KResult.Failure(listOf("Must be modulus of 3"))
}
Since
0.2.0
See also
if success side carries a list