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