Coverage Summary for Class: KVar (kweb.state)

Class Method, % Branch, % Line, % Instruction, %
KVar 80% (4/5) 100% (2/2) 81.8% (18/22) 93.8% (91/97)
KVar$map$1 100% (1/1) 100% (1/1) 100% (5/5)
KVar$map$2 100% (1/1) 100% (1/1) 100% (5/5)
KVar$map$3 100% (1/1) 100% (1/1) 100% (5/5)
KVar$map$myChangeHandle$1 100% (1/1) 50% (1/2) 60% (3/5) 59.1% (13/22)
KVar$map$origChangeHandle$1 100% (1/1) 100% (1/1) 100% (10/10)
KVar$special$$inlined$observable$1
Total 90% (9/10) 75% (3/4) 80.6% (25/31) 89.6% (129/144)


 package kweb.state
 
 import mu.KotlinLogging
 import kotlin.contracts.ExperimentalContracts
 import kotlin.contracts.InvocationKind
 import kotlin.contracts.contract
 import kotlin.properties.Delegates
 import kotlin.reflect.KProperty1
 import kotlin.reflect.full.instanceParameter
 import kotlin.reflect.full.memberFunctions
 
 private val logger = KotlinLogging.logger {}
 
 /**
  * A KVar is an observable container for a value of type T.  It must be initialized with [initialValue], and
  * this can then be modified by setting the [KVar.value] property. Listeners may be added using
  * [KVar.addListener], and these will be called whenever the value is changed.
  *
  * From within Kweb's DSL, you can use the [ElementCreator.kvar] function to create a KVar without needing
  * to import KVar, and which will also call [KVar.close] when this part of the DOM is cleaned up.
  */
 class KVar<T : Any?>(initialValue: T) : KVal<T>(initialValue) {
 
     /**
      * The current value of this KVar.  Setting this property to a different value will notify
      * all listeners, but if the new value is the same as the old value then it will be ignored.
      */
     override var value: T by Delegates.observable(initialValue) { _, old, new ->
         if (old != new) {
             verifyNotClosed("modify KVar.value")
             listeners.values.forEach { listener ->
                 try {
                     listener(old, new)
                 } catch (e: Exception) {
                     logger.warn("Exception thrown by listener", e)
                 }
             }
         }
     }
 
     /**
      * Create another KVar that is a bi-directional mapping of this KVar.  [ReversibleFunction.invoke] will be called
      * whenever this KVar changes, and the new KVar will be updated with the result of this mapping function.
      *
      * Similarly, if the other KVar is modified then this KVar will be updated with the result of the
      * [ReversibleFunction.reverse] function.
      */
     fun <O : Any?> map(reversibleFunction: ReversibleFunction<T, O>): KVar<O> {
         verifyNotClosed("create a mapping")
         val mappedKVar = KVar(reversibleFunction(value))
         val myChangeHandle = addListener { old, new ->
             if (old != new) {
                 try {
                     mappedKVar.value = reversibleFunction.invoke(new)
                 } catch (throwable: Throwable) {
                     mappedKVar.close(CloseReason("Closed because mapper threw an error or exception", throwable))
                 }
             }
         }
         onClose { removeListener(myChangeHandle) }
         mappedKVar.onClose { removeListener(myChangeHandle) }
         val origChangeHandle = mappedKVar.addListener { _, new ->
             value = reversibleFunction.reverse(value, new)
         }
         onClose { mappedKVar.removeListener(origChangeHandle) }
         return mappedKVar
     }
 
     override fun toString(): String {
         verifyNotClosed("call KVar.toString()")
         return "KVar($value)"
     }
 
 }
 
 /**
  * Use reflection to create a [KVar] that bi-directionally maps to a mutable property of an
  * object.
  */
 inline fun <O, reified T : Any?> KVar<T>.property(property: KProperty1<T, O>): KVar<O> {
     return this.map(object : ReversibleFunction<T, O>("prop: ${property.name}") {
 
         private val kClass = T::class
         private val copyFunc = kClass.memberFunctions.firstOrNull { it.name == "copy" }
                 ?: error("Can't find `copy` function in class ${kClass.simpleName}, are you sure it's a data object?")
         private val instanceParam = copyFunc.instanceParameter
                 ?: error("Unable to obtain instanceParam")
         private val fieldParam = copyFunc.parameters.firstOrNull { it.name == property.name }
                 ?: error("Unable to identify parameter for ${property.name} in ${kClass.simpleName}.copy() function")
 
         override fun invoke(from: T): O = property.invoke(from)
 
         override fun reverse(original: T, change: O): T = copyFunc.callBy(mapOf(instanceParam to original, fieldParam to change)) as T
     })
 }
 
 /**
  * Bi-directionally map a [KVar] with nullable type to its non-nullable equivalent.
  */
 fun <O : Any> KVar<O?>.notNull(default: O? = null, invertDefault: Boolean = true): KVar<O> {
     return this.map(object : ReversibleFunction<O?, O>(label = "notNull") {
         override fun invoke(from: O?): O = from ?: default!!
 
         override fun reverse(original: O?, change: O): O? = if (invertDefault) {
             if (change != default) change else null
         } else change
 
     })
 }
 
 fun <A, B> Pair<KVar<A>, KVar<B>>.combine(): KVar<Pair<A, B>> {
     val newKVar = KVar(this.first.value to this.second.value)
     val listener1 = this.first.addListener { _, n -> newKVar.value = n to this.second.value }
     val listener2 = this.second.addListener { _, n -> newKVar.value = this.first.value to n }
 
     newKVar.addListener { o, n ->
         this.first.value = n.first
         this.second.value = n.second
     }
 
     this.first.onClose {
         newKVar.close(CloseReason("Closed because first KVar was closed"))
     }
 
     this.second.onClose {
         newKVar.close(CloseReason("Closed because second KVar was closed"))
     }
 
     newKVar.onClose {
         this.first.removeListener(listener1)
         this.second.removeListener(listener2)
     }
 
     return newKVar
 }