Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin
.gradle/
**/build
/*/ignite/
Expand Down
16 changes: 16 additions & 0 deletions model-api/src/commonMain/kotlin/org/modelix/model/api/IRole.kt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,22 @@ interface IRoleReferenceFactory<E : IRoleReference> {
fun fromIdAndName(id: String?, name: String?): E
fun fromLegacyApi(value: String?): E = IRoleReference.decodeStringFromLegacyApi(value, this)
fun fromString(value: String?): E = fromLegacyApi(value)

/**
* Convert a role object, string representation, or legacy role interface into this reference type.
* Handles IRoleReference objects, IRoleDefinition objects, and string representations uniformly.
* @param value can be a String, IRoleReference, IRoleDefinition, or null
* @return converted reference
*/
fun fromRoleOrString(value: Any?): E {
return when (value) {
is String -> fromString(value)
is IRoleReference -> value as? E ?: fromString(value.stringForLegacyApi())
is IRoleDefinition -> fromRoleOrString(value.toReference())
null -> fromNull()
else -> error("Unsupported role type: ${value::class}")
}
}
}

fun IRoleReference.matches(other: IRoleReference): Boolean {
Expand Down
15 changes: 15 additions & 0 deletions model-api/src/commonTest/kotlin/ReferenceSerializationTests.kt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import org.modelix.model.api.IReferenceLinkReference
import org.modelix.model.api.IRoleReference
import org.modelix.model.api.PNodeReference
import org.modelix.model.api.UnclassifiedReferenceLinkReference
import kotlin.test.Test
import kotlin.test.assertEquals

Expand All @@ -19,4 +22,16 @@ class ReferenceSerializationTests {
PNodeReference.deserialize("modelix:2bfd9f5e-95d0-11ee-b9d1-0242ac120002/abcd1234"),
)
}

