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

commonMain.FormUtils.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
@file:JvmName("FormUtils")

package io.kform

import io.github.oshai.kotlinlogging.KotlinLogging
import io.kform.internal.dependenciesInfo
import io.kform.internal.schemaInfoImpl
import io.kform.internal.validateValidation
import io.kform.internal.valueInfoImpl
import kotlin.jvm.JvmName
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.flow.*

/** Logger used by the form util. */
private val logger = KotlinLogging.logger {}

/** External contexts. */
public typealias ExternalContexts = Map

/** External validations. */
public typealias ExternalValidations = Map>>

/**
 * Validates that the provided [path] points to a schema of [formSchema].
 *
 * @throws InvalidPathException If the path is invalid.
 */
public fun validatePath(formSchema: Schema<*>, path: Path): Unit =
    path.toAbsolutePath().let { absolutePath ->
        if (!schemaInfoImpl(formSchema, absolutePath).any()) {
            throw InvalidPathException(absolutePath, "No schema matches this path.")
        }
    }

/**
 * Validates all validations of the provided [formSchema] by checking that all validation
 * dependencies are valid (i.e. that they point to valid locations and have valid types).
 *
 * @throws InvalidDependencyPathException If a validation has an invalid dependency path.
 * @throws InvalidDependencyTypeException If a validation has an invalid dependency type.
 */
public fun validateSchemaValidations(formSchema: Schema<*>) {
    for ((schema, path) in schemaInfo(formSchema, AbsolutePath.MATCH_ALL)) {
        for (validation in schema.validations) {
            validateValidation(formSchema, path, validation)
        }
    }
}

/**
 * Validates the provided [external validations][externalValidations] in the context of the given
 * [formSchema] by checking that all validation dependencies are valid (i.e. that they point to
 * valid locations and have valid types).
 *
 * @throws InvalidPathException If an external validation path is invalid.
 * @throws InvalidDependencyPathException If a validation has an invalid dependency path.
 * @throws InvalidDependencyTypeException If a validation has an invalid dependency type.
 */
public fun validateExternalValidations(
    formSchema: Schema<*>,
    externalValidations: Map>>
) {
    for ((path, validations) in externalValidations) {
        val absolutePath = path.toAbsolutePath()
        validatePath(formSchema, absolutePath)
        for (validation in validations) {
            validateValidation(formSchema, absolutePath, validation)
        }
    }
}

/**
 * Returns whether there exists at least one schema within [formSchema] matching [path].
 *
 * Paths that match no schemas are deemed invalid, and most functions called with them will throw.
 */
public fun isValidPath(formSchema: Schema<*>, path: Path): Boolean =
    schemaInfoImpl(formSchema, AbsolutePath(path)).any()

/**
 * Returns whether there exists at least one schema within [formSchema] matching [path].
 *
 * Paths that match no schemas are deemed invalid, and most functions called with them will throw.
 */
public fun isValidPath(formSchema: Schema<*>, path: String): Boolean =
    isValidPath(formSchema, AbsolutePath(path))

/**
 * Returns a sequence of information about the schemas within [formSchema] matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun schemaInfo(
    formSchema: Schema<*>,
    path: Path = AbsolutePath.MATCH_ALL
): Sequence> =
    path.toAbsolutePath().let { absolutePath ->
        validatePath(formSchema, absolutePath)
        schemaInfoImpl(formSchema, absolutePath)
    }

/**
 * Returns a sequence of information about the schemas within [formSchema] matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun schemaInfo(formSchema: Schema<*>, path: String): Sequence> =
    schemaInfo(formSchema, AbsolutePath(path))

/**
 * Returns the single schema within [formSchema] matching [path].
 *
 * To get information about all schemas matching a path use [schemaInfo] instead.
 *
 * @throws InvalidPathException If [path] matches no schemas or more than one schema.
 */
public fun schema(formSchema: Schema<*>, path: Path): Schema<*> =
    path.toAbsolutePath().let { absolutePath ->
        try {
            schemaInfo(formSchema, absolutePath).single().schema
        } catch (ex: IllegalArgumentException) {
            throw InvalidPathException(absolutePath, "Path matches more than one schema.")
        }
    }

/**
 * Returns the single schema within [formSchema] matching [path].
 *
 * To get information about all schemas matching a path use [schemaInfo] instead.
 *
 * @throws InvalidPathException If [path] matches no schemas or more than one schema.
 */
public fun schema(formSchema: Schema<*>, path: String): Schema<*> =
    schema(formSchema, AbsolutePath(path))

/**
 * Returns a flow of information about the parts of the form value [formValue] (with schema
 * [formSchema]) matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun  valueInfo(
    formSchema: Schema,
    formValue: T,
    path: Path = AbsolutePath.MATCH_ALL
): Flow> =
    path.toAbsolutePath().let { absolutePath ->
        validatePath(formSchema, absolutePath)
        valueInfoImpl(formSchema, formValue, absolutePath)
    }

/**
 * Returns a flow of information about the parts of the form value [formValue] (with schema
 * [formSchema]) matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun  valueInfo(formSchema: Schema, formValue: T, path: String): Flow> =
    valueInfo(formSchema, formValue, AbsolutePath(path))

/**
 * Returns whether there exists a part of the form value [formValue] (with schema [formSchema])
 * matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public suspend fun  has(formSchema: Schema, formValue: T, path: Path): Boolean =
    valueInfo(formSchema, formValue, path).firstOrNull() != null

/**
 * Returns whether there exists a part of the form value [formValue] (with schema [formSchema])
 * matching [path].
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public suspend fun  has(formSchema: Schema, formValue: T, path: String): Boolean =
    has(formSchema, formValue, AbsolutePath(path))

/**
 * Returns the single part of the form value [formValue] (with schema [formSchema]) matching [path].
 *
 * To get information about multiple parts of a form value at once, use [valueInfo] instead.
 *
 * @throws InvalidPathException If [path] contains wildcards or matches no schemas.
 * @throws IllegalStateException If [path] matches more than one value.
 * @throws NoSuchElementException If no part of [formValue] matches [path].
 */
public suspend fun  get(formSchema: Schema, formValue: T, path: Path): Any? =
    path.toAbsolutePath().let { absolutePath ->
        if (absolutePath.hasAnyWildcard()) {
            throw InvalidPathException(absolutePath, "Path cannot contain wildcards.")
        }
        validatePath(formSchema, absolutePath)
        valueInfoImpl(formSchema, formValue, absolutePath).single().value
    }

/**
 * Returns the single part of the form value [formValue] (with schema [formSchema]) matching [path].
 *
 * To get information about multiple parts of a form value at once, use [valueInfo] instead.
 *
 * @throws InvalidPathException If [path] contains wildcards or matches no schemas.
 * @throws IllegalStateException If [path] matches more than one value.
 * @throws NoSuchElementException If no part of [formValue] matches [path].
 */
public suspend fun  get(formSchema: Schema, formValue: T, path: String): Any? =
    get(formSchema, formValue, AbsolutePath(path))

/**
 * Returns a clone (deep copy) of the single part of the form value [formValue] (with schema
 * [formSchema]) matching [path].
 *
 * @throws InvalidPathException If [path] contains wildcards or matches no schemas.
 * @throws IllegalStateException If [path] matches more than one value.
 * @throws NoSuchElementException If no part of [formValue] matches [path].
 */
public suspend fun  getClone(formSchema: Schema, formValue: T, path: Path): Any? =
    path.toAbsolutePath().let { absolutePath ->
        if (absolutePath.hasAnyWildcard()) {
            throw InvalidPathException(absolutePath, "Path cannot contain wildcards.")
        }
        validatePath(formSchema, absolutePath)
        @Suppress("UNCHECKED_CAST")
        val info = valueInfoImpl(formSchema, formValue, absolutePath).single() as ValueInfo
        info.schema.clone(info.value)
    }

/**
 * Returns a clone (deep copy) of the single part of the form value [formValue] (with schema
 * [formSchema]) matching [path].
 *
 * @throws InvalidPathException If [path] contains wildcards or matches no schemas.
 * @throws IllegalStateException If [path] matches more than one value.
 * @throws NoSuchElementException If no part of [formValue] matches [path].
 */
public suspend fun  getClone(formSchema: Schema, formValue: T, path: String): Any? =
    getClone(formSchema, formValue, AbsolutePath(path))

/**
 * Validates the parts of the form value [formValue] matching [path] against [formSchema]. Returns a
 * flow of found [validation issues][LocatedValidationIssue].
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun  validate(
    formSchema: Schema,
    formValue: T,
    path: Path,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Flow =
    path.toAbsolutePath().let { absolutePath ->
        validatePath(formSchema, absolutePath)
        flow {
            var hasErrors = false

            // Run schema validations
            valueInfoImpl(formSchema, formValue, absolutePath).collect { info ->
                for (validation in info.schema.validations) {
                    @Suppress("UNCHECKED_CAST")
                    runValidation(
                            formSchema,
                            formValue,
                            externalContexts,
                            validation as Validation,
                            info
                        )
                        .collect { issue ->
                            if (issue.severity == ValidationIssueSeverity.Error) {
                                hasErrors = true
                            }
                            emit(issue)
                        }
                }
            }

            // Run external validations
            if (!hasErrors && externalValidations != null) {
                for ((validatingPath, validations) in externalValidations) {
                    valueInfoImpl(formSchema, formValue, validatingPath.toAbsolutePath()).collect {
                        info ->
                        for (validation in validations) {
                            emitAll(
                                @Suppress("UNCHECKED_CAST")
                                runValidation(
                                    formSchema,
                                    formValue,
                                    externalContexts,
                                    validation as Validation,
                                    info
                                )
                            )
                        }
                    }
                }
            }
        }
    }

/**
 * Validates the parts of the form value [formValue] matching [path] against [formSchema]. Returns a
 * flow of found [validation issues][LocatedValidationIssue].
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public fun  validate(
    formSchema: Schema,
    formValue: T,
    path: String,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Flow =
    validate(formSchema, formValue, AbsolutePath(path), externalContexts, externalValidations)

/**
 * Validates all parts of the form value [formValue] against [formSchema]. Returns a flow of found
 * [validation issues][LocatedValidationIssue].
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 */
public fun  validate(
    formSchema: Schema,
    formValue: T,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Flow =
    validate(formSchema, formValue, AbsolutePath.MATCH_ALL, externalContexts, externalValidations)

/** Runs a single validation and returns a flow over its validation issues. */
private suspend fun  runValidation(
    formSchema: Schema,
    formValue: T,
    externalContexts: ExternalContexts?,
    validation: Validation,
    info: ValueInfo
): Flow = flow {
    val validationContext =
        ValidationContext(
            info.value,
            info.schema,
            info.path,
            info.schemaPath,
            dependenciesInfo(formSchema, formValue, info.path, validation.dependencies),
            validation.externalContextDependencies.associateWith { externalContexts?.get(it) }
        )

    suspend fun handleException(ex: Throwable) {
        if (ex !is CancellationException) {
            logger.error(ex) { "At '${info.path}': Failed to run validation '$validation'" }
            emit(LocatedValidationIssue(info.path, validation, ValidationExceptionError(ex)))
        }
    }

    // Wrap code in `try/catch` since we're calling user code (`validate`) and an error may
    // occur when **creating** the flow. Typically, however, if an error occurs, it will be
    // within the flow.
    var issuesFlow: Flow? = null
    try {
        issuesFlow = validation.run { validationContext.validate() }
    } catch (ex: Throwable) {
        handleException(ex)
    }
    // If the validation throws, we still emit the issues up to the point it threw, plus a
    // "validation exception error" issue
    issuesFlow
        ?.catch { ex -> handleException(ex) }
        ?.collect { issue -> emit(LocatedValidationIssue(info.path, validation, issue)) }
}

/**
 * Returns whether the parts of the form value [formValue] (with schema [formSchema]) matching
 * [path] are valid according to their schemas. These parts are said to be valid if they contain no
 * validation errors.
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public suspend fun  isValid(
    formSchema: Schema,
    formValue: T,
    path: Path,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Boolean =
    validate(formSchema, formValue, path, externalContexts, externalValidations).containsNoErrors()

/**
 * Returns whether the parts of the form value [formValue] (with schema [formSchema]) matching
 * [path] are valid according to their schemas. These parts are said to be valid if they contain no
 * validation errors.
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 *
 * @throws InvalidPathException If [path] matches no schemas.
 */
public suspend fun  isValid(
    formSchema: Schema,
    formValue: T,
    path: String,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Boolean =
    isValid(formSchema, formValue, AbsolutePath(path), externalContexts, externalValidations)

/**
 * Returns whether all parts of the form value [formValue] (with schema [formSchema]) are valid
 * according to their schemas. These parts are said to be valid if they contain no validation
 * errors.
 *
 * A map of [external contexts][externalContexts] may be provided for validations that depend on
 * them.
 *
 * [External validations][externalValidations] may be provided to further validate the form against
 * validations not present in the schema.
 */
public suspend fun  isValid(
    formSchema: Schema,
    formValue: T,
    externalContexts: ExternalContexts? = null,
    externalValidations: ExternalValidations? = null
): Boolean =
    isValid(formSchema, formValue, AbsolutePath.MATCH_ALL, externalContexts, externalValidations)




© 2015 - 2024 Weber Informatics LLC | Privacy Policy