-
Notifications
You must be signed in to change notification settings - Fork 37
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement delegated properties on BotContext
- Loading branch information
1 parent
25b184a
commit cd66800
Showing
4 changed files
with
484 additions
and
92 deletions.
There are no files selected for viewing
185 changes: 185 additions & 0 deletions
185
core/src/main/kotlin/com/justai/jaicf/helpers/context/BotContextProperty.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} |
92 changes: 92 additions & 0 deletions
92
core/src/testFixtures/kotlin/com/justai/jaicf/core/test/managers/BotContextBaseTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.