commonMain.dev.fritz2.core.lens.kt Maven / Gradle / Ivy
Show all versions of core-jvm Show documentation
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