Change validation to use ValidationError interface for errors instead of exceptions

This commit is contained in:
Wolf-Martell Montwé 2023-05-31 09:40:48 +02:00
parent 9bcd64f54e
commit 48ac48fda3
No known key found for this signature in database
GPG key ID: 6D45B21512ACBF72
11 changed files with 114 additions and 39 deletions

View file

@ -0,0 +1,3 @@
package app.k9mail.core.common.domain.usecase.validation
interface ValidationError

View file

@ -1,7 +1,7 @@
package app.k9mail.core.common.domain.usecase.validation
interface ValidationResult {
sealed interface ValidationResult {
object Success : ValidationResult
data class Failure(val error: Exception) : ValidationResult
data class Failure(val error: ValidationError) : ValidationResult
}

View file

@ -1,5 +1,7 @@
package app.k9mail.feature.account.setup.domain.input
import app.k9mail.core.common.domain.usecase.validation.ValidationError
/**
* InputField is an interface defining the state of an input field.
*
@ -7,7 +9,7 @@ package app.k9mail.feature.account.setup.domain.input
*/
interface InputField<T> {
val value: T
val errorMessage: String?
val error: ValidationError?
val isValid: Boolean
/**
@ -19,11 +21,11 @@ interface InputField<T> {
fun updateValue(value: T): InputField<T>
/**
* Updates the current error message of the input field.
* Updates the current error of the input field.
*
* @param errorMessage The new error message to be set for the input field.
* @param error The new error to be set for the input field.
*/
fun updateErrorMessage(errorMessage: String?): InputField<T>
fun updateError(error: ValidationError?): InputField<T>
/**
* Updates the current validity of the input field.
@ -38,6 +40,6 @@ interface InputField<T> {
* @return a Boolean indicating whether the input field has an error.
*/
fun hasError(): Boolean {
return errorMessage != null
return error != null
}
}

View file

@ -1,23 +1,26 @@
package app.k9mail.feature.account.setup.domain.input
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
data class StringInputField(
override val value: String = "",
override val errorMessage: String? = null,
override val error: ValidationError? = null,
override val isValid: Boolean = false,
) : InputField<String> {
override fun updateValue(value: String): StringInputField {
return StringInputField(
value = value,
errorMessage = null,
error = null,
isValid = false,
)
}
override fun updateErrorMessage(errorMessage: String?): StringInputField {
override fun updateError(error: ValidationError?): StringInputField {
return StringInputField(
value = value,
errorMessage = errorMessage,
error = error,
isValid = false,
)
}
@ -27,8 +30,22 @@ data class StringInputField(
return StringInputField(
value = value,
errorMessage = null,
error = null,
isValid = isValid,
)
}
}
fun StringInputField.fromValidationResult(result: ValidationResult): StringInputField {
return when (result) {
is ValidationResult.Success -> copy(
error = null,
isValid = true,
)
is ValidationResult.Failure -> copy(
error = result.error,
isValid = false,
)
}
}

View file

@ -1,15 +1,19 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
class ValidateAccountName : ValidationUseCase<String> {
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(EmptyAccountName())
input.isEmpty() -> ValidationResult.Success
input.isBlank() -> ValidationResult.Failure(ValidateAccountNameError.BlankAccountName)
else -> ValidationResult.Success
}
}
class EmptyAccountName : Exception()
sealed interface ValidateAccountNameError : ValidationError {
object BlankAccountName : ValidateAccountNameError
}
}

View file

@ -1,5 +1,6 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
@ -7,10 +8,12 @@ class ValidateDisplayName : ValidationUseCase<String> {
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(EmptyDisplayName())
input.isBlank() -> ValidationResult.Failure(ValidateDisplayNameError.EmptyDisplayName)
else -> ValidationResult.Success
}
}
class EmptyDisplayName : Exception()
sealed interface ValidateDisplayNameError : ValidationError {
object EmptyDisplayName : ValidateDisplayNameError
}
}

View file

@ -1,17 +1,22 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.core.common.domain.usecase.validation.ValidationUseCase
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError.BlankEmailSignature
// TODO check signature for input validity
class ValidateEmailSignature : ValidationUseCase<String> {
override fun execute(input: String): ValidationResult {
return when {
input.isBlank() -> ValidationResult.Failure(EmptyEmailSignature())
input.isEmpty() -> ValidationResult.Success
input.isBlank() -> ValidationResult.Failure(error = BlankEmailSignature)
else -> ValidationResult.Success
}
}
class EmptyEmailSignature : Exception()
sealed interface ValidateEmailSignatureError : ValidationError {
object BlankEmailSignature : ValidateEmailSignatureError
}
}

View file

