diff --git a/toolkit/presentation/presentation/pom.xml b/toolkit/presentation/presentation/pom.xml index 670b4211..3ba96d8d 100644 --- a/toolkit/presentation/presentation/pom.xml +++ b/toolkit/presentation/presentation/pom.xml @@ -19,6 +19,11 @@ toolkit-konstraints 0.1.0-SNAPSHOT + + tech.coner.trailer + toolkit-validation + 0.1.0-SNAPSHOT + \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt index eee15b89..fb77dc83 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/adapter/LoadableItemAdapter.kt @@ -1,11 +1,11 @@ package tech.coner.trailer.toolkit.presentation.adapter import tech.coner.trailer.toolkit.presentation.model.ItemModel +import tech.coner.trailer.toolkit.validation.Feedback -abstract class LoadableItemAdapter - where ITEM_MODEL : ItemModel { +abstract class LoadableItemAdapter + where ITEM_MODEL : ItemModel { - abstract val argumentToModelAdapter: ((ARGUMENT) -> ARGUMENT_MODEL)? abstract val itemToModelAdapter: (ITEM) -> ITEM_MODEL abstract val modelToItemAdapter: (ITEM_MODEL) -> ITEM } diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt index 3e1f007c..b5e661e3 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/BaseItemModel.kt @@ -1,76 +1,65 @@ package tech.coner.trailer.toolkit.presentation.model -import kotlin.reflect.KProperty1 -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update -import tech.coner.trailer.toolkit.konstraints.CompositeConstraint -import tech.coner.trailer.toolkit.konstraints.ConstraintViolationException +import tech.coner.trailer.toolkit.validation.Feedback +import tech.coner.trailer.toolkit.validation.ValidationResult +import tech.coner.trailer.toolkit.validation.Validator -abstract class BaseItemModel> : ItemModel { - abstract val constraints: C +abstract class BaseItemModel + : ItemModel + where VALIDATOR_FEEDBACK : Feedback { - abstract val initialItem: I + abstract val validator: Validator + abstract val validatorContext: VALIDATOR_CONTEXT + + abstract val initialItem: ITEM private val _itemFlow by lazy { MutableStateFlow(initialItem) } final override val itemFlow by lazy { _itemFlow.asStateFlow() } - final override val item: I + final override val item: ITEM get() = _itemFlow.value private val _pendingItemFlow by lazy { MutableStateFlow(initialItem) } final override val pendingItemFlow by lazy { _pendingItemFlow.asStateFlow() } - final override var pendingItem: I + final override var pendingItem: ITEM get() = pendingItemFlow.value set(value) = mutatePendingItem { value } - private val _pendingItemValidationFlow by lazy { MutableStateFlow(emptyList()) } + + private val _pendingItemValidationFlow by lazy { + MutableStateFlow>( + ValidationResult(emptyMap()) + ) + } final override val pendingItemValidationFlow by lazy { _pendingItemValidationFlow.asStateFlow() } final override val pendingItemValidation get() = _pendingItemValidationFlow.value - final override fun mutatePendingItem(forceValidate: Boolean?, mutatePendingItemFn: (I) -> I) { + final override fun mutatePendingItem(forceValidate: Boolean?, mutatePendingItemFn: (ITEM) -> ITEM) { _pendingItemFlow.update { pending -> mutatePendingItemFn(pending) } if (forceValidate == true) { validate() } } - override fun

validatedPropertyFlow(property: KProperty1, fn: (I) -> P): Flow> { - val constraints = constraints.propertyConstraints[property] - ?: throw Exception("propertyConstraints does not contain any constraints for property: $property") - return pendingItemFlow.map { item -> - Validated( - fn(item), - constraints - .mapNotNull { constraint -> constraint(item).exceptionOrNull() as ConstraintViolationException? } - .map { Violation(it) } - ) - } - } - final override val isPendingItemValid - get() = _pendingItemValidationFlow.value.isValid() + get() = _pendingItemValidationFlow.value.isValid final override val isPendingItemDirty get() = item != pendingItem - final override fun validate(): List { - return constraints.all - .mapNotNull { it.invoke(pendingItem).exceptionOrNull() as? ConstraintViolationException } - .map { ValidationContent.Error(it.message ?: "Invalid" /* TODO not hard-code english string */) } + final override fun validate(): ValidationResult { + return validator(validatorContext, pendingItem) .also { _pendingItemValidationFlow.value = it } } - override fun commit(forceValidate: Boolean): Result { - if (forceValidate) { - validate() - .also { - if (!it.isValid()) { - return Result.failure(ModelNotReadyToCommitException()) - } - } + override fun commit(requireValid: Boolean, successFn: (ITEM) -> Unit) { + if (requireValid) { + if (!validate().isValid) { + return + } } - return pendingItem + pendingItem .also { _itemFlow.value = it } - .let { Result.success(it) } + .also { successFn(it) } } } \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt index 417004ab..c6447efe 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ItemModel.kt @@ -1,23 +1,27 @@ package tech.coner.trailer.toolkit.presentation.model -import kotlin.reflect.KProperty1 import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import tech.coner.trailer.toolkit.validation.Feedback +import tech.coner.trailer.toolkit.validation.ValidationResult + +interface ItemModel : Model { + val itemFlow: StateFlow + val item: ITEM + + val pendingItemFlow: StateFlow + var pendingItem: ITEM -interface ItemModel : Model { - val itemFlow: StateFlow - val item: I - val pendingItemFlow: StateFlow - var pendingItem: I - val pendingItemValidationFlow: Flow> - val pendingItemValidation: List - val isPendingItemValid: Boolean val isPendingItemDirty: Boolean - fun mutatePendingItem(forceValidate: Boolean? = null, mutatePendingItemFn: (I) -> I) - fun

validatedPropertyFlow(property: KProperty1, fn: (I) -> P): Flow> + val pendingItemValidationFlow: Flow> + val pendingItemValidation: ValidationResult + + val isPendingItemValid: Boolean + + fun mutatePendingItem(forceValidate: Boolean? = null, mutatePendingItemFn: (ITEM) -> ITEM) - fun validate(): List + fun validate(): ValidationResult - fun commit(forceValidate: Boolean = true): Result + fun commit(requireValid: Boolean = true, successFn: (ITEM) -> Unit) } \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt index e724ecc9..2262b47e 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/LoadableModel.kt @@ -1,13 +1,15 @@ package tech.coner.trailer.toolkit.presentation.model -sealed class LoadableModel> +import tech.coner.trailer.toolkit.validation.Feedback + +sealed class LoadableModel, FEEDBACK : Feedback> : Model { /** * Initial model corresponding to the presenter having initial state, * prior to starting to load anything, or if it was fully reset. */ - class Empty> : LoadableModel() { + class Empty, FEEDBACK : Feedback> : LoadableModel() { override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -28,26 +30,26 @@ sealed class LoadableModel> * It may be helpful to implement the partial model with a different * type than the loaded item. */ - data class Loading>( + data class Loading, FEEDBACK : Feedback>( val partial: ITEM_MODEL? = null - ) : LoadableModel() + ) : LoadableModel() /** * Model indicates the item has loaded. * * @property item the item resulting from the load operation. */ - data class Loaded>( + data class Loaded, FEEDBACK : Feedback>( val item: ITEM_MODEL - ) : LoadableModel() + ) : LoadableModel() /** * Model indicates the load operation failed. * * @property cause the cause of the failure, if known */ - data class LoadFailed>( + data class LoadFailed, FEEDBACK : Feedback>( val cause: Throwable - ) : LoadableModel() + ) : LoadableModel() } \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelNotReadyToCommitException.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelNotReadyToCommitException.kt deleted file mode 100644 index cd7150be..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelNotReadyToCommitException.kt +++ /dev/null @@ -1,5 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.model - -class ModelNotReadyToCommitException(cause: Throwable? = null) : Throwable( - message = "Model was not ready to commit" -) diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelValidationException.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelValidationException.kt deleted file mode 100644 index b4671458..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ModelValidationException.kt +++ /dev/null @@ -1,6 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.model - -class ModelValidationException( - val violations: List -) : Throwable() { -} \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Validated.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Validated.kt deleted file mode 100644 index e79f72f3..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Validated.kt +++ /dev/null @@ -1,7 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.model - -class Validated(val value: V, val violations: List) { - - val isValid: Boolean - get() = violations.isEmpty() -} diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ValidationContent.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ValidationContent.kt deleted file mode 100644 index 799f176f..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/ValidationContent.kt +++ /dev/null @@ -1,28 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.model - -sealed class ValidationContent { - abstract val valid: Boolean - abstract val message: String - - data class Error(override val message: String) : ValidationContent() { - override val valid: Boolean = false - } - - sealed class ValidValidationContent : ValidationContent() { - override val valid: Boolean = true - } - - data class Warning(override val message: String) : ValidValidationContent() - data class Success(override val message: String) : ValidValidationContent() - data class Info(override val message: String) : ValidValidationContent() - -} - -fun List.isValid(): Boolean { - return isEmpty() - || all { it.valid } -} - -fun List.isInvalid(): Boolean { - return isNotEmpty() && any { !it.valid } -} \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Violation.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Violation.kt deleted file mode 100644 index b6951cb5..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/model/Violation.kt +++ /dev/null @@ -1,5 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.model - -import tech.coner.trailer.toolkit.konstraints.ConstraintViolationException - -class Violation(val cause: ConstraintViolationException) diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/BaseItemPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/BaseItemPresenter.kt deleted file mode 100644 index f014c2c2..00000000 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/BaseItemPresenter.kt +++ /dev/null @@ -1,102 +0,0 @@ -package tech.coner.trailer.toolkit.presentation.presenter - -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.mapNotNull -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.flow.zip -import tech.coner.trailer.toolkit.presentation.adapter.Adapter -import tech.coner.trailer.toolkit.presentation.model.ItemModel -import tech.coner.trailer.toolkit.presentation.model.ModelNotReadyToCommitException -import tech.coner.trailer.toolkit.presentation.model.ModelValidationException - -/** - * A base presenter that deals with a loadable entity as the focus of its state - */ -@Deprecated("Replace with LoadableItemPresenter") -abstract class BaseItemPresenter< - ARGUMENT : Presenter.Argument, - ENTITY, - ADAPTER : Adapter, - ITEM_MODEL : ItemModel - > : BasePresenter() { - - abstract val entityDefault: ENTITY - - protected abstract val adapter: ADAPTER - - private val _itemModelFlow by lazy { MutableStateFlow(Result.success(adapter(entityDefault))) } - val itemModelFlow: StateFlow> by lazy { _itemModelFlow.asStateFlow() } - val itemModel: ITEM_MODEL - get() = itemModelFlow.value.getOrThrow() - - private val _loadingFlow = MutableStateFlow(false) - val loadingFlow: StateFlow = _loadingFlow.asStateFlow() - - private val _loadedFlow = MutableStateFlow(false) - val loadedFlow: StateFlow = _loadedFlow.asStateFlow() - - suspend fun load() { - _loadingFlow.emit(true) - performLoad() - .onSuccess { - _loadedFlow.emit(true) - _itemModelFlow.emit(Result.success(adapter(it))) - } - .onFailure { - _itemModelFlow.emit(Result.failure(it)) - } - _loadingFlow.emit(false) - } - - suspend fun awaitLoadedItemModel(): Result { - return itemModelFlow.zip(loadedFlow) { model, loaded -> model to loaded } - .mapNotNull { item -> - val (result: Result, loaded) = item - when { - loaded && result.isSuccess -> result - result.isFailure -> result - else -> null - } - } - .first() - } - - protected abstract suspend fun performLoad(): Result - - suspend fun commit(): Result { - var commitReturn: Result = _itemModelFlow.value - _itemModelFlow.update { result -> - val itemModel = result.getOrNull() - if (itemModel != null) { - // itemModel can be validated - if (itemModel.isPendingItemValid) { - // itemModel is valid - Result.success(adapter(itemModel.pendingItem)) - .also { commitReturn = it } - } else { - // itemModel is invalid - commitReturn = itemModel.validate() - .let { Result.failure(ModelValidationException(/* this will cause breakage but this thing is deprecated */emptyList() /*it*/)) } - result // will retain the same result/model unchanged - } - } else { - // itemModel isn't ready for validation - // perhaps there was a failure loading it - commitReturn = result.exceptionOrNull() - ?.let { Result.failure(ModelNotReadyToCommitException(it)) } - ?: Result.failure(Exception("Failed to find cause of commit not ready.")) - result // will retain the same result/model unchanged - } - } - return commitReturn - } - - fun rollback() { - _itemModelFlow.update { item -> - Result.success(adapter(item.getOrNull()?.item ?: entityDefault)) - } - } -} \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt index b85c9289..0afe3cec 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/LoadableItemPresenter.kt @@ -1,41 +1,24 @@ package tech.coner.trailer.toolkit.presentation.presenter -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.update -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withTimeout import tech.coner.trailer.toolkit.presentation.adapter.LoadableItemAdapter import tech.coner.trailer.toolkit.presentation.model.ItemModel import tech.coner.trailer.toolkit.presentation.model.LoadableModel +import tech.coner.trailer.toolkit.presentation.model.Model import tech.coner.trailer.toolkit.presentation.state.LoadableItemState +import tech.coner.trailer.toolkit.validation.Feedback -abstract class LoadableItemPresenter< - ARGUMENT, - ITEM, - ARGUMENT_MODEL, - ITEM_MODEL - > - : CoroutineScope - where ITEM_MODEL : ItemModel { +abstract class LoadableItemPresenter( + override val initialState: LoadableItemState = LoadableItemState(LoadableModel.Empty()) +) + : SecondDraftPresenter< + LoadableItemState + >(), CoroutineScope + where ITEM_MODEL : ItemModel { - protected abstract val adapter: LoadableItemAdapter - - protected abstract val argument: ARGUMENT? - val argumentModel: ARGUMENT_MODEL? by lazy { - argument?.let { adapter.argumentToModelAdapter?.invoke(it) } - } - - private val initialState = LoadableItemState(LoadableModel.Empty()) - private val _stateMutex = Mutex() - private val _stateFlow: MutableStateFlow> by lazy { MutableStateFlow(initialState) } - val stateFlow: StateFlow> by lazy { _stateFlow.asStateFlow() } + protected abstract val adapter: LoadableItemAdapter suspend fun load() { update { old -> old.copy(LoadableModel.Loading()) } @@ -46,7 +29,7 @@ abstract class LoadableItemPresenter< protected abstract suspend fun performLoad(): Result - suspend fun awaitModelLoadedOrFailed(): LoadableModel { + suspend fun awaitModelLoadedOrFailed(): LoadableModel { return stateFlow .map { it.loadable } .first { @@ -57,7 +40,7 @@ abstract class LoadableItemPresenter< } } - suspend fun awaitModelLoadedOrThrow(): LoadableModel.Loaded { + suspend fun awaitModelLoadedOrThrow(): LoadableModel.Loaded { return stateFlow .map { it.loadable } .first { @@ -66,14 +49,6 @@ abstract class LoadableItemPresenter< is LoadableModel.LoadFailed -> throw it.cause else -> false } - } as LoadableModel.Loaded - } - - protected suspend fun update(reduceFn: (old: LoadableItemState) -> LoadableItemState) { - _stateMutex.withLock { - withTimeout(10.milliseconds) { - _stateFlow.update(reduceFn) - } - } + } as LoadableModel.Loaded } } \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt new file mode 100644 index 00000000..39e921b0 --- /dev/null +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/presenter/SecondDraftPresenter.kt @@ -0,0 +1,32 @@ +package tech.coner.trailer.toolkit.presentation.presenter + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import tech.coner.trailer.toolkit.presentation.model.Model +import tech.coner.trailer.toolkit.presentation.state.State + +abstract class SecondDraftPresenter + where STATE : State { + + abstract val initialState: STATE + private val _stateMutex = Mutex() + private val _stateFlow: MutableStateFlow by lazy { MutableStateFlow(initialState) } + val stateFlow: StateFlow by lazy { _stateFlow.asStateFlow() } + + protected suspend fun update(reduceFn: (old: STATE) -> STATE) { + _stateMutex.withLock { + _stateFlow.update(reduceFn) + } + } + + interface WithArgument { + + val argument: ARGUMENT + val argumentModel: ARGUMENT_MODEL + + } +} \ No newline at end of file diff --git a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt index b1a0aae3..f9a9b8b8 100644 --- a/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt +++ b/toolkit/presentation/presentation/src/main/kotlin/tech/coner/trailer/toolkit/presentation/state/LoadableItemState.kt @@ -2,7 +2,8 @@ package tech.coner.trailer.toolkit.presentation.state import tech.coner.trailer.toolkit.presentation.model.ItemModel import tech.coner.trailer.toolkit.presentation.model.LoadableModel +import tech.coner.trailer.toolkit.validation.Feedback -data class LoadableItemState>( - val loadable: LoadableModel +data class LoadableItemState, FEEDBACK : Feedback>( + val loadable: LoadableModel ) : State diff --git a/toolkit/samples/common/pom.xml b/toolkit/samples/common/pom.xml index 89a8ab9f..77703d36 100644 --- a/toolkit/samples/common/pom.xml +++ b/toolkit/samples/common/pom.xml @@ -23,10 +23,18 @@ toolkit-validation 0.1.0-SNAPSHOT + + tech.coner.trailer + toolkit-presentation + 0.1.0-SNAPSHOT + + + tech.coner.trailer toolkit-validation-testsupport 0.1.0-SNAPSHOT + test diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseApplicationValidator.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseApplicationValidator.kt index f44e7e05..616309ff 100644 --- a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseApplicationValidator.kt +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseApplicationValidator.kt @@ -10,30 +10,29 @@ import tech.coner.trailer.toolkit.validation.Validator typealias DriversLicenseClerk = Validator -val driversLicenseClerk: DriversLicenseClerk - get() = Validator { - DriversLicenseApplication::name { name -> - NameMustNotBeBlank.takeIf { name.isBlank() } - } - DriversLicenseApplication::age { age -> - when { - age < GraduatedLearnerPermit.MIN_AGE -> - TooYoung( - suggestOtherLicenseType = GraduatedLearnerPermit.takeIf { input.licenseType != it }, - reapplyWhenAge = GraduatedLearnerPermit.MIN_AGE - ) +fun DriversLicenseClerk(): DriversLicenseClerk = Validator { + DriversLicenseApplication::name { name -> + NameMustNotBeBlank.takeIf { name.isBlank() } + } + DriversLicenseApplication::age { age -> + when { + age < GraduatedLearnerPermit.MIN_AGE -> + TooYoung( + suggestOtherLicenseType = GraduatedLearnerPermit.takeIf { input.licenseType != it }, + reapplyWhenAge = GraduatedLearnerPermit.MIN_AGE + ) - age in GraduatedLearnerPermit.AGE_RANGE && input.licenseType != GraduatedLearnerPermit -> - TooYoung( - suggestOtherLicenseType = GraduatedLearnerPermit, - ) + age in GraduatedLearnerPermit.AGE_RANGE && input.licenseType != GraduatedLearnerPermit -> + TooYoung( + suggestOtherLicenseType = GraduatedLearnerPermit, + ) - age in 18..Int.MAX_VALUE && input.licenseType == GraduatedLearnerPermit -> - TooOld( - suggestOtherLicenseType = LearnerPermit - ) + age in 18..Int.MAX_VALUE && input.licenseType == GraduatedLearnerPermit -> + TooOld( + suggestOtherLicenseType = LearnerPermit + ) - else -> null - } + else -> null } + } } diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/DriversLicenseApplicationPresenter.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/DriversLicenseApplicationPresenter.kt new file mode 100644 index 00000000..ce3ad2e4 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/DriversLicenseApplicationPresenter.kt @@ -0,0 +1,18 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation + +import tech.coner.trailer.toolkit.presentation.presenter.SecondDraftPresenter +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationItemModel +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.state.DriversLicenseApplicationState +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelValidator + +class DriversLicenseApplicationPresenter( + initialState: DriversLicenseApplicationState? = null +) : SecondDraftPresenter() { + + override val initialState = initialState + ?: DriversLicenseApplicationState( + model = DriversLicenseApplicationItemModel( + validator = DriversLicenseApplicationModelValidator() + ) + ) +} \ No newline at end of file diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/adapter/PresentationModelToDomainEntityAdapters.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/adapter/PresentationModelToDomainEntityAdapters.kt new file mode 100644 index 00000000..8e665c73 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/adapter/PresentationModelToDomainEntityAdapters.kt @@ -0,0 +1,15 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.adapter + +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicenseApplication +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationModel + +fun DriversLicenseApplicationModel.toDomainEntity(): DriversLicenseApplication? { + return when { + name != null && age != null && licenseType != null -> DriversLicenseApplication( + name = name, + age = age, + licenseType = licenseType + ) + else -> null + } +} \ No newline at end of file diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt new file mode 100644 index 00000000..12da184b --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationItemModel.kt @@ -0,0 +1,31 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.model + +import kotlinx.coroutines.flow.map +import tech.coner.trailer.toolkit.presentation.model.BaseItemModel +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback +import tech.coner.trailer.toolkit.validation.Validator + +class DriversLicenseApplicationItemModel( + override val validator: Validator, +) : BaseItemModel() { + + override val initialItem: DriversLicenseApplicationModel = DriversLicenseApplicationModel() + override val validatorContext = Unit + + var name: String? + get() = pendingItem.name + set(value) = mutatePendingItem { it.copy(name = value) } + val nameFlow = pendingItemFlow.map { it.name } + + var age: Int? + get() = pendingItem.age + set(value) = mutatePendingItem { it.copy(age = age) } + val ageFlow = pendingItemFlow.map { it.age } + + var licenseType: LicenseType? + get() = pendingItem.licenseType + set(value) = mutatePendingItem { it.copy(licenseType = licenseType) } + val licenseTypeFLow = pendingItemFlow.map { it.licenseType } + +} diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationModel.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationModel.kt new file mode 100644 index 00000000..66626441 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/model/DriversLicenseApplicationModel.kt @@ -0,0 +1,10 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.model + +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType + +data class DriversLicenseApplicationModel( + val name: String? = null, + val age: Int? = null, + val licenseType: LicenseType? = null +) + diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/state/DriversLicenseApplicationState.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/state/DriversLicenseApplicationState.kt new file mode 100644 index 00000000..7e779985 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/state/DriversLicenseApplicationState.kt @@ -0,0 +1,8 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.state + +import tech.coner.trailer.toolkit.presentation.state.State +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationItemModel + +data class DriversLicenseApplicationState( + val model: DriversLicenseApplicationItemModel +) : State diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelFeedback.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelFeedback.kt new file mode 100644 index 00000000..18f05f13 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelFeedback.kt @@ -0,0 +1,26 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation + +import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseApplicationFeedback +import tech.coner.trailer.toolkit.validation.Feedback +import tech.coner.trailer.toolkit.validation.Severity +import tech.coner.trailer.toolkit.validation.Severity.Error + +sealed class DriversLicenseApplicationModelFeedback : Feedback { + + data object NameIsRequired : DriversLicenseApplicationModelFeedback() { + override val severity = Error + } + data object AgeIsRequired : DriversLicenseApplicationModelFeedback() { + override val severity = Error + } + data object LicenseTypeIsRequired : DriversLicenseApplicationModelFeedback() { + override val severity = Error + } + + data class DelegatedFeedback( + val feedback: DriversLicenseApplicationFeedback + ) : DriversLicenseApplicationModelFeedback() { + override val severity: Severity + get() = feedback.severity + } +} diff --git a/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidator.kt b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidator.kt new file mode 100644 index 00000000..63e9cb86 --- /dev/null +++ b/toolkit/samples/dmvapp/src/main/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidator.kt @@ -0,0 +1,34 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation + +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.DriversLicenseApplication +import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseClerk +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.adapter.toDomainEntity +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationModel +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.AgeIsRequired +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.DelegatedFeedback +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.LicenseTypeIsRequired +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.NameIsRequired +import tech.coner.trailer.toolkit.validation.Severity +import tech.coner.trailer.toolkit.validation.Validator +import tech.coner.trailer.toolkit.validation.input + +typealias DriversLicenseApplicationModelValidator = Validator + +fun DriversLicenseApplicationModelValidator( + driversLicenseClerk: DriversLicenseClerk = DriversLicenseClerk() +): DriversLicenseApplicationModelValidator = Validator { + DriversLicenseApplicationModel::name { if (it == null) NameIsRequired else null } + DriversLicenseApplicationModel::age { if (it == null) AgeIsRequired else null } + DriversLicenseApplicationModel::licenseType { licenseType -> LicenseTypeIsRequired.takeIf { licenseType == null } } + returnEarlyIfAny { it.severity == Severity.Error } + input( + validator = driversLicenseClerk, + mapInputFn = { it.toDomainEntity()!! }, + mapFeedbackKeys = mapOf( + DriversLicenseApplication::name to DriversLicenseApplicationModel::name, + DriversLicenseApplication::age to DriversLicenseApplicationModel::age, + DriversLicenseApplication::licenseType to DriversLicenseApplicationModel::licenseType + ), + mapFeedbackObjectFn = { DelegatedFeedback(it) } + ) +} diff --git a/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseValidatorTest.kt b/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseValidatorTest.kt index 0128e43f..3365c59f 100644 --- a/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseValidatorTest.kt +++ b/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/domain/validation/DriversLicenseValidatorTest.kt @@ -19,6 +19,8 @@ import tech.coner.trailer.toolkit.validation.testsupport.isValid class DriversLicenseValidatorTest { + private val clerk = DriversLicenseClerk() + enum class DriversLicenseApplicationScenario( val input: DriversLicenseApplication, val expectedAgeFeedback: List? = null, @@ -124,7 +126,7 @@ class DriversLicenseValidatorTest { @ParameterizedTest @EnumSource fun itShouldValidateDriversLicenseApplications(scenario: DriversLicenseApplicationScenario) { - val actual = driversLicenseClerk(scenario.input) + val actual = clerk(scenario.input) assertThat(actual).all { feedback().all { diff --git a/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidatorTest.kt b/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidatorTest.kt new file mode 100644 index 00000000..817e42e4 --- /dev/null +++ b/toolkit/samples/dmvapp/src/test/kotlin/tech/coner/trailer/toolkit/sample/dmvapp/presentation/validation/DriversLicenseApplicationModelValidatorTest.kt @@ -0,0 +1,110 @@ +package tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation + +import assertk.all +import assertk.assertThat +import assertk.assertions.isEqualTo +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType +import tech.coner.trailer.toolkit.sample.dmvapp.domain.entity.LicenseType.FullLicense +import tech.coner.trailer.toolkit.sample.dmvapp.domain.validation.DriversLicenseApplicationFeedback.TooYoung +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.model.DriversLicenseApplicationModel +import tech.coner.trailer.toolkit.sample.dmvapp.presentation.validation.DriversLicenseApplicationModelFeedback.* +import tech.coner.trailer.toolkit.validation.invoke +import tech.coner.trailer.toolkit.validation.testsupport.feedback +import tech.coner.trailer.toolkit.validation.testsupport.isInvalid +import tech.coner.trailer.toolkit.validation.testsupport.isValid +import kotlin.reflect.KProperty1 + +class DriversLicenseApplicationModelValidatorTest { + + private val validator = DriversLicenseApplicationModelValidator() + + enum class Scenario( + val input: DriversLicenseApplicationModel, + val expectedFeedback: Map, List> = emptyMap(), + val expectedIsValid: Boolean + ) { + FAIL_MODEL_FROM_DEFAULT_CONSTRUCTOR( + input = DriversLicenseApplicationModel(), + expectedFeedback = mapOf( + DriversLicenseApplicationModel::name to listOf(NameIsRequired), + DriversLicenseApplicationModel::age to listOf(AgeIsRequired), + DriversLicenseApplicationModel::licenseType to listOf(LicenseTypeIsRequired) + ), + expectedIsValid = false + ), + + FAIL_MODEL_WITH_NAME_NULL( + input = DriversLicenseApplicationModel( + name = null, + age = 42, + licenseType = FullLicense + ), + expectedFeedback = mapOf( + DriversLicenseApplicationModel::name to listOf(NameIsRequired) + ), + expectedIsValid = false + ), + + FAIL_MODEL_WITH_AGE_NULL( + input = DriversLicenseApplicationModel( + name = "not null", + age = null, + licenseType = FullLicense + ), + expectedFeedback = mapOf( + DriversLicenseApplicationModel::age to listOf(AgeIsRequired) + ), + expectedIsValid = false + ), + + FAIL_MODEL_WITH_LICENSE_TYPE_NULL( + input = DriversLicenseApplicationModel( + name = "not null", + age = 42, + licenseType = null + ), + expectedFeedback = mapOf( + DriversLicenseApplicationModel::licenseType to listOf(LicenseTypeIsRequired) + ), + expectedIsValid = false + ), + + FAIL_MAPPED_DELEGATED_TO_DOMAIN_VALIDATOR_FAILED( + input = DriversLicenseApplicationModel( + name = "some kiddo", + age = 15, + licenseType = FullLicense + ), + expectedFeedback = mapOf( + DriversLicenseApplicationModel::age to listOf( + DelegatedFeedback(TooYoung(suggestOtherLicenseType = LicenseType.GraduatedLearnerPermit)) + ) + ), + expectedIsValid = false + ), + + PASS_MAPPED_DELEGATED_TO_DOMAIN_VALIDATOR_PASSED( + input = DriversLicenseApplicationModel( + name = "experienced driver", + age = 40, + licenseType = FullLicense + ), + expectedFeedback = emptyMap(), + expectedIsValid = true + ) + } + + @ParameterizedTest + @EnumSource + fun itShouldValidateDriversLicenseApplicationModels(scenario: Scenario) { + val actual = validator(scenario.input) + + assertThat(actual).all { + feedback().isEqualTo(scenario.expectedFeedback) + isValid().isEqualTo(scenario.expectedIsValid) + isInvalid().isEqualTo(!scenario.expectedIsValid) + } + } +} \ No newline at end of file diff --git a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/Validator.kt b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/Validator.kt index f774024b..0847cc2f 100644 --- a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/Validator.kt +++ b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/Validator.kt @@ -37,9 +37,38 @@ interface Validator { fun input( vararg ruleFns: ValidationRuleContext.(INPUT) -> FEEDBACK? ) + + fun input( + validator: Validator, + mapContextFn: CONTEXT.(INPUT) -> DELEGATE_CONTEXT, + mapInputFn: CONTEXT.(INPUT) -> DELEGATE_INPUT, + mapFeedbackKeys: Map?, KProperty1?>, + mapFeedbackObjectFn: (DELEGATE_FEEDBACK) -> FEEDBACK + ) + + fun returnEarlyIfAny(matchFn: (FEEDBACK) -> Boolean) } } +/** + * Convenience for invoking Unit-context validators, omitting the redundant Unit context parameter + */ operator fun Validator.invoke(input: INPUT): ValidationResult { return invoke(Unit, input) } + +/** + * Convenience for delegating object validation from one Unit-context validator to another + */ +fun Validator.Builder.input( + validator: Validator, + mapInputFn: Unit.(INPUT) -> DELEGATE_INPUT, + mapFeedbackKeys: Map?, KProperty1?>, + mapFeedbackObjectFn: (DELEGATE_FEEDBACK) -> FEEDBACK +) = input( + validator = validator, + mapContextFn = { }, + mapInputFn = mapInputFn, + mapFeedbackKeys = mapFeedbackKeys, + mapFeedbackObjectFn = mapFeedbackObjectFn +) \ No newline at end of file diff --git a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/ValidatorImpl.kt b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/ValidatorImpl.kt index 3cc64744..5a95aebc 100644 --- a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/ValidatorImpl.kt +++ b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/ValidatorImpl.kt @@ -21,22 +21,35 @@ internal class ValidatorImpl( override fun invoke(context: CONTEXT, input: INPUT): ValidationResult { val feedback: MutableMap?, MutableList> = mutableMapOf() - entries.forEach { entry -> - when (entry) { - is ValidationEntry.InputPropertySingleFeedback -> { - entry(context, input) - ?.also { feedback.createOrAppend(entry.property, it) } - } - is ValidationEntry.InputObjectSingleFeedback -> { - entry(context, input) - ?.also { feedback.createOrAppend(null, it) } - } - is ValidationEntry.InputPropertyDelegatesToValidator -> { - entry(context, input) - .also { feedback.createOrAppend(it.feedback) } + for (entry in entries) { + when (entry) { + is ValidationEntry.InputPropertySingleFeedback -> { + entry(context, input) + ?.also { feedback.createOrAppend(entry.property, it) } + } + + is ValidationEntry.InputObjectSingleFeedback -> { + entry(context, input) + ?.also { feedback.createOrAppend(null, it) } + } + + is ValidationEntry.InputPropertyDelegatesToValidator -> { + entry(context, input) + .also { feedback.createOrAppend(it.feedback) } + } + + is ValidationEntry.InputObjectDelegatesToValidator -> { + entry(context, input) + .also { feedback.createOrAppend(it.feedback) } + } + + is ValidationEntry.ReturnEarlyIfAny -> { + if (entry(feedback.toMap())) { + break } } } + } return ValidationResult(feedback.toMap()) } @@ -92,5 +105,25 @@ internal class ValidatorImpl( ) } } + + override fun input( + validator: Validator, + mapContextFn: CONTEXT.(INPUT) -> DELEGATE_CONTEXT, + mapInputFn: CONTEXT.(INPUT) -> DELEGATE_INPUT, + mapFeedbackKeys: Map?, KProperty1?>, + mapFeedbackObjectFn: (DELEGATE_FEEDBACK) -> FEEDBACK + ) { + entries += ValidationEntry.InputObjectDelegatesToValidator( + validator = validator, + mapContextFn = mapContextFn, + mapInputFn = mapInputFn, + mapFeedbackKeys = mapFeedbackKeys, + mapFeedbackObjectFn = mapFeedbackObjectFn + ) + } + + override fun returnEarlyIfAny(matchFn: (FEEDBACK) -> Boolean) { + entries += ValidationEntry.ReturnEarlyIfAny(matchFn) + } } } diff --git a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/entry/ValidationEntry.kt b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/entry/ValidationEntry.kt index e7a39f17..8f6dc9b5 100644 --- a/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/entry/ValidationEntry.kt +++ b/toolkit/validation/validation/src/main/kotlin/tech/coner/trailer/toolkit/validation/impl/entry/ValidationEntry.kt @@ -55,4 +55,37 @@ internal sealed class ValidationEntry { ) } } + + internal class InputObjectDelegatesToValidator( + private val validator: Validator, + private val mapContextFn: CONTEXT.(INPUT) -> DELEGATE_CONTEXT, + private val mapInputFn: CONTEXT.(INPUT) -> DELEGATE_INPUT, + private val mapFeedbackKeys: Map?, KProperty1?>, + private val mapFeedbackObjectFn: (DELEGATE_FEEDBACK) -> FEEDBACK + ) : ValidationEntry() { + operator fun invoke(context: CONTEXT, input: INPUT): ValidationResult { + return ValidationResult( + validator( + context = mapContextFn(context, input), + input = mapInputFn(context, input) + ) + .feedback + .map { mapFeedbackKeys[it.key] to it.value.map(mapFeedbackObjectFn) } + .toMap() + ) + } + } + + internal class ReturnEarlyIfAny( + private val matchFn: (FEEDBACK) -> Boolean + ) + : ValidationEntry() { + + operator fun invoke(feedback: Map?, List>): Boolean { + return feedback + .values + .flatten() + .any { matchFn(it) } + } + } } \ No newline at end of file