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

commonMain.validations.UniqueItems.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
@file:OptIn(ExperimentalTypeInference::class)

package io.kform.validations

import io.kform.Validation
import io.kform.ValidationContext
import io.kform.ValidationIssue
import io.kform.ValidationIssueSeverity
import io.kform.datatypes.Table
import io.kform.validations.UniqueItems.Companion.DEFAULT_CODE
import io.kform.validations.UniqueItemsBy.Companion.DEFAULT_CODE
import kotlin.experimental.ExperimentalTypeInference
import kotlin.jvm.JvmOverloads
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

/**
 * Validation that ensures that a value does not contain repeated items, where the uniqueness of an
 * item is determined by its key as returned by [selector]. Values of type [Collection], [Array]
 * (including variants), and [Table] are supported.
 *
 * Depending on [emitAllRepetitions], an issue with the provided [code] (defaults to [DEFAULT_CODE])
 * is emitted for each pair of repeated items (when `true`), or only for the first found repetition
 * (when `false`). [emitAllRepetitions] defaults to `true`.
 *
 * Each emitted issue has a `firstIndex` and `secondIndex` data properties with the two indices of
 * the conflicting items.
 *
 * [Pair]s, [Triple]s, and [List]s work well to represent composite keys. E.g. say that you have a
 * value of items that should be unique in respect to their fields `A` and `B`; you can use
 * `UniqueBy` as such:
 * ```kotlin
 * UniqueBy { Pair(it.A, it.B) }
 * ```
 *
 * When the result of calling [selector] on an item is `null`, that item is always considered
 * unique. I.e. if `selector(A) == null` and `selector(B) == null`, then `A` is considered different
 * to `B`.
 *
 * If you wish to treat multiple `null` values as equal to one another, consider wrapping said
 * values in an object (e.g. a list with a single item).
 *
 * This validation is **not** stateful and depends on all descendants of the value.
 *
 * @property emitAllRepetitions Whether to emit an issue per each repeated item or only for the
 *   first repetition.
 * @property code Issue code to use when two items of the value are repeated.
 * @property severity Severity of the issue emitted when two items of the value are repeated.
 * @property selector Selector function used to specify the key of an item of the value.
 */
public open class UniqueItemsBy
@JvmOverloads
constructor(
    public val emitAllRepetitions: Boolean = true,
    public val code: String = DEFAULT_CODE,
    public val severity: ValidationIssueSeverity = ValidationIssueSeverity.Error,
    @BuilderInference public val selector: (item: T) -> TKey?,
) : Validation() {
    override fun toString(): String = "UniqueItemsBy"

    override val dependsOnDescendants: Boolean = true

    @Suppress("UNCHECKED_CAST")
    override fun ValidationContext.validate(): Flow = flow {
        val map = HashMap(size(value))
        for ((i, el) in iterableWithIndex(value)) {
            val key = selector(el as T)
            if (key != null) {
                val conflict = map[key]
                if (conflict != null) {
                    emit(
                        ValidationIssue(
                            code,
                            severity,
                            mapOf("firstIndex" to "$conflict", "secondIndex" to "$i")
                        )
                    )
                    if (!emitAllRepetitions) {
                        break
                    }
                } else {
                    map[key] = i
                }
            }
        }
    }

    public companion object {
        /** Default issue code representing that two items of the value are repeated. */
        public const val DEFAULT_CODE: String = "itemsRepeated"
    }
}

/**
 * Validation that ensures that a value does not contain repeated items. Values of type
 * [Collection], [Array] (including variants), and [Table] are supported.
 *
 * Depending on [emitAllRepetitions], an issue with the provided [code] (defaults to [DEFAULT_CODE])
 * is emitted for each pair of repeated items (when `true`), or only for the first found repetition
 * (when `false`). [emitAllRepetitions] defaults to `true`.
 *
 * Each emitted issue has a `firstIndex` and `secondIndex` data properties with the two indices of
 * the conflicting items.
 *
 * Depending on [treatNullAsUnique], `null` values can be considered unique. I.e. when `true` (the
 * default), the value `listOf(null, null)` is considered to **not** contain repeated items.
 *
 * This validation is **not** stateful and depends on all descendants of the value.
 *
 * @param emitAllRepetitions Whether to emit an issue per each repeated item or only for the first
 *   repetition.
 * @param code Issue code to use when two items of the value are repeated.
 * @param severity Severity of the issue emitted when two items of the value are repeated.
 * @property treatNullAsUnique Whether to treat `null` values as being unique.
 */
public open class UniqueItems
@JvmOverloads
constructor(
    emitAllRepetitions: Boolean = true,
    public val treatNullAsUnique: Boolean = true,
    code: String = DEFAULT_CODE,
    severity: ValidationIssueSeverity = ValidationIssueSeverity.Error,
) :
    UniqueItemsBy(
        emitAllRepetitions,
        code,
        severity,
        { if (treatNullAsUnique) it else listOf(it) }
    ) {
    override fun toString(): String = "UniqueItems"

    public companion object {
        /** Default issue code representing that two items of the value are repeated. */
        public const val DEFAULT_CODE: String = "itemsRepeated"
    }
}

// private typealias StatefulUniqueState = Pair>, MutableList>
//
// public open class StatefulUniqueBy(
//    public val code: String = DEFAULT_CODE,
//    public val emitAllRepetitions: Boolean = true,
//    @BuilderInference public val selector: (T) -> TKey?,
// ) : StatefulValidation, StatefulUniqueState>() {
//    override suspend fun ValidationContext.initState(value: Collection):
// StatefulUniqueState
// {
//        val keyIndices = HashMap>(value.size)
//        val repeatedKeys = mutableListOf()
//        for ((i, el) in value.withIndex()) {
//            val key = selector(el)
//            if (key != null) {
//                val indices = keyIndices.getOrPut(key) { mutableListOf() }
//                indices += i
//                if (indices.size == 2) {
//                    repeatedKeys += key
//                }
//            }
//        }
//        return Pair(keyIndices, repeatedKeys)
//    }
//
//    override fun ValidationContext.validateFromState(
//        value: Collection,
//        state: StatefulUniqueState
//    ): Flow = flow {
//        val (keyIndices, repeatedKeys) = state
//        for (key in repeatedKeys) {
//            val indices = keyIndices[key] ?: error("Invalid 'StatefulUniqueBy' validation state.")
//            for (i in 0.. -> value.size
        is Array<*> -> value.size
        is Table<*> -> value.size
        // Array variants
        is BooleanArray -> value.size
        is ByteArray -> value.size
        is CharArray -> value.size
        is DoubleArray -> value.size
        is FloatArray -> value.size
        is IntArray -> value.size
        is LongArray -> value.size
        is ShortArray -> value.size
        else ->
            error(
                "Unsupported value type: supported types are `Collection`, `Array` (and " +
                    "variants), and `Table`."
            )
    }

/** Iterable with index over a [Collection], [Array] (including variants), or [Table]. */
private fun iterableWithIndex(value: Any): Iterable> =
    when (value) {
        is Collection<*> -> value.withIndex()
        is Array<*> -> value.withIndex()
        is Table<*> -> value.values.withIndex()
        // Array variants
        is BooleanArray -> value.withIndex()
        is ByteArray -> value.withIndex()
        is CharArray -> value.withIndex()
        is DoubleArray -> value.withIndex()
        is FloatArray -> value.withIndex()
        is IntArray -> value.withIndex()
        is LongArray -> value.withIndex()
        is ShortArray -> value.withIndex()
        else ->
            error(
                "Unsupported value type: supported types are `Collection`, `Array` (including " +
                    "variants), and `Table`."
            )
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy