value: String) : ValidationError("Did not contain $value") data class MaxLength(val value: Int) : ValidationError("Exceeded length of $value") data class NotAnEmail(val reasons: Nel<ValidationError>) : ValidationError("Not a valid email") } data class FormField(val label: String, val value: String) data class Email(val value: String)
ErrorAccumulation : Strategy() } /** Abstracts away invoke strategy **/ object Rules { private fun FormField.contains(needle: String): ValidatedNel<ValidationError, FormField> = if (value.contains(needle, false)) validNel() else ValidationError.DoesNotContain(needle).invalidNel() private fun FormField.maxLength(maxLength: Int): ValidatedNel<ValidationError, FormField> = if (value.length <= maxLength) validNel() else ValidationError.MaxLength(maxLength).invalidNel() private fun FormField.validateErrorAccumulate(): ValidatedNel<ValidationError, Email> = contains("@").zip( Semigroup.nonEmptyList(), // accumulates errors in a non empty list, can be omited for NonEmptyList maxLength(250) ) { _, _ -> Email(value) }.handleErrorWith { ValidationError.NotAnEmail(it).invalidNel() } /** either blocks support binding over Validated values with no additional cost or need to convert first to Either **/ private fun FormField.validateFailFast(): Either<Nel<ValidationError>, Email> = either.eager { contains("@").bind() // fails fast on first error found maxLength(250).bind() Email(value) } operator fun invoke(strategy: Strategy, fields: List<FormField>): Either<Nel<ValidationError>, List<Email>> = when (strategy) { Strategy.FailFast -> fields.traverseEither { it.validateFailFast() } Strategy.ErrorAccumulation -> fields.traverseValidated(Semigroup.nonEmptyList()) { it.validateErrorAccumulate() }.toEither() } } https://arrow-kt.io/docs/patterns/error_handling/ Boilerplate