Skip to content

Commit

Permalink
feat: Inline expressions using yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
0ffz committed Aug 2, 2024
1 parent fa76ca7 commit 1c136d4
Show file tree
Hide file tree
Showing 13 changed files with 165 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class ActionGroup(
if (entry.loop != null) {
entry.loop.evaluate(context).forEach { loopEntry ->
val subcontext = context.copy()
subcontext.register("item", loopEntry)
subcontext.register("it", loopEntry)
executeEntry(subcontext, entry)
}
} else
Expand Down Expand Up @@ -83,7 +83,7 @@ class ActionGroup(
comp is ActionWhen -> condition = comp.conditions
comp is ActionRegister -> register = comp.register
comp is ActionOnFail -> onFail = comp.action
comp is ActionLoop -> loop = Expression.Evaluate(comp.expression)
comp is ActionLoop -> loop = Expression.parseExpression(comp.expression, serializersModule) as Expression<List<Any>>
action != null -> geary.logger.w { "Multiple actions defined in one block!" }
else -> action = EmitEventAction.wrapIfNotAction(comp)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,6 @@ class BecomeAction(
serialName = "geary:become",
inner = EntityExpression.serializer(),
inverseTransform = { it.become },
transform = ::BecomeAction
transform = { BecomeAction(it) }
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mineinabyss.geary.actions.actions

import com.mineinabyss.geary.actions.Action
import com.mineinabyss.geary.actions.ActionGroupContext
import com.mineinabyss.geary.actions.expressions.Expression
import com.mineinabyss.geary.actions.expressions.InlineExpressionSerializer
import com.mineinabyss.geary.serialization.serializers.InnerSerializer
import kotlinx.serialization.Serializable

@Serializable(with = EvalAction.Serializer::class)
class EvalAction(
val expression: Expression<*>,
) : Action {
override fun ActionGroupContext.execute() =
expression.evaluate(this)

object Serializer : InnerSerializer<Expression<*>, EvalAction>(
serialName = "geary:eval",
inner = InlineExpressionSerializer,
inverseTransform = { it.expression },
transform = { EvalAction(it) }
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ class ActionWhen(val conditions: List<EnsureAction>) {
serialName = "geary:when",
inner = ListSerializer(EnsureAction.serializer()),
inverseTransform = ActionWhen::conditions,
transform = ::ActionWhen
transform = { ActionWhen(it) }
)
}

Expand All @@ -51,7 +51,7 @@ class ActionOnFail(val action: ActionGroup) {
serialName = "geary:on_fail",
inner = ActionGroup.Serializer(),
inverseTransform = ActionOnFail::action,
transform = ::ActionOnFail
transform = { ActionOnFail(it) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Passive(
serialName = "geary:passive",
inner = ListSerializer(SystemBind.serializer()),
inverseTransform = Passive::systems,
transform = ::Passive
transform = { Passive(it) }
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,6 @@ value class EntityExpression(
) : Expression<GearyEntity> {
override fun evaluate(context: ActionGroupContext): GearyEntity {
return if (expression == "parent") context.entity.parent!!
else Expression.Evaluate<GearyEntity>(expression).evaluate(context)
else Expression.Variable<GearyEntity>(expression).evaluate(context)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@ 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
Expand All @@ -23,67 +21,59 @@ sealed interface Expression<T> {
override fun evaluate(context: ActionGroupContext): T = value
}

data class Evaluate<T>(
data class Variable<T>(
val expression: String,
) : Expression<T> {
override fun evaluate(context: ActionGroupContext): T {
if (expression == "entity") return context.entity as T
return context.environment[expression] as? T ?: error("Expression $expression not found in context")
}
}

companion object {
val funcRegex = Regex("[.{}]")
fun parseExpression(string: String, module: SerializersModule): Expression<*> {
val before = string.substringBefore(".")
val after = string.substringAfter(".")
val reference = Evaluate<Any>(before)
return foldFunctions(reference, after, module)
val (name, rem) = getFunctionName(string)
val reference = Variable<Any>(name)
if(rem == "") return reference
return foldFunctions(reference, rem, module)
}

// entity.doSomething(at: {{ entity.location }}, )

tailrec fun foldFunctions(
reference: Expression<*>,
remainder: String,
module: SerializersModule
module: SerializersModule,
): Expression<*> {
val (name, afterName) = getFunctionName(remainder)
val (yaml, afterYaml) = getYaml(afterName ?: "{}")
val (yaml, afterYaml) = getYaml(afterName)
val functionExpr = FunctionExpression.parse(reference, name, yaml, module)
if (afterYaml == "") return functionExpr
return foldFunctions(functionExpr, afterYaml, module)
}

fun getYaml(expr: String): Pair<String, String> {
if (!expr.startsWith("{")) return "{}" to expr
var brackets = 0
val yamlEndIndex = expr.indexOfFirst {
if (it == '{') brackets++
if (it == '}') brackets--
brackets != 0
brackets == 0
}
return expr.take(yamlEndIndex - 1) to expr.drop(yamlEndIndex - 1)
if (yamlEndIndex <= 0) return "{}" to expr
return expr.take(yamlEndIndex + 1).trim() to expr.drop(yamlEndIndex + 1).trim()
}

fun getFunctionName(expr: String): Pair<String, String?> {
fun getFunctionName(expr: String): Pair<String, String> {
val yamlStart = expr.indexOf('{').toUInt()
val nextSection = expr.indexOf('.').toUInt()
val end = min(yamlStart, nextSection).toInt()
if (end == -1) return expr to null
return expr.take(end) to expr.drop(end)
}

fun of(string: String): Expression<*> {

if (end == -1) return expr.trim() to ""
return expr.take(end).trim() to expr.drop(end).removePrefix(".").trim()
}
}

// 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 val descriptor: SerialDescriptor = ContextualSerializer(Any::class).descriptor

override fun deserialize(decoder: Decoder): Expression<T> {
// Try reading string value, if serial type isn't string, this fails
Expand All @@ -93,7 +83,10 @@ sealed interface Expression<T> {
}
}.onSuccess { string ->
if (string.startsWith("{{") && string.endsWith("}}"))
return Evaluate(string.removePrefix("{{").removeSuffix("}}").trim())
return parseExpression(
string.removePrefix("{{").removeSuffix("}}").trim(),
decoder.serializersModule
) as Expression<T>
}

// Fallback to reading the value in-place
Expand All @@ -106,24 +99,7 @@ sealed interface Expression<T> {
TODO("Not yet implemented")
}
}
}

abstract class FunctionExpression<I, O>(
val input: Expression<I>,
val name: String,
val yaml: String
) : Expression<O> {
companion object {
fun parse(ref: Expression<*>, name: String, yaml: String, module: SerializersModule): FunctionExpression<*, *> {
TODO()
}
}

abstract fun map(input: I, context: ActionGroupContext): O

override fun evaluate(context: ActionGroupContext): O {
return map(context.eval(input), context)
}
}

fun <T> expr(value: T) = Expression.Fixed(value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.mineinabyss.geary.actions.expressions

import com.mineinabyss.geary.actions.ActionGroupContext
import com.mineinabyss.geary.serialization.serializableComponents
import com.mineinabyss.geary.serialization.serializers.SerializableComponentId
import kotlinx.serialization.modules.SerializersModule

interface FunctionExpression<I, O> {
companion object {
fun parse(
ref: Expression<*>,
name: String,
yaml: String,
module: SerializersModule,
): FunctionExpressionWithInput<*, *> {
val compClass = SerializableComponentId.Serializer.getComponent(name, module)
val serializer = serializableComponents.serializers.getSerializerFor(compClass)
?: error("No serializer found for component $name")
val expr =
serializableComponents.formats["yml"]!!.decodeFromString<FunctionExpression<*, *>>(serializer, yaml)
return FunctionExpressionWithInput(ref, expr)
}
}

fun ActionGroupContext.map(input: I): O

fun map(input: I, context: ActionGroupContext): O {
return with(context) { map(input) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.mineinabyss.geary.actions.expressions

import com.mineinabyss.geary.actions.ActionGroupContext

class FunctionExpressionWithInput<I, O>(
val ref: Expression<*>,
val expr: FunctionExpression<I, O>,
) : Expression<O> {
override fun evaluate(context: ActionGroupContext): O {
val input = ref.evaluate(context) as I
return expr.map(input, context)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.mineinabyss.geary.actions.expressions

import kotlinx.serialization.KSerializer
import kotlinx.serialization.builtins.serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

object InlineExpressionSerializer : KSerializer<Expression<*>> {
override val descriptor: SerialDescriptor = String.serializer().descriptor

override fun deserialize(decoder: Decoder): Expression<*> {
return Expression.parseExpression(
decoder.decodeString(),
decoder.serializersModule
)
}

override fun serialize(encoder: Encoder, value: Expression<*>) {
TODO("Not yet implemented")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ package com.mineinabyss.geary.actions

import com.charleskorn.kaml.Yaml
import com.mineinabyss.geary.actions.expressions.Expression
import com.mineinabyss.geary.actions.expressions.FunctionExpression
import com.mineinabyss.geary.datatypes.GearyEntity
import com.mineinabyss.geary.helpers.entity
import com.mineinabyss.geary.modules.TestEngineModule
import com.mineinabyss.geary.modules.geary
import com.mineinabyss.geary.serialization.dsl.serialization
import com.mineinabyss.geary.serialization.formats.YamlFormat
import com.mineinabyss.geary.serialization.serializableComponents
import com.mineinabyss.idofront.di.DI
import io.kotest.matchers.shouldBe
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
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 {
Expand All @@ -37,7 +40,7 @@ class ExpressionDecodingTest {
// )
// }

@org.junit.jupiter.api.Test
@Test
fun `should correctly decode yaml`() {
val input = """
{
Expand All @@ -48,8 +51,35 @@ class ExpressionDecodingTest {
""".trimIndent()
Yaml.default.decodeFromString(TestData.serializer(), input) shouldBe TestData(
name = Expression.Fixed("variable"),
age = Expression.Evaluate("test"),
age = Expression.Variable("test"),
regular = "{{ asdf }}"
)
}

@Serializable
@SerialName("geary:test_function")
class TestFunction(val string: String) : FunctionExpression<GearyEntity, String> {
override fun ActionGroupContext.map(input: GearyEntity): String {
return string
}
}

@Test
fun shouldCorrectlyParseExpressionFunctions() {
DI.clear()
geary(TestEngineModule){
serialization {
components {
component(TestFunction.serializer())
}
format("yml", ::YamlFormat)
}

}

geary.pipeline.runStartupTasks()
val input = "'{{ entity.geary:testFunction{ string: test } }}'"
val expr = Yaml.default.decodeFromString(Expression.Serializer(String.serializer()), input)
expr.evaluate(ActionGroupContext(entity = entity())) shouldBe "test"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import kotlinx.serialization.encoding.Encoder
abstract class InnerSerializer<I, O>(
val serialName: String,
val inner: KSerializer<I>,
val transform: (I) -> O,
val transform: Decoder.(I) -> O,
val inverseTransform: (O) -> I,
) : KSerializer<O> {
override val descriptor =
Expand All @@ -19,7 +19,7 @@ abstract class InnerSerializer<I, O>(
else SerialDescriptor(serialName, inner.descriptor)

override fun deserialize(decoder: Decoder): O {
return transform(inner.deserialize(decoder))
return transform(decoder, inner.deserialize(decoder))
}

override fun serialize(encoder: Encoder, value: O) {
Expand Down
Loading

0 comments on commit 1c136d4

Please sign in to comment.