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

commonMain.schemas.ClassSchema.kt Maven / Gradle / Ivy

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

package io.kform.schemas

import io.kform.*
import io.kform.internal.constructFromKClass
import io.kform.schemas.util.commonRestrictions
import kotlin.coroutines.coroutineContext
import kotlin.experimental.ExperimentalTypeInference
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.isActive

/** Child property schema. */
private data class ChildPropSchema(
    val property: KMutableProperty1,
    val schema: Schema
)

/**
 * Function responsible for creating an instance of type `T` from two maps mapping children names to
 * their values and properties.
 *
 * @param T Type of instance being created.
 */
public typealias ConstructorFunction =
    (childValues: Map, childProps: Map>) -> T

/**
 * Implementation of a schema representing values of a given class [T] with [KClass] [kClass]. Use
 * the [ClassSchema.invoke] function to create an instance of this class.
 *
 * @property kClass [KClass] of the represented class.
 * @property childrenSchemas Map of children schemas.
 * @property construct Function used to construct values of type [T].
 */
public open class ClassSchema
@PublishedApi
internal constructor(
    public val kClass: KClass,
    public val childrenSchemas: Map, Schema<*>>,
    validations: Iterable> = emptyList(),
    initialValue: T? = null,
    public val construct: ConstructorFunction? = null
) : ParentSchema {
    override val typeInfo: TypeInfo =
        TypeInfo(kClass, restrictions = commonRestrictions(validations))

    override val validations: List> = validations.toList()

    override val supportsConcurrentSets: Boolean = true

    /** Children information by name. */
    private val childrenInfoByName: Map> = run {
        val map = LinkedHashMap>(childrenSchemas.size)
        @Suppress("UNCHECKED_CAST")
        for ((property, schema) in childrenSchemas) {
            map[property.name] =
                ChildPropSchema(property as KMutableProperty1, schema as Schema)
        }
        map
    }

    override val initialValue: T =
        initialValue
            ?: run {
                val childValues = LinkedHashMap(childrenSchemas.size)
                val childProps =
                    LinkedHashMap>(childrenSchemas.size)
                for ((name, childInfo) in childrenInfoByName) {
                    childValues[name] = childInfo.schema.initialValue
                    childProps[name] = childInfo.property
                }
                newInstance(childValues, childProps)
            }

    override suspend fun clone(value: T): T {
        val childValues = LinkedHashMap(childrenSchemas.size)
        val childProps = LinkedHashMap>(childrenSchemas.size)
        for ((name, childInfo) in childrenInfoByName) {
            childValues[name] = childInfo.schema.clone(childInfo.property.get(value))
            childProps[name] = childInfo.property
        }
        return newInstance(childValues, childProps)
    }

    override fun assignableTo(type: KType): Boolean =
        (type.classifier as? KClass<*>)?.isInstance(initialValue) == true

    override fun isValidChildSchemaFragment(fragment: AbsolutePathFragment): Boolean =
        fragment is AbsolutePathFragment.Id && childrenInfoByName.containsKey(fragment.id)

    override fun childrenSchemas(
        path: AbsolutePath,
        queriedPath: AbsolutePath,
        fragment: AbsolutePathFragment
    ): Sequence> = sequence {
        if (fragment is AbsolutePathFragment.Wildcard) {
            for ((name, childInfo) in childrenInfoByName) {
                yield(
                    SchemaInfo(
                        childInfo.schema,
                        path.append(AbsolutePathFragment.Id(name)),
                        queriedPath.append(AbsolutePathFragment.Id(name))
                    )
                )
            }
        } else {
            fragment as AbsolutePathFragment.Id
            val childInfo =
                childrenInfoByName[fragment.id] ?: error("Invalid fragment '$fragment'.")
            yield(SchemaInfo(childInfo.schema, path.append(fragment), queriedPath.append(fragment)))
        }
    }

    override suspend fun isValidChildFragment(value: T, fragment: AbsolutePathFragment): Boolean =
        isValidChildSchemaFragment(fragment)

    override fun children(
        path: AbsolutePath,
        schemaPath: AbsolutePath,
        value: T,
        fragment: AbsolutePathFragment
    ): Flow> = flow {
        if (fragment is AbsolutePathFragment.Wildcard) {
            for ((name, childInfo) in childrenInfoByName) {
                val childId = AbsolutePathFragment.Id(name)
                emit(
                    ValueInfo(
                        childInfo.property.get(value),
                        childInfo.schema,
                        path.append(childId),
                        schemaPath.append(childId),
                    )
                )
            }
        } else {
            fragment as AbsolutePathFragment.Id
            val childInfo =
                childrenInfoByName[fragment.id] ?: error("Invalid fragment '$fragment'.")
            emit(
                ValueInfo(
                    childInfo.property.get(value),
                    childInfo.schema,
                    path.append(fragment),
                    schemaPath.append(fragment)
                )
            )
        }
    }

    @Suppress("UNCHECKED_CAST")
    override suspend fun init(path: AbsolutePath, fromValue: Any?, eventsBus: SchemaEventsBus): T {
        fromValue as? T ?: error("Cannot initialise value from '$fromValue'.")
        val childValues = LinkedHashMap(childrenSchemas.size)
        val childProps = LinkedHashMap>(childrenSchemas.size)
        for ((name, childInfo) in childrenInfoByName) {
            childValues[name] =
                childInfo.schema.init(
                    path.append(AbsolutePathFragment.Id(name)),
                    childInfo.property.get(fromValue),
                    eventsBus
                )
            childProps[name] = childInfo.property
        }
        val newValue = newInstance(childValues, childProps)
        eventsBus.emit(ValueEvent.Init(newValue, path, this))
        return newValue
    }

    @Suppress("UNCHECKED_CAST")
    override suspend fun change(
        path: AbsolutePath,
        value: T,
        intoValue: Any?,
        eventsBus: SchemaEventsBus
    ): T {
        intoValue as? T ?: error("Cannot initialise value from '$intoValue'.")
        for ((name, childInfo) in childrenInfoByName) {
            if (!coroutineContext.isActive) break // Don't do more work than necessary
            childInfo.property.set(
                value,
                childInfo.schema.change(
                    path.append(AbsolutePathFragment.Id(name)),
                    childInfo.property.get(value),
                    childInfo.property.get(intoValue),
                    eventsBus
                )
            )
        }
        return value
    }

    override suspend fun destroy(path: AbsolutePath, value: T, eventsBus: SchemaEventsBus): T {
        eventsBus.emit(ValueEvent.Destroy(value, path, this))
        for ((name, childInfo) in childrenInfoByName) {
            childInfo.schema.destroy(
                path.append(AbsolutePathFragment.Id(name)),
                childInfo.property.get(value),
                eventsBus
            )
        }
        return value
    }

    override suspend fun isValidSetFragment(value: T, fragment: AbsolutePathFragment): Boolean =
        isValidChildFragment(value, fragment)

    override suspend fun set(
        path: AbsolutePath,
        value: T,
        fragment: AbsolutePathFragment,
        childValue: Any?,
        eventsBus: SchemaEventsBus
    ) {
        fragment as AbsolutePathFragment.Id
        val childInfo = childrenInfoByName[fragment.id] ?: error("Invalid fragment '$fragment'.")
        childInfo.property.set(
            value,
            childInfo.schema.change(
                path.append(fragment),
                childInfo.property.get(value),
                childValue,
                eventsBus
            )
        )
    }

    override fun childrenStatesContainer(): ParentState =
        ClassState(childrenInfoByName.mapValues { (_, info) -> info.schema })

    /**
     * Returns a new instance of the class represented by this schema given a map of [childValues]
     * and their [childProps], mapping the name of the arguments to their values and properties
     * respectively.
     */
    private fun newInstance(
        childValues: Map,
        childProps: Map>
    ): T =
        construct?.invoke(childValues, childProps)
            ?: constructFromKClass(kClass, childValues, childProps)

    public companion object {
        /**
         * Function used to build a schema representing values of a given class [T]. The children
         * schemas are built via a class schema builder.
         *
         * Example defining a `PersonSchema` for a class `Person` with `name` and `married`
         * properties:
         * ```kotlin
         * data class Person(var name: String, var married: Boolean)
         *
         * val personSchema = ClassSchema {
         *     Person::name { StringSchema() }
         *     Person::married { BooleanSchema() }
         * }
         * ```
         *
         * This schema will attempt to create values of the provided class by calling its primary
         * constructor and matching argument names to children names. E.g. the above `PersonSchema`
         * will attempt to create an instance of `Person` by calling `Person(name, married)`. If the
         * class' primary constructor has parameters with different names or requires other
         * non-default arguments, then the user must provide a [construct] function, instructing how
         * to properly construct the class.
         */
        public inline operator fun  invoke(
            validations: Iterable> = emptyList(),
            initialValue: T? = null,
            noinline construct: ConstructorFunction? = null,
            @BuilderInference builder: ClassSchemaBuilder.() -> Unit
        ): ClassSchema {
            val schemaBuilder = ClassSchemaBuilder()
            schemaBuilder.builder()
            return ClassSchema(
                T::class,
                schemaBuilder.childrenSchemas,
                validations,
                initialValue,
                construct
            )
        }

        public inline operator fun  invoke(
            vararg validations: Validation,
            initialValue: T? = null,
            noinline construct: ConstructorFunction? = null,
            @BuilderInference builder: ClassSchemaBuilder.() -> Unit
        ): ClassSchema = ClassSchema(validations.toList(), initialValue, construct, builder)
    }
}

/**
 * Builder of a class schema. Use the [ClassSchema] constructor function to build a class schema.
 */
public class ClassSchemaBuilder {
    @PublishedApi
    internal val childrenSchemas: MutableMap, Schema<*>> = mutableMapOf()

    /** Declare that the child schema of property [property] is [schema]. */
    public fun  childSchema(
        property: KMutableProperty1,
        schema: Schema
    ) {
        childrenSchemas[property] = schema
    }

    /**
     * Declare that the child schema of the receiving property is the one returned by
     * [schemaBuilder].
     */
    public inline operator fun  KMutableProperty1.invoke(
        @BuilderInference schemaBuilder: () -> Schema
    ) {
        childSchema(this, schemaBuilder())
    }
}

/** Class responsible for holding the states of the children of a class. */
public class ClassState(private val childrenSchemas: Map>) : ParentState {
    /** Map containing the state of each child of the class. */
    private val childrenStates = HashMap(childrenSchemas.size)

    override fun childrenStates(
        path: AbsolutePath,
        fragment: AbsolutePathFragment
    ): Sequence> = sequence {
        if (fragment is AbsolutePathFragment.Wildcard) {
            for ((name, schema) in childrenSchemas) {
                yield(StateInfo(childrenStates[name], schema, path + AbsolutePathFragment.Id(name)))
            }
        } else {
            fragment as AbsolutePathFragment.Id
            val schema = childrenSchemas[fragment.id] ?: error("Invalid fragment '$fragment'.")
            yield(StateInfo(childrenStates[fragment.id], schema, path + fragment))
        }
    }

    override fun setState(fragment: AbsolutePathFragment.Id, state: State?) {
        childrenStates[fragment.id] = state
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy