From 6ad5f162a1c4a15757d37ba603c93da8982df643 Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Mon, 31 Aug 2020 12:02:06 +0300 Subject: [PATCH] Refactor Name and NameToken. Breaking change --- .../kotlin/hep/dataforge/data/DataNode.kt | 16 +-- .../kotlin/hep/dataforge/meta/Meta.kt | 2 +- .../kotlin/hep/dataforge/meta/MutableMeta.kt | 6 +- .../meta/descriptors/ItemDescriptor.kt | 6 +- .../kotlin/hep/dataforge/meta/metaMatcher.kt | 5 +- .../kotlin/hep/dataforge/names/Name.kt | 112 ++++++++++-------- 6 files changed, 78 insertions(+), 69 deletions(-) diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt index 246d9796..1eac4745 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt @@ -89,8 +89,8 @@ val DataItem?.data: Data? get() = (this as? DataItem.Leaf)?.d operator fun DataNode.get(name: Name): DataItem? = when (name.length) { 0 -> error("Empty name") - 1 -> items[name.first()] - else -> get(name.first()!!.asName()).node?.get(name.cutFirst()) + 1 -> items[name.firstOrNull()] + else -> get(name.firstOrNull()!!.asName()).node?.get(name.cutFirst()) } operator fun DataNode.get(name: String): DataItem? = get(name.toName()) @@ -168,24 +168,24 @@ class DataTreeBuilder(val type: KClass) { private fun buildNode(name: Name): DataTreeBuilder { return when (name.length) { 0 -> this - 1 -> buildNode(name.first()!!) - else -> buildNode(name.first()!!).buildNode(name.cutFirst()) + 1 -> buildNode(name.firstOrNull()!!) + else -> buildNode(name.firstOrNull()!!).buildNode(name.cutFirst()) } } operator fun set(name: Name, data: Data) { when (name.length) { 0 -> error("Can't add data with empty name") - 1 -> set(name.first()!!, data) - 2 -> buildNode(name.cutLast())[name.last()!!] = data + 1 -> set(name.firstOrNull()!!, data) + 2 -> buildNode(name.cutLast())[name.lastOrNull()!!] = data } } operator fun set(name: Name, node: DataTreeBuilder) { when (name.length) { 0 -> error("Can't add data with empty name") - 1 -> set(name.first()!!, node) - 2 -> buildNode(name.cutLast())[name.last()!!] = node + 1 -> set(name.firstOrNull()!!, node) + 2 -> buildNode(name.cutLast())[name.lastOrNull()!!] = node } } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt index fdbdab72..8bb5af94 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt @@ -115,7 +115,7 @@ public interface Meta : MetaRepr, ItemProvider { override fun getItem(name: Name): MetaItem<*>? { if (name.isEmpty()) return NodeItem(this) - return name.first()?.let { token -> + return name.firstOrNull()?.let { token -> val tail = name.cutFirst() when (tail.length) { 0 -> items[token] diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt index e782db30..64466554 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt @@ -56,12 +56,12 @@ abstract class AbstractMutableMeta> : AbstractMetaNode(), when (name.length) { 0 -> error("Can't setValue meta item for empty name") 1 -> { - val token = name.first()!! + val token = name.firstOrNull()!! @Suppress("UNCHECKED_CAST") val oldItem: MetaItem? = get(name) as? MetaItem replaceItem(token, oldItem, wrapItem(item)) } else -> { - val token = name.first()!! + val token = name.firstOrNull()!! //get existing or create new node. Query is ignored for new node if (items[token] == null) { replaceItem(token, null, MetaItem.NodeItem(empty())) @@ -159,7 +159,7 @@ operator fun MutableMeta<*>.set(name: String, metas: Iterable): Unit = set */ fun > M.append(name: Name, value: Any?) { require(!name.isEmpty()) { "Name could not be empty for append operation" } - val newIndex = name.last()!!.index + val newIndex = name.lastOrNull()!!.index if (newIndex != null) { set(name, value) } else { diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt index a277e18d..338e1443 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt @@ -141,7 +141,7 @@ class NodeDescriptor(config: Config = Config()) : ItemDescriptor(config) { } NodeDescriptor(config) } - else -> buildNode(name.first()?.asName()!!).buildNode(name.cutFirst()) + else -> buildNode(name.firstOrNull()?.asName()!!).buildNode(name.cutFirst()) } } @@ -155,7 +155,7 @@ class NodeDescriptor(config: Config = Config()) : ItemDescriptor(config) { } fun item(name: Name, descriptor: ItemDescriptor) { - buildNode(name.cutLast()).newItem(name.last().toString(), descriptor) + buildNode(name.cutLast()).newItem(name.lastOrNull().toString(), descriptor) } fun item(name: String, descriptor: ItemDescriptor) { @@ -199,7 +199,7 @@ operator fun ItemDescriptor.get(name: Name): ItemDescriptor? { if (name.isEmpty()) return this return when (this) { is ValueDescriptor -> null // empty name already checked - is NodeDescriptor -> items[name.first()!!.toString()]?.get(name.cutFirst()) + is NodeDescriptor -> items[name.firstOrNull()!!.toString()]?.get(name.cutFirst()) } } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt index 26dc009c..0ed3190b 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt @@ -1,7 +1,6 @@ package hep.dataforge.meta -import hep.dataforge.names.Name -import hep.dataforge.names.toName +import hep.dataforge.names.* /** * Get all items matching given name. The index of the last element, if present is used as a [Regex], @@ -14,7 +13,7 @@ fun Meta.getIndexed(name: Name): Map> { else -> this[name.cutLast()].node ?: return emptyMap() } - val (body, index) = name.last()!! + val (body, index) = name.lastOrNull()!! return if (index == null) { root.items.filter { it.key.body == body }.mapKeys { it.key.index } } else { diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt index 327291d0..0791e080 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt @@ -1,5 +1,6 @@ package hep.dataforge.names +import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.Serializer @@ -16,31 +17,8 @@ import kotlinx.serialization.encoding.Encoder * Each token could contain additional index in square brackets. */ @Serializable -class Name(val tokens: List) { - - val length get() = tokens.size - - /** - * First token of the name or null if it is empty - */ - fun first(): NameToken? = tokens.firstOrNull() - - /** - * Last token of the name or null if it is empty - */ - fun last(): NameToken? = tokens.lastOrNull() - - /** - * The reminder of the name after first element is cut. For empty name return itself. - */ - fun cutFirst(): Name = Name(tokens.drop(1)) - - /** - * The reminder of the name after last element is cut. For empty name return itself. - */ - fun cutLast(): Name = Name(tokens.dropLast(1)) - - operator fun get(i: Int): NameToken = tokens[i] +public class Name(public val tokens: List) { + //TODO to be transformed into inline class after they are supported with serialization override fun toString(): String = tokens.joinToString(separator = NAME_SEPARATOR) { it.toString() } @@ -60,11 +38,12 @@ class Name(val tokens: List) { } } + @OptIn(ExperimentalSerializationApi::class) @Serializer(Name::class) - companion object : KSerializer { - const val NAME_SEPARATOR = "." + public companion object : KSerializer { + public const val NAME_SEPARATOR: String = "." - val EMPTY = Name(emptyList()) + public val EMPTY: Name = Name(emptyList()) override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("hep.dataforge.names.Name", PrimitiveKind.STRING) @@ -79,13 +58,37 @@ class Name(val tokens: List) { } } +public operator fun Name.get(i: Int): NameToken = tokens[i] + +/** + * The reminder of the name after last element is cut. For empty name return itself. + */ +public fun Name.cutLast(): Name = Name(tokens.dropLast(1)) + +/** + * The reminder of the name after first element is cut. For empty name return itself. + */ +public fun Name.cutFirst(): Name = Name(tokens.drop(1)) + +public val Name.length: Int get() = tokens.size + +/** + * Last token of the name or null if it is empty + */ +public fun Name.lastOrNull(): NameToken? = tokens.lastOrNull() + +/** + * First token of the name or null if it is empty + */ +public fun Name.firstOrNull(): NameToken? = tokens.firstOrNull() + /** * A single name token. Body is not allowed to be empty. * Following symbols are prohibited in name tokens: `{}.:\`. * A name token could have appendix in square brackets called *index* */ @Serializable -data class NameToken(val body: String, val index: String? = null) { +public data class NameToken(val body: String, val index: String? = null) { init { if (body.isEmpty()) error("Syntax error: Name token body is empty") @@ -103,15 +106,14 @@ data class NameToken(val body: String, val index: String? = null) { body.escape() } - fun hasIndex() = index != null - + @OptIn(ExperimentalSerializationApi::class) @Serializer(NameToken::class) - companion object : KSerializer { + public companion object : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("hep.dataforge.names.NameToken", PrimitiveKind.STRING) override fun deserialize(decoder: Decoder): NameToken { - return decoder.decodeString().toName().first()!! + return decoder.decodeString().toName().firstOrNull()!! } override fun serialize(encoder: Encoder, value: NameToken) { @@ -120,13 +122,21 @@ data class NameToken(val body: String, val index: String? = null) { } } -fun NameToken.withIndex(newIndex: String) = NameToken(body, newIndex) +/** + * Check if index is defined for this token + */ +public fun NameToken.hasIndex(): Boolean = index != null + +/** + * Add or replace index part of this token + */ +public fun NameToken.withIndex(newIndex: String): NameToken = NameToken(body, newIndex) /** * Convert a [String] to name parsing it and extracting name tokens and index syntax. * This operation is rather heavy so it should be used with care in high performance code. */ -fun String.toName(): Name { +public fun String.toName(): Name { if (isBlank()) return Name.EMPTY val tokens = sequence { var bodyBuilder = StringBuilder() @@ -181,26 +191,26 @@ fun String.toName(): Name { * Convert the [String] to a [Name] by simply wrapping it in a single name token without parsing. * The input string could contain dots and braces, but they are just escaped, not parsed. */ -fun String.asName(): Name = if (isBlank()) Name.EMPTY else NameToken(this).asName() +public fun String.asName(): Name = if (isBlank()) Name.EMPTY else NameToken(this).asName() -operator fun NameToken.plus(other: Name): Name = Name(listOf(this) + other.tokens) +public operator fun NameToken.plus(other: Name): Name = Name(listOf(this) + other.tokens) -operator fun Name.plus(other: Name): Name = Name(this.tokens + other.tokens) +public operator fun Name.plus(other: Name): Name = Name(this.tokens + other.tokens) -operator fun Name.plus(other: String): Name = this + other.toName() +public operator fun Name.plus(other: String): Name = this + other.toName() -operator fun Name.plus(other: NameToken): Name = Name(tokens + other) +public operator fun Name.plus(other: NameToken): Name = Name(tokens + other) -fun Name.appendLeft(other: String): Name = NameToken(other) + this +public fun Name.appendLeft(other: String): Name = NameToken(other) + this -fun NameToken.asName() = Name(listOf(this)) +public fun NameToken.asName(): Name = Name(listOf(this)) -fun Name.isEmpty(): Boolean = this.length == 0 +public fun Name.isEmpty(): Boolean = this.length == 0 /** * Set or replace last token index */ -fun Name.withIndex(index: String): Name { +public fun Name.withIndex(index: String): Name { val last = NameToken(tokens.last().body, index) if (length == 0) error("Can't add index to empty name") if (length == 1) { @@ -215,19 +225,19 @@ fun Name.withIndex(index: String): Name { /** * Fast [String]-based accessor for item map */ -operator fun Map.get(body: String, query: String? = null): T? = get(NameToken(body, query)) +public operator fun Map.get(body: String, query: String? = null): T? = get(NameToken(body, query)) -operator fun Map.get(name: String) = get(name.toName()) -operator fun MutableMap.set(name: String, value: T) = set(name.toName(), value) +public operator fun Map.get(name: String): T? = get(name.toName()) +public operator fun MutableMap.set(name: String, value: T): Unit = set(name.toName(), value) /* Name comparison operations */ -fun Name.startsWith(token: NameToken): Boolean = first() == token +public fun Name.startsWith(token: NameToken): Boolean = firstOrNull() == token -fun Name.endsWith(token: NameToken): Boolean = last() == token +public fun Name.endsWith(token: NameToken): Boolean = lastOrNull() == token -fun Name.startsWith(name: Name): Boolean = +public fun Name.startsWith(name: Name): Boolean = this.length >= name.length && tokens.subList(0, name.length) == name.tokens -fun Name.endsWith(name: Name): Boolean = +public fun Name.endsWith(name: Name): Boolean = this.length >= name.length && tokens.subList(length - name.length, length) == name.tokens \ No newline at end of file