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

commonMain.dev.fritz2.core.lens.kt Maven / Gradle / Ivy

The newest version!
package dev.fritz2.core

/**
 * Used by the fritz2 gradle-plugin to identify data classes it should generate [Lens]es for.
 */
@Target(AnnotationTarget.CLASS)
annotation class Lenses

/**
 * Used by the fritz2 gradle-plugin to identify properties in sealed classes or interfaces, that should get ignored
 * by the lens generation.
 *
 * Typical use case are const properties, that are overridden inside the data class body and not the ctor.
 */
@Target(AnnotationTarget.PROPERTY)
annotation class NoLens

/**
 * Describes a focus point into a data structure, i.e. a property of a given complex entity for read and write
 * access.
 *
 * @property id identifies the focus of this lens
 */
interface Lens {
    val id: String

    /**
     * gets the value of the focus target
     *
     * @param parent concrete instance to apply the focus tos
     */
    fun get(parent: P): T

    /**
     * sets the value of the focus target
     *
     * @param parent concrete instance to apply the focus to
     * @param value the new value of the focus target
     */
    fun set(parent: P, value: T): P

    /**
     * manipulates the focus target's value inside the [parent]
     *
     * @param parent concrete instance to apply the focus to
     * @param mapper function defining the manipulation
     */
    suspend fun apply(parent: P, mapper: suspend (T) -> T): P = set(parent, mapper(get(parent)))

    /**
     * appends to [Lens]es so that the resulting [Lens] points from the parent of the [Lens] this is called on to
     * the target of [other]
     *
     * @param other [Lens] to append to this one
     */
    operator fun  plus(other: Lens): Lens = object : Lens {
        override val id = "${[email protected]}.${other.id}".trimEnd('.')
        override fun get(parent: P): X = other.get([email protected](parent))
        override fun set(parent: P, value: X): P = [email protected](parent, other.set([email protected](parent), value))
    }

    /**
     * For a lens on a non-nullable parent this method creates a lens that can be used on a nullable-parent
     * Use this method only if you made sure, that it is never called on a null parent.
     * Otherwise, a [NullPointerException] is thrown.
     */
    fun withNullParent(): Lens = object : Lens {
        override val id: String = [email protected]

        override fun get(parent: P?): T =
            if (parent != null) [email protected](parent)
            else throw NullPointerException("get called with null parent on not-nullable lens@$id")

        override fun set(parent: P?, value: T): P? =
            if (parent != null) [email protected](parent, value)
            else throw NullPointerException("set called with null parent on not-nullable lens@$id")
    }
}

/**
 * convenience function to create a [Lens]
 *
 * @param id of the [Lens]
 * @param getter of the [Lens]
 * @param setter of the [Lens]
 */
inline fun  lensOf(id: String, crossinline getter: (P) -> T, crossinline setter: (P, T) -> P): Lens =
    object : Lens {
        override val id: String = id
        override fun get(parent: P): T = getter(parent)
        override fun set(parent: P, value: T): P = setter(parent, value)
    }

/**
 * creates a [Lens] converting [P] to and from a [String]
 *
 * @param format function for formatting a [P] to [String]
 * @param parse function for parsing a [String] to [P]
 */
inline fun 

lensOf(crossinline format: (P) -> String, crossinline parse: (String) -> P): Lens = object : Lens { override val id: String = "" override fun get(parent: P): String = format(parent) override fun set(parent: P, value: String): P = parse(value) } /** * function to derive a valid id for a given instance that does not change over time. */ typealias IdProvider = (T) -> I /** * Occurs when [Lens] points to non-existing element. */ class CollectionLensGetException : Exception() // is needed to cancel the coroutine correctly /** * Occurs when [Lens] tries to update a non-existing element. */ class CollectionLensSetException(message: String) : Exception(message) /** * creates a [Lens] pointing to a certain element in a [List] * * @param element current instance of the element to focus on * @param idProvider to identify the element in the list (i.e. when it's content changes over time) */ fun lensForElement(element: T, idProvider: IdProvider): Lens, T> = object : Lens, T> { override val id: String = idProvider(element).toString() override fun get(parent: List): T = parent.find { idProvider(it) == idProvider(element) } ?: throw CollectionLensGetException() override fun set(parent: List, value: T): List = ArrayList(parent.size).apply { var count = 0 parent.forEach { item -> if (idProvider(item) == idProvider(element)) { count++ add(value) } else add(item) } if (count == 0) throw CollectionLensSetException("no item found with id='${idProvider(element)}'") else if (count > 1) throw CollectionLensSetException("$count ambiguous items found with id='${idProvider(element)}'") } } /** * creates a [Lens] pointing to a certain [index] in a list * * @param index position to focus on */ fun lensForElement(index: Int): Lens, T> = object : Lens, T> { override val id: String = index.toString() override fun get(parent: List): T = parent.getOrNull(index) ?: throw CollectionLensGetException() override fun set(parent: List, value: T): List = if (index < 0 || index >= parent.size) throw CollectionLensSetException("no item found with index='$index'") else parent.mapIndexed { i, it -> if (i == index) value else it } } /** * creates a [Lens] pointing to a certain element in a [Map] * * @param key of the entry to focus on */ fun lensForElement(key: K): Lens, V> = object : Lens, V> { override val id: String = key.toString() override fun get(parent: Map): V = parent[key] ?: throw CollectionLensGetException() override fun set(parent: Map, value: V): Map = if (parent.containsKey(key)) parent + (key to value) else throw CollectionLensSetException("no item found with key='$key'") } /** * Creates a lens from a nullable parent to a non-nullable value using a given default-value. * Use this method to apply a default value that will be used in the case that the real value is null. * When setting that value to the default value it will accordingly translate to null. * * @param default value to be used instead of null */ internal fun defaultLens(id: String, default: T): Lens = object : Lens { override val id: String = id override fun get(parent: T?): T = parent ?: default override fun set(parent: T?, value: T): T? = value.takeUnless { it == default } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy