>(
+ 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