From 36683e9db24ebdf3863b30611ad2e6b511981ac8 Mon Sep 17 00:00:00 2001 From: Jurriaan Mous Date: Sun, 7 Jul 2024 11:22:54 +0200 Subject: [PATCH] Add support for referencing direct Map keys in graphs. --- .../core/properties/graph/GraphContext.kt | 5 +- .../core/properties/graph/GraphMapItem.kt | 125 ++++++++++++++++++ .../core/properties/graph/IsPropRefGraph.kt | 37 ++++-- .../properties/graph/IsPropRefGraphNode.kt | 2 +- .../core/properties/graph/PropRefGraph.kt | 12 +- .../core/properties/graph/PropRefGraphType.kt | 3 +- .../core/properties/graph/RootPropRefGraph.kt | 31 +++-- .../properties/graph/RootPropRefGraphTest.kt | 24 ++++ 8 files changed, 215 insertions(+), 24 deletions(-) create mode 100644 core/src/commonMain/kotlin/maryk/core/properties/graph/GraphMapItem.kt diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphContext.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphContext.kt index 71def2698..0f4c6bfc8 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphContext.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphContext.kt @@ -1,10 +1,13 @@ package maryk.core.properties.graph import maryk.core.models.IsDataModel +import maryk.core.properties.definitions.IsSerializablePropertyDefinition +import maryk.core.properties.references.IsPropertyReference import maryk.core.query.ContainsDataModelContext /** Context for Graph serializing */ class GraphContext( override var dataModel: IsDataModel? = null, - var subDataModel: IsDataModel? = null + var subDataModel: IsDataModel? = null, + var reference: IsPropertyReference<*, IsSerializablePropertyDefinition<*, *>, *>? = null, ) : ContainsDataModelContext diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphMapItem.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphMapItem.kt new file mode 100644 index 000000000..ba4e005c0 --- /dev/null +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/GraphMapItem.kt @@ -0,0 +1,125 @@ +package maryk.core.properties.graph + +import maryk.core.exceptions.ContextNotFoundException +import maryk.core.models.ContextualDataModel +import maryk.core.models.IsDataModel +import maryk.core.models.IsValuesDataModel +import maryk.core.models.serializers.ObjectDataModelSerializer +import maryk.core.models.values +import maryk.core.properties.IsPropertyContext +import maryk.core.properties.definitions.IsMapDefinition +import maryk.core.properties.definitions.contextual.ContextualPropertyReferenceDefinition +import maryk.core.properties.definitions.contextual.ContextualSubDefinition +import maryk.core.properties.definitions.wrapper.IsDefinitionWrapper +import maryk.core.properties.definitions.wrapper.IsMapDefinitionWrapper +import maryk.core.properties.definitions.wrapper.contextual +import maryk.core.properties.graph.PropRefGraph.Companion.parent +import maryk.core.properties.graph.PropRefGraph.Companion.properties +import maryk.core.properties.references.IsMapReference +import maryk.core.query.ContainsDataModelContext +import maryk.core.values.ObjectValues +import maryk.json.IsJsonLikeReader +import maryk.json.IsJsonLikeWriter +import maryk.json.JsonToken +import maryk.lib.exceptions.ParseException + + +operator fun IsMapDefinitionWrapper.get( + key: K +) = GraphMapItem( + mapReference = this.ref() as IsMapReference<*, *, *, *>, + key = key, +) + +/** + * Represents a Property Reference Graph branch below a [parent] with all [properties] to fetch + * [properties] should always be sorted by index so processing graphs is a lot easier + */ +data class GraphMapItem internal constructor( + val mapReference: IsMapReference<*, *, *, *>, + val key: K, +) : IsPropRefGraphNode, IsTransportablePropRefGraphNode { + override val index = mapReference.index + override val graphType = PropRefGraphType.MapKey + + override fun toString() = "${this.mapReference.name}[$key]" + + companion object : ContextualDataModel, Companion, ContainsDataModelContext<*>, GraphContext>( + contextTransformer = { + if (it is GraphContext && it.subDataModel != null) { + GraphContext(it.subDataModel) + } else { + GraphContext(it?.dataModel) + } + } + ) { + val mapReference by contextual( + index = 1u, + getter = GraphMapItem<*, *>::mapReference, + definition = ContextualPropertyReferenceDefinition( + contextualResolver = { context: GraphContext? -> + (context?.subDataModel ?: context?.dataModel) as? IsValuesDataModel? ?: throw ContextNotFoundException() + } + ), + capturer = { context, value -> + @Suppress("UNCHECKED_CAST") + context.reference = value as IsMapReference, Any, IsPropertyContext, IsMapDefinitionWrapper, Any, Any, IsPropertyContext, Any>> + } + ) + + val key by contextual( + index = 2u, + getter = GraphMapItem<*, *>::key, + definition = ContextualSubDefinition( + contextualResolver = { context: GraphContext? -> + @Suppress("UNCHECKED_CAST") + (context?.reference as IsMapReference, Any, IsPropertyContext, IsMapDefinitionWrapper, Any, Any, IsPropertyContext, Any>>?)?.propertyDefinition?.definition?.keyDefinition + ?: throw ContextNotFoundException() + } + ) + ) + + override fun invoke(values: ObjectValues, Companion>): GraphMapItem<*, *> = GraphMapItem( + mapReference = values(1u), + key = values(2u) + ) + + override val Serializer = object: ObjectDataModelSerializer, Companion, ContainsDataModelContext<*>, GraphContext>(this) { + override fun writeObjectAsJson( + obj: GraphMapItem<*, *>, + writer: IsJsonLikeWriter, + context: GraphContext?, + skip: List>>? + ) { + writer.writeString("${obj.mapReference.name}[${obj.key}]") + } + + override fun readJson( + reader: IsJsonLikeReader, + context: GraphContext? + ): ObjectValues, Companion> { + val tokenToParse: JsonToken.Value<*> = reader.currentToken as? JsonToken.Value<*> + ?: throw ParseException("Expected value token") + + val (name, keyAsString) = tokenToParse.value.toString().split("[", "]") + + val mapReference = context?.dataModel?.getPropertyReferenceByName(name, context) as? IsMapReference<*, *, *, *> + ?: throw ParseException("Expected MapReference") + + @Suppress("UNCHECKED_CAST") + val key = when (val mapDef = mapReference.comparablePropertyDefinition) { + is IsMapDefinition<*, *, *> -> mapDef.keyDefinition.fromString(keyAsString) as Comparable + else -> throw ParseException("Unknown MapReference type") + } + + return values { + mapNonNulls( + this.mapReference withSerializable mapReference, + this.key withSerializable key + ) + } + } + } + } +} + diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraph.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraph.kt index aab26309f..5d2d43caa 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraph.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraph.kt @@ -5,8 +5,10 @@ import maryk.core.models.IsDataModel import maryk.core.properties.definitions.wrapper.IsDefinitionWrapper import maryk.core.properties.references.EmbeddedObjectPropertyRef import maryk.core.properties.references.EmbeddedValuesPropertyRef +import maryk.core.properties.references.IsMapReference import maryk.core.properties.references.IsPropertyReference import maryk.core.properties.references.IsPropertyReferenceForValues +import maryk.core.properties.references.MapValueReference /** Defines a graph element */ interface IsPropRefGraph { @@ -41,23 +43,36 @@ interface IsPropRefGraph { fun contains(reference: IsPropertyReference<*, *, *>): Boolean { val elements = reference.unwrap() - var referenceIndex = 0 - var currentReference = elements[referenceIndex++] - var currentSelect: IsPropRefGraph<*> = this + var currentNode: IsPropRefGraph<*> = this + var currentMapKey: GraphMapItem<*, *>? = null - loop@ while (referenceIndex <= elements.size) { + for ((index, currentReference) in elements.withIndex()) { return when (currentReference) { + is IsMapReference<*, *, *, *> -> { + if (index != elements.lastIndex) { + when (val node = currentNode.selectNodeOrNull(currentReference.index)) { + is PropRefGraph<*, *> -> break // Should not contain a PropRefGraph + is GraphMapItem<*, *> -> { + currentMapKey = node + continue // To next MapValueReference + } + } + } + currentNode.contains(currentReference.index) + } + is MapValueReference<*, *, *> -> { + currentMapKey != null && currentMapKey.key == currentReference.key + } is IsPropertyReferenceForValues<*, *, *, *> -> { - if (referenceIndex < elements.size && currentReference is EmbeddedValuesPropertyRef<*, *> || currentReference is EmbeddedObjectPropertyRef<*, *, *, *, *>) { - when (val node = currentSelect.selectNodeOrNull(currentReference.index)) { + if (index != elements.lastIndex && currentReference is EmbeddedValuesPropertyRef<*, *> || currentReference is EmbeddedObjectPropertyRef<*, *, *, *, *>) { + when (val node = currentNode.selectNodeOrNull(currentReference.index)) { is PropRefGraph<*, *> -> { - currentReference = elements[referenceIndex++] - currentSelect = node - continue@loop + currentNode = node + continue } - else -> currentSelect.contains(currentReference.index) + else -> currentNode.contains(currentReference.index) } - } else currentSelect.contains(currentReference.index) + } else currentNode.contains(currentReference.index) } else -> false } diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraphNode.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraphNode.kt index 777695652..3f2e7f9e6 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraphNode.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/IsPropRefGraphNode.kt @@ -3,7 +3,7 @@ package maryk.core.properties.graph import maryk.core.models.IsDataModel /** Defines an element which can be used within a graph */ -interface IsPropRefGraphNode { +interface IsPropRefGraphNode { val index: UInt val graphType: PropRefGraphType } diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraph.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraph.kt index e4f3d45f5..a6c5ae49b 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraph.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraph.kt @@ -17,6 +17,7 @@ import maryk.core.properties.definitions.wrapper.IsDefinitionWrapper import maryk.core.properties.definitions.wrapper.ListDefinitionWrapper import maryk.core.properties.definitions.wrapper.contextual import maryk.core.properties.graph.PropRefGraphType.Graph +import maryk.core.properties.graph.PropRefGraphType.MapKey import maryk.core.properties.graph.PropRefGraphType.PropRef import maryk.core.properties.references.AnyPropertyReference import maryk.core.properties.references.IsPropertyReferenceForValues @@ -94,6 +95,9 @@ data class PropRefGraph interna Graph to EmbeddedObjectDefinition( dataModel = { this@Companion } ), + MapKey to EmbeddedObjectDefinition( + dataModel = { GraphMapItem } + ), PropRef to ContextualPropertyReferenceDefinition( contextualResolver = { context: GraphContext? -> context?.subDataModel as? IsValuesDataModel? ?: throw ContextNotFoundException() @@ -108,6 +112,7 @@ data class PropRefGraph interna when (it) { is IsDefinitionWrapper<*, *, *, *> -> TypedValue(it.graphType, it.ref() as IsTransportablePropRefGraphNode) is PropRefGraph<*, *> -> TypedValue(it.graphType, it) + is GraphMapItem<*, *> -> TypedValue(it.graphType, it) else -> throw ParseException("Unknown PropRefGraphType ${it.graphType}") } } @@ -116,6 +121,7 @@ data class PropRefGraph interna when (value.type) { PropRef -> (value.value as IsPropertyReferenceForValues<*, *, *, *>).propertyDefinition Graph -> value.value as IsPropRefGraphNode<*> + MapKey -> value.value as GraphMapItem<*, *> } } ) @@ -133,7 +139,8 @@ data class PropRefGraph interna context: GraphContext?, skip: List>>? ) { - writeJsonValues(obj.parent.ref(), obj.properties, writer, context) + val newContext = transformContext(context) + writeJsonValues(obj.parent.ref(), obj.properties, writer, newContext) } private fun writeJsonValues( @@ -240,6 +247,9 @@ internal fun writePropertiesToJson( is PropRefGraph<*, *> -> PropRefGraph.Serializer.writeObjectAsJson( value, writer, context ) + is GraphMapItem<*, *> -> GraphMapItem.Serializer.writeObjectAsJson( + value, writer, context + ) is IsDefinitionWrapper<*, *, *, *> -> { writer.writeString(value.ref().completeName) } diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraphType.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraphType.kt index eb547f103..7d3052d96 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraphType.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/PropRefGraphType.kt @@ -11,7 +11,8 @@ enum class PropRefGraphType( override val alternativeNames: Set? = null ) : IndexedEnumComparable, IsCoreEnum, TypeEnum { PropRef(1u), - Graph(2u); + Graph(2u), + MapKey(3u); companion object : IndexedEnumDefinition( PropRefGraphType::class, { entries } diff --git a/core/src/commonMain/kotlin/maryk/core/properties/graph/RootPropRefGraph.kt b/core/src/commonMain/kotlin/maryk/core/properties/graph/RootPropRefGraph.kt index fe0ce1246..37b0887c7 100644 --- a/core/src/commonMain/kotlin/maryk/core/properties/graph/RootPropRefGraph.kt +++ b/core/src/commonMain/kotlin/maryk/core/properties/graph/RootPropRefGraph.kt @@ -14,6 +14,7 @@ import maryk.core.properties.definitions.list import maryk.core.properties.definitions.wrapper.IsDefinitionWrapper import maryk.core.properties.definitions.wrapper.ListDefinitionWrapper import maryk.core.properties.graph.PropRefGraphType.Graph +import maryk.core.properties.graph.PropRefGraphType.MapKey import maryk.core.properties.graph.PropRefGraphType.PropRef import maryk.core.properties.references.IsPropertyReferenceForValues import maryk.core.properties.types.TypedValue @@ -47,24 +48,27 @@ data class RootPropRefGraph internal constructor( contextualResolver = { context: GraphContext? -> context?.dataModel as? IsValuesDataModel? ?: throw ContextNotFoundException() } + ), + MapKey to EmbeddedObjectDefinition( + dataModel = { GraphMapItem } ) ), typeEnum = PropRefGraphType ), getter = RootPropRefGraph<*>::properties, toSerializable = { value: IsPropRefGraphNode<*> -> - value.let { - when (it) { - is IsDefinitionWrapper<*, *, *, *> -> TypedValue(it.graphType, it.ref() as IsTransportablePropRefGraphNode) - is PropRefGraph<*, *> -> TypedValue(it.graphType, it) - else -> throw ParseException("Unknown PropRefGraphType ${it.graphType}") - } + when (value) { + is IsDefinitionWrapper<*, *, *, *> -> TypedValue(value.graphType, value.ref() as IsTransportablePropRefGraphNode) + is PropRefGraph<*, *> -> TypedValue(value.graphType, value) + is GraphMapItem<*, *> -> TypedValue(value.graphType, value) + else -> throw ParseException("Unknown PropRefGraphType ${value.graphType}") } }, fromSerializable = { value: TypedValue -> when (value.type) { PropRef -> (value.value as IsPropertyReferenceForValues<*, *, *, *>).propertyDefinition Graph -> value.value as IsPropRefGraphNode<*> + MapKey -> value.value as IsPropRefGraphNode<*> } } ) @@ -111,10 +115,12 @@ data class RootPropRefGraph internal constructor( while (currentToken != JsonToken.EndArray && currentToken !is JsonToken.Stopped) { when (currentToken) { is JsonToken.StartObject -> { + val newContext = transformContext(context) + propertiesList.add( TypedValue( Graph, - PropRefGraph.Serializer.readJson(reader, context).toDataObject() + PropRefGraph.Serializer.readJson(reader, newContext).toDataObject() ) ) } @@ -122,10 +128,17 @@ data class RootPropRefGraph internal constructor( val multiTypeDefinition = this@Companion.properties.valueDefinition as IsMultiTypeDefinition + val currentTokenValue = currentToken.value + + val type = when { + currentTokenValue is String && currentTokenValue.contains(char = '[') -> MapKey + else -> PropRef + } + propertiesList.add( TypedValue( - PropRef, - multiTypeDefinition.definition(PropRef)!!.readJson(reader, context) + type, + multiTypeDefinition.definition(type)!!.readJson(reader, context) ) ) } diff --git a/core/src/commonTest/kotlin/maryk/core/properties/graph/RootPropRefGraphTest.kt b/core/src/commonTest/kotlin/maryk/core/properties/graph/RootPropRefGraphTest.kt index 275763357..7ea0f1480 100644 --- a/core/src/commonTest/kotlin/maryk/core/properties/graph/RootPropRefGraphTest.kt +++ b/core/src/commonTest/kotlin/maryk/core/properties/graph/RootPropRefGraphTest.kt @@ -1,5 +1,6 @@ package maryk.core.properties.graph +import kotlinx.datetime.LocalTime import maryk.checkJsonConversion import maryk.checkProtoBufConversion import maryk.checkYamlConversion @@ -8,6 +9,8 @@ import maryk.core.properties.definitions.contextual.DataModelReference import maryk.core.query.RequestContext import maryk.test.models.TestMarykModel import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue import kotlin.test.expect @@ -16,6 +19,8 @@ class RootPropRefGraphTest { listOf( string, set, + map[LocalTime(12, 34, 56)], + incMap[2u], graph(embeddedValues) { listOf( value, @@ -34,6 +39,23 @@ class RootPropRefGraphTest { dataModel = TestMarykModel ) + @Test + fun containsReference() { + assertTrue(graph.contains(TestMarykModel { string::ref })) + assertFalse(graph.contains(TestMarykModel { int::ref })) + + assertTrue(graph.contains(TestMarykModel { map::ref })) + assertTrue(graph.contains(TestMarykModel { map.refAt(LocalTime(12, 34, 56)) })) + assertFalse(graph.contains(TestMarykModel { map.refAt(LocalTime(1, 2, 3)) })) + + assertTrue(graph.contains(TestMarykModel { incMap::ref })) + assertTrue(graph.contains(TestMarykModel { incMap.refAt(2u) })) + assertFalse(graph.contains(TestMarykModel { incMap.refAt(3u) })) + + assertTrue(graph.contains(TestMarykModel { embeddedValues::ref })) + assertTrue(graph.contains(TestMarykModel { embeddedValues { value::ref } })) + } + @Test fun convertToProtoBufAndBack() { checkProtoBufConversion(this.graph, RootPropRefGraph, { this.context }) @@ -50,10 +72,12 @@ class RootPropRefGraphTest { """ - string - set + - 'map[12:34:56]' - embeddedValues: - value - model: - value + - incMap[2] """.trimIndent() ) {