Skip to content

Commit a3d529e

Browse files
authored
Merge pull request #677 from modelix/fix/bulk-sync-update-prev-unresolvable-refs
fix(bulk-model-sync): update previously unresolvable references if they can be resolved
2 parents 62d84b4 + 9713c48 commit a3d529e

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

bulk-model-sync-lib/src/commonMain/kotlin/org/modelix/model/sync/bulk/ModelImporter.kt

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -80,20 +80,23 @@ class ModelImporter(
8080
}
8181
}
8282

83-
data class PostponedReference(
83+
private data class PostponedReference(
8484
val expectedTargetId: String,
85-
val mpsNode: INode,
85+
val node: INode,
8686
val role: String,
8787
)
8888

8989
private fun PostponedReference.setPostponedReference() {
9090
val expectedRefTarget = originalIdToExisting[expectedTargetId]
91-
if (expectedRefTarget == null) {
91+
if (expectedRefTarget != null) {
92+
node.setReferenceTarget(role, expectedRefTarget)
93+
return
94+
}
95+
// Ensure unresolvable reference wasn't stored before
96+
if (node.getReferenceTargetRef(role)?.serialize() != expectedTargetId) {
9297
// The target node is not part of the model. Assuming it exists in some other model we can
9398
// store the reference and try to resolve it dynamically on access.
94-
mpsNode.setReferenceTarget(role, SerializedNodeReference(expectedTargetId))
95-
} else {
96-
mpsNode.setReferenceTarget(role, expectedRefTarget)
99+
node.setReferenceTarget(role, SerializedNodeReference(expectedTargetId))
97100
}
98101
}
99102

@@ -293,16 +296,13 @@ class ModelImporter(
293296
}
294297

295298
private fun syncReferences(node: INode, nodeData: NodeData) {
296-
nodeData.references.forEach {
297-
val expectedTargetId = it.value
298-
val actualTargetId = node.getReferenceTarget(it.key)?.originalId()
299-
?: node.getReferenceTargetRef(it.key)?.serialize()
300-
if (actualTargetId != expectedTargetId) {
299+
nodeData.references.forEach { (referenceRole, expectedTargetId) ->
300+
if (referenceNeedsUpdate(node, referenceRole, expectedTargetId)) {
301301
val expectedTarget = originalIdToExisting[expectedTargetId]
302302
if (expectedTarget == null) {
303-
postponedReferences += PostponedReference(expectedTargetId, node, it.key)
303+
postponedReferences += PostponedReference(expectedTargetId, node, referenceRole)
304304
} else {
305-
node.setReferenceTarget(it.key, expectedTarget)
305+
node.setReferenceTarget(referenceRole, expectedTarget)
306306
}
307307
}
308308
}
@@ -311,6 +311,14 @@ class ModelImporter(
311311
node.setReferenceTarget(it, null as INodeReference?)
312312
}
313313
}
314+
315+
private fun referenceNeedsUpdate(node: INode, referenceRole: String, expectedTargetRef: String): Boolean {
316+
val actualTarget = node.getReferenceTarget(referenceRole)
317+
?: return true // previously unresolvable reference might become resolvable e.g., when a referenced node was added
318+
val actualTargetRef = actualTarget.originalId() ?: node.getReferenceTargetRef(referenceRole)?.serialize()
319+
320+
return actualTargetRef != expectedTargetRef
321+
}
314322
}
315323

316324
internal fun INode.originalId(): String? {

bulk-model-sync-lib/src/commonTest/kotlin/org/modelix/model/sync/bulk/ModelImporterTest.kt

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.modelix.model.api.IBranch
2020
import org.modelix.model.api.IProperty
2121
import org.modelix.model.api.IReferenceLink
2222
import org.modelix.model.api.PBranch
23+
import org.modelix.model.api.PNodeReference
2324
import org.modelix.model.api.getRootNode
2425
import org.modelix.model.client.IdGenerator
2526
import org.modelix.model.data.ModelData
@@ -42,6 +43,7 @@ import kotlin.random.Random
4243
import kotlin.reflect.KClass
4344
import kotlin.test.Test
4445
import kotlin.test.assertEquals
46+
import kotlin.test.assertNotNull
4547
import kotlin.test.assertTrue
4648

4749
class ModelImporterTest {
@@ -198,6 +200,75 @@ class ModelImporterTest {
198200
assertEquals(expectedOperations, branch.getNumOfUsedOperationsByType())
199201
}
200202

203+
@Test
204+
@JsName("can_update_previously_unresolvable_references")
205+
fun `can update previously unresolvable reference`() {
206+
// language=json
207+
val initialData = """
208+
{
209+
"root": {
210+
"id": "root",
211+
"children": [
212+
{
213+
"id": "node:001",
214+
"references": {
215+
"myRef": "node:002",
216+
"myRef2": "node:unresolvable"
217+
}
218+
}
219+
]
220+
}
221+
}
222+
""".trimIndent().let { ModelData.fromJson(it) }
223+
224+
// language=json
225+
val expectedData = """
226+
{
227+
"root": {
228+
"id": "root",
229+
"children": [
230+
{
231+
"id": "node:001",
232+
"references": {
233+
"myRef": "node:002",
234+
"myRef2": "node:unresolvable"
235+
}
236+
},
237+
{
238+
"id": "node:002",
239+
"properties": {
240+
"name": "PreviouslyUnresolvableNode"
241+
}
242+
}
243+
]
244+
}
245+
}
246+
""".trimIndent().let { ModelData.fromJson(it) }
247+
248+
val expectedOperations: Map<KClass<out IOperation>, Int> = mapOf(
249+
SetReferenceOp::class to 1,
250+
AddNewChildOp::class to 1,
251+
SetPropertyOp::class to 2, // name and originalRef of new node
252+
)
253+
254+
val branch = createOTBranchFromModel(initialData)
255+
branch.importIncrementally(expectedData)
256+
257+
branch.runRead {
258+
val root = branch.getRootNode()
259+
val sourceNode = root.allChildren.first()
260+
val referenceLink = IReferenceLink.fromName("myRef")
261+
262+
val targetReference = sourceNode.getReferenceTargetRef(referenceLink)
263+
val targetNode = sourceNode.getReferenceTarget(referenceLink)
264+
265+
assertNotNull(targetNode)
266+
assertTrue { targetReference is PNodeReference }
267+
assertAllNodesConformToSpec(expectedData.root, root)
268+
assertEquals(expectedOperations, branch.getNumOfUsedOperationsByType())
269+
}
270+
}
271+
201272
@Test
202273
@JsName("can_sync_children")
203274
fun `can sync children`() {

0 commit comments

Comments
 (0)