commonMain.validations.ScopedValidations.kt Maven / Gradle / Ivy
package io.kform.validations
import io.kform.*
import io.kform.validations.ValidationScopes.NotOneOf
import io.kform.validations.ValidationScopes.OneOf
import kotlin.jvm.JvmName
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
/**
* Validation scopes under which a validation is allowed to run. These can be specified via an allow
* list with [OneOf] or via a deny list with [NotOneOf].
*
* Note that, if no scope is provided as an external context during runtime, the default scope will
* be `null`. In this scenario, for the allow list case, unless `null` is explicitely passed to
* [OneOf], the validation will never run; similarly, for the deny list case, unless `null` is
* explicitely passed to [NotOneOf], the validation will always run.
*/
public sealed class ValidationScopes(scopes: Set) : Set by scopes {
/** Allow list of scopes under which the validation should run. */
public class OneOf(scopes: Iterable) : ValidationScopes(scopes.toSet()) {
public constructor(vararg scopes: T?) : this(scopes.toSet())
override fun allows(scope: T?): Boolean = scope in this
}
/** Deny list of scopes under which the validation should not run. */
public class NotOneOf(scopes: Iterable) : ValidationScopes(scopes.toSet()) {
public constructor(vararg scopes: T?) : this(scopes.toSet())
override fun allows(scope: T?): Boolean = scope !in this
}
/** Whether the validation scopes allow the provided [scope]. */
public abstract fun allows(scope: T?): Boolean
}
/**
* A scoped validation is a validation that should only run when the current "validation scope" is
* allowed by the provided [scopes].
*
* The current validation scope is obtained via an external context with a name defaulting to
* [DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME] (which can be overriden during class construction).
*/
public abstract class ScopedValidation
@JvmOverloads
constructor(scopeExternalContextName: String = DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME) :
Validation() {
/** Validation scopes of this validation. */
public abstract val scopes: ValidationScopes
/** Current validation scope. */
protected val ValidationContext.scope: TScope? by
externalContextOrNull(scopeExternalContextName)
public final override fun ValidationContext.validate(): Flow = flow {
if (scopes.allows(scope)) {
emitAll(scopedValidate())
}
}
/**
* Runs the scoped validation (when the current "validation scope" is allowed by the provided
* [scopes]) within a [ValidationContext] containing the value being validated and the value of
* all declared dependencies. Returns a flow over all found issues.
*/
public abstract fun ValidationContext.scopedValidate(): Flow
public companion object {
/** Default name of the external context from which to obtain the current scope. */
public const val DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME: String = "scope"
/**
* Creates a new scoped validation from an already existing [validation] that runs when the
* current "validation scope" is allowed by the provided [scopes].
*
* The current validation scope is obtained via an external context named
* [scopeExternalContextName] (which defaults to [DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME]).
*/
@JvmOverloads
@JvmStatic
@JvmName("create")
public operator fun invoke(
validation: Validation,
scopes: ValidationScopes,
scopeExternalContextName: String = DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME
): ScopedValidation =
WrappedScopedValidation(validation, scopes, scopeExternalContextName)
/**
* Creates a new scoped stateful validation from an already existing stateful [validation]
* that runs when the current "validation scope" is allowed by the provided [scopes].
*
* The current validation scope is obtained via an external context named
* [scopeExternalContextName] (which defaults to [DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME]).
*/
@JvmOverloads
@JvmStatic
@JvmName("create")
public operator fun invoke(
validation: StatefulValidation,
scopes: ValidationScopes,
scopeExternalContextName: String = DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME
): ScopedStatefulValidation =
WrappedScopedStatefulValidation(validation, scopes, scopeExternalContextName)
}
}
/**
* A scoped stateful validation is a stateful validation that should only run when the current
* "validation scope" is allowed by the provided [scopes].
*
* The current validation scope is obtained via an external context with a name defaulting to
* [ScopedValidation.DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME] (which can be overriden during class
* construction).
*/
public abstract class ScopedStatefulValidation
@JvmOverloads
constructor(
scopeExternalContextName: String = ScopedValidation.DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME
) : StatefulValidation() {
/** Validation scopes of this validation. */
public abstract val scopes: ValidationScopes
/** Current validation scope. */
protected val ValidationContext.scope: TScope? by
externalContextOrNull(scopeExternalContextName)
final override fun ValidationContext.validateFromState(state: TState): Flow =
flow {
if (scopes.allows(scope)) {
emitAll(scopedValidateFromState(state))
}
}
final override fun ValidationContext.validate(): Flow = flow {
if (scopes.allows(scope)) {
emitAll(scopedValidateFromState(initState()))
}
}
/**
* Runs the scoped stateful validation (when the current "validation scope" is allowed by the
* provided [scopes]), given its [state], within a [ValidationContext] containing the value
* being validated and the value of all declared dependencies. Returns a flow over all found
* issues.
*/
public abstract fun ValidationContext.scopedValidateFromState(
state: TState
): Flow
/**
* Runs the scoped validation (when the current "validation scope" is allowed by the provided
* [scopes]) within a [ValidationContext] containing the value being validated and the value of
* all declared dependencies. Returns a flow over all found issues.
*/
// Default [scopedValidate] implementation for scoped stateful validations.
public open fun ValidationContext.scopedValidate(): Flow = flow {
emitAll(scopedValidateFromState(initState()))
}
}
/** [Scoped validation][ScopedValidation] wrapping [validation]. */
private class WrappedScopedValidation(
val validation: Validation,
override val scopes: ValidationScopes,
scopeExternalContextName: String = DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME
) : ScopedValidation(scopeExternalContextName) {
override val dependencies: Map = validation.dependencies
override val dependsOnDescendants: Boolean = validation.dependsOnDescendants
init {
(externalContextDependencies as MutableSet) +=
validation.externalContextDependencies
}
override fun toString(): String = "Scoped$validation"
override fun ValidationContext.scopedValidate(): Flow =
validation.run { validate() }
}
/** [Scoped stateful validation][ScopedStatefulValidation] wrapping [validation]. */
private class WrappedScopedStatefulValidation(
val validation: StatefulValidation,
override val scopes: ValidationScopes,
scopeExternalContextName: String = ScopedValidation.DEFAULT_SCOPE_EXTERNAL_CONTEXT_NAME
) : ScopedStatefulValidation(scopeExternalContextName) {
override val dependencies: Map = validation.dependencies
override val dependsOnDescendants: Boolean = validation.dependsOnDescendants
override val observers: List> = validation.observers
init {
(externalContextDependencies as MutableSet) +=
validation.externalContextDependencies
}
override fun toString(): String = "Scoped$validation"
override suspend fun ValidationContext.initState(): TState = validation.run { initState() }
override suspend fun destroyState(state: TState): Unit = validation.run { destroyState(state) }
override fun ValidationContext.scopedValidate(): Flow =
validation.run { validate() }
override fun ValidationContext.scopedValidateFromState(state: TState): Flow =
validation.run { validateFromState(state) }
}