Skip to content

Commit

Permalink
Implement delegated properties on BotContext
Browse files Browse the repository at this point in the history
  • Loading branch information
nikvoloshin committed Apr 8, 2022
1 parent 25b184a commit cd66800
Show file tree
Hide file tree
Showing 4 changed files with 484 additions and 92 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.justai.jaicf.helpers.context

import com.justai.jaicf.context.BotContext
import kotlin.properties.ReadOnlyProperty
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/**
* An alias for a property delegate of type [V] backed by [BotContext]
*/
typealias BotContextProperty<V> = MapBackedProperty<BotContext, V>

/**
* Creates a property delegate of type [V] backed by [BotContext.client].
*
* Definition example:
* ```kotlin
*
* var BotContext.username by clientProperty<String>()
* var BotContext.isUserBlocked by clientProperty("blockStatus") { false }
* var BotContext.order by clientProperty<Order?>(removeOnNull = true) { null }
* val BotContext.userInfo by clientProperty<UserInfo>(saveDefault = true) { getUserInfo(it.clientId) }
*
* ```
*
* Usage example:
* ```kotlin
*
* action {
* if (context.isUserBlocked) return@action
* reactions.say("Hello, ${context.username}!")
* }
*
* ```
*
* @param key the key of the entry where to store the property value, if `null` property name is used
* @param saveDefault whether to save generated [default] value in the [BotContext.client], `false` by default
* @param removeOnNull whether to remove entry from [BotContext.client] on null set, `false` by default
* @param default provider of a default value for the entry, [NoSuchElementException] will be thrown by default
*/
fun <V> clientProperty(
key: String? = null,
saveDefault: Boolean = false,
removeOnNull: Boolean = false,
default: (BotContext) -> V = { throw NoSuchElementException("No value found for the key specified") }
): BotContextProperty<V> = MapBackedProperty(BotContext::client, key, saveDefault, removeOnNull, default)

/**
* Creates a property delegate of type [V] backed by [BotContext.session].
*
* @param key the key of the entry where to store the property value, if `null` property name is used
* @param saveDefault whether to save generated [default] value in the [BotContext.session], `false` by default
* @param removeOnNull whether to remove entry from [BotContext.session] on null set, `false` by default
* @param default provider of a default value for the entry, [NoSuchElementException] will be thrown by default
*
* @see clientProperty for examples
*/
fun <V> sessionProperty(
key: String? = null,
saveDefault: Boolean = false,
removeOnNull: Boolean = false,
default: (BotContext) -> V = { throw NoSuchElementException("No value found for the key specified") }
): BotContextProperty<V> = MapBackedProperty(BotContext::session, key, saveDefault, removeOnNull, default)

/**
* Creates a property delegate of type [V] backed by [BotContext.temp].
*
* @param key the key of the entry where to store the property value, if `null` property name is used
* @param saveDefault whether to save generated [default] value in the [BotContext.temp], `false` by default
* @param removeOnNull whether to remove entry from [BotContext.temp] on null set, `false` by default
* @param default provider of a default value for the entry, [NoSuchElementException] will be thrown by default
*
* @see clientProperty for examples
*/
fun <V> tempProperty(
key: String? = null,
saveDefault: Boolean = false,
removeOnNull: Boolean = false,
default: (BotContext) -> V = { throw NoSuchElementException("No value found for the key specified") }
): BotContextProperty<V> = MapBackedProperty(BotContext::temp, key, saveDefault, removeOnNull, default)

/**
* Allows to bind [ReadWriteProperty] defined on [BotContext] to a receiver of any type [T]
* by providing [BotContext] selector function.
*
* Example:
* ```kotlin
* val DefaultActionContext.userName by clientProperty<String?>() withContext { context }
* ```
*
* @param context provider of a [BotContext] for type [T]
*
* @return delegated property on a type [T] backed by the given [ReadWriteProperty]
*/
infix fun <T, V> ReadWriteProperty<BotContext, V>.withContext(context: T.() -> BotContext): ReadWriteProperty<T, V> =
object : ReadWriteProperty<T, V> {
override fun getValue(thisRef: T, property: KProperty<*>): V {
return this@withContext.getValue(thisRef.context(), property)
}

override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
this@withContext.setValue(thisRef.context(), property, value)
}
}

/**
* Allows to bind [ReadOnlyProperty] defined on [BotContext] to a receiver of any type [T]
* by providing [BotContext] selector function.
*
* @param context provider of a [BotContext] for type [T]
*
* @return delegated peroperty on a type [T] backed by the given [ReadOnlyProperty]
*/
infix fun <T, V> ReadOnlyProperty<BotContext, V>.withContext(context: T.() -> BotContext): ReadOnlyProperty<T, V> =
ReadOnlyProperty<T, V> { thisRef, property -> this@withContext.getValue(thisRef.context(), property) }

/**
* Allows to bind [MapBackedProperty] defined on [BotContext] to a receiver of any type [T]
* by providing [BotContext] selector function.
*
* Also allows to override default value with [T] as a receiver.
*
* Example:
* ```kotlin
* val DefaultActionContext.userName by clientProperty<String>(saveDefault = true).with({ context }) { request.getUserName() }
* ```
*
* @param context provider of a [BotContext] for type [T]
* @param default new overriding default value
*
* @return delegated property on a receiver of type [T] backed by the given [MapBackedProperty]
*/
fun <T, V> MapBackedProperty<BotContext, V>.with(
context: T.() -> BotContext,
default: T.() -> V = { this@with.default(context()) }
): MapBackedProperty<T, V> = MapBackedProperty({ mapSelector(it.context()) }, key, saveDefault, removeOnNull, default)

/**
* An implementation of [ReadWriteProperty] backed by some [MutableMap].
*
* @param T receiver type
* @param V property type
* @param mapSelector selector of the underlying [MutableMap]
* @param key the key of the entry where to store the property value, if `null` property name is used
* @param saveDefault whether to save generated [default] value in the map, `false` by default
* @param removeOnNull whether to remove entry from the map on null set, `false` by default
* @param default provider of a default value for the entry, [NoSuchElementException] will be thrown by default
*/
class MapBackedProperty<T, V>(
internal val mapSelector: (T) -> MutableMap<String, Any?>,
internal val key: String?,
internal val saveDefault: Boolean,
internal val removeOnNull: Boolean,
internal val default: (T) -> V,
) : ReadWriteProperty<T, V> {

override fun getValue(thisRef: T, property: KProperty<*>): V {
val map = mapSelector(thisRef)
val key = key ?: property.name

val value = if (map.containsKey(key)) {
map[key]
} else {
val default = default(thisRef)
if (saveDefault) {
map[key] = default
}
default
}

@Suppress("UNCHECKED_CAST")
return value as V
}

override fun setValue(thisRef: T, property: KProperty<*>, value: V) {
val map = mapSelector(thisRef)
val key = key ?: property.name

if (value == null && removeOnNull) {
map.remove(key)
} else {
map[key] = value
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package com.justai.jaicf.core.test.managers

import com.justai.jaicf.context.BotContext
import com.justai.jaicf.context.manager.BotContextManager
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.io.Serializable

open class BotContextBaseTest(override val manager: BotContextManager) : BotContextManagerTest {
@Test
fun `Saves simple value`() {
val context = BotContext("client1").apply {
result = "some result"
session["key1"] = "some value"
client["key1"] = "some value"
}

val result = manager.exchangeContext(context)

Assertions.assertNotNull(result)
Assertions.assertEquals(context.result, result.result)
Assertions.assertEquals(context.session, result.session)
Assertions.assertEquals(context.client, result.client)
}

@Test
fun `Saves custom bean`() {
val context = BotContext("client2").apply {
result = CustomValue(1)
session["value"] = CustomValue(CustomValue(2))
client["value"] = CustomValue(CustomValue(2))
}

val result = manager.exchangeContext(context)

Assertions.assertNotNull(result)
Assertions.assertTrue(result.result is CustomValue)
Assertions.assertTrue(result.session["value"] is CustomValue)
Assertions.assertTrue(result.client["value"] is CustomValue)
}

@Test
fun `Saves transition history`() {
var context = BotContext("client3")
Assertions.assertEquals(listOf("/"), context.dialogContext.transitionHistory.toList())

context.dialogContext.saveToTransitionHistory("/a")

context = manager.exchangeContext(context)
Assertions.assertEquals(listOf("/", "/a"), context.dialogContext.transitionHistory.toList())

context.dialogContext.saveToTransitionHistory("/a/b")

context = manager.exchangeContext(context)
Assertions.assertEquals(listOf("/", "/a", "/a/b"), context.dialogContext.transitionHistory.toList())
}

@Test
fun `Saves back states stack`() {
var context = BotContext("client3")
Assertions.assertEquals(emptyList<String>(), context.dialogContext.backStateStack.toList())

context.dialogContext.backStateStack.add("/a")

context = manager.exchangeContext(context)
Assertions.assertEquals(listOf("/a"), context.dialogContext.backStateStack.toList())

context.dialogContext.backStateStack.add("/a/b")

context = manager.exchangeContext(context)
Assertions.assertEquals(listOf("/a", "/a/b"), context.dialogContext.backStateStack.toList())
}

@Test
fun `Saves transitions`() {
var context = BotContext("client3")
Assertions.assertEquals(emptyMap<String, String>(), context.dialogContext.transitions)

context.dialogContext.transitions.put("a", "/a")

context = manager.exchangeContext(context)
Assertions.assertEquals(mutableMapOf("a" to "/a"), context.dialogContext.transitions)

context.dialogContext.transitions.put("b", "/b")

context = manager.exchangeContext(context)
Assertions.assertEquals(mutableMapOf("a" to "/a", "b" to "/b"), context.dialogContext.transitions)
}

data class CustomValue(val value: Any) : Serializable
}

0 comments on commit cd66800

Please sign in to comment.