Skip to content

Commit 8589d66

Browse files
committed
feat(model-datastructure): a version can now store additional arbitrary attributes
Useful for attaching information about the import source (git commit ID) that can later be used for an incremental re-import.
1 parent a16add5 commit 8589d66

File tree

8 files changed

+104
-15
lines changed

8 files changed

+104
-15
lines changed

model-datastructure/src/commonMain/kotlin/org/modelix/model/IVersion.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package org.modelix.model
22

33
import org.modelix.datastructures.model.IGenericModelTree
4+
import org.modelix.datastructures.objects.Object
5+
import org.modelix.datastructures.objects.ObjectHash
46
import org.modelix.model.api.INodeReference
57
import org.modelix.model.api.ITree
8+
import org.modelix.model.lazy.VersionBuilder
69
import kotlin.jvm.JvmInline
710

811
interface IVersion {
@@ -14,6 +17,16 @@ interface IVersion {
1417
fun getTrees(): Map<TreeType, ITree>
1518

1619
fun getModelTree(): IGenericModelTree<INodeReference>
20+
21+
fun getAttributes(): Map<String, String>
22+
23+
fun asObject(): Object<*>
24+
25+
fun getObjectHash(): ObjectHash = asObject().getHash()
26+
27+
companion object {
28+
fun builder(): VersionBuilder = VersionBuilder()
29+
}
1730
}
1831

1932
@JvmInline

model-datastructure/src/commonMain/kotlin/org/modelix/model/VersionMerger.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ class VersionMerger(private val idGenerator: IIdGenerator) {
102102
.tree(mutableTree.getTransaction().tree)
103103
.autoMerge(commonBase.obj.ref, leftVersion.obj.ref, rightVersion.obj.ref)
104104
.operations(appliedOps.map { it.getOriginalOp() })
105-
.build()
105+
.buildLegacy()
106106
}
107107
if (mergedVersion == null) {
108108
throw RuntimeException("Failed to merge ${leftVersion.getObjectHash()} and ${rightVersion.getObjectHash()}")

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/CLVersion.kt

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
8585
get() = getContentHash()
8686

8787
override fun getContentHash(): String = getObjectHash().toString()
88-
fun getObjectHash(): ObjectHash = resolvedData.ref.getHash()
88+
override fun getObjectHash(): ObjectHash = resolvedData.ref.getHash()
89+
override fun asObject() = obj
8990

9091
@Deprecated("Use getTree()", ReplaceWith("getTree()"))
9192
@get:JvmName("getTree_()")
@@ -153,6 +154,8 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
153154
return data.operations != null
154155
}
155156

157+
override fun getAttributes(): Map<String, String> = obj.data.attributes
158+
156159
fun isMerge() = this.data.mergedVersion1 != null
157160

158161
fun getMergedVersion1() = obj.data.mergedVersion1?.let { CLVersion(it.resolveLater().query()) }
@@ -216,7 +219,7 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
216219
.tree(tree)
217220
.autoMerge(baseVersion.obj.ref, mergedVersion1.obj.ref, mergedVersion2.obj.ref)
218221
.operations(operations)
219-
.build()
222+
.buildLegacy()
220223
}
221224

222225
@Deprecated("Use builder()")
@@ -237,7 +240,7 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
237240
.tree(tree)
238241
.also { if (baseVersion != null) it.regularUpdate(baseVersion.obj.ref) }
239242
.operations(operations)
240-
.build()
243+
.buildLegacy()
241244
}
242245

243246
@Deprecated("Use builder()")
@@ -256,7 +259,7 @@ class CLVersion(val obj: Object<CPVersion>) : IVersion {
256259
.tree(tree)
257260
.also { if (baseVersion != null) it.regularUpdate(baseVersion.obj.ref) }
258261
.operations(operations)
259-
.build()
262+
.buildLegacy()
260263
}
261264

