From 17779921909fb1e192e2fb2f3aedcc81b677861a Mon Sep 17 00:00:00 2001 From: Danielle Voznyy Date: Sat, 27 Jul 2024 19:37:13 -0400 Subject: [PATCH] feat: Implement expressions with yaml config support feat: Add support for registering action results under variable names, and conditions per action --- addons/geary-actions/build.gradle.kts | 1 + .../com/mineinabyss/geary/actions/Action.kt | 2 +- .../mineinabyss/geary/actions/ActionGroup.kt | 21 ++++++- .../geary/actions/ActionGroupContext.kt | 6 ++ .../geary/actions/actions/EmitEventAction.kt | 4 +- .../actions/event_binds/EntityObservers.kt | 55 ++++++++++++++++-- .../geary/actions/event_binds/EventBind.kt | 3 +- .../event_binds/ParseEntityObservers.kt | 4 +- .../geary/actions/expressions/Expression.kt | 58 ++++++++++++++++++- .../geary/actions/ExpressionDecodingTest.kt | 55 ++++++++++++++++++ .../mineinabyss/geary/prefabs/PrefabLoader.kt | 2 +- .../PolymorphicListAsMapSerializer.kt | 15 ++--- .../serializers/ProvidedConfig.kt | 2 +- 13 files changed, 202 insertions(+), 26 deletions(-) create mode 100644 addons/geary-actions/src/jvmTest/kotlin/com/mineinabyss/geary/actions/ExpressionDecodingTest.kt diff --git a/addons/geary-actions/build.gradle.kts b/addons/geary-actions/build.gradle.kts index ac29d641..f57f158e 100644 --- a/addons/geary-actions/build.gradle.kts +++ b/addons/geary-actions/build.gradle.kts @@ -19,6 +19,7 @@ kotlin { dependencies { implementation(kotlin("test")) implementation(idofrontLibs.kotlinx.coroutines.test) + implementation(idofrontLibs.kotlinx.serialization.kaml) implementation(idofrontLibs.kotest.assertions) implementation(idofrontLibs.kotest.property) implementation(idofrontLibs.idofront.di) diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/Action.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/Action.kt index dcb50e35..085efc1d 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/Action.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/Action.kt @@ -1,5 +1,5 @@ package com.mineinabyss.geary.actions interface Action { - fun ActionGroupContext.execute() + fun ActionGroupContext.execute(): Any? } diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroup.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroup.kt index 1f3e5ab6..a8e576ff 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroup.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroup.kt @@ -1,12 +1,27 @@ package com.mineinabyss.geary.actions +import com.mineinabyss.geary.actions.actions.EnsureAction + +class ActionEntry( + val action: Action, + val conditions: List?, + val register: String?, +) + class ActionGroup( - val actions: List, + val actions: List, ) { fun execute(context: ActionGroupContext) { - actions.forEach { + actions.forEach { entry -> try { - with(it) { context.execute() } + entry.conditions?.forEach { condition -> + with(condition) { context.execute() } + } + + val returned = with(entry.action) { context.execute() } + + if (entry.register != null) + context.register(entry.register, returned) } catch (e: ActionsCancelledException) { return } diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroupContext.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroupContext.kt index 9d90310e..cee8faf2 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroupContext.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/ActionGroupContext.kt @@ -6,5 +6,11 @@ import com.mineinabyss.geary.datatypes.GearyEntity class ActionGroupContext( var entity: GearyEntity, ) { + val environment: MutableMap = mutableMapOf() + fun eval(expression: Expression): T = expression.evaluate(this) + + fun register(name: String, value: Any?) { + environment[name] = value + } } diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/actions/EmitEventAction.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/actions/EmitEventAction.kt index c491a3b3..6c45e45c 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/actions/EmitEventAction.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/actions/EmitEventAction.kt @@ -8,7 +8,7 @@ import com.mineinabyss.geary.helpers.componentId class EmitEventAction( val eventId: ComponentId, val data: Any?, -): Action { +) : Action { override fun ActionGroupContext.execute() { entity.emit(event = eventId, data = data) } @@ -16,6 +16,6 @@ class EmitEventAction( companion object { fun from(data: Any) = EmitEventAction(componentId(data::class), data) - fun wrapIfNotAction(data: Any) = if(data is Action) data else from(data) + fun wrapIfNotAction(data: Any) = if (data is Action) data else from(data) } } diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EntityObservers.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EntityObservers.kt index b08a7baf..573c54cb 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EntityObservers.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EntityObservers.kt @@ -1,5 +1,9 @@ package com.mineinabyss.geary.actions.event_binds +import com.mineinabyss.geary.actions.Action +import com.mineinabyss.geary.actions.ActionEntry +import com.mineinabyss.geary.actions.actions.EmitEventAction +import com.mineinabyss.geary.actions.actions.EnsureAction import com.mineinabyss.geary.serialization.serializers.InnerSerializer import com.mineinabyss.geary.serialization.serializers.PolymorphicListAsMapSerializer import com.mineinabyss.geary.serialization.serializers.SerializableComponentId @@ -7,18 +11,61 @@ import com.mineinabyss.geary.serialization.serializers.SerializedComponents import kotlinx.serialization.Serializable import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.MapSerializer +import kotlin.jvm.JvmInline @Serializable(with = EntityObservers.Serializer::class) -class EntityObservers(val observers: List) { +class EntityObservers( + val observers: List, +) { class Serializer : InnerSerializer>, EntityObservers>( serialName = "geary:observe", inner = MapSerializer( SerializableComponentId.serializer(), - ListSerializer(PolymorphicListAsMapSerializer.ofComponents()) + ListSerializer( + PolymorphicListAsMapSerializer.ofComponents( + PolymorphicListAsMapSerializer.Config( + customKeys = mapOf( + "when" to ActionWhen.serializer(), + "register" to ActionRegister.serializer() + ) + ) + ) + ) ), - inverseTransform = { it.observers.associate { it.event to it.emit } }, - transform = { EntityObservers(it.map { (event, emit) -> EventBind(event, emit = emit) }) } + inverseTransform = { TODO() }, + transform = { + EntityObservers( + it.map { (event, emit) -> + val actions = emit.map { components -> + var action: Action? = null + var condition: List? = null + var register: String? = null + components.forEach { comp -> + when { + comp is ActionWhen -> condition = comp.conditions + comp is ActionRegister -> register = comp.register + action != null -> error("Multiple actions defined in one block!") + else -> action = EmitEventAction.wrapIfNotAction(comp) + } + } + ActionEntry( + action = action!!, + conditions = condition, + register = register + ) + } + EventBind(event, emit = actions) + } + ) + } ) } +@JvmInline +@Serializable +value class ActionWhen(val conditions: List) + +@JvmInline +@Serializable +value class ActionRegister(val register: String) diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EventBind.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EventBind.kt index 47e64bf3..84c8e94c 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EventBind.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/EventBind.kt @@ -1,10 +1,11 @@ package com.mineinabyss.geary.actions.event_binds +import com.mineinabyss.geary.actions.ActionEntry import com.mineinabyss.geary.serialization.serializers.SerializableComponentId import com.mineinabyss.geary.serialization.serializers.SerializedComponents class EventBind( val event: SerializableComponentId, val involving: List = listOf(), - val emit: List, + val emit: List, ) diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/ParseEntityObservers.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/ParseEntityObservers.kt index b6820164..a70e21e4 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/ParseEntityObservers.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/event_binds/ParseEntityObservers.kt @@ -14,9 +14,7 @@ fun GearyModule.bindEntityObservers() = observe() .involving(query()) .exec { (observers) -> observers.observers.forEach { observer -> - val actionGroup = ActionGroup( - actions = observer.emit.flatten().map { EmitEventAction.wrapIfNotAction(it) } - ) + val actionGroup = ActionGroup(observer.emit) entity.observe(observer.event.id).involving(EntityType(observer.involving.map { it.id })).exec { val context = ActionGroupContext(entity) actionGroup.execute(context) diff --git a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/expressions/Expression.kt b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/expressions/Expression.kt index 75fa1f69..0fb32cc4 100644 --- a/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/expressions/Expression.kt +++ b/addons/geary-actions/src/commonMain/kotlin/com/mineinabyss/geary/actions/expressions/Expression.kt @@ -1,9 +1,61 @@ package com.mineinabyss.geary.actions.expressions import com.mineinabyss.geary.actions.ActionGroupContext +import kotlinx.serialization.ContextualSerializer +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.AbstractDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.modules.SerializersModule -@Serializable -abstract class Expression { - abstract fun evaluate(context: ActionGroupContext): T +@Serializable(with = Expression.Serializer::class) +sealed interface Expression { + fun evaluate(context: ActionGroupContext): T + data class Fixed( + val value: T, + ) : Expression { + override fun evaluate(context: ActionGroupContext): T = value + } + + data class Evaluate( + val expression: String, + ) : Expression { + override fun evaluate(context: ActionGroupContext): T { + return context.environment[expression] as? T ?: error("Expression $expression not found in context") + } + } + + // TODO kaml handles contextual completely different form Json, can we somehow allow both? Otherwise + // kaml also has broken contextual serializer support that we need to work around :( + class Serializer(val serializer: KSerializer) : KSerializer> { + @OptIn(InternalSerializationApi::class) + override val descriptor: SerialDescriptor = + ContextualSerializer(Any::class).descriptor//buildSerialDescriptor("ExpressionSerializer", SerialKind.CONTEXTUAL) + + override fun deserialize(decoder: Decoder): Expression { + // Try reading string value, if serial type isn't string, this fails + runCatching { + decoder.decodeStructure(String.serializer().descriptor) { + decodeSerializableElement(String.serializer().descriptor, 0, String.serializer()) + } + }.onSuccess { string -> + if (string.startsWith("{{") && string.endsWith("}}")) + return Evaluate(string.removePrefix("{{").removeSuffix("}}").trim()) + } + + // Fallback to reading the value in-place + return decoder.decodeStructure(serializer.descriptor) { + Fixed(decodeSerializableElement(serializer.descriptor, 0, serializer)) + } + } + + override fun serialize(encoder: Encoder, value: Expression) { + TODO("Not yet implemented") + } + } } diff --git a/addons/geary-actions/src/jvmTest/kotlin/com/mineinabyss/geary/actions/ExpressionDecodingTest.kt b/addons/geary-actions/src/jvmTest/kotlin/com/mineinabyss/geary/actions/ExpressionDecodingTest.kt new file mode 100644 index 00000000..fc1ae9a1 --- /dev/null +++ b/addons/geary-actions/src/jvmTest/kotlin/com/mineinabyss/geary/actions/ExpressionDecodingTest.kt @@ -0,0 +1,55 @@ +package com.mineinabyss.geary.actions + +import com.charleskorn.kaml.Yaml +import com.mineinabyss.geary.actions.expressions.Expression +import com.mineinabyss.geary.serialization.formats.YamlFormat +import com.mineinabyss.geary.serialization.serializableComponents +import io.kotest.matchers.shouldBe +import kotlinx.serialization.Contextual +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.contextual +import org.junit.jupiter.api.Test + +class ExpressionDecodingTest { + @Serializable + data class TestData( + val name: Expression, + val age: Expression, + val regular: String, + ) + +// @org.junit.jupiter.api.Test +// fun `should correctly decode json`() { +// val input = """ +// { +// "age": "{{ test }}", +// "name": "variable", +// "regular": "{{ asdf }}" +// } +// """.trimIndent() +// Json.decodeFromString(input) shouldBe TestData( +// name = Expression.Fixed("variable"), +// age = Expression.Evaluate("test"), +// regular = "{{ asdf }}" +// ) +// } + + @org.junit.jupiter.api.Test + fun `should correctly decode yaml`() { + val input = """ + { + "age": "{{ test }}", + "name": "variable", + "regular": "{{ asdf }}" + } + """.trimIndent() + Yaml.default.decodeFromString(TestData.serializer(), input) shouldBe TestData( + name = Expression.Fixed("variable"), + age = Expression.Evaluate("test"), + regular = "{{ asdf }}" + ) + } +} diff --git a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt index 7b36713c..9b4abda0 100644 --- a/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt +++ b/addons/geary-prefabs/src/commonMain/kotlin/com/mineinabyss/geary/prefabs/PrefabLoader.kt @@ -88,7 +88,7 @@ class PrefabLoader { var hadMalformed = false val key = PrefabKey.of(namespace, path.name.substringBeforeLast('.')) val decoded = runCatching { - val config = PolymorphicListAsMapSerializer.Config( + val config = PolymorphicListAsMapSerializer.Config( whenComponentMalformed = { if (!hadMalformed) logger.e("[$key] Problems reading components") hadMalformed = true diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/PolymorphicListAsMapSerializer.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/PolymorphicListAsMapSerializer.kt index 07c91ac1..23243083 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/PolymorphicListAsMapSerializer.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/PolymorphicListAsMapSerializer.kt @@ -21,7 +21,7 @@ open class PolymorphicListAsMapSerializer( serializer: KSerializer, ) : KSerializer> { // We need primary constructor to be a single serializer for generic serialization to work, use of() if manually creating - private var config: Config = Config() + private var config: Config = Config() val polymorphicSerializer = serializer as? PolymorphicSerializer ?: error("Serializer is not polymorphic") @@ -49,7 +49,7 @@ open class PolymorphicListAsMapSerializer( } else -> { - val componentSerializer = findSerializerFor(compositeDecoder.serializersModule, namespaces, key) + val componentSerializer = config.customKeys[key] ?: findSerializerFor(compositeDecoder.serializersModule, namespaces, key) .getOrElse { if (config.onMissingSerializer != OnMissing.IGNORE) { config.whenComponentMalformed(key) @@ -87,7 +87,7 @@ open class PolymorphicListAsMapSerializer( return components } - fun getParentConfig(serializersModule: SerializersModule): Config? { + fun getParentConfig(serializersModule: SerializersModule): Config<*>? { return (serializersModule.getContextual(ProvidedConfig::class) as? ProvidedConfig)?.config } @@ -116,18 +116,19 @@ open class PolymorphicListAsMapSerializer( ERROR, WARN, IGNORE } - data class Config( + data class Config( val namespaces: List = listOf(), val prefix: String = "", val onMissingSerializer: OnMissing = OnMissing.WARN, val skipMalformedComponents: Boolean = true, val whenComponentMalformed: (String) -> Unit = {}, + val customKeys: Map> = mapOf(), ) companion object { fun of( serializer: PolymorphicSerializer, - config: Config = Config(), + config: Config = Config(), ): PolymorphicListAsMapSerializer { return PolymorphicListAsMapSerializer(serializer).apply { this.config = config @@ -135,12 +136,12 @@ open class PolymorphicListAsMapSerializer( } fun ofComponents( - config: Config = Config(), + config: Config = Config(), ) = of(PolymorphicSerializer(GearyComponent::class)).apply { this.config = config } - fun SerializersModuleBuilder.provideConfig(config: Config) { + fun SerializersModuleBuilder.provideConfig(config: Config<*>) { contextual(ProvidedConfig::class, ProvidedConfig(config)) } } diff --git a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/ProvidedConfig.kt b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/ProvidedConfig.kt index ef59bfb8..28650b3c 100644 --- a/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/ProvidedConfig.kt +++ b/addons/geary-serialization/src/commonMain/kotlin/com/mineinabyss/geary/serialization/serializers/ProvidedConfig.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.descriptors.buildSerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder -class ProvidedConfig(val config: PolymorphicListAsMapSerializer.Config) : KSerializer { +class ProvidedConfig(val config: PolymorphicListAsMapSerializer.Config<*>) : KSerializer { @OptIn(InternalSerializationApi::class) override val descriptor: SerialDescriptor = buildSerialDescriptor("PolymorphicListAsMapSerializer.Config", PolymorphicKind.SEALED)