All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.test.ValidationRunner.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
package io.kform.test

import io.kform.*
import io.kform.internal.schemaInfoImpl
import io.kform.internal.validateValidation
import io.kform.internal.valueInfoImpl
import io.kform.schemas.AnySchema
import kotlin.jvm.JvmOverloads
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emitAll
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.single

/**
 * Runs a given [validation] within a form with schema [formSchema] and returns a flow over its
 * issues.
 *
 * It is necessary to provide all information required to run the validation: the [path] of the
 * value being validated, the [value] itself, the [values of the dependencies][dependencyValues] (by
 * dependency name), and the values of the [external contexts][externalContexts] that the validation
 * depends on.
 *
 * If the validation depends on values within the value being validated, then these may be omitted
 * from the provided [dependencyValues].
 *
 * Otherwise, missing dependencies or external contexts will be passed to the validation as `null`,
 * in which case the validation may or may not throw depending on its implementation.
 *
 * Exceptions thrown while running the validation will not be caught by this function. As opposed to
 * how a [form validator][FormValidator] or [form manager][FormManager] operate, exceptions are
 * **not** wrapped in a [ValidationExceptionError] nor emitted as part of the issues flow.
 *
 * @throws InvalidPathException If [path] matches no schemas or contains fragments other than ids.
 * @throws InvalidDependencyPathException If [validation] contains an invalid dependency.
 * @throws IllegalArgumentException When providing unknown names for [dependencyValues] or
 *   [externalContexts] or when providing mismatching dependency values for dependencies within
 *   [value].
 */
@Suppress("UNCHECKED_CAST")
@JvmOverloads
public fun  runValidation(
    formSchema: Schema<*>,
    validation: Validation,
    value: T,
    path: Path = AbsolutePath.ROOT,
    dependencyValues: Map? = null,
    externalContexts: ExternalContexts? = null
): Flow =
    path.toAbsolutePath().let { absolutePath ->
        // Validate path
        if (!isValidPath(formSchema, absolutePath)) {
            throw InvalidPathException(absolutePath, "No schema matches this path.")
        }
        if (absolutePath.fragments.any { it !is AbsolutePathFragment.Id }) {
            throw InvalidPathException(
                absolutePath,
                "The path of the value being validated must only contain ids."
            )
        }
        // Validate keys of provided dependencies
        for (key in dependencyValues?.keys ?: emptyList()) {
            require(key in validation.dependencies) {
                "No dependency with key '$key' was found in the validation."
            }
        }
        // Validate names of provided external contexts
        for (name in externalContexts?.keys ?: emptyList()) {
            require(name in validation.externalContextDependencies) {
                "No external context dependency named '$name' was found in the validation."
            }
        }
        // Validate validation dependencies
        validateValidation(formSchema, absolutePath, validation)

        flow {
            val schemaInfo = schemaInfo(formSchema, absolutePath).single() as SchemaInfo
            val validationContext =
                ValidationContext(
                    value,
                    schemaInfo.schema,
                    absolutePath,
                    schemaInfo.path,
                    dependenciesInfo(
                        formSchema,
                        validation,
                        value,
                        schemaInfo.schema,
                        absolutePath,
                        dependencyValues
                    ),
                    validation.externalContextDependencies.associateWith {
                        externalContexts?.get(it)
                    }
                )

            validation.run { emitAll(validationContext.validate()) }
        }
    }

/**
 * Runs a given [validation] within a form with schema [formSchema] and returns a flow over its
 * issues.
 *
 * It is necessary to provide all information required to run the validation: the [path] of the
 * value being validated, the [value] itself, the [values of the dependencies][dependencyValues] (by
 * dependency name), and the values of the [external contexts][externalContexts] that the validation
 * depends on.
 *
 * If the validation depends on values within the value being validated, then these may be omitted
 * from the provided [dependencyValues].
 *
 * Otherwise, missing dependencies or external contexts will be passed to the validation as `null`,
 * in which case the validation may or may not throw depending on its implementation.
 *
 * Exceptions thrown while running the validation will not be caught by this function. As opposed to
 * how a [form validator][FormValidator] or [form manager][FormManager] operate, exceptions are
 * **not** wrapped in a [ValidationExceptionError] nor emitted as part of the issues flow.
 *
 * @throws InvalidPathException If [path] matches no schemas or contains fragments other than ids.
 * @throws InvalidDependencyPathException If [validation] contains an invalid dependency.
 * @throws IllegalArgumentException When providing unknown names for [dependencyValues] or
 *   [externalContexts] or when providing mismatching dependency values for dependencies within
 *   [value].
 */
@JvmOverloads
public fun  runValidation(
    formSchema: Schema<*>,
    validation: Validation,
    value: T,
    path: String,
    dependencyValues: Map? = null,
    externalContexts: ExternalContexts? = null
): Flow =
    runValidation(
        formSchema,
        validation,
        value,
        AbsolutePath(path),
        dependencyValues,
        externalContexts
    )

