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: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)