initial commit
This commit is contained in:
commit
4ca81518d5
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
.idea/
|
||||||
|
*.iws
|
||||||
|
out/
|
||||||
|
.gradle
|
||||||
|
/build/
|
||||||
|
|
||||||
|
|
||||||
|
!gradle-wrapper.jar
|
||||||
|
|
23
build.gradle
Normal file
23
build.gradle
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
plugins {
|
||||||
|
id 'kotlin-platform-common' version '1.2.70'
|
||||||
|
}
|
||||||
|
|
||||||
|
description = "The basic interfaces for DataForge meta-data"
|
||||||
|
|
||||||
|
group 'hep.dataforge'
|
||||||
|
version '0.1.0-SNAPSHOT'
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
compile "org.jetbrains.kotlin:kotlin-stdlib-common"
|
||||||
|
testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common"
|
||||||
|
testCompile "org.jetbrains.kotlin:kotlin-test-common"
|
||||||
|
}
|
||||||
|
kotlin {
|
||||||
|
experimental {
|
||||||
|
coroutines "enable"
|
||||||
|
}
|
||||||
|
}
|
11
settings.gradle
Normal file
11
settings.gradle
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
pluginManagement {
|
||||||
|
resolutionStrategy {
|
||||||
|
eachPlugin {
|
||||||
|
if (requested.id.id == "kotlin-platform-common") {
|
||||||
|
useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rootProject.name = 'dataforge-meta'
|
||||||
|
|
71
src/main/kotlin/hep/dataforge/meta/Meta.kt
Normal file
71
src/main/kotlin/hep/dataforge/meta/Meta.kt
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package hep.dataforge.meta
|
||||||
|
|
||||||
|
import hep.dataforge.names.Name
|
||||||
|
import hep.dataforge.names.NameToken
|
||||||
|
import hep.dataforge.names.toName
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A member of the meta tree. Could be represented as one of following:
|
||||||
|
* * a value
|
||||||
|
* * a single node
|
||||||
|
* * a list of nodes
|
||||||
|
*/
|
||||||
|
sealed class MetaItem<M : Meta<M>> {
|
||||||
|
class ValueItem<M : Meta<M>>(val value: Value) : MetaItem<M>()
|
||||||
|
class SingleNodeItem<M : Meta<M>>(val node: M) : MetaItem<M>()
|
||||||
|
class MultiNodeItem<M : Meta<M>>(val nodes: List<M>) : MetaItem<M>()
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun <M : Meta<M>> List<M>.get(query: String): M? {
|
||||||
|
return if (query.isEmpty()) {
|
||||||
|
first()
|
||||||
|
} else {
|
||||||
|
//TODO add custom key queries
|
||||||
|
get(query.toInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic meta tree representation. Elements are [MetaItem] objects that could be represented by three different entities:
|
||||||
|
* * [MetaItem.ValueItem] (leaf)
|
||||||
|
* * [MetaItem.SingleNodeItem] single node
|
||||||
|
* * [MetaItem.MultiNodeItem] multi-value node
|
||||||
|
*/
|
||||||
|
interface Meta<M : Meta<M>> {
|
||||||
|
val items: Map<String, MetaItem<M>>
|
||||||
|
|
||||||
|
operator fun get(name: Name): MetaItem<M>? {
|
||||||
|
return when (name.length) {
|
||||||
|
0 -> error("Can't resolve element from empty name")
|
||||||
|
1 -> items[name.first()!!.body]
|
||||||
|
else -> name.first()!!.let{ token -> items[token.body]?.nodes?.get(token.query)}?.get(name.cutFirst())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(key: String): MetaItem<M>? = get(key.toName())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The meta implementation which is guaranteed to be immutable.
|
||||||
|
*
|
||||||
|
* If the argument is possibly mutable node, it is copied on creation
|
||||||
|
*/
|
||||||
|
class SealedMeta(meta: Meta<*>) : Meta<SealedMeta> {
|
||||||
|
override val items: Map<String, MetaItem<SealedMeta>> = if (meta is SealedMeta) {
|
||||||
|
meta.items
|
||||||
|
} else {
|
||||||
|
meta.items.mapValues { entry ->
|
||||||
|
val item = entry.value
|
||||||
|
when (item) {
|
||||||
|
is MetaItem.ValueItem -> MetaItem.ValueItem(item.value)
|
||||||
|
is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(SealedMeta(item.node))
|
||||||
|
is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { SealedMeta(it) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate sealed node from [this]. If it is already sealed return it as is
|
||||||
|
*/
|
||||||
|
fun Meta<*>.seal(): SealedMeta = this as? SealedMeta ?: SealedMeta(this)
|
33
src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt
Normal file
33
src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package hep.dataforge.meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DSL builder for meta
|
||||||
|
*/
|
||||||
|
class MetaBuilder : MutableMeta<MetaBuilder>() {
|
||||||
|
override fun wrap(meta: Meta<*>): MetaBuilder = meta.builder()
|
||||||
|
override fun empty(): MetaBuilder = MetaBuilder()
|
||||||
|
|
||||||
|
infix fun String.to(value: Any) {
|
||||||
|
this@MetaBuilder[this] = Value.of(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
infix fun String.to(metaBuilder: MetaBuilder.() -> Unit) {
|
||||||
|
this@MetaBuilder[this] = MetaBuilder().apply(metaBuilder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For safety, builder always copies the initial meta even if it is builder itself
|
||||||
|
*/
|
||||||
|
fun Meta<*>.builder(): MetaBuilder {
|
||||||
|
return MetaBuilder().also { builder ->
|
||||||
|
items.mapValues { entry ->
|
||||||
|
val item = entry.value
|
||||||
|
builder[entry.key] = when (item) {
|
||||||
|
is MetaItem.ValueItem -> MetaItem.ValueItem(item.value)
|
||||||
|
is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(item.node.builder())
|
||||||
|
is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { it.builder() })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
src/main/kotlin/hep/dataforge/meta/MetaUtils.kt
Normal file
40
src/main/kotlin/hep/dataforge/meta/MetaUtils.kt
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package hep.dataforge.meta
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsafe methods to access values and nodes directly from [MetaItem]
|
||||||
|
*/
|
||||||
|
|
||||||
|
val MetaItem<*>.value
|
||||||
|
get() = (this as? MetaItem.ValueItem)?.value ?: error("Trying to interpret node meta item as value item")
|
||||||
|
val MetaItem<*>.string get() = value.string
|
||||||
|
val MetaItem<*>.boolean get() = value.boolean
|
||||||
|
val MetaItem<*>.number get() = value.number
|
||||||
|
val MetaItem<*>.double get() = number.toDouble()
|
||||||
|
val MetaItem<*>.int get() = number.toInt()
|
||||||
|
val MetaItem<*>.long get() = number.toLong()
|
||||||
|
|
||||||
|
val <M : Meta<M>> MetaItem<M>.node: M
|
||||||
|
get() = when (this) {
|
||||||
|
is MetaItem.ValueItem -> error("Trying to interpret value meta item as node item")
|
||||||
|
is MetaItem.SingleNodeItem -> node
|
||||||
|
is MetaItem.MultiNodeItem -> nodes.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility method to access item content as list of nodes.
|
||||||
|
* Returns empty list if it is value item.
|
||||||
|
*/
|
||||||
|
val <M : Meta<M>> MetaItem<M>.nodes: List<M>
|
||||||
|
get() = when (this) {
|
||||||
|
is MetaItem.ValueItem -> emptyList()//error("Trying to interpret value meta item as node item")
|
||||||
|
is MetaItem.SingleNodeItem -> listOf(node)
|
||||||
|
is MetaItem.MultiNodeItem -> nodes
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <M : Meta<M>> MetaItem<M>.indexOf(meta: M): Int {
|
||||||
|
return when (this) {
|
||||||
|
is MetaItem.ValueItem -> -1
|
||||||
|
is MetaItem.SingleNodeItem -> if (node == meta) 0 else -1
|
||||||
|
is MetaItem.MultiNodeItem -> nodes.indexOf(meta)
|
||||||
|
}
|
||||||
|
}
|
93
src/main/kotlin/hep/dataforge/meta/MutableMeta.kt
Normal file
93
src/main/kotlin/hep/dataforge/meta/MutableMeta.kt
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
package hep.dataforge.meta
|
||||||
|
|
||||||
|
import hep.dataforge.names.Name
|
||||||
|
import hep.dataforge.names.plus
|
||||||
|
import hep.dataforge.names.toName
|
||||||
|
|
||||||
|
class MetaListener(val owner: Any? = null, val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit) {
|
||||||
|
operator fun invoke(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) = action(name, oldItem, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A mutable meta node with attachable change listener
|
||||||
|
*/
|
||||||
|
abstract class MutableMeta<M : MutableMeta<M>> : Meta<M> {
|
||||||
|
private val listeners = HashSet<MetaListener>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add change listener to this meta. Owner is declared to be able to remove listeners later. Listener without owner could not be removed
|
||||||
|
*/
|
||||||
|
fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) {
|
||||||
|
listeners.add(MetaListener(owner, action))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove all listeners belonging to given owner
|
||||||
|
*/
|
||||||
|
fun removeListener(owner: Any) {
|
||||||
|
listeners.removeAll { it.owner === owner }
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _items: MutableMap<String, MetaItem<M>> = HashMap()
|
||||||
|
|
||||||
|
override val items: Map<String, MetaItem<M>>
|
||||||
|
get() = _items
|
||||||
|
|
||||||
|
private fun itemChanged(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) {
|
||||||
|
listeners.forEach { it(name, oldItem, newItem) }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun replaceItem(key: String, oldItem: MetaItem<M>?, newItem: MetaItem<M>?) {
|
||||||
|
if (newItem == null) {
|
||||||
|
_items.remove(key)
|
||||||
|
oldItem?.nodes?.forEach {
|
||||||
|
it.removeListener(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
_items[key] = newItem
|
||||||
|
newItem.nodes.forEach {
|
||||||
|
it.onChange(this) { name, oldItem, newItem ->
|
||||||
|
itemChanged(key.toName() + name, oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemChanged(key.toName(), oldItem, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform given meta to node type of this meta tree
|
||||||
|
*/
|
||||||
|
protected abstract fun wrap(meta: Meta<*>): M
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create empty node
|
||||||
|
*/
|
||||||
|
protected abstract fun empty(): M
|
||||||
|
|
||||||
|
operator fun set(name: Name, item: MetaItem<M>) {
|
||||||
|
when (name.length) {
|
||||||
|
0 -> error("Can't set meta item for empty name")
|
||||||
|
1 -> {
|
||||||
|
val token = name.first()!!
|
||||||
|
if (token.hasQuery()) TODO("Queries are not supported in set operations on meta")
|
||||||
|
replaceItem(token.body, get(name), item)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val token = name.first()!!
|
||||||
|
//get existing or create new node. Query is ignored for new node
|
||||||
|
val child = this.items[token.body]?.nodes?.get(token.query)
|
||||||
|
?: empty().also { this[token.body.toName()] = MetaItem.SingleNodeItem(it) }
|
||||||
|
child[name.cutFirst()] = item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun set(name: Name, value: Value) = set(name, MetaItem.ValueItem(value))
|
||||||
|
operator fun set(name: Name, meta: Meta<*>) = set(name, MetaItem.SingleNodeItem(wrap(meta)))
|
||||||
|
operator fun set(name: Name, metas: List<Meta<*>>) = set(name, MetaItem.MultiNodeItem(metas.map { wrap(it) }))
|
||||||
|
|
||||||
|
operator fun set(name: String, item: MetaItem<M>) = set(name.toName(), item)
|
||||||
|
operator fun set(name: String, value: Value) = set(name.toName(), MetaItem.ValueItem(value))
|
||||||
|
operator fun set(name: String, meta: Meta<*>) = set(name.toName(), MetaItem.SingleNodeItem(wrap(meta)))
|
||||||
|
operator fun set(name: String, metas: List<Meta<*>>) = set(name.toName(), MetaItem.MultiNodeItem(metas.map { wrap(it) }))
|
||||||
|
}
|
181
src/main/kotlin/hep/dataforge/meta/Value.kt
Normal file
181
src/main/kotlin/hep/dataforge/meta/Value.kt
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package hep.dataforge.meta
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of supported Value types.
|
||||||
|
*
|
||||||
|
* Time value and binary value are represented by string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
enum class ValueType {
|
||||||
|
NUMBER, STRING, BOOLEAN, NULL
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class for both Number and non-Number objects.
|
||||||
|
*
|
||||||
|
* Value can represent a list of value objects.
|
||||||
|
*/
|
||||||
|
interface Value {
|
||||||
|
/**
|
||||||
|
* Get raw value of this value
|
||||||
|
*/
|
||||||
|
val value: Any?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The type of the value
|
||||||
|
*/
|
||||||
|
val type: ValueType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get this value represented as Number
|
||||||
|
*/
|
||||||
|
val number: Number
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get this value represented as String
|
||||||
|
*/
|
||||||
|
val string: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get this value represented as List
|
||||||
|
*/
|
||||||
|
val list: List<Value>
|
||||||
|
get() = listOf(this)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Convert object to value
|
||||||
|
*/
|
||||||
|
fun of(value: Any?): Value {
|
||||||
|
return when (value) {
|
||||||
|
null -> Null
|
||||||
|
true -> True
|
||||||
|
false -> False
|
||||||
|
is Number -> NumberValue(value)
|
||||||
|
is Iterable<*> -> ListValue(value.map { of(it) })
|
||||||
|
is Enum<*> -> EnumValue(value)
|
||||||
|
is CharSequence -> StringValue(value.toString())
|
||||||
|
else -> throw IllegalArgumentException("Unrecognized type of the object converted to Value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A singleton null value
|
||||||
|
*/
|
||||||
|
object Null : Value {
|
||||||
|
override val value: Any? = null
|
||||||
|
override val type: ValueType = ValueType.NULL
|
||||||
|
override val number: Number = Double.NaN
|
||||||
|
override val string: String = "@null"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if value is null
|
||||||
|
*/
|
||||||
|
fun Value.isNull(): Boolean = this == Null
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton true value
|
||||||
|
*/
|
||||||
|
object True : Value {
|
||||||
|
override val value: Any? = true
|
||||||
|
override val type: ValueType = ValueType.BOOLEAN
|
||||||
|
override val number: Number = 1.0
|
||||||
|
override val string: String = "+"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Singleton false value
|
||||||
|
*/
|
||||||
|
object False : Value {
|
||||||
|
override val value: Any? = false
|
||||||
|
override val type: ValueType = ValueType.BOOLEAN
|
||||||
|
override val number: Number = -1.0
|
||||||
|
override val string: String = "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
val Value.boolean get() = this == True || this.list.firstOrNull() == True || (type == ValueType.STRING && string.toBoolean())
|
||||||
|
|
||||||
|
class NumberValue(override val number: Number) : Value {
|
||||||
|
override val value: Any? get() = number
|
||||||
|
override val type: ValueType = ValueType.NUMBER
|
||||||
|
override val string: String get() = number.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
class StringValue(override val string: String) : Value {
|
||||||
|
override val value: Any? get() = string
|
||||||
|
override val type: ValueType = ValueType.STRING
|
||||||
|
override val number: Number get() = string.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
class EnumValue<E : Enum<*>>(override val value: E) : Value {
|
||||||
|
override val type: ValueType = ValueType.STRING
|
||||||
|
override val number: Number = value.ordinal
|
||||||
|
override val string: String = value.name
|
||||||
|
}
|
||||||
|
|
||||||
|
class ListValue(override val list: List<Value>) : Value {
|
||||||
|
init {
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
throw IllegalArgumentException("Can't create list value from empty list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val value: Any? get() = list
|
||||||
|
override val type: ValueType get() = list.first().type
|
||||||
|
override val number: Number get() = list.first().number
|
||||||
|
override val string: String get() = list.first().string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if value is list
|
||||||
|
*/
|
||||||
|
fun Value.isList(): Boolean = this.list.size > 1
|
||||||
|
|
||||||
|
|
||||||
|
fun Number.asValue(): Value = NumberValue(this)
|
||||||
|
|
||||||
|
fun Boolean.asValue(): Value = if (this) True else False
|
||||||
|
|
||||||
|
fun String.asValue(): Value = StringValue(this)
|
||||||
|
|
||||||
|
fun Collection<Value>.asValue(): Value = ListValue(this.toList())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Value from String using closest match conversion
|
||||||
|
*/
|
||||||
|
fun String.parseValue(): Value {
|
||||||
|
|
||||||
|
//Trying to get integer
|
||||||
|
if (isEmpty() || this == Null.string) {
|
||||||
|
return Null
|
||||||
|
}
|
||||||
|
|
||||||
|
//string constants
|
||||||
|
if (startsWith("\"") && endsWith("\"")) {
|
||||||
|
return StringValue(substring(1, length - 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
toIntOrNull()?.let {
|
||||||
|
return NumberValue(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
toDoubleOrNull()?.let {
|
||||||
|
return NumberValue(it)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("true" == this) {
|
||||||
|
return True
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("false" == this) {
|
||||||
|
return False
|
||||||
|
}
|
||||||
|
|
||||||
|
//Give up and return a StringValue
|
||||||
|
return StringValue(this)
|
||||||
|
}
|
112
src/main/kotlin/hep/dataforge/names/Name.kt
Normal file
112
src/main/kotlin/hep/dataforge/names/Name.kt
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
package hep.dataforge.names
|
||||||
|
|
||||||
|
import kotlin.coroutines.experimental.buildSequence
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The general interface for working with names.
|
||||||
|
* The name is a dot separated list of strings like `token1.token2.token3`.
|
||||||
|
* Each token could contain additional query in square brackets.
|
||||||
|
*/
|
||||||
|
class Name internal constructor(val tokens: List<NameToken>) {
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
fun cutFirst(): Name = Name(tokens.drop(1))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The reminder of the name after last element is cut
|
||||||
|
*/
|
||||||
|
fun cutLast(): Name = Name(tokens.dropLast(1))
|
||||||
|
|
||||||
|
operator fun get(i: Int): NameToken = tokens[i]
|
||||||
|
|
||||||
|
override fun toString(): String = tokens.joinToString(separator = NAME_SEPARATOR) { it.toString() }
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (other !is Name) return false
|
||||||
|
|
||||||
|
if (tokens != other.tokens) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return tokens.hashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val NAME_SEPARATOR = "."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 *query*
|
||||||
|
*/
|
||||||
|
data class NameToken internal constructor(val body: String, val query: String) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (body.isEmpty()) error("Syntax error: Name token body is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String = "$body[$query]"
|
||||||
|
|
||||||
|
fun hasQuery() = query.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun String.toName(): Name {
|
||||||
|
val tokens = buildSequence<NameToken> {
|
||||||
|
var bodyBuilder = StringBuilder()
|
||||||
|
var queryBuilder = StringBuilder()
|
||||||
|
var bracketCount: Int = 0
|
||||||
|
fun queryOn() = bracketCount > 0
|
||||||
|
|
||||||
|
this@toName.asSequence().forEach {
|
||||||
|
if (queryOn()) {
|
||||||
|
when (it) {
|
||||||
|
'[' -> bracketCount++
|
||||||
|
']' -> bracketCount--
|
||||||
|
}
|
||||||
|
if (queryOn()) queryBuilder.append(it)
|
||||||
|
} else {
|
||||||
|
when (it) {
|
||||||
|
'.' -> {
|
||||||
|
yield(NameToken(bodyBuilder.toString(), queryBuilder.toString()))
|
||||||
|
bodyBuilder = StringBuilder()
|
||||||
|
queryBuilder = StringBuilder()
|
||||||
|
}
|
||||||
|
'[' -> bracketCount++
|
||||||
|
']' -> error("Syntax error: closing bracket ] not have not matching open bracket")
|
||||||
|
else -> {
|
||||||
|
if (queryBuilder.isNotEmpty()) error("Syntax error: only name end and name separator are allowed after query")
|
||||||
|
bodyBuilder.append(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield(NameToken(bodyBuilder.toString(), queryBuilder.toString()))
|
||||||
|
}
|
||||||
|
return Name(tokens.toList())
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun Name.plus(other: Name): Name = Name(this.tokens + other.tokens)
|
||||||
|
|
||||||
|
operator fun Name.plus(other: String): Name = this + other.toName()
|
||||||
|
|
||||||
|
fun NameToken.toName() = Name(listOf(this))
|
12
src/test/kotlin/hep/dataforge/names/NameTest.kt
Normal file
12
src/test/kotlin/hep/dataforge/names/NameTest.kt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
package hep.dataforge.names
|
||||||
|
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
|
||||||
|
class NameTest{
|
||||||
|
@Test
|
||||||
|
fun simpleName(){
|
||||||
|
val name = "token1.token2.token3".toName()
|
||||||
|
assertEquals("token2", name[2].toString())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user