@Test
fun decodeUnclassifiedReferenceLinkReference() {
val unclassified = UnclassifiedReferenceLinkReference(
"2bca1aa3-c113-4542-8ac2-2a6a30636981/6006699537885399164/6006699537885399165",
)

assertEquals(
IRoleReference.decodeStringFromLegacyApi(unclassified.stringForLegacyApi(), IReferenceLinkReference),
IReferenceLinkReference.fromId("2bca1aa3-c113-4542-8ac2-2a6a30636981/6006699537885399164/6006699537885399165"),
)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package org.modelix.model.api

import ReferenceRole

@JsExport
sealed class MapWithRoleKey<V : Any>(private val type: IRoleReferenceFactory<*>) {
private val entries = ArrayList<Pair<IRoleReference, V>>()

private fun get(role: IRoleReference): V? {
private fun _get(role: IRoleReference): V? {
val index = entries.indexOfFirst { it.first.matches(role) }
if (index < 0) return null

Expand All @@ -15,7 +17,7 @@ sealed class MapWithRoleKey<V : Any>(private val type: IRoleReferenceFactory<*>)
return entries[index].second
}

private fun put(role: IRoleReference, value: V) {
private fun _put(role: IRoleReference, value: V) {
val index = entries.indexOfFirst { it.first.matches(role) }
if (index >= 0) {
entries[index] = role.merge(entries[index].first) to value
Expand All @@ -24,17 +26,17 @@ sealed class MapWithRoleKey<V : Any>(private val type: IRoleReferenceFactory<*>)
}
}

fun put(roleString: String, value: V) {
put(type.fromString(roleString), value)
fun put(role: ReferenceRole, value: V) {
_put(type.fromRoleOrString(role), value)
}

fun get(roleString: String): V? {
return get(type.fromString(roleString))
fun get(role: ReferenceRole): V? {
return _get(type.fromRoleOrString(role))
}

fun getOrPut(roleString: String, initializer: () -> V): V {
val role = type.fromString(roleString)
return get(role) ?: initializer().also { put(role.merge(role), it) }
fun getOrPut(role: ReferenceRole, initializer: () -> V): V {
val convertedRole = type.fromRoleOrString(role)
return _get(convertedRole) ?: initializer().also { _put(convertedRole.merge(convertedRole), it) }
}
}

Expand Down
43 changes: 9 additions & 34 deletions model-api/src/jsMain/kotlin/org/modelix/model/api/NodeAdapterJS.kt
Original file line number Diff line number Diff line change
Expand Up @@ -74,20 +74,20 @@ class NodeAdapterJS(val node: INode) : INodeJS_ {
override fun getParent(): INodeJS? = node.parent?.let { NodeAdapterJS(it) }

override fun getChildren(role: ChildRole?): Array<INodeJS> {
return node.asReadableNode().getChildren(toChildLink(role)).map { NodeAdapterJS(it.asLegacyNode()) }.toTypedArray()
return node.asReadableNode().getChildren(IChildLinkReference.fromRoleOrString(role)).map { NodeAdapterJS(it.asLegacyNode()) }.toTypedArray()
}

override fun getAllChildren(): Array<INodeJS> {
return node.asReadableNode().getAllChildren().map { NodeAdapterJS(it.asLegacyNode()) }.toTypedArray()
}

override fun moveChild(role: ChildRole?, index: Number, child: INodeJS) {
node.asWritableNode().moveChild(toChildLink(role), index.toInt(), (child as NodeAdapterJS).node.asWritableNode())
node.asWritableNode().moveChild(IChildLinkReference.fromRoleOrString(role), index.toInt(), (child as NodeAdapterJS).node.asWritableNode())
}

override fun addNewChild(role: ChildRole?, index: Number, concept: IConceptJS?): INodeJS {
val conceptRef = concept?.getUID()?.let { ConceptReference(it) } ?: NullConcept.getReference()
return node.asWritableNode().addNewChild(toChildLink(role), index.toInt(), conceptRef)
return node.asWritableNode().addNewChild(IChildLinkReference.fromRoleOrString(role), index.toInt(), conceptRef)
.let { NodeAdapterJS(it.asLegacyNode()) }
}

Expand All @@ -104,22 +104,22 @@ class NodeAdapterJS(val node: INode) : INodeJS_ {
}

override fun getReferenceTargetNode(role: ReferenceRole): INodeJS? {
return node.asReadableNode().getReferenceTarget(toReferenceLink(role))
return node.asReadableNode().getReferenceTarget(IReferenceLinkReference.fromRoleOrString(role))
?.let { NodeAdapterJS(it.asLegacyNode()) }
}

override fun getReferenceTargetRef(role: ReferenceRole): INodeReferenceJS? {
return node.asReadableNode().getReferenceTargetRef(toReferenceLink(role))?.serialize()
return node.asReadableNode().getReferenceTargetRef(IReferenceLinkReference.fromRoleOrString(role))?.serialize()
}

override fun setReferenceTargetNode(role: ReferenceRole, target: INodeJS?) {
val unwrappedTarget = if (target == null) null else (target as NodeAdapterJS).node
node.asWritableNode().setReferenceTarget(toReferenceLink(role), unwrappedTarget?.asWritableNode())
node.asWritableNode().setReferenceTarget(IReferenceLinkReference.fromRoleOrString(role), unwrappedTarget?.asWritableNode())
}

override fun setReferenceTargetRef(role: ReferenceRole, target: INodeReferenceJS?) {
node.asWritableNode().setReferenceTargetRef(
toReferenceLink(role),
IReferenceLinkReference.fromRoleOrString(role),
target
?.let { INodeReferenceSerializer.deserialize(it as String) },
)
Expand All @@ -130,11 +130,11 @@ class NodeAdapterJS(val node: INode) : INodeJS_ {
}

override fun getPropertyValue(role: PropertyRole): String? {
return node.asReadableNode().getPropertyValue(toPropertyReference(role)) ?: undefined
return node.asReadableNode().getPropertyValue(IPropertyReference.fromRoleOrString(role)) ?: undefined
}

override fun setPropertyValue(role: PropertyRole, value: String?) {
node.asWritableNode().setPropertyValue(toPropertyReference(role), value)
node.asWritableNode().setPropertyValue(IPropertyReference.fromRoleOrString(role), value)
}

override fun equals(other: Any?): Boolean {
Expand All @@ -151,29 +151,4 @@ class NodeAdapterJS(val node: INode) : INodeJS_ {
override fun hashCode(): Int {
return node.hashCode()
}

private fun toPropertyReference(role: PropertyRole): IPropertyReference = when (role) {
is String -> IPropertyReference.fromString(role)
is IPropertyReference -> role
is IProperty -> role.toReference()
is IPropertyDefinition -> role.toReference()
else -> error("Not a property role: $role")
}

private fun toReferenceLink(role: ReferenceRole): IReferenceLinkReference = when (role) {
is String -> IReferenceLinkReference.fromString(role)
is IReferenceLinkReference -> role
is IReferenceLink -> role.toReference()
is IReferenceLinkDefinition -> role.toReference()
else -> error("Not a reference role: $role")
}

private fun toChildLink(role: ChildRole): IChildLinkReference = when (role) {
is String -> IChildLinkReference.fromString(role)
is IChildLinkReference -> role
is IChildLink -> role.toReference()
is IChildLinkDefinition -> role.toReference()
null -> NullChildLinkReference
else -> error("Not a child role: $role")
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package org.modelix.model.api

import org.modelix.model.ModelFacade
import kotlin.test.Test
import kotlin.test.assertEquals

class MapWithRoleKeyTest {
@Test
fun referenceLinkMapCachesValueByRoleObject() {
println("\r\n # referenceLinkMapCachesValueByRoleObject")
val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree())
val (source, target) = branch.computeWrite {
val root = branch.getRootNode()
val source = root.addNewChild("source")
val target = root.addNewChild("target")
source.setReferenceTarget(IReferenceLinkReference.fromId("testRef").toLegacy(), target)
source to target
}

val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()

val first = map.getOrPut(roleObject) { jsTarget }
assertEquals(jsTarget, first)

val second = map.getOrPut(roleObject) { NodeAdapterJS(target) }
assertEquals(jsTarget, second)
}

@Test
fun IReferenceLinkReference_stringForLegacyApi() {
val aNode = ModelFacade.toLocalBranch(ModelFacade.newLocalTree()).getRootNode()
assertEquals(
IReferenceLinkReference.fromIdAndName("refId", "refName").toLegacy().key(aNode),
":refId:refName",
)
}

@Test
fun mapFindsValueAcrossRoleRepresentations() {
println("\r\n # mapFindsValueAcrossRoleRepresentations")
val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree())
val (source, target) = branch.computeWrite {
val root = branch.getRootNode()
val source = root.addNewChild("source")
val target = root.addNewChild("target")
source.setReferenceTarget(IReferenceLinkReference.fromIdAndName("refId", "refName").toLegacy(), target)
source to target
}

println("source is a ${source::class}")
val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()

map.getOrPut(roleObject) { jsTarget }

// Once we've looked it up with a combined string, the entry remembers both as keys
assertEquals(jsTarget, map.getOrPut(":refId:refName") { error("Miss with combined string") })

// All these lookups should hit the same cached value
assertEquals(jsTarget, map.getOrPut(IReferenceLinkReference.fromId("refId")) { error("Miss with id") })
assertEquals(jsTarget, map.getOrPut(IReferenceLinkReference.fromName("refName")) { error("Miss with name") })
}

private fun setupReferencingNodes(usesRoleIds: Boolean = false): Triple<INode, INode, INode> {
val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree(usesRoleIds))
return branch.computeWrite {
val root = branch.getRootNode()
val source = root.addNewChild("source")
val target = root.addNewChild("target")
source.setReferenceTarget(IReferenceLinkReference.fromIdAndName("refId", "refName").toLegacy(), target)
Triple(source, target, root.addNewChild("another"))
}
}

@Test
fun usingRoleIds_lookup_by_name_only_misses() {
println("\r\n # usingRoleIds_lookup_by_name_only_misses")
// given I have some nodes
val (source, target, another) = setupReferencingNodes(usesRoleIds = true)

// and the cache has warmed up
val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()
map.getOrPut(roleObject) { jsTarget }

// then I still miss with a name-only lookup
assertEquals(another, map.getOrPut(IReferenceLinkReference.fromName("refName")) { NodeAdapterJS(another) }.node)
}

@Test
fun usingRoleIds_lookup_with_id_only_hits() {
println("\r\n # usingRoleIds_lookup_with_id_only_hits")
// given I have some nodes
val (source, target, another) = setupReferencingNodes(usesRoleIds = true)

// and the cache has warmed up
val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()
map.getOrPut(roleObject) { jsTarget }

// then I hit the cache with an id-only lookup
assertEquals(target, map.getOrPut(IReferenceLinkReference.fromId("refId")) { error("Miss with id") }.node)
}

@Test
fun notUsingRoleIds_lookup_by_id_only_misses() {
println("\r\n # notUsingRoleIds_lookup_by_id_only_misses")
// given I have some nodes
val (source, target, another) = setupReferencingNodes(usesRoleIds = false)

// and the cache has warmed up
val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()
map.getOrPut(roleObject) { jsTarget }

// then I still miss with an id-only lookup
assertEquals(another, map.getOrPut(IReferenceLinkReference.fromId("refId")) { NodeAdapterJS(another) }.node)
}

@Test
fun notUsingRoleIds_lookup_with_name_only_hits() {
println("\r\n # notUsingRoleIds_lookup_with_name_only_hits")
// given I have some nodes
val (source, target, another) = setupReferencingNodes(usesRoleIds = false)

// and the cache has warmed up
val jsTarget = NodeAdapterJS(target)
val roleObject = NodeAdapterJS(source).getReferenceRoles().first()
val map = MapWithReferenceRoleKey<NodeAdapterJS>()
map.getOrPut(roleObject) { jsTarget }

// then I hit the cache with a name-only lookup
assertEquals(target, map.getOrPut(IReferenceLinkReference.fromName("refName")) { error("Miss with name") }.node)
}

@Test
fun childLinkMapCachesValueByRoleObject() {
println("\r\n # childLinkMapCachesValueByRoleObject")
val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree())
val child = branch.computeWrite {
branch.getRootNode().addNewChild("children")
}

val roleObject = NodeAdapterJS(child).getRoleInParent() ?: error("No role")
val map = MapWithChildRoleKey<String>()

val first = map.getOrPut(roleObject) { "cached" }
assertEquals("cached", first)

val second = map.getOrPut(roleObject) { "uncached" }
assertEquals("cached", second)
}

@Test
fun propertyMapFindsCachedValueByStringAndRoleObject() {
println("\r\n # propertyMapFindsCachedValueByStringAndRoleObject")
val branch = ModelFacade.toLocalBranch(ModelFacade.newLocalTree())
val node = branch.computeWrite {
val n = branch.getRootNode()
n.setPropertyValue("name", "testValue")
n
}

val jsNode = NodeAdapterJS(node)
val roleObject = jsNode.getPropertyRoles().first()
val map = MapWithPropertyRoleKey<String>()

val stored = map.getOrPut(roleObject) { "stored" }
assertEquals("stored", stored)

// Lookup by string should find the same cached value
val found = map.getOrPut("name") { "uncached" }
assertEquals("stored", found)
}
}
Loading
Loading