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