@ -1,5 +1,7 @@
package app.k9mail.feature.account.setup.domain.input
import app.k9mail.core.common.domain.usecase.validation.ValidationError
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import assertk.all
import assertk.assertThat
import assertk.assertions.isEqualTo
@ -17,20 +19,23 @@ class StringInputFieldTest {
assertThat(stringInputState).all {
prop(StringInputField::value).isEqualTo("")
prop(StringInputField::errorMessage).isNull()
prop(StringInputField::error).isNull()
prop(StringInputField::isValid).isFalse()
}
}
@Test
fun `should reset errorMessage and isValid when value changed`() {
val initialInputState = StringInputField(errorMessage = "error", isValid = false)
val initialInputState = StringInputField(
error = TestValidationError,
isValid = false,
)
val result = initialInputState.updateValue("new value")
assertThat(result).all {
prop(StringInputField::value).isEqualTo("new value")
prop(StringInputField::errorMessage).isNull()
prop(StringInputField::error).isNull()
prop(StringInputField::isValid).isFalse()
}
}
@ -39,24 +44,27 @@ class StringInputFieldTest {
fun `should reset isValid when error set`() {
val initialInputState = StringInputField(value = "input", isValid = true)
val result = initialInputState.updateErrorMessage("error")
val result = initialInputState.updateError(TestValidationError)
assertThat(result).all {
prop(StringInputField::value).isEqualTo("input")
prop(StringInputField::errorMessage).isEqualTo("error")
prop(StringInputField::error).isEqualTo(TestValidationError)
prop(StringInputField::isValid).isFalse()
}
}
@Test
fun `should reset errorMessage when valid`() {
val initialInputState = StringInputField(value = "input", errorMessage = "error")
val initialInputState = StringInputField(
value = "input",
error = TestValidationError,
)
val result = initialInputState.updateValidity(isValid = true)
assertThat(result).all {
prop(StringInputField::value).isEqualTo("input")
prop(StringInputField::errorMessage).isNull()
prop(StringInputField::error).isNull()
prop(StringInputField::isValid).isTrue()
}
}
@ -65,15 +73,49 @@ class StringInputFieldTest {
fun `should not reset errorMessage when invalid`() {
val initialInputState = StringInputField(
value = "input",
errorMessage = "error",
error = TestValidationError,
)
val result = initialInputState.updateValidity(isValid = false)
assertThat(result).all {
prop(StringInputField::value).isEqualTo("input")
prop(StringInputField::errorMessage).isEqualTo("error")
prop(StringInputField::error).isEqualTo(TestValidationError)
prop(StringInputField::isValid).isFalse()
}
}
@Test
fun `should map from success ValidationResult`() {
val initialInputState = StringInputField(
value = "input",
error = TestValidationError,
)
val result = initialInputState.fromValidationResult(ValidationResult.Success)
assertThat(result).all {
prop(StringInputField::value).isEqualTo("input")
prop(StringInputField::error).isNull()
prop(StringInputField::isValid).isTrue()
}
}
@Test
fun `should map from failure ValidationResult`() {
val initialInputState = StringInputField(
value = "input",
error = null,
)
val result = initialInputState.fromValidationResult(ValidationResult.Failure(TestValidationError))
assertThat(result).all {
prop(StringInputField::value).isEqualTo("input")
prop(StringInputField::error).isEqualTo(TestValidationError)
prop(StringInputField::isValid).isFalse()
}
}
private object TestValidationError : ValidationError
}

View file

@ -1,6 +1,7 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateAccountName.ValidateAccountNameError
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
@ -18,14 +19,12 @@ class ValidateAccountNameTest {
}
@Test
fun `should fail when account name is empty`() {
fun `should succeed when account name is empty`() {
val useCase = ValidateAccountName()
val result = useCase.execute("")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateAccountName.EmptyAccountName::class)
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
@ -36,6 +35,6 @@ class ValidateAccountNameTest {
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateAccountName.EmptyAccountName::class)
.isInstanceOf(ValidateAccountNameError.BlankAccountName::class)
}
}

View file

@ -1,6 +1,7 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateDisplayName.ValidateDisplayNameError
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
@ -25,7 +26,7 @@ class ValidateDisplayNameTest {
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateDisplayName.EmptyDisplayName::class)
.isInstanceOf(ValidateDisplayNameError.EmptyDisplayName::class)
}
@Test
@ -36,6 +37,6 @@ class ValidateDisplayNameTest {
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateDisplayName.EmptyDisplayName::class)
.isInstanceOf(ValidateDisplayNameError.EmptyDisplayName::class)
}
}

View file

@ -1,6 +1,7 @@
package app.k9mail.feature.account.setup.domain.usecase
import app.k9mail.core.common.domain.usecase.validation.ValidationResult
import app.k9mail.feature.account.setup.domain.usecase.ValidateEmailSignature.ValidateEmailSignatureError
import assertk.assertThat
import assertk.assertions.isInstanceOf
import assertk.assertions.prop
@ -18,14 +19,12 @@ class ValidateEmailSignatureTest {
}
@Test
fun `should fail when email signature is empty`() {
fun `should succeed when email signature is empty`() {
val useCase = ValidateEmailSignature()
val result = useCase.execute("")
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateEmailSignature.EmptyEmailSignature::class)
assertThat(result).isInstanceOf(ValidationResult.Success::class)
}
@Test
@ -36,6 +35,6 @@ class ValidateEmailSignatureTest {
assertThat(result).isInstanceOf(ValidationResult.Failure::class)
.prop(ValidationResult.Failure::error)
.isInstanceOf(ValidateEmailSignature.EmptyEmailSignature::class)
.isInstanceOf(ValidateEmailSignatureError.BlankEmailSignature::class)
}
}