Skip to content

Commit

Permalink
feat: Implement expressions with yaml config support
Browse files Browse the repository at this point in the history
feat: Add support for registering action results under variable names, and conditions per action
  • Loading branch information
0ffz committed Jul 27, 2024
1 parent 96951e1 commit 1777992
Show file tree
Hide file tree
Showing 13 changed files with 202 additions and 26 deletions.
1 change: 1 addition & 0 deletions addons/geary-actions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.mineinabyss.geary.actions

interface Action {
fun ActionGroupContext.execute()
fun ActionGroupContext.execute(): Any?
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
package com.mineinabyss.geary.actions

import com.mineinabyss.geary.actions.actions.EnsureAction

class ActionEntry(
val action: Action,
val conditions: List<EnsureAction>?,
val register: String?,
)

class ActionGroup(
val actions: List<Action>,
val actions: List<ActionEntry>,
) {
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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,11 @@ import com.mineinabyss.geary.datatypes.GearyEntity
class ActionGroupContext(
var entity: GearyEntity,
) {
val environment: MutableMap<String, Any?> = mutableMapOf()

fun <T> eval(expression: Expression<T>): T = expression.evaluate(this)

fun register(name: String, value: Any?) {
environment[name] = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ 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)
}

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)
}
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,71 @@
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
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<EventBind>) {
class EntityObservers(
val observers: List<EventBind>,
) {
class Serializer : InnerSerializer<Map<SerializableComponentId, List<SerializedComponents>>, 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<EnsureAction>? = 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<EnsureAction>)

@JvmInline
@Serializable
value class ActionRegister(val register: String)
Original file line number Diff line number Diff line change
@@ -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<SerializableComponentId> = listOf(),
val emit: List<SerializedComponents>,
val emit: List<ActionEntry>,
)
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ fun GearyModule.bindEntityObservers() = observe<OnSet>()
.involving(query<EntityObservers>())
.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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<T> {
abstract fun evaluate(context: ActionGroupContext): T
@Serializable(with = Expression.Serializer::class)
sealed interface Expression<T> {
fun evaluate(context: ActionGroupContext): T
data class Fixed<T>(
val value: T,
) : Expression<T> {
override fun evaluate(context: ActionGroupContext): T = value
}

data class Evaluate<T>(
val expression: String,
) : Expression<T> {
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<T : Any>(val serializer: KSerializer<T>) : KSerializer<Expression<T>> {
@OptIn(InternalSerializationApi::class)
override val descriptor: SerialDescriptor =
ContextualSerializer(Any::class).descriptor//buildSerialDescriptor("ExpressionSerializer", SerialKind.CONTEXTUAL)

override fun deserialize(decoder: Decoder): Expression<T> {
// 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<T>) {
TODO("Not yet implemented")
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String>,
val age: Expression<Int>,
val regular: String,
)

// @org.junit.jupiter.api.Test
// fun `should correctly decode json`() {
// val input = """
// {
// "age": "{{ test }}",
// "name": "variable",
// "regular": "{{ asdf }}"
// }
// """.trimIndent()
// Json.decodeFromString<TestData>(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 }}"
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Any>(
whenComponentMalformed = {
if (!hadMalformed) logger.e("[$key] Problems reading components")
hadMalformed = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ open class PolymorphicListAsMapSerializer<T : Any>(
serializer: KSerializer<T>,
) : KSerializer<List<T>> {
// 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<T> = Config()

val polymorphicSerializer = serializer as? PolymorphicSerializer<T> ?: error("Serializer is not polymorphic")

Expand Down Expand Up @@ -49,7 +49,7 @@ open class PolymorphicListAsMapSerializer<T : Any>(
}

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)
Expand Down Expand Up @@ -87,7 +87,7 @@ open class PolymorphicListAsMapSerializer<T : Any>(
return components
}

fun getParentConfig(serializersModule: SerializersModule): Config? {
fun getParentConfig(serializersModule: SerializersModule): Config<*>? {
return (serializersModule.getContextual(ProvidedConfig::class) as? ProvidedConfig)?.config
}

Expand Down Expand Up @@ -116,31 +116,32 @@ open class PolymorphicListAsMapSerializer<T : Any>(
ERROR, WARN, IGNORE
}

data class Config(
data class Config<T : Any>(
val namespaces: List<String> = listOf(),
val prefix: String = "",
val onMissingSerializer: OnMissing = OnMissing.WARN,
val skipMalformedComponents: Boolean = true,
val whenComponentMalformed: (String) -> Unit = {},
val customKeys: Map<String, KSerializer<out T>> = mapOf(),
)

companion object {
fun <T : Any> of(
serializer: PolymorphicSerializer<T>,
config: Config = Config(),
config: Config<T> = Config(),
): PolymorphicListAsMapSerializer<T> {
return PolymorphicListAsMapSerializer(serializer).apply {
this.config = config
}
}

fun ofComponents(
config: Config = Config(),
config: Config<Any> = Config(),
) = of(PolymorphicSerializer(GearyComponent::class)).apply {
this.config = config
}

fun SerializersModuleBuilder.provideConfig(config: Config) {
fun SerializersModuleBuilder.provideConfig(config: Config<*>) {
contextual(ProvidedConfig::class, ProvidedConfig(config))
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProvidedConfig> {
class ProvidedConfig(val config: PolymorphicListAsMapSerializer.Config<*>) : KSerializer<ProvidedConfig> {
@OptIn(InternalSerializationApi::class)
override val descriptor: SerialDescriptor =
buildSerialDescriptor("PolymorphicListAsMapSerializer.Config", PolymorphicKind.SEALED)
Expand Down

0 comments on commit 1777992

Please sign in to comment.