Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
@file:OptIn(ExperimentalTypeInference::class)
package io.kform
import kotlin.experimental.ExperimentalTypeInference
import kotlin.js.JsName
import kotlin.jvm.JvmName
import kotlin.properties.PropertyDelegateProvider
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KType
import kotlin.reflect.typeOf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
/** Value information of a validation's dependencies. */
public typealias DependenciesValueInfo = Map?>
/**
* Context provided to a validation when running it, contains the values of the validation's
* dependencies at the time the validation was executed.
*/
public open class ValidationContext(
private val value: Any?,
private val schema: Schema<*>,
/** Path of the value being validated. */
public val path: AbsolutePath,
/** Path of the schema of the value being validated. */
public val schemaPath: AbsolutePath,
/**
* Value information for each dependency of the validation. Mapping of keys as declared in the
* [validation dependencies][Validation.dependencies] to their respective value information.
*/
private val dependenciesInfo: DependenciesValueInfo,
private val externalContexts: ExternalContexts,
) {
// internal var invalidateAfter: Duration? = null
/** Schema of the value being validated. */
@Suppress("UNCHECKED_CAST") public fun schema(): Schema = schema as Schema
/** Value being validated. */
@Suppress("UNCHECKED_CAST") public fun value(): T = value as T
/**
* Obtains the value information of the dependency with key [dependencyKey] or `null` if the
* dependency could not be found in the form.
*
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
@Suppress("UNCHECKED_CAST")
public fun dependencyInfoOrNull(dependencyKey: String): ValueInfo? {
require(dependencyKey in dependenciesInfo) {
"No dependency found with key '$dependencyKey'."
}
return dependenciesInfo[dependencyKey] as ValueInfo?
}
/**
* Obtains the value information of the dependency with key [dependencyKey].
*
* @throws DependencyNotFound If the dependency could not be found in the form.
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencyInfo(dependencyKey: String): ValueInfo =
dependencyInfoOrNull(dependencyKey) ?: throw DependencyNotFound(dependencyKey)
/**
* Returns the path of the dependency with key [dependencyKey] or `null` if the dependency could
* not be found in the form.
*
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencyPathOrNull(dependencyKey: String): AbsolutePath? =
dependencyInfoOrNull(dependencyKey)?.path
/**
* Returns the path of the dependency with key [dependencyKey].
*
* @throws DependencyNotFound If the dependency could not be found in the form.
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencyPath(dependencyKey: String): AbsolutePath =
dependencyInfo(dependencyKey).path
/**
* Returns the schema of the dependency with key [dependencyKey] or `null` if the dependency
* could not be found in the form.
*
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencySchemaOrNull(dependencyKey: String): Schema? =
dependencyInfoOrNull(dependencyKey)?.schema
/**
* Returns the schema of the dependency with key [dependencyKey].
*
* @throws DependencyNotFound If the dependency could not be found in the form.
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencySchema(dependencyKey: String): Schema =
dependencyInfo(dependencyKey).schema
/**
* Returns the value of the dependency with key [dependencyKey] or `null` if the dependency
* could not be found in the form.
*
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependencyOrNull(dependencyKey: String): T? =
dependencyInfoOrNull(dependencyKey)?.value
/**
* Returns the value of the dependency with key [dependencyKey].
*
* @throws DependencyNotFound If the dependency could not be found in the form.
* @throws IllegalArgumentException If no dependency with key [dependencyKey] was defined in the
* validation.
*/
public fun dependency(dependencyKey: String): T = dependencyInfo(dependencyKey).value
/**
* Returns the external context with name [externalContextName] available to the validation or
* `null` if the external context could not be found.
*
* @throws IllegalArgumentException If no external context with key [externalContextName] was
* defined in the validation.
*/
@Suppress("UNCHECKED_CAST")
public fun externalContextOrNull(externalContextName: String): T? {
require(externalContextName in externalContexts) {
"No external context found with name '$externalContextName'."
}
return externalContexts[externalContextName] as T?
}
/**
* Returns the external context with name [externalContextName] available to the validation.
*
* @throws ExternalContextNotFound If the external context could not be found during the
* validation.
* @throws IllegalArgumentException If no external context with key [externalContextName] was
* defined in the validation.
*/
public fun externalContext(externalContextName: String): T =
externalContextOrNull(externalContextName)
?: throw ExternalContextNotFound(externalContextName)
// TODO: Implement something like this
// private fun invalidateAfter(duration: Duration) {
// require(duration >= Duration.ZERO) { "Duration must not be negative." }
// invalidateAfter = duration
// }
}
/** Information about a validation dependency, includes its [path] and [type]. */
public data class DependencyInfo(val path: Path, val type: KType)
/**
* Validation for values of type [T].
*
* Validations may depend on other values. These dependencies may be defined via the [dependency]
* function, by providing the path of the dependency relative to the value being validated.
*
* Example validation that emits an error when the integer being validated is odd if the value of a
* dependency `allowOdd` is `false`:
* ```kotlin
* object DisallowOdd : Validation() {
* private val ValidationContext.allowOdd: Boolean by dependency("../allowOdd")
*
* ValidationContext.validate() = flow {
* if (!allowOdd && value % 2 != 0) {
* emit(ValidationError("oddNotAllowed"))
* }
* }
* }
* ```
*/
@JsName("ValidationKt")
public abstract class Validation {
/** Schema of the value being validated. */
protected inline val ValidationContext.schema: Schema<@UnsafeVariance T>
get() = schema()
/** Value being validated. */
protected inline val ValidationContext.value: @UnsafeVariance T
get() = value()
/**
* Dependencies of the validation. Mapping of keys to the paths this validation depends on. Keys
* can be used within a [ValidationContext] to access the value of the dependencies.
*
* Validation dependency paths may contain a single recursive wildcard at the end, indicating
* that the validation should be reevaluated by the [form manager][FormManager] whenever a child
* value of the dependency changes. Otherwise, a dependency must contain no wildcards.
*/
public open val dependencies: Map = hashMapOf()
/**
* Whether the [form manager][FormManager] should reevaluate this validation whenever a
* descendant of the value being validated changes. This is `false` by default.
*
* Conceptually, when `false`, it is as if the validation has an implicit dependency on the path
* `"."` (itself). When `true`, this dependency becomes `"./∗∗"` (itself and all of its
* descendants).
*/
public open val dependsOnDescendants: Boolean
get() = false
/**
* Set of external context dependencies of the validation.
*
* Each entry of this set represents the name of the external context being depended upon.
*/
public open val externalContextDependencies: Set = hashSetOf()
/**
* Declares a dependency to [path], accessible in the validation's context via key
* [dependencyKey].
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
@JvmName("reifiedAddDependency")
protected inline fun addDependency(dependencyKey: String, path: Path) {
(dependencies as MutableMap)[dependencyKey] =
DependencyInfo(path, typeOf())
}
/**
* Declares a dependency to [path], accessible in the validation's context via key
* [dependencyKey].
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addDependency(dependencyKey: String, path: Path): Unit =
addDependency(dependencyKey, path)
/**
* Declares a dependency to [path], accessible in the validation's context via key
* [dependencyKey].
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
@JvmName("reifiedAddDependency")
protected inline fun addDependency(
dependencyKey: String,
path: String
): Unit = addDependency(dependencyKey, Path(path))
/**
* Declares a dependency to [path], accessible in the validation's context via key
* [dependencyKey].
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addDependency(dependencyKey: String, path: String): Unit =
addDependency(dependencyKey, path)
/**
* Declares an external context dependency to [externalContextName].
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addExternalContextDependency(externalContextName: String) {
(externalContextDependencies as MutableSet).add(externalContextName)
}
/**
* Function used to declare a dependency to a [path] and delegate access to its value within a
* [ValidationContext].
*
* If the dependency could not be found in the form during the execution of the validation, then
* accessing this dependency will return `null`.
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.dependencyKey: Type? by dependencyOrNull(path)
* ```
*
* Validation dependency paths may contain a single recursive wildcard at the end, indicating
* that the validation should be reevaluated by the [form manager][FormManager] whenever a child
* value of the dependency changes. Otherwise, a dependency must contain no wildcards.
*/
protected inline fun dependencyOrNull(
path: Path,
dependencyKey: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = PropertyDelegateProvider { validation, property ->
validation.addDependency(dependencyKey ?: property.name, path)
// Return a delegate to the dependency value within a [ValidationContext]
ReadOnlyProperty { ctx, prop -> ctx.dependencyOrNull(dependencyKey ?: prop.name) }
}
/**
* Function used to declare a dependency to a [path] and delegate access to its value within a
* [ValidationContext].
*
* If the dependency could not be found in the form during the execution of the validation, then
* accessing this dependency will return `null`.
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.dependencyKey: Type? by dependencyOrNull(path)
* ```
*
* Validation dependency paths may contain a single recursive wildcard at the end, indicating
* that the validation should be reevaluated by the [form manager][FormManager] whenever a child
* value of the dependency changes. Otherwise, a dependency must contain no wildcards.
*/
protected inline fun dependencyOrNull(
path: String,
dependencyKey: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = dependencyOrNull(Path(path), dependencyKey)
/**
* Function used to declare a dependency to a [path] and delegate access to its value within a
* [ValidationContext].
*
* If the dependency could not be found in the form during the execution of the validation, then
* accessing this dependency will throw [DependencyNotFound].
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.dependencyKey: Type by dependency(path)
* ```
*
* Validation dependency paths may contain a single recursive wildcard at the end, indicating
* that the validation should be reevaluated by the [form manager][FormManager] whenever a child
* value of the dependency changes. Otherwise, a dependency must contain no wildcards.
*/
protected inline fun dependency(
path: Path,
dependencyKey: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = PropertyDelegateProvider { validation, property ->
validation.addDependency(dependencyKey ?: property.name, path)
// Return a delegate to the dependency value within a [ValidationContext]
ReadOnlyProperty { ctx, prop -> ctx.dependency(dependencyKey ?: prop.name) }
}
/**
* Function used to declare a dependency to a [path] and delegate access to its value within a
* [ValidationContext].
*
* If the dependency could not be found in the form during the execution of the validation, then
* accessing this dependency will throw [DependencyNotFound].
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.dependencyKey: Type by dependency(path)
* ```
*
* Validation dependency paths may contain a single recursive wildcard at the end, indicating
* that the validation should be reevaluated by the [form manager][FormManager] whenever a child
* value of the dependency changes. Otherwise, a dependency must contain no wildcards.
*/
protected inline fun dependency(
path: String,
dependencyKey: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = dependency(Path(path), dependencyKey)
/**
* Function used to declare a dependency to an external context and delegate access to its value
* within a [ValidationContext].
*
* If the external context could not be found during the execution of the validation, then
* accessing this external context will return `null`.
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.externalContextName: Type? by externalContextOrNull()
* ```
*/
protected fun externalContextOrNull(
externalContextName: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = PropertyDelegateProvider { validation, property ->
validation.addExternalContextDependency(externalContextName ?: property.name)
// Return a delegate to the external context dependency value within a [ValidationContext]
ReadOnlyProperty { ctx, prop ->
ctx.externalContextOrNull(externalContextName ?: prop.name)
}
}
/**
* Function used to declare a dependency to an external context and delegate access to its value
* within a [ValidationContext].
*
* If the external context could not be found during the execution of the validation, then
* accessing this external context will throw [ExternalContextNotFound].
*
* It should be used as follows:
* ```kotlin
* private val ValidationContext.externalContextName: Type by externalContext()
* ```
*/
protected fun externalContext(
externalContextName: String? = null
): PropertyDelegateProvider<
Validation<@UnsafeVariance T>,
ReadOnlyProperty
> = PropertyDelegateProvider { validation, property ->
validation.addExternalContextDependency(externalContextName ?: property.name)
// Return a delegate to the external context dependency value within a [ValidationContext]
ReadOnlyProperty { ctx, prop -> ctx.externalContext(externalContextName ?: prop.name) }
}
/**
* Runs the validation 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.validate(): Flow
// Default [toString] implementation
public override fun toString(): String = this::class.simpleName ?: super.toString()
}
/**
* Function used to update the state of a stateful validation.
*
* This function is used to update the validation's state given the previous `state` and the `event`
* whose path matches the path being observed.
*/
public typealias UpdateStateFn = suspend (state: TState, event: ValueEvent) -> TState
/**
* Observer used within a stateful validation.
*
* Defines a path to observe and a function to update the state of the validation whenever an event
* with a path matching the one being observed occurs.
*/
public open class Observer(
/** Path to observe. */
public val toObserve: Path,
/**
* Updates the validation's state given the previous state and the event whose path matches the
* path being observed.
*
* This function must return the new validation state which, unless it hasn't changed, should be
* different ([equals]-wise) from the previous validation state (as such, when using an object
* as state, a new instance should be returned when the state is updated). If the new state
* differs from the old one (using [equals]), the value will be revalidated by the
* [form manager] [FormManager] using [validateFromState][StatefulValidation.validateFromState].
*/
public val updateState: UpdateStateFn
)
/**
* Validation for values of type [T] where the [form manager][FormManager] maintains a validation
* state of type [TState] for each managed value.
*
* Stateful validations are useful when validations require an expensive computation over data and
* if it is possible to save the result of such expensive computation and tweak it as data changes
* instead of running the expensive computation all over again.
*
* As an example, imagine that we have a list of people and that we want to validate that the
* average age of all people isn't over a certain age. Instead of iterating over the whole list
* every time a person is added or removed, we can use a stateful validation to save, as state, the
* sum of all ages, and simply tweak this sum as people are added or removed. Having access to the
* sum of all ages as state allows us to implement the validation function with a complexity of O(1)
* as opposed to O(N).
*
* The following snippet implements the example above of making sure that the average age of all
* people doesn't surpass a dependency `maxAvgAge`:
* ```kotlin
* object AvgAgeNotOverMax : StatefulValidation, Int>() {
* private val ValidationContext.maxAvgAge: Int by dependency("../maxAvgAge")
*
* override suspend fun ValidationContext.initState(value: List): Int =
* value.fold(0) { sum, person -> sum + person.age }
*
* private val ageObserver by observe("∗/age") { agesSum, event ->
* when (event) {
* is ValueEvent.Init -> agesSum + event.newValue
* is ValueEvent.Change -> agesSum + event.newValue - event.oldValue
* is ValueEvent.Destroy -> agesSum - event.oldValue
* else -> agesSum
* }
* }
*
* override fun ValidationContext.validateFromState(value: List, state: Int) = flow {
* val avgAge = state / value.size
* if (avgAge > maxAvgAge) {
* emit(ValidationError("avgAgeOverMax"))
* }
* }
* }
* ```
*/
@JsName("StatefulValidationKt")
public abstract class StatefulValidation : Validation() {
/**
* List of observers.
*
* Each observer contains a path to be observed and a function to update the state of the
* validation, which will be called whenever an event with a path matching the one being
* observed occurs.
*/
public open val observers: List> = mutableListOf()
/**
* Adds an [observer] to the path [observer.toObserve][Observer.toObserve] to update the
* validation state via [observer.updateState][Observer.updateState] whenever an event with a
* path matching [observer.toObserve][Observer.toObserve] occurs.
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addObserver(observer: Observer) {
@Suppress("UNCHECKED_CAST")
(observers as MutableList>) += observer
}
/**
* Adds an observer to the path [pathToObserve] to update the validation state via [updateState]
* whenever an event with a path matching [pathToObserve] occurs.
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addObserver(
pathToObserve: Path,
@BuilderInference updateState: UpdateStateFn
): Unit = addObserver(Observer(pathToObserve, updateState))
/**
* Adds an observer to the path [pathToObserve] to update the validation state via [updateState]
* whenever an event with a path matching [pathToObserve] occurs.
*
* **NOTE**: This method must only be called during the initialization process of the class.
*/
protected fun addObserver(
pathToObserve: String,
@BuilderInference updateState: UpdateStateFn
): Unit = addObserver(Path(pathToObserve), updateState)
/**
* Function used to observe the path [pathToObserve] and update the validation state via
* [updateState] whenever an event with a path matching [pathToObserve] occurs.
*
* It should be used as follows:
* ```kotlin
* private val observer by observe(path) { state, event ->
* when (event) {
* is ValueEvent.Init -> // Compute new state
* is ValueEvent.Change -> // Compute new state
* is ValueEvent.Destroy -> // Compute new state
*
* // These events only occur when observing a collection:
* is ValueEvent.Add -> // Compute new state
* is ValueEvent.Remove -> // Compute new state
* }
* }
* ```
*
* The provided function must return the new validation state which, unless it hasn't changed,
* should be different ([equals]-wise) from the previous validation state (as such, when using
* an object as state, a new instance should be returned when the state is updated). If the new
* state differs from the old one (using [equals]), the value will need to be revalidated by the
* [form manager][FormManager] using [validateFromState].
*/
protected fun observe(
pathToObserve: Path,
@BuilderInference updateState: UpdateStateFn
): PropertyDelegateProvider<
StatefulValidation<@UnsafeVariance T, TState>,
ReadOnlyProperty, Observer>
> = PropertyDelegateProvider { validation, _ ->
val observer = Observer(pathToObserve, updateState)
validation.addObserver(observer)
ReadOnlyProperty { _, _ -> observer }
}
/**
* Function used to observe the path [pathToObserve] and update the validation state via
* [updateState] whenever an event with a path matching [pathToObserve] occurs.
*
* It should be used as follows:
* ```kotlin
* private val observer by observe(path) { state, event ->
* when (event) {
* is ValueEvent.Init -> // Compute new state
* is ValueEvent.Change -> // Compute new state
* is ValueEvent.Destroy -> // Compute new state
*
* // These events only occur when observing a collection:
* is ValueEvent.Add -> // Compute new state
* is ValueEvent.Remove -> // Compute new state
* }
* }
* ```
*
* The provided function must return the new validation state which, unless it hasn't changed,
* should be different ([equals]-wise) from the previous validation state (as such, when using
* an object as state, a new instance should be returned when the state is updated). If the new
* state differs from the old one (using [equals]), the value will need to be revalidated by the
* [form manager][FormManager] using [validateFromState].
*/
protected fun observe(
pathToObserve: String,
@BuilderInference updateState: UpdateStateFn
): PropertyDelegateProvider<
StatefulValidation<@UnsafeVariance T, TState>,
ReadOnlyProperty, Observer>
> = observe(Path(pathToObserve), updateState)
/**
* Initialises and returns the validation's state, given the [value] to validate within a
* [ValidationContext] containing the values of all declared dependencies.
*
* This method is called by the [form manager][FormManager] whenever a new value that needs to
* be validated by this validation is initialised.
*/
public abstract suspend fun ValidationContext.initState(): TState
/**
* Destroys the validation [state].
*
* Called by the [form manager][FormManager] whenever a state is no longer needed (i.e. when a
* newer state has been produced or when the value being validated has been destroyed).
*/
public open suspend fun destroyState(state: TState): Unit = Unit
/**
* Runs the validation, 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.
*
* This method is called by the [form manager][FormManager] instead of [validate] when
* validating stateful validations.
*
* Note that the form utilities [validate][io.kform.validate] function (and, by consequence, the
* [form validator][FormValidator]) does **not** maintain validation states and, as such, does
* **not** ever explicitly call any methods specific to stateful validations like
* [validateFromState]. This is why we provide the following default implementation of
* [validate]:
* ```kotlin
* override fun ValidationContext.validate(): Flow = flow {
* emitAll(validateFromState(initState(value)))
* }
* ```
*/
public abstract fun ValidationContext.validateFromState(state: TState): Flow
// Default [validate] implementation for stateful validations.
override fun ValidationContext.validate(): Flow = flow {
emitAll(validateFromState(initState()))
}
}