262265
fun loadFromHash(hash: String, store: IDeserializingKeyValueStore): CLVersion {

model-datastructure/src/commonMain/kotlin/org/modelix/model/lazy/VersionBuilder.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package org.modelix.model.lazy
22

33
import kotlinx.datetime.Clock
44
import kotlinx.datetime.Instant
5-
import org.modelix.datastructures.model.IModelTree
5+
import org.modelix.datastructures.model.IGenericModelTree
66
import org.modelix.datastructures.objects.IObjectGraph
77
import org.modelix.datastructures.objects.Object
88
import org.modelix.datastructures.objects.ObjectReference
@@ -30,6 +30,7 @@ class VersionBuilder {
3030
private var operationsHash: ObjectReference<OperationsList>? = null
3131
private var numberOfOperations: Int = 0
3232
private var graph: IObjectGraph? = null
33+
private var attributes: Map<String, String> = emptyMap()
3334

3435
@Deprecated("Not mandatory anymore. Usages of the ID should be replaced by the ObjectHash.")
3536
fun id(value: Long) = also { this.id = value }
@@ -44,7 +45,7 @@ class VersionBuilder {
4445
}
4546
fun tree(value: Object<CPTree>) = tree(TreeType.Companion.MAIN, value)
4647
fun tree(value: ITree) = tree(value.asObject() as Object<CPTree>)
47-
fun tree(value: IModelTree) = tree(value.asObject() as Object<CPTree>)
48+
fun tree(value: IGenericModelTree<*>) = tree(value.asObject() as Object<CPTree>)
4849
fun graph(value: IObjectGraph?) = also { it.graph = value }
4950

5051
fun regularUpdate(baseVersion: IVersion) = regularUpdate((baseVersion as CLVersion).obj.ref)
@@ -84,6 +85,10 @@ class VersionBuilder {
8485
}
8586
}
8687

88+
fun attribute(key: String, value: String) = also {
89+
attributes += key to value
90+
}
91+
8792
fun buildData() = CPVersion(
8893
id = id ?: 0,
8994
time = time,
@@ -99,9 +104,14 @@ class VersionBuilder {
99104
operations = operations,
100105
operationsHash = operationsHash,
101106
numberOfOperations = numberOfOperations,
107+
attributes = attributes,
102108
)
103109

104-
fun build(): CLVersion {
110+
fun build(): IVersion {
111+
return buildLegacy()
112+
}
113+
114+
fun buildLegacy(): CLVersion {
105115
return CLVersion(buildData().asObject(checkNotNull(graph) { "object graph not specified" }))
106116
}
107117

model-datastructure/src/commonMain/kotlin/org/modelix/model/persistent/CPVersion.kt

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import org.modelix.datastructures.objects.Object
88
import org.modelix.datastructures.objects.ObjectReference
99
import org.modelix.datastructures.objects.getHashString
1010
import org.modelix.datastructures.patricia.PatriciaNode
11+
import org.modelix.kotlin.utils.urlDecode
12+
import org.modelix.kotlin.utils.urlEncode
1113
import org.modelix.model.TreeType
1214
import org.modelix.model.lazy.CLVersion.Companion.INLINED_OPS_LIMIT
1315
import org.modelix.model.operations.IOperation
@@ -45,6 +47,11 @@ data class CPVersion(
4547
val operations: List<IOperation>?,
4648
val operationsHash: ObjectReference<OperationsList>?,
4749
val numberOfOperations: Int,
50+
51+
/**
52+
* Additional information such as the Git commit ID that was imported.
53+
*/
54+
val attributes: Map<String, String> = emptyMap(),
4855
) : IObjectData {
4956

5057
init {
@@ -93,6 +100,14 @@ data class CPVersion(
93100
}
94101

95102
val s = Separators.LEVEL1
103+
val attributesPart = if (attributes.isEmpty()) {
104+
""
105+
} else {
106+
s + attributes.entries.sortedBy { it.key }.joinToString(Separators.LEVEL2) {
107+
it.key.urlEncode() + Separators.MAPPING + it.value.urlEncode()
108+
}
109+
}
110+
96111
return longToHex(id) +
97112
s + escape(time) +
98113
s + escape(author) +
@@ -101,7 +116,8 @@ data class CPVersion(
101116
s + nullAsEmptyString(mergedVersion1?.getHashString()) +
102117
s + nullAsEmptyString(mergedVersion2?.getHashString()) +
103118
s + numberOfOperations +
104-
s + opsPart
119+
s + opsPart +
120+
attributesPart
105121
}
106122

107123
override fun getContainmentReferences(): List<ObjectReference<out IObjectData>> {
@@ -130,7 +146,8 @@ data class CPVersion(
130146
): CPVersion {
131147
try {
132148
val parts = serialized.split(Separators.LEVEL1).toTypedArray()
133-
if (parts.size == 9) {
149+
if (parts.size >= 9) {
150+
// <id>/<time>/<author>/<tree>/<baseVersion>/<mergedVersion1>/<mergedVersion2>/<numOps>/<ops>[/attributes]
134151
var opsHash: String? = null
135152
var ops: List<IOperation>? = null
136153
if (HashUtil.isSha256(parts[8])) {
@@ -153,10 +170,17 @@ data class CPVersion(
153170
}
154171
}
155172

173+
val attributes = parts.getOrNull(9)?.let {
174+
it.split(Separators.LEVEL2).mapNotNull {
175+
val (key, value) = it.split(Separators.MAPPING)
176+
(key.urlDecode() ?: return@mapNotNull null) to (value.urlDecode() ?: return@mapNotNull null)
177+
}.toMap()
178+
} ?: emptyMap()
179+
156180
val data = CPVersion(
157-
longFromHex(parts[0]),
158-
unescape(parts[1]),
159-
unescape(parts[2]),
181+
id = longFromHex(parts[0]),
182+
time = unescape(parts[1]),
183+
author = unescape(parts[2]),
160184
treeRefs = treeHashes,
161185
previousVersion = null,
162186
originalVersion = null,
@@ -166,10 +190,12 @@ data class CPVersion(
166190
operations = ops,
167191
operationsHash = opsHash?.let { referenceFactory(it, OperationsList.DESERIALIZER) },
168192
numberOfOperations = parts[7].toInt(),
193+
attributes = attributes,
169194
)
170195
return data
171196
} else {
172197
// legacy serialization format
198+
// <id>/<time>/<author>/<tree>/<previousVersion>/<ops>/<numOps>/<originalVersion>
173199

174200
var opsHash: String? = null
175201
var ops: List<IOperation>? = null

model-datastructure/src/commonTest/kotlin/LinearHistoryTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ class LinearHistoryTest {
148148
.id(id)
149149
.tree(initialTree)
150150
.baseVersion(base)
151-
.build()
151+
.buildLegacy()
152152
}
153153

154154
private fun merge(id: Long, v1: CLVersion, v2: CLVersion): CLVersion {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import org.modelix.datastructures.model.IModelTree
2+
import org.modelix.datastructures.objects.FullyLoadedObjectGraph
3+
import org.modelix.datastructures.objects.getDescendantsAndSelf
4+
import org.modelix.model.IVersion
5+
import org.modelix.model.lazy.CLVersion
6+
import org.modelix.model.persistent.CPVersion
7+
import org.modelix.streams.getBlocking
8+
import kotlin.test.Test
9+
import kotlin.test.assertEquals
10+
11+
class VersionAttributesTest {
12+
13+
@Test
14+
fun `serialize and deserialize version with attributes`() {
15+
val graph1 = FullyLoadedObjectGraph()
16+
17+
val key = "git-commit!&?*#+-ID"
18+
val value = "a16add525cbe04329708f83dbe7057e69bb1922d"
19+
20+
IModelTree.builder().graph(graph1).build()
21+
val originalVersion: IVersion = IVersion.builder()
22+
.tree(IModelTree.builder().graph(graph1).build())
23+
.attribute(key, value)
24+
.build()
25+
26+
assertEquals(mapOf(key to value), originalVersion.getAttributes())
27+
28+
val graph2 = FullyLoadedObjectGraph()
29+
val restoredVersion = graph2.loadObjects(
30+
rootHash = originalVersion.getObjectHash(),
31+
rootDeserializer = CPVersion,
32+
receivedObjects = originalVersion.asObject().getDescendantsAndSelf().toMap({ it.getHash() }, { it.data.serialize() }).getBlocking(graph1),
33+
).let { CLVersion(it) }
34+
35+
assertEquals(mapOf(key to value), restoredVersion.getAttributes())
36+
}
37+
}

model-server/src/main/kotlin/org/modelix/model/server/handlers/RepositoriesManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ class RepositoriesManager(val stores: StoreManager) : IRepositoriesManager {
177177
.time(Clock.System.now())
178178
.author(userName)
179179
.tree(tree)
180-
.build()
180+
.buildLegacy()
181181
initialVersion.write()
182182
validateVersion(initialVersion, null)
183183
putVersionHash(masterBranch, initialVersion.getContentHash())

0 commit comments

Comments
 (0)