/**
 * Runs a given [validation] within a form with a schema of type [AnySchema] and returns a flow over
 * its issues.
 *
 * It is necessary to provide all information required to run the validation: the [path] of the
 * value being validated, the [value] itself, the [values of the dependencies][dependencyValues] (by
 * dependency name), and the values of the [external contexts][externalContexts] that the validation
 * depends on.
 *
 * If the validation depends on values within the value being validated, then these may be omitted
 * from the provided [dependencyValues].
 *
 * Otherwise, missing dependencies or external contexts will be passed to the validation as `null`,
 * in which case the validation may or may not throw depending on its implementation.
 *
 * Exceptions thrown while running the validation will not be caught by this function. As opposed to
 * how a [form validator][FormValidator] or [form manager][FormManager] operate, exceptions are
 * **not** wrapped in a [ValidationExceptionError] nor emitted as part of the issues flow.
 *
 * @throws InvalidPathException If [path] matches no schemas or contains fragments other than ids.
 * @throws InvalidDependencyPathException If [validation] contains an invalid dependency.
 * @throws IllegalArgumentException When providing unknown names for [dependencyValues] or
 *   [externalContexts] or when providing mismatching dependency values for dependencies within
 *   [value].
 */
@JvmOverloads
public fun  runValidation(
    validation: Validation,
    value: T,
    path: Path = AbsolutePath.ROOT,
    dependencyValues: Map? = null,
    externalContexts: ExternalContexts? = null
): Flow =
    runValidation(AnySchema(), validation, value, path, dependencyValues, externalContexts)

/**
 * Runs a given [validation] within a form with a schema of type [AnySchema] and returns a flow over
 * its issues.
 *
 * It is necessary to provide all information required to run the validation: the [path] of the
 * value being validated, the [value] itself, the [values of the dependencies][dependencyValues] (by
 * dependency name), and the values of the [external contexts][externalContexts] that the validation
 * depends on.
 *
 * If the validation depends on values within the value being validated, then these may be omitted
 * from the provided [dependencyValues].
 *
 * Otherwise, missing dependencies or external contexts will be passed to the validation as `null`,
 * in which case the validation may or may not throw depending on its implementation.
 *
 * Exceptions thrown while running the validation will not be caught by this function. As opposed to
 * how a [form validator][FormValidator] or [form manager][FormManager] operate, exceptions are
 * **not** wrapped in a [ValidationExceptionError] nor emitted as part of the issues flow.
 *
 * @throws InvalidPathException If [path] matches no schemas or contains fragments other than ids.
 * @throws InvalidDependencyPathException If [validation] contains an invalid dependency.
 * @throws IllegalArgumentException When providing unknown names for [dependencyValues] or
 *   [externalContexts] or when providing mismatching dependency values for dependencies within
 *   [value].
 */
@JvmOverloads
public fun  runValidation(
    validation: Validation,
    value: T,
    path: String,
    dependencyValues: Map? = null,
    externalContexts: ExternalContexts? = null
): Flow =
    runValidation(validation, value, AbsolutePath(path), dependencyValues, externalContexts)

/**
 * Returns the value info of the validation dependencies as needed by [validation] when validating a
 * [value] with [schema] at [absolutePath].
 */
private suspend fun  dependenciesInfo(
    formSchema: Schema<*>,
    validation: Validation,
    value: T,
    schema: Schema,
    absolutePath: AbsolutePath,
    dependencyValues: Map?
): Map?> =
    validation.dependencies.mapValues { (name, dep) ->
        val resolvedDep = absolutePath.resolve(dep.path)
        val resolvedDepWithoutDescendants =
            if (resolvedDep.lastFragment is AbsolutePathFragment.RecursiveWildcard)
                resolvedDep.parent()
            else resolvedDep

        val depValue =
            // When the dependency is a descendant of [value], then we can just extract the
            // dependency value from it; otherwise, the dependency value should be provided
            if (
                absolutePath
                    .append(AbsolutePathFragment.RecursiveWildcard)
                    .contains(resolvedDepWithoutDescendants)
            )
                valueInfoImpl(
                        schema,
                        value,
                        AbsolutePath(resolvedDepWithoutDescendants.relativeTo(absolutePath))
                    )
                    .single()
                    .value
                    // If the dependency value is explicitely provided but differs from the one
                    // obtained from [value] then we throw, since otherwise we'd have to pick one of
                    // them as the source of truth, which seems arbitrary
                    .also {
                        require(
                            dependencyValues == null ||
                                name !in dependencyValues ||
                                dependencyValues[name] == it
                        ) {
                            "Provided value of dependency '$name' <${dependencyValues!![name]}> " +
                                "does not match its counterpart value within the value being " +
                                "validated <$it>.\n" +
                                "Because dependency '$name' is part of the value being " +
                                "validated, you should either omit it from the provided " +
                                "dependencies map or make sure that its value matches its " +
                                "counterpart within the value being validated."
                        }
                    }
            else dependencyValues?.get(name)

        val depInfo = schemaInfoImpl(formSchema, resolvedDepWithoutDescendants).single()
        @Suppress("UNCHECKED_CAST")
        ValueInfo(
            depValue,
            depInfo.schema as Schema,
            resolvedDepWithoutDescendants,
            depInfo.path
        )
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy