diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 85ea102d..adc74adf 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -1,4 +1,4 @@ -name: Java CI +name: Gradle build on: [push] diff --git a/README.md b/README.md index b712501c..a6e6ed99 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ - +[](https://confluence.jetbrains.com/display/ALL/JetBrains+on+GitHub) [](https://zenodo.org/badge/latestdoi/148831678) + + +[  ](https://bintray.com/mipt-npm/dataforge/dataforge-meta/_latestVersion) + + + # Questions and Answers # In this section we will try to cover DataForge main ideas in the form of questions and answers. diff --git a/build.gradle.kts b/build.gradle.kts index 04cce594..1fe59eff 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,10 +1,12 @@ + plugins { - id("scientifik.mpp") version "0.2.1" apply false - id("scientifik.jvm") version "0.2.1" apply false - id("scientifik.publish") version "0.2.1" apply false + val toolsVersion = "0.4.0" + id("scientifik.mpp") version toolsVersion apply false + id("scientifik.jvm") version toolsVersion apply false + id("scientifik.publish") version toolsVersion apply false } -val dataforgeVersion by extra("0.1.4") +val dataforgeVersion by extra("0.1.5") val bintrayRepo by extra("dataforge") val githubProject by extra("dataforge-core") @@ -12,10 +14,12 @@ val githubProject by extra("dataforge-core") allprojects { group = "hep.dataforge" version = dataforgeVersion + + repositories { + mavenLocal() + } } subprojects { - if (name.startsWith("dataforge")) { - apply(plugin = "scientifik.publish") - } + apply(plugin = "scientifik.publish") } \ No newline at end of file diff --git a/dataforge-context/build.gradle.kts b/dataforge-context/build.gradle.kts index 896e7b89..dd581254 100644 --- a/dataforge-context/build.gradle.kts +++ b/dataforge-context/build.gradle.kts @@ -1,10 +1,12 @@ +import scientifik.coroutines + plugins { id("scientifik.mpp") } description = "Context and provider definitions" -val coroutinesVersion: String = Scientifik.coroutinesVersion +coroutines() kotlin { sourceSets { @@ -12,21 +14,18 @@ kotlin { dependencies { api(project(":dataforge-meta")) api(kotlin("reflect")) - api("io.github.microutils:kotlin-logging-common:1.7.2") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") + api("io.github.microutils:kotlin-logging-common:1.7.8") } } val jvmMain by getting { dependencies { - api("io.github.microutils:kotlin-logging:1.7.2") + api("io.github.microutils:kotlin-logging:1.7.8") api("ch.qos.logback:logback-classic:1.2.3") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") } } val jsMain by getting { dependencies { - api("io.github.microutils:kotlin-logging-js:1.7.2") - api("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutinesVersion") + api("io.github.microutils:kotlin-logging-js:1.7.8") } } } diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt index ffc5b197..b3adcbfd 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt @@ -103,7 +103,7 @@ open class Context( plugins.forEach { it.detach() } } - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "parent" to parent?.name "properties" put properties.seal() "plugins" put plugins.map { it.toMeta() } diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt index 58a03554..1f267c37 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt @@ -1,5 +1,7 @@ package hep.dataforge.context +import hep.dataforge.meta.DFBuilder +import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaBuilder import hep.dataforge.meta.buildMeta import hep.dataforge.names.toName @@ -7,6 +9,7 @@ import hep.dataforge.names.toName /** * A convenience builder for context */ +@DFBuilder class ContextBuilder(var name: String = "@anonymous", val parent: Context = Global) { private val plugins = ArrayList<Plugin>() private var meta = MetaBuilder() @@ -20,11 +23,11 @@ class ContextBuilder(var name: String = "@anonymous", val parent: Context = Glob } fun plugin(tag: PluginTag, action: MetaBuilder.() -> Unit = {}) { - plugins.add(PluginRepository.fetch(tag, buildMeta(action))) + plugins.add(PluginRepository.fetch(tag, Meta(action))) } fun plugin(builder: PluginFactory<*>, action: MetaBuilder.() -> Unit = {}) { - plugins.add(builder.invoke(buildMeta(action))) + plugins.add(builder.invoke(Meta(action))) } fun plugin(name: String, group: String = "", version: String = "", action: MetaBuilder.() -> Unit = {}) { diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt index 90d669e0..6a937962 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt @@ -65,7 +65,7 @@ interface Plugin : Named, ContextAware, Provider, MetaRepr { */ fun detach() - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "context" put context.name.toString() "type" to this::class.simpleName "tag" put tag diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt index dd114f79..5f35d876 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt @@ -62,7 +62,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug * @return */ @Suppress("UNCHECKED_CAST") - operator fun <T : Any> get(type: KClass<T>, tag: PluginTag? = null, recursive: Boolean = true): T? = + operator fun <T : Any> get(type: KClass<out T>, tag: PluginTag? = null, recursive: Boolean = true): T? = find(recursive) { type.isInstance(it) && (tag == null || tag.matches(it.tag)) } as T? inline operator fun <reified T : Any> get(tag: PluginTag? = null, recursive: Boolean = true): T? = @@ -102,7 +102,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug load(factory(meta, context)) fun <T : Plugin> load(factory: PluginFactory<T>, metaBuilder: MetaBuilder.() -> Unit): T = - load(factory, buildMeta(metaBuilder)) + load(factory, Meta(metaBuilder)) /** * Remove a plugin from [PluginManager] @@ -134,7 +134,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable<Plug factory: PluginFactory<T>, recursive: Boolean = true, metaBuilder: MetaBuilder.() -> Unit - ): T = fetch(factory, recursive, buildMeta(metaBuilder)) + ): T = fetch(factory, recursive, Meta(metaBuilder)) override fun iterator(): Iterator<Plugin> = plugins.iterator() diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt index 390de7bc..f02308fd 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt @@ -36,7 +36,7 @@ data class PluginTag( override fun toString(): String = listOf(group, name, version).joinToString(separator = ":") - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "name" put name "group" put group "version" put version diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt new file mode 100644 index 00000000..cadd4231 --- /dev/null +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/annotations.kt @@ -0,0 +1,130 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package hep.dataforge.descriptors + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.values.ValueType +import kotlin.reflect.KClass + +@MustBeDocumented +annotation class Attribute( + val key: String, + val value: String +) + +@MustBeDocumented +annotation class Attributes( + val attrs: Array<Attribute> +) + +@MustBeDocumented +annotation class ItemDef( + val info: String = "", + val multiple: Boolean = false, + val required: Boolean = false +) + +@Target(AnnotationTarget.PROPERTY) +@MustBeDocumented +annotation class ValueDef( + val type: Array<ValueType> = [ValueType.STRING], + val def: String = "", + val allowed: Array<String> = [], + val enumeration: KClass<*> = Any::class +) + +///** +// * Description text for meta property, node or whole object +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class Description(val value: String) +// +///** +// * Annotation for value property which states that lists are expected +// */ +//@Target(AnnotationTarget.PROPERTY) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class Multiple +// +///** +// * Descriptor target +// * The DataForge path to the resource containing the description. Following targets are supported: +// * 1. resource +// * 1. file +// * 1. class +// * 1. method +// * 1. property +// * +// * +// * Does not work if [type] is provided +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class Descriptor(val value: String) +// +// +///** +// * Aggregator class for descriptor nodes +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class DescriptorNodes(vararg val nodes: NodeDef) +// +///** +// * Aggregator class for descriptor values +// */ +//@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class DescriptorValues(vararg val nodes: ValueDef) +// +///** +// * Alternative name for property descriptor declaration +// */ +//@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class DescriptorName(val name: String) +// +//@Target(AnnotationTarget.PROPERTY) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class DescriptorValue(val def: ValueDef) +////TODO enter fields directly? +// +//@Target(AnnotationTarget.PROPERTY) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class ValueProperty( +// val name: String = "", +// val type: Array<ValueType> = arrayOf(ValueType.STRING), +// val multiple: Boolean = false, +// val def: String = "", +// val enumeration: KClass<*> = Any::class, +// val tags: Array<String> = emptyArray() +//) +// +// +//@Target(AnnotationTarget.PROPERTY) +//@Retention(AnnotationRetention.RUNTIME) +//@MustBeDocumented +//annotation class NodeProperty(val name: String = "") diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt new file mode 100644 index 00000000..0015436c --- /dev/null +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/descriptors/reflectiveDescriptors.kt @@ -0,0 +1,65 @@ +package hep.dataforge.descriptors + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.ItemDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.attributes +import hep.dataforge.meta.scheme.ConfigurableDelegate +import hep.dataforge.meta.scheme.Scheme +import hep.dataforge.values.parseValue +import kotlin.reflect.KProperty1 +import kotlin.reflect.full.findAnnotation +import kotlin.reflect.full.isSubclassOf +import kotlin.reflect.full.memberProperties + + +//inline fun <reified T : Scheme> T.buildDescriptor(): NodeDescriptor = NodeDescriptor { +// T::class.apply { +// findAnnotation<ItemDef>()?.let { def -> +// info = def.info +// required = def.required +// multiple = def.multiple +// } +// findAnnotation<Attribute>()?.let { attr -> +// attributes { +// this[attr.key] = attr.value.parseValue() +// } +// } +// findAnnotation<Attributes>()?.attrs?.forEach { attr -> +// attributes { +// this[attr.key] = attr.value.parseValue() +// } +// } +// } +// T::class.memberProperties.forEach { property -> +// val delegate = property.getDelegate(this@buildDescriptor) +// +// val descriptor: ItemDescriptor = when (delegate) { +// is ConfigurableDelegate -> buildPropertyDescriptor(property, delegate) +// is ReadWriteDelegateWrapper<*, *> -> { +// if (delegate.delegate is ConfigurableDelegate) { +// buildPropertyDescriptor(property, delegate.delegate as ConfigurableDelegate) +// } else { +// return@forEach +// } +// } +// else -> return@forEach +// } +// defineItem(property.name, descriptor) +// } +//} + +//inline fun <T : Scheme, reified V : Any?> buildPropertyDescriptor( +// property: KProperty1<T, V>, +// delegate: ConfigurableDelegate +//): ItemDescriptor { +// when { +// V::class.isSubclassOf(Scheme::class) -> NodeDescriptor { +// default = delegate.default.node +// } +// V::class.isSubclassOf(Meta::class) -> NodeDescriptor { +// default = delegate.default.node +// } +// +// } +//} diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt index 718fb46f..4d787b69 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt @@ -19,7 +19,7 @@ interface Data<out T : Any> : Goal<T>, MetaRepr{ */ val meta: Meta - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "type" put (type.simpleName?:"undefined") if(!meta.isEmpty()) { "meta" put meta @@ -91,7 +91,7 @@ fun <T : Any, R : Any> Data<T>.map( meta: Meta = this.meta, block: suspend CoroutineScope.(T) -> R ): Data<R> = DynamicData(outputType, meta, coroutineContext, listOf(this)) { - block(await(this)) + block(await()) } @@ -103,7 +103,7 @@ inline fun <T : Any, reified R : Any> Data<T>.map( meta: Meta = this.meta, noinline block: suspend CoroutineScope.(T) -> R ): Data<R> = DynamicData(R::class, meta, coroutineContext, listOf(this)) { - block(await(this)) + block(await()) } /** @@ -119,7 +119,7 @@ inline fun <T : Any, reified R : Any> Collection<Data<T>>.reduce( coroutineContext, this ) { - block(map { run { it.await(this) } }) + block(map { run { it.await() } }) } fun <K, T : Any, R : Any> Map<K, Data<T>>.reduce( @@ -133,7 +133,7 @@ fun <K, T : Any, R : Any> Map<K, Data<T>>.reduce( coroutineContext, this.values ) { - block(mapValues { it.value.await(this) }) + block(mapValues { it.value.await() }) } @@ -153,7 +153,7 @@ inline fun <K, T : Any, reified R : Any> Map<K, Data<T>>.reduce( coroutineContext, this.values ) { - block(mapValues { it.value.await(this) }) + block(mapValues { it.value.await() }) } diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt index a55aac9d..bc9caaec 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt @@ -1,10 +1,13 @@ package hep.dataforge.data import hep.dataforge.meta.* +import hep.dataforge.meta.scheme.Scheme +import hep.dataforge.meta.scheme.SchemeSpec +import hep.dataforge.meta.scheme.string import hep.dataforge.names.toName -class DataFilter(override val config: Config) : Specific { +class DataFilter : Scheme() { /** * A source node for the filter */ @@ -22,9 +25,7 @@ class DataFilter(override val config: Config) : Specific { fun isEmpty(): Boolean = config.isEmpty() - companion object : Specification<DataFilter> { - override fun wrap(config: Config): DataFilter = DataFilter(config) - } + companion object : SchemeSpec<DataFilter>(::DataFilter) } /** @@ -54,4 +55,4 @@ fun <T : Any> DataNode<T>.filter(filter: Meta): DataNode<T> = filter(DataFilter. * Filter data using [DataFilter] builder */ fun <T : Any> DataNode<T>.filter(filterBuilder: DataFilter.() -> Unit): DataNode<T> = - filter(DataFilter.build(filterBuilder)) \ No newline at end of file + filter(DataFilter.invoke(filterBuilder)) \ No newline at end of file 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 12bb06ab..ee8d9c4d 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt @@ -4,6 +4,7 @@ import hep.dataforge.meta.* import hep.dataforge.names.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job +import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch import kotlin.collections.component1 import kotlin.collections.component2 @@ -13,16 +14,22 @@ import kotlin.reflect.KClass sealed class DataItem<out T : Any> : MetaRepr { abstract val type: KClass<out T> - class Node<out T : Any>(val value: DataNode<T>) : DataItem<T>() { - override val type: KClass<out T> get() = value.type + abstract val meta: Meta - override fun toMeta(): Meta = value.toMeta() + class Node<out T : Any>(val node: DataNode<T>) : DataItem<T>() { + override val type: KClass<out T> get() = node.type + + override fun toMeta(): Meta = node.toMeta() + + override val meta: Meta get() = node.meta } - class Leaf<out T : Any>(val value: Data<T>) : DataItem<T>() { - override val type: KClass<out T> get() = value.type + class Leaf<out T : Any>(val data: Data<T>) : DataItem<T>() { + override val type: KClass<out T> get() = data.type - override fun toMeta(): Meta = value.toMeta() + override fun toMeta(): Meta = data.toMeta() + + override val meta: Meta get() = data.meta } } @@ -38,7 +45,9 @@ interface DataNode<out T : Any> : MetaRepr { val items: Map<NameToken, DataItem<T>> - override fun toMeta(): Meta = buildMeta { + val meta: Meta + + override fun toMeta(): Meta = Meta { "type" put (type.simpleName ?: "undefined") "items" put { this@DataNode.items.forEach { @@ -47,6 +56,19 @@ interface DataNode<out T : Any> : MetaRepr { } } + /** + * Start computation for all goals in data node and return a job for the whole node + */ + @Suppress("DeferredResultUnused") + fun CoroutineScope.startAll(): Job = launch { + items.values.forEach { + when (it) { + is DataItem.Node<*> -> it.node.run { startAll() } + is DataItem.Leaf<*> -> it.data.run { startAsync() } + } + } + } + companion object { const val TYPE = "dataNode" @@ -60,28 +82,10 @@ interface DataNode<out T : Any> : MetaRepr { } } -val <T : Any> DataItem<T>?.node: DataNode<T>? get() = (this as? DataItem.Node<T>)?.value -val <T : Any> DataItem<T>?.data: Data<T>? get() = (this as? DataItem.Leaf<T>)?.value +suspend fun <T: Any> DataNode<T>.join(): Unit = coroutineScope { startAll().join() } -/** - * Start computation for all goals in data node - */ -fun DataNode<*>.startAll(scope: CoroutineScope): Unit = items.values.forEach { - when (it) { - is DataItem.Node<*> -> it.value.startAll(scope) - is DataItem.Leaf<*> -> it.value.start(scope) - } -} - -fun DataNode<*>.joinAll(scope: CoroutineScope): Job = scope.launch { - startAll(scope) - items.forEach { - when (val value = it.value) { - is DataItem.Node -> value.value.joinAll(this).join() - is DataItem.Leaf -> value.value.await(scope) - } - } -} +val <T : Any> DataItem<T>?.node: DataNode<T>? get() = (this as? DataItem.Node<T>)?.node +val <T : Any> DataItem<T>?.data: Data<T>? get() = (this as? DataItem.Leaf<T>)?.data operator fun <T : Any> DataNode<T>.get(name: Name): DataItem<T>? = when (name.length) { 0 -> error("Empty name") @@ -98,7 +102,7 @@ fun <T : Any> DataNode<T>.asSequence(): Sequence<Pair<Name, DataItem<T>>> = sequ items.forEach { (head, item) -> yield(head.asName() to item) if (item is DataItem.Node) { - val subSequence = item.value.asSequence() + val subSequence = item.node.asSequence() .map { (name, data) -> (head.asName() + name) to data } yieldAll(subSequence) } @@ -111,9 +115,9 @@ fun <T : Any> DataNode<T>.asSequence(): Sequence<Pair<Name, DataItem<T>>> = sequ fun <T : Any> DataNode<T>.dataSequence(): Sequence<Pair<Name, Data<T>>> = sequence { items.forEach { (head, item) -> when (item) { - is DataItem.Leaf -> yield(head.asName() to item.value) + is DataItem.Leaf -> yield(head.asName() to item.data) is DataItem.Node -> { - val subSequence = item.value.dataSequence() + val subSequence = item.node.dataSequence() .map { (name, data) -> (head.asName() + name) to data } yieldAll(subSequence) } @@ -125,12 +129,9 @@ operator fun <T : Any> DataNode<T>.iterator(): Iterator<Pair<Name, DataItem<T>>> class DataTree<out T : Any> internal constructor( override val type: KClass<out T>, - override val items: Map<NameToken, DataItem<T>> -) : DataNode<T> { - override fun toString(): String { - return super.toString() - } -} + override val items: Map<NameToken, DataItem<T>>, + override val meta: Meta +) : DataNode<T> private sealed class DataTreeBuilderItem<out T : Any> { class Node<T : Any>(val tree: DataTreeBuilder<T>) : DataTreeBuilderItem<T>() @@ -144,6 +145,8 @@ private sealed class DataTreeBuilderItem<out T : Any> { class DataTreeBuilder<T : Any>(val type: KClass<out T>) { private val map = HashMap<NameToken, DataTreeBuilderItem<T>>() + private var meta = MetaBuilder() + operator fun set(token: NameToken, node: DataTreeBuilder<out T>) { if (map.containsKey(token)) error("Tree entry with name $token is not empty") map[token] = DataTreeBuilderItem.Node(node) @@ -189,8 +192,8 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) { operator fun set(name: Name, node: DataNode<T>) = set(name, node.builder()) operator fun set(name: Name, item: DataItem<T>) = when (item) { - is DataItem.Node<T> -> set(name, item.value.builder()) - is DataItem.Leaf<T> -> set(name, item.value) + is DataItem.Node<T> -> set(name, item.node.builder()) + is DataItem.Leaf<T> -> set(name, item.data) } /** @@ -211,11 +214,21 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) { infix fun String.put(block: DataTreeBuilder<T>.() -> Unit) = set(toName(), DataTreeBuilder(type).apply(block)) + /** + * Update data with given node data and meta with node meta. + */ fun update(node: DataNode<T>) { node.dataSequence().forEach { //TODO check if the place is occupied this[it.first] = it.second } + meta.update(node.meta) + } + + fun meta(block: MetaBuilder.() -> Unit) = meta.apply(block) + + fun meta(meta: Meta) { + this.meta = meta.builder() } fun build(): DataTree<T> { @@ -225,7 +238,7 @@ class DataTreeBuilder<T : Any>(val type: KClass<out T>) { is DataTreeBuilderItem.Node -> DataItem.Node(value.tree.build()) } } - return DataTree(type, resMap) + return DataTree(type, resMap, meta.seal()) } } @@ -242,11 +255,11 @@ fun <T : Any> DataTreeBuilder<T>.static(name: Name, data: T, meta: Meta = EmptyM } fun <T : Any> DataTreeBuilder<T>.static(name: Name, data: T, block: MetaBuilder.() -> Unit = {}) { - this[name] = Data.static(data, buildMeta(block)) + this[name] = Data.static(data, Meta(block)) } fun <T : Any> DataTreeBuilder<T>.static(name: String, data: T, block: MetaBuilder.() -> Unit = {}) { - this[name.toName()] = Data.static(data, buildMeta(block)) + this[name.toName()] = Data.static(data, Meta(block)) } fun <T : Any> DataTreeBuilder<T>.node(name: Name, node: DataNode<T>) { diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt index 8275d31e..8c0eeec7 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt @@ -15,9 +15,7 @@ interface Goal<out T> { * Get ongoing computation or start a new one. * Does not guarantee thread safety. In case of multi-thread access, could create orphan computations. */ - fun startAsync(scope: CoroutineScope): Deferred<T> - - suspend fun CoroutineScope.await(): T = startAsync(this).await() + fun CoroutineScope.startAsync(): Deferred<T> /** * Reset the computation @@ -29,17 +27,15 @@ interface Goal<out T> { } } -fun Goal<*>.start(scope: CoroutineScope): Job = startAsync(scope) +suspend fun <T> Goal<T>.await(): T = coroutineScope { startAsync().await() } val Goal<*>.isComplete get() = result?.isCompleted ?: false -suspend fun <T> Goal<T>.await(scope: CoroutineScope): T = scope.await() - open class StaticGoal<T>(val value: T) : Goal<T> { override val dependencies: Collection<Goal<*>> get() = emptyList() override val result: Deferred<T> = CompletableDeferred(value) - override fun startAsync(scope: CoroutineScope): Deferred<T> = result + override fun CoroutineScope.startAsync(): Deferred<T> = result override fun reset() { //doNothing @@ -59,18 +55,19 @@ open class DynamicGoal<T>( * Get ongoing computation or start a new one. * Does not guarantee thread safety. In case of multi-thread access, could create orphan computations. */ - override fun startAsync(scope: CoroutineScope): Deferred<T> { - val startedDependencies = this.dependencies.map { goal -> - goal.startAsync(scope) + override fun CoroutineScope.startAsync(): Deferred<T> { + val startedDependencies = this@DynamicGoal.dependencies.map { goal -> + goal.run { startAsync() } } - return result ?: scope.async(coroutineContext + CoroutineMonitor() + Dependencies(startedDependencies)) { - startedDependencies.forEach { deferred -> - deferred.invokeOnCompletion { error -> - if (error != null) cancel(CancellationException("Dependency $deferred failed with error: ${error.message}")) + return result + ?: async(this@DynamicGoal.coroutineContext + CoroutineMonitor() + Dependencies(startedDependencies)) { + startedDependencies.forEach { deferred -> + deferred.invokeOnCompletion { error -> + if (error != null) cancel(CancellationException("Dependency $deferred failed with error: ${error.message}")) + } } - } - block() - }.also { result = it } + block() + }.also { result = it } } /** @@ -89,7 +86,7 @@ fun <T, R> Goal<T>.map( coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.(T) -> R ): Goal<R> = DynamicGoal(coroutineContext, listOf(this)) { - block(await(this)) + block(await()) } /** @@ -99,7 +96,7 @@ fun <T, R> Collection<Goal<T>>.reduce( coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.(Collection<T>) -> R ): Goal<R> = DynamicGoal(coroutineContext, this) { - block(map { run { it.await(this) } }) + block(map { run { it.await() } }) } /** @@ -112,6 +109,6 @@ fun <K, T, R> Map<K, Goal<T>>.reduce( coroutineContext: CoroutineContext = EmptyCoroutineContext, block: suspend CoroutineScope.(Map<K, T>) -> R ): Goal<R> = DynamicGoal(coroutineContext, this.values) { - block(mapValues { it.value.await(this) }) + block(mapValues { it.value.await() }) } diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt index 8c543927..89e887db 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/MapAction.kt @@ -1,9 +1,6 @@ package hep.dataforge.data -import hep.dataforge.meta.Meta -import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.builder -import hep.dataforge.meta.seal +import hep.dataforge.meta.* import hep.dataforge.names.Name import kotlin.reflect.KClass @@ -20,6 +17,7 @@ data class ActionEnv( /** * Action environment */ +@DFBuilder class MapActionBuilder<T, R>(var name: Name, var meta: MetaBuilder, val actionMeta: Meta) { lateinit var result: suspend ActionEnv.(T) -> R diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt index 556b77fc..21301dd4 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/dataCast.kt @@ -28,8 +28,8 @@ expect fun <R : Any> DataNode<*>.canCast(type: KClass<out R>): Boolean expect fun <R : Any> Data<*>.canCast(type: KClass<out R>): Boolean fun <R : Any> DataItem<*>.canCast(type: KClass<out R>): Boolean = when (this) { - is DataItem.Node -> value.canCast(type) - is DataItem.Leaf -> value.canCast(type) + is DataItem.Node -> node.canCast(type) + is DataItem.Leaf -> data.canCast(type) } /** @@ -41,7 +41,7 @@ fun <R : Any> Data<*>.cast(type: KClass<out R>): Data<R> { override val meta: Meta get() = this@cast.meta override val dependencies: Collection<Goal<*>> get() = this@cast.dependencies override val result: Deferred<R>? get() = this@cast.result as Deferred<R> - override fun startAsync(scope: CoroutineScope): Deferred<R> = this@cast.startAsync(scope) as Deferred<R> + override fun CoroutineScope.startAsync(): Deferred<R> = this@cast.run { startAsync() as Deferred<R> } override fun reset() = this@cast.reset() override val type: KClass<out R> = type } @@ -52,6 +52,7 @@ inline fun <reified R : Any> Data<*>.cast(): Data<R> = cast(R::class) @Suppress("UNCHECKED_CAST") fun <R : Any> DataNode<*>.cast(type: KClass<out R>): DataNode<R> { return object : DataNode<R> { + override val meta: Meta get() = this@cast.meta override val type: KClass<out R> = type override val items: Map<NameToken, DataItem<R>> get() = this@cast.items as Map<NameToken, DataItem<R>> } diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt index d24de964..3590679c 100644 --- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/TypeFilteredDataNode.kt @@ -1,5 +1,6 @@ package hep.dataforge.data +import hep.dataforge.meta.Meta import hep.dataforge.names.NameToken import kotlin.reflect.KClass @@ -8,16 +9,17 @@ import kotlin.reflect.KClass * A zero-copy data node wrapper that returns only children with appropriate type. */ class TypeFilteredDataNode<out T : Any>(val origin: DataNode<*>, override val type: KClass<out T>) : DataNode<T> { + override val meta: Meta get() = origin.meta override val items: Map<NameToken, DataItem<T>> by lazy { origin.items.mapNotNull { (key, item) -> when (item) { is DataItem.Leaf -> { - (item.value.filterIsInstance(type))?.let { + (item.data.filterIsInstance(type))?.let { key to DataItem.Leaf(it) } } is DataItem.Node -> { - key to DataItem.Node(item.value.filterIsInstance(type)) + key to DataItem.Node(item.node.filterIsInstance(type)) } } }.associate { it } diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt index 5b5507b2..f354c2f7 100644 --- a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/dataJVM.kt @@ -30,12 +30,10 @@ fun <R : Any> Data<*>.filterIsInstance(type: KClass<out R>): Data<R>? = * but could contain empty nodes */ fun <R : Any> DataNode<*>.filterIsInstance(type: KClass<out R>): DataNode<R> { - return if (canCast(type)) { - cast(type) - } else if (this is TypeFilteredDataNode) { - origin.filterIsInstance(type) - } else { - TypeFilteredDataNode(this, type) + return when { + canCast(type) -> cast(type) + this is TypeFilteredDataNode -> origin.filterIsInstance(type) + else -> TypeFilteredDataNode(this, type) } } @@ -44,8 +42,8 @@ fun <R : Any> DataNode<*>.filterIsInstance(type: KClass<out R>): DataNode<R> { */ fun <R : Any> DataItem<*>?.filterIsInstance(type: KClass<out R>): DataItem<R>? = when (this) { null -> null - is DataItem.Node -> DataItem.Node(this.value.filterIsInstance(type)) - is DataItem.Leaf -> this.value.filterIsInstance(type)?.let { DataItem.Leaf(it) } + is DataItem.Node -> DataItem.Node(this.node.filterIsInstance(type)) + is DataItem.Leaf -> this.data.filterIsInstance(type)?.let { DataItem.Leaf(it) } } inline fun <reified R : Any> DataItem<*>?.filterIsInstance(): DataItem<R>? = this@filterIsInstance.filterIsInstance(R::class) \ No newline at end of file diff --git a/dataforge-io/build.gradle.kts b/dataforge-io/build.gradle.kts index 083e9d53..1760f613 100644 --- a/dataforge-io/build.gradle.kts +++ b/dataforge-io/build.gradle.kts @@ -1,30 +1,24 @@ +import scientifik.DependencySourceSet.TEST +import scientifik.serialization + plugins { id("scientifik.mpp") } description = "IO module" -scientifik{ - withSerialization() - withIO() +serialization(sourceSet = TEST){ + cbor() } +val ioVersion by rootProject.extra("0.2.0-npm-dev-4") kotlin { sourceSets { - commonMain{ + commonMain { dependencies { api(project(":dataforge-context")) - } - } - jvmMain{ - dependencies { - - } - } - jsMain{ - dependencies{ - api(npm("text-encoding")) + api("org.jetbrains.kotlinx:kotlinx-io:$ioVersion") } } } diff --git a/dataforge-io/dataforge-io-yaml/build.gradle.kts b/dataforge-io/dataforge-io-yaml/build.gradle.kts index 74ba43cf..14ea2c19 100644 --- a/dataforge-io/dataforge-io-yaml/build.gradle.kts +++ b/dataforge-io/dataforge-io-yaml/build.gradle.kts @@ -1,12 +1,16 @@ +import scientifik.serialization + plugins { id("scientifik.jvm") } description = "YAML meta IO" +serialization{ + yaml() +} + dependencies { api(project(":dataforge-io")) - api("org.yaml:snakeyaml:1.25") - testImplementation(kotlin("test")) - testImplementation(kotlin("test-junit")) + api("org.yaml:snakeyaml:1.26") } diff --git a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt index db701625..6361c5dd 100644 --- a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt +++ b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/FrontMatterEnvelopeFormat.kt @@ -5,7 +5,10 @@ import hep.dataforge.io.* import hep.dataforge.meta.DFExperimental import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta -import kotlinx.io.core.* +import kotlinx.io.* +import kotlinx.io.text.readUtf8Line +import kotlinx.io.text.writeRawString +import kotlinx.io.text.writeUtf8String import kotlinx.serialization.toUtf8Bytes @DFExperimental @@ -18,52 +21,61 @@ class FrontMatterEnvelopeFormat( var line: String = "" var offset = 0u do { - line = readUTF8Line() ?: error("Input does not contain front matter separator") + line = readUtf8Line() //?: error("Input does not contain front matter separator") offset += line.toUtf8Bytes().size.toUInt() } while (!line.startsWith(SEPARATOR)) val readMetaFormat = metaTypeRegex.matchEntire(line)?.groupValues?.first() - ?.let { io.metaFormat(it) } ?: YamlMetaFormat.default + ?.let { io.metaFormat(it) } ?: YamlMetaFormat - val metaBlock = buildPacket { + val meta = buildBytes { do { - line = readUTF8Line() ?: error("Input does not contain closing front matter separator") - appendln(line) + line = readUtf8Line() + writeUtf8String(line + "\r\n") offset += line.toUtf8Bytes().size.toUInt() } while (!line.startsWith(SEPARATOR)) + }.read { + readMetaFormat.run { + readMeta() + } } - val meta = readMetaFormat.fromBytes(metaBlock) return PartialEnvelope(meta, offset, null) } override fun Input.readObject(): Envelope { var line: String = "" do { - line = readUTF8Line() ?: error("Input does not contain front matter separator") + line = readUtf8Line() //?: error("Input does not contain front matter separator") } while (!line.startsWith(SEPARATOR)) val readMetaFormat = metaTypeRegex.matchEntire(line)?.groupValues?.first() - ?.let { io.metaFormat(it) } ?: YamlMetaFormat.default + ?.let { io.metaFormat(it) } ?: YamlMetaFormat - val metaBlock = buildPacket { + val meta = buildBytes { do { - appendln(readUTF8Line() ?: error("Input does not contain closing front matter separator")) + writeUtf8String(readUtf8Line() + "\r\n") } while (!line.startsWith(SEPARATOR)) + }.read { + readMetaFormat.run { + readMeta() + } } - val meta = readMetaFormat.fromBytes(metaBlock) - val bytes = readBytes() + val bytes = readRemaining() val data = bytes.asBinary() return SimpleEnvelope(meta, data) } override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) { val metaFormat = metaFormatFactory(formatMeta, io.context) - writeText("$SEPARATOR\r\n") + writeRawString("$SEPARATOR\r\n") metaFormat.run { writeObject(envelope.meta) } - writeText("$SEPARATOR\r\n") - envelope.data?.read { copyTo(this@writeEnvelope) } + writeRawString("$SEPARATOR\r\n") + //Printing data + envelope.data?.let { data -> + writeBinary(data) + } } companion object : EnvelopeFormatFactory { @@ -72,17 +84,28 @@ class FrontMatterEnvelopeFormat( private val metaTypeRegex = "---(\\w*)\\s*".toRegex() override fun invoke(meta: Meta, context: Context): EnvelopeFormat { - return FrontMatterEnvelopeFormat(context.io, meta) + return FrontMatterEnvelopeFormat(context.io, meta) } override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? { - val line = input.readUTF8Line(3, 30) - return if (line != null && line.startsWith("---")) { + val line = input.readUtf8Line() + return if (line.startsWith("---")) { invoke() } else { null } } + private val default by lazy { invoke() } + + override fun Input.readPartial(): PartialEnvelope = + default.run { readPartial() } + + override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) = + default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) } + + override fun Input.readObject(): Envelope = + default.run { readObject() } + } } \ No newline at end of file diff --git a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt index 24ea44ec..d1ab09e4 100644 --- a/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt +++ b/dataforge-io/dataforge-io-yaml/src/main/kotlin/hep/dataforge/io/yaml/YamlMetaFormat.kt @@ -1,25 +1,23 @@ package hep.dataforge.io.yaml import hep.dataforge.context.Context -import hep.dataforge.descriptors.NodeDescriptor import hep.dataforge.io.MetaFormat import hep.dataforge.io.MetaFormatFactory import hep.dataforge.meta.DFExperimental import hep.dataforge.meta.Meta +import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.toMap import hep.dataforge.meta.toMeta -import hep.dataforge.names.Name -import hep.dataforge.names.plus -import kotlinx.io.core.Input -import kotlinx.io.core.Output -import kotlinx.io.core.readUByte -import kotlinx.io.core.writeText +import kotlinx.io.Input +import kotlinx.io.Output +import kotlinx.io.readUByte +import kotlinx.io.text.writeUtf8String import org.yaml.snakeyaml.Yaml import java.io.InputStream private class InputAsStream(val input: Input) : InputStream() { override fun read(): Int { - if (input.endOfInput) return -1 + if (input.eof()) return -1 return input.readUByte().toInt() } @@ -36,7 +34,7 @@ class YamlMetaFormat(val meta: Meta) : MetaFormat { override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) { val string = yaml.dump(meta.toMap(descriptor)) - writeText(string) + writeUtf8String(string) } override fun Input.readMeta(descriptor: NodeDescriptor?): Meta { @@ -45,12 +43,18 @@ class YamlMetaFormat(val meta: Meta) : MetaFormat { } companion object : MetaFormatFactory { - val default = YamlMetaFormat() - override fun invoke(meta: Meta, context: Context): MetaFormat = YamlMetaFormat(meta) - override val name: Name = super.name + "yaml" + override val shortName = "yaml" override val key: Short = 0x594d //YM + + private val default = YamlMetaFormat() + + override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) = + default.run { writeMeta(meta, descriptor) } + + override fun Input.readMeta(descriptor: NodeDescriptor?): Meta = + default.run { readMeta(descriptor) } } } \ No newline at end of file diff --git a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt index 414162f7..24fb6593 100644 --- a/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt +++ b/dataforge-io/dataforge-io-yaml/src/test/kotlin/hep/dataforge/io/yaml/YamlMetaFormatTest.kt @@ -3,17 +3,16 @@ package hep.dataforge.io.yaml import hep.dataforge.io.parse import hep.dataforge.io.toString import hep.dataforge.meta.Meta -import hep.dataforge.meta.buildMeta import hep.dataforge.meta.get import hep.dataforge.meta.seal -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals -class YamlMetaFormatTest{ +class YamlMetaFormatTest { @Test - fun testYamlMetaFormat(){ - val meta = buildMeta { + fun testYamlMetaFormat() { + val meta = Meta { "a" put 22 "node" put { "b" put "DDD" diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt deleted file mode 100644 index ca05de4d..00000000 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Binary.kt +++ /dev/null @@ -1,87 +0,0 @@ -package hep.dataforge.io - -import kotlinx.io.core.* -import kotlin.math.min - -/** - * A source of binary data - */ -interface Binary { - /** - * The size of binary in bytes - */ - val size: ULong - - /** - * Read continuous [Input] from this binary stating from the beginning. - * The input is automatically closed on scope close. - * Some implementation may forbid this to be called twice. In this case second call will throw an exception. - */ - fun <R> read(block: Input.() -> R): R -} - -/** - * A [Binary] with addition random access functionality. It by default allows multiple [read] operations. - */ -@ExperimentalUnsignedTypes -interface RandomAccessBinary : Binary { - /** - * Read at most [size] of bytes starting at [from] offset from the beginning of the binary. - * This method could be called multiple times simultaneously. - * - * If size - */ - fun <R> read(from: UInt, size: UInt = UInt.MAX_VALUE, block: Input.() -> R): R - - override fun <R> read(block: Input.() -> R): R = read(0.toUInt(), UInt.MAX_VALUE, block) -} - -fun Binary.toBytes(): ByteArray = read { - this.readBytes() -} - -@ExperimentalUnsignedTypes -fun RandomAccessBinary.readPacket(from: UInt, size: UInt): ByteReadPacket = read(from, size) { - buildPacket { copyTo(this) } -} - -@ExperimentalUnsignedTypes -object EmptyBinary : RandomAccessBinary { - - override val size: ULong = 0.toULong() - - override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R { - error("The binary is empty") - } - -} - -@ExperimentalUnsignedTypes -inline class ArrayBinary(val array: ByteArray) : RandomAccessBinary { - override val size: ULong get() = array.size.toULong() - - override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R { - val theSize = min(size, array.size.toUInt() - from) - return buildPacket { - writeFully(array, from.toInt(), theSize.toInt()) - }.block() - } -} - -fun ByteArray.asBinary() = ArrayBinary(this) - -/** - * Read given binary as object using given format - */ -fun <T : Any> Binary.readWith(format: IOFormat<T>): T = format.run { - read { - readObject() - } -} - -fun <T : Any> IOFormat<T>.writeBinary(obj: T): Binary { - val packet = buildPacket { - writeObject(obj) - } - return ArrayBinary(packet.readBytes()) -} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt index ba9886d8..e753b56c 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt @@ -1,18 +1,18 @@ package hep.dataforge.io import hep.dataforge.context.Context -import hep.dataforge.descriptors.NodeDescriptor -import hep.dataforge.meta.* -import hep.dataforge.names.Name -import hep.dataforge.names.plus +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.setItem import hep.dataforge.values.* -import kotlinx.io.core.Input -import kotlinx.io.core.Output -import kotlinx.io.core.readText -import kotlinx.io.core.writeText +import kotlinx.io.* +import kotlinx.io.text.readUtf8String +import kotlinx.io.text.writeUtf8String object BinaryMetaFormat : MetaFormat, MetaFormatFactory { - override val name: Name = super.name + "bin" + override val shortName: String = "bin" override val key: Short = 0x4249//BI override fun invoke(meta: Meta, context: Context): MetaFormat = this @@ -25,7 +25,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory { private fun Output.writeString(str: String) { writeInt(str.length) - writeText(str) + writeUtf8String(str) } fun Output.writeValue(value: Value) { @@ -93,7 +93,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory { private fun Input.readString(): String { val length = readInt() - return readText(max = length) + return readUtf8String(length) } @Suppress("UNCHECKED_CAST") @@ -115,7 +115,7 @@ object BinaryMetaFormat : MetaFormat, MetaFormatFactory { } 'M' -> { val length = readInt() - val meta = buildMeta { + val meta = Meta { (1..length).forEach { _ -> val name = readString() val item = readMetaItem() diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt index 80e07b56..d7c60116 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt @@ -1,11 +1,12 @@ package hep.dataforge.io -import hep.dataforge.meta.* +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.string import hep.dataforge.names.asName import hep.dataforge.names.plus -import kotlinx.io.core.Output -import kotlinx.io.core.buildPacket -import kotlinx.io.core.readBytes +import kotlinx.io.Binary interface Envelope { val meta: Meta @@ -21,12 +22,13 @@ interface Envelope { val ENVELOPE_DATA_TYPE_KEY = ENVELOPE_NODE_KEY + "dataType" val ENVELOPE_DATA_ID_KEY = ENVELOPE_NODE_KEY + "dataID" val ENVELOPE_DESCRIPTION_KEY = ENVELOPE_NODE_KEY + "description" + val ENVELOPE_NAME_KEY = ENVELOPE_NODE_KEY + "name" //const val ENVELOPE_TIME_KEY = "@envelope.time" /** * Build a static envelope using provided builder */ - operator fun invoke(block: EnvelopeBuilder.() -> Unit) = EnvelopeBuilder().apply(block).build() + inline operator fun invoke(block: EnvelopeBuilder.() -> Unit) = EnvelopeBuilder().apply(block).build() } } @@ -82,33 +84,3 @@ fun Envelope.withMetaLayers(vararg layers: Meta): Envelope { else -> ProxyEnvelope(this, *layers) } } - -class EnvelopeBuilder { - private val metaBuilder = MetaBuilder() - var data: Binary? = null - - fun meta(block: MetaBuilder.() -> Unit) { - metaBuilder.apply(block) - } - - fun meta(meta: Meta) { - metaBuilder.update(meta) - } - - var type by metaBuilder.string(key = Envelope.ENVELOPE_TYPE_KEY) - var dataType by metaBuilder.string(key = Envelope.ENVELOPE_DATA_TYPE_KEY) - var dataID by metaBuilder.string(key = Envelope.ENVELOPE_DATA_ID_KEY) - var description by metaBuilder.string(key = Envelope.ENVELOPE_DESCRIPTION_KEY) - - /** - * Construct a binary and transform it into byte-array based buffer - */ - fun data(block: Output.() -> Unit) { - val bytes = buildPacket { - block() - } - data = ArrayBinary(bytes.readBytes()) - } - - internal fun build() = SimpleEnvelope(metaBuilder.seal(), data) -} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt new file mode 100644 index 00000000..94f4e59d --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeBuilder.kt @@ -0,0 +1,52 @@ +package hep.dataforge.io + +import hep.dataforge.meta.* +import kotlinx.io.ArrayBinary +import kotlinx.io.Binary +import kotlinx.io.ExperimentalIoApi +import kotlinx.io.Output + +class EnvelopeBuilder { + private val metaBuilder = MetaBuilder() + var data: Binary? = null + + fun meta(block: MetaBuilder.() -> Unit) { + metaBuilder.apply(block) + } + + fun meta(meta: Meta) { + metaBuilder.update(meta) + } + + /** + * The general purpose of the envelope + */ + var type by metaBuilder.string(key = Envelope.ENVELOPE_TYPE_KEY) + var dataType by metaBuilder.string(key = Envelope.ENVELOPE_DATA_TYPE_KEY) + + /** + * Data unique identifier to bypass identity checks + */ + var dataID by metaBuilder.string(key = Envelope.ENVELOPE_DATA_ID_KEY) + var description by metaBuilder.string(key = Envelope.ENVELOPE_DESCRIPTION_KEY) + var name by metaBuilder.string(key = Envelope.ENVELOPE_NAME_KEY) + + /** + * Construct a data binary from given builder + */ + @ExperimentalIoApi + fun data(block: Output.() -> Unit) { + data = ArrayBinary.write(builder = block) + } + + fun build() = SimpleEnvelope(metaBuilder.seal(), data) + +} + +//@ExperimentalContracts +//suspend fun EnvelopeBuilder.buildData(block: suspend Output.() -> Unit): Binary{ +// contract { +// callsInPlace(block, InvocationKind.EXACTLY_ONCE) +// } +// val scope = CoroutineScope(coroutineContext) +//} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt index c52b9e1d..bf5b85f5 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeFormat.kt @@ -7,8 +7,8 @@ import hep.dataforge.meta.Meta import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.provider.Type -import kotlinx.io.core.Input -import kotlinx.io.core.Output +import kotlinx.io.Input +import kotlinx.io.Output import kotlin.reflect.KClass /** @@ -23,15 +23,19 @@ interface EnvelopeFormat : IOFormat<Envelope> { fun Input.readPartial(): PartialEnvelope - fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta = EmptyMeta) + fun Output.writeEnvelope( + envelope: Envelope, + metaFormatFactory: MetaFormatFactory = defaultMetaFormat, + formatMeta: Meta = EmptyMeta + ) override fun Input.readObject(): Envelope - override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj, defaultMetaFormat) + override fun Output.writeObject(obj: Envelope): Unit = writeEnvelope(obj) } @Type(ENVELOPE_FORMAT_TYPE) -interface EnvelopeFormatFactory : IOFormatFactory<Envelope> { +interface EnvelopeFormatFactory : IOFormatFactory<Envelope>, EnvelopeFormat { override val name: Name get() = "envelope".asName() override val type: KClass<out Envelope> get() = Envelope::class diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt new file mode 100644 index 00000000..9541c8fb --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/EnvelopeParts.kt @@ -0,0 +1,125 @@ +package hep.dataforge.io + +import hep.dataforge.context.Global +import hep.dataforge.io.EnvelopeParts.FORMAT_META_KEY +import hep.dataforge.io.EnvelopeParts.FORMAT_NAME_KEY +import hep.dataforge.io.EnvelopeParts.INDEX_KEY +import hep.dataforge.io.EnvelopeParts.MULTIPART_DATA_SEPARATOR +import hep.dataforge.io.EnvelopeParts.MULTIPART_DATA_TYPE +import hep.dataforge.io.EnvelopeParts.SIZE_KEY +import hep.dataforge.meta.* +import hep.dataforge.names.asName +import hep.dataforge.names.plus +import hep.dataforge.names.toName +import kotlinx.io.text.readRawString +import kotlinx.io.text.writeRawString + +object EnvelopeParts { + val MULTIPART_KEY = "multipart".asName() + val SIZE_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "size" + val INDEX_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "index" + val FORMAT_NAME_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "format" + val FORMAT_META_KEY = Envelope.ENVELOPE_NODE_KEY + MULTIPART_KEY + "meta" + + const val MULTIPART_DATA_SEPARATOR = "\r\n#~PART~#\r\n" + + const val MULTIPART_DATA_TYPE = "envelope.multipart" +} + +/** + * Append multiple serialized envelopes to the data block. Previous data is erased if it was present + */ +@DFExperimental +fun EnvelopeBuilder.multipart( + envelopes: Collection<Envelope>, + format: EnvelopeFormatFactory, + formatMeta: Meta = EmptyMeta +) { + dataType = MULTIPART_DATA_TYPE + meta { + SIZE_KEY put envelopes.size + FORMAT_NAME_KEY put format.name.toString() + if (!formatMeta.isEmpty()) { + FORMAT_META_KEY put formatMeta + } + } + data { + format(formatMeta).run { + envelopes.forEach { + writeRawString(MULTIPART_DATA_SEPARATOR) + writeEnvelope(it) + } + } + } +} + +/** + * Create a multipart partition in the envelope adding additional name-index mapping in meta + */ +@DFExperimental +fun EnvelopeBuilder.multipart( + envelopes: Map<String, Envelope>, + format: EnvelopeFormatFactory, + formatMeta: Meta = EmptyMeta +) { + dataType = MULTIPART_DATA_TYPE + meta { + SIZE_KEY put envelopes.size + FORMAT_NAME_KEY put format.name.toString() + if (!formatMeta.isEmpty()) { + FORMAT_META_KEY put formatMeta + } + } + data { + format.run { + var counter = 0 + envelopes.forEach { (key, envelope) -> + writeRawString(MULTIPART_DATA_SEPARATOR) + writeEnvelope(envelope) + meta { + append(INDEX_KEY, Meta { + "key" put key + "index" put counter + }) + } + counter++ + } + } + } +} + +@DFExperimental +fun EnvelopeBuilder.multipart( + formatFactory: EnvelopeFormatFactory, + formatMeta: Meta = EmptyMeta, + builder: suspend SequenceScope<Envelope>.() -> Unit +) = multipart(sequence(builder).toList(), formatFactory, formatMeta) + +/** + * If given envelope supports multipart data, return a sequence of those parts (could be empty). Otherwise return null. + */ +@DFExperimental +fun Envelope.parts(io: IOPlugin = Global.plugins.fetch(IOPlugin)): Sequence<Envelope>? { + return when (dataType) { + MULTIPART_DATA_TYPE -> { + val size = meta[SIZE_KEY].int ?: error("Unsized parts not supported yet") + val formatName = meta[FORMAT_NAME_KEY].string?.toName() + ?: error("Inferring parts format is not supported at the moment") + val formatMeta = meta[FORMAT_META_KEY].node ?: EmptyMeta + val format = io.envelopeFormat(formatName, formatMeta) + ?: error("Format $formatName is not resolved by $io") + return format.run { + data?.read { + sequence { + repeat(size) { + val separator = readRawString(MULTIPART_DATA_SEPARATOR.length) + if(separator!= MULTIPART_DATA_SEPARATOR) error("Separator is expected, but $separator found") + yield(readObject()) + } + } + } ?: emptySequence() + } + } + else -> null + } +} diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt index 093ffbc8..44f68738 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt @@ -10,13 +10,9 @@ import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.provider.Type import hep.dataforge.values.Value -import kotlinx.io.core.* +import kotlinx.io.* +import kotlinx.io.buffer.Buffer import kotlinx.io.pool.ObjectPool -import kotlinx.serialization.ImplicitReflectionSerializer -import kotlinx.serialization.KSerializer -import kotlinx.serialization.cbor.Cbor -import kotlinx.serialization.serializer -import kotlin.math.min import kotlin.reflect.KClass /** @@ -50,7 +46,7 @@ class ListIOFormat<T : Any>(val format: IOFormat<T>) : IOFormat<List<T>> { val <T : Any> IOFormat<T>.list get() = ListIOFormat(this) -fun ObjectPool<IoBuffer>.fill(block: IoBuffer.() -> Unit): IoBuffer { +fun ObjectPool<Buffer>.fill(block: Buffer.() -> Unit): Buffer { val buffer = borrow() return try { buffer.apply(block) @@ -72,41 +68,11 @@ interface IOFormatFactory<T : Any> : Factory<IOFormat<T>>, Named { } } -@Deprecated("To be removed in io-2") -inline fun buildPacketWithoutPool(headerSizeHint: Int = 0, block: BytePacketBuilder.() -> Unit): ByteReadPacket { - val builder = BytePacketBuilder(headerSizeHint, IoBuffer.NoPool) - block(builder) - return builder.build() -} +fun <T : Any> IOFormat<T>.writeBytes(obj: T): Bytes = buildBytes { writeObject(obj) } -fun <T : Any> IOFormat<T>.writePacket(obj: T): ByteReadPacket = buildPacket { writeObject(obj) } -fun <T : Any> IOFormat<T>.writeBytes(obj: T): ByteArray = buildPacket { writeObject(obj) }.readBytes() -fun <T : Any> IOFormat<T>.readBytes(array: ByteArray): T { - //= ByteReadPacket(array).readThis() - val byteArrayInput: Input = object : AbstractInput( - IoBuffer.Pool.borrow(), - remaining = array.size.toLong(), - pool = IoBuffer.Pool - ) { - var written = 0 - override fun closeSource() { - // do nothing - } - override fun fill(): IoBuffer? { - if (array.size - written <= 0) return null - - return IoBuffer.Pool.fill { - reserveEndGap(IoBuffer.ReservedSize) - val toWrite = min(capacity, array.size - written) - writeFully(array, written, toWrite) - written += toWrite - } - } - - } - return byteArrayInput.readObject() -} +fun <T : Any> IOFormat<T>.writeByteArray(obj: T): ByteArray = buildBytes { writeObject(obj) }.toByteArray() +fun <T : Any> IOFormat<T>.readByteArray(array: ByteArray): T = array.asBinary().read { readObject() } object DoubleIOFormat : IOFormat<Double>, IOFormatFactory<Double> { override fun invoke(meta: Meta, context: Context): IOFormat<Double> = this @@ -140,25 +106,10 @@ object ValueIOFormat : IOFormat<Value>, IOFormatFactory<Value> { } /** - * Experimental + * Read given binary as object using given format */ -@ImplicitReflectionSerializer -class SerializerIOFormat<T : Any>( - type: KClass<T>, - val serializer: KSerializer<T> = type.serializer() -) : IOFormat<T> { - - //override val name: Name = type.simpleName?.toName() ?: EmptyName - - - override fun Output.writeObject(obj: T) { - val bytes = Cbor.plain.dump(serializer, obj) - writeFully(bytes) - } - - override fun Input.readObject(): T { - //FIXME reads the whole input - val bytes = readBytes() - return Cbor.plain.load(serializer, bytes) +fun <T : Any> Binary.readWith(format: IOFormat<T>): T = format.run { + read { + readObject() } } \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt index 7e61924f..6144a211 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOPlugin.kt @@ -20,12 +20,15 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) { metaFormatFactories.find { it.key == key }?.invoke(meta) fun metaFormat(name: String, meta: Meta = EmptyMeta): MetaFormat? = - metaFormatFactories.find { it.name.toString() == name }?.invoke(meta) + metaFormatFactories.find { it.shortName == name }?.invoke(meta) val envelopeFormatFactories by lazy { context.content<EnvelopeFormatFactory>(ENVELOPE_FORMAT_TYPE).values } + fun envelopeFormat(name: Name, meta: Meta = EmptyMeta) = + envelopeFormatFactories.find { it.name == name }?.invoke(meta, context) + override fun provideTop(target: String): Map<Name, Any> { return when (target) { META_FORMAT_TYPE -> defaultMetaFormats.toMap() @@ -49,7 +52,7 @@ class IOPlugin(meta: Meta) : AbstractPlugin(meta) { companion object : PluginFactory<IOPlugin> { val defaultMetaFormats: List<MetaFormatFactory> = listOf(JsonMetaFormat, BinaryMetaFormat) - val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat) + val defaultEnvelopeFormats = listOf(TaggedEnvelopeFormat, TaglessEnvelopeFormat) override val tag: PluginTag = PluginTag("io", group = PluginTag.DATAFORGE_GROUP) diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt index a95cdec4..01606e6d 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt @@ -2,171 +2,50 @@ package hep.dataforge.io + import hep.dataforge.context.Context -import hep.dataforge.descriptors.ItemDescriptor -import hep.dataforge.descriptors.NodeDescriptor -import hep.dataforge.descriptors.ValueDescriptor import hep.dataforge.meta.Meta -import hep.dataforge.meta.MetaBase -import hep.dataforge.meta.MetaItem -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import hep.dataforge.names.plus -import hep.dataforge.names.toName -import hep.dataforge.values.* -import kotlinx.io.core.Input -import kotlinx.io.core.Output -import kotlinx.io.core.readText -import kotlinx.io.core.writeText -import kotlinx.serialization.json.* -import kotlin.collections.component1 -import kotlin.collections.component2 -import kotlin.collections.set +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.node +import hep.dataforge.meta.toJson +import hep.dataforge.meta.toMetaItem +import kotlinx.io.Input +import kotlinx.io.Output +import kotlinx.io.text.readUtf8String +import kotlinx.io.text.writeUtf8String +import kotlinx.serialization.UnstableDefault +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObjectSerializer - -class JsonMetaFormat(private val json: Json = Json.indented) : MetaFormat { +@OptIn(UnstableDefault::class) +class JsonMetaFormat(private val json: Json = DEFAULT_JSON) : MetaFormat { override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) { val jsonObject = meta.toJson(descriptor) - writeText(json.stringify(JsonObjectSerializer, jsonObject)) + writeUtf8String(json.stringify(JsonObjectSerializer, jsonObject)) } override fun Input.readMeta(descriptor: NodeDescriptor?): Meta { - val str = readText() + val str = readUtf8String() val jsonElement = json.parseJson(str) - return jsonElement.toMeta() + val item = jsonElement.toMetaItem(descriptor) + return item.node ?: Meta.EMPTY } companion object : MetaFormatFactory { - val default = JsonMetaFormat() + val DEFAULT_JSON = Json { prettyPrint = true } override fun invoke(meta: Meta, context: Context): MetaFormat = default - override val name: Name = super.name + "json" + override val shortName = "json" override val key: Short = 0x4a53//"JS" + + private val default = JsonMetaFormat() + + override fun Output.writeMeta(meta: Meta, descriptor: NodeDescriptor?) = + default.run { writeMeta(meta, descriptor) } + + override fun Input.readMeta(descriptor: NodeDescriptor?): Meta = + default.run { readMeta(descriptor) } } } - -/** - * @param descriptor reserved for custom serialization in future - */ -fun Value.toJson(descriptor: ValueDescriptor? = null): JsonElement { - return if (isList()) { - JsonArray(list.map { it.toJson() }) - } else { - when (type) { - ValueType.NUMBER -> JsonPrimitive(number) - ValueType.STRING -> JsonPrimitive(string) - ValueType.BOOLEAN -> JsonPrimitive(boolean) - ValueType.NULL -> JsonNull - } - } -} - -//Use these methods to customize JSON key mapping -private fun NameToken.toJsonKey(descriptor: ItemDescriptor?) = toString() - -private fun NodeDescriptor?.getDescriptor(key: String) = this?.items?.get(key) - -fun Meta.toJson(descriptor: NodeDescriptor? = null): JsonObject { - - //TODO search for same name siblings and arrange them into arrays - val map = this.items.entries.associate { (name, item) -> - val itemDescriptor = descriptor?.items?.get(name.body) - val key = name.toJsonKey(itemDescriptor) - val value = when (item) { - is MetaItem.ValueItem -> { - item.value.toJson(itemDescriptor as? ValueDescriptor) - } - is MetaItem.NodeItem -> { - item.node.toJson(itemDescriptor as? NodeDescriptor) - } - } - key to value - } - return JsonObject(map) -} - -fun JsonElement.toMeta(descriptor: NodeDescriptor? = null): Meta { - return when (val item = toMetaItem(descriptor)) { - is MetaItem.NodeItem<*> -> item.node - is MetaItem.ValueItem ->item.value.toMeta() - } -} - -fun JsonPrimitive.toValue(descriptor: ValueDescriptor?): Value { - return when (this) { - JsonNull -> Null - else -> this.content.parseValue() // Optimize number and boolean parsing - } -} - -fun JsonElement.toMetaItem(descriptor: ItemDescriptor? = null): MetaItem<JsonMeta> = when (this) { - is JsonPrimitive -> { - val value = this.toValue(descriptor as? ValueDescriptor) - MetaItem.ValueItem(value) - } - is JsonObject -> { - val meta = JsonMeta(this, descriptor as? NodeDescriptor) - MetaItem.NodeItem(meta) - } - is JsonArray -> { - if (this.all { it is JsonPrimitive }) { - val value = if (isEmpty()) { - Null - } else { - ListValue( - map<JsonElement, Value> { - //We already checked that all values are primitives - (it as JsonPrimitive).toValue(descriptor as? ValueDescriptor) - } - ) - } - MetaItem.ValueItem(value) - } else { - json { - "@value" to this@toMetaItem - }.toMetaItem(descriptor) - } - } -} - -class JsonMeta(val json: JsonObject, val descriptor: NodeDescriptor? = null) : MetaBase() { - - @Suppress("UNCHECKED_CAST") - private operator fun MutableMap<String, MetaItem<JsonMeta>>.set(key: String, value: JsonElement): Unit { - val itemDescriptor = descriptor.getDescriptor(key) - //use name from descriptor in case descriptor name differs from json key - val name = itemDescriptor?.name ?: key - return when (value) { - is JsonPrimitive -> { - this[name] = MetaItem.ValueItem(value.toValue(itemDescriptor as? ValueDescriptor)) as MetaItem<JsonMeta> - } - is JsonObject -> { - this[name] = MetaItem.NodeItem(JsonMeta(value, itemDescriptor as? NodeDescriptor)) - } - is JsonArray -> { - when { - value.all { it is JsonPrimitive } -> { - val listValue = ListValue( - value.map { - //We already checked that all values are primitives - (it as JsonPrimitive).toValue(itemDescriptor as? ValueDescriptor) - } - ) - this[name] = MetaItem.ValueItem(listValue) as MetaItem<JsonMeta> - } - else -> value.forEachIndexed { index, jsonElement -> - this["$name[$index]"] = jsonElement.toMetaItem(itemDescriptor) - } - } - } - } - } - - override val items: Map<NameToken, MetaItem<JsonMeta>> by lazy { - val map = HashMap<String, MetaItem<JsonMeta>>() - json.forEach { (key, value) -> map[key] = value } - map.mapKeys { it.key.toName().first()!! } - } -} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt index ca9a53a2..4776e015 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt @@ -1,13 +1,14 @@ package hep.dataforge.io import hep.dataforge.context.Context -import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.io.MetaFormatFactory.Companion.META_FORMAT_TYPE import hep.dataforge.meta.Meta import hep.dataforge.names.Name import hep.dataforge.names.asName +import hep.dataforge.names.plus import hep.dataforge.provider.Type -import kotlinx.io.core.* +import kotlinx.io.* import kotlin.reflect.KClass /** @@ -27,12 +28,14 @@ interface MetaFormat : IOFormat<Meta> { } @Type(META_FORMAT_TYPE) -interface MetaFormatFactory : IOFormatFactory<Meta> { - override val name: Name get() = "meta".asName() +interface MetaFormatFactory : IOFormatFactory<Meta>, MetaFormat { + val shortName: String + + override val name: Name get() = "meta".asName() + shortName override val type: KClass<out Meta> get() = Meta::class - val key: Short + val key: Short get() = name.hashCode().toShort() override operator fun invoke(meta: Meta, context: Context): MetaFormat @@ -41,24 +44,16 @@ interface MetaFormatFactory : IOFormatFactory<Meta> { } } -fun Meta.toString(format: MetaFormat): String = buildPacket { +fun Meta.toString(format: MetaFormat): String = buildBytes { format.run { writeObject(this@toString) } -}.readText() +}.toByteArray().decodeToString() fun Meta.toString(formatFactory: MetaFormatFactory): String = toString(formatFactory()) -fun Meta.toBytes(format: MetaFormat = JsonMetaFormat.default): ByteReadPacket = buildPacket { - format.run { writeObject(this@toBytes) } -} - fun MetaFormat.parse(str: String): Meta { - return buildPacket { writeText(str) }.readObject() + return str.encodeToByteArray().read { readObject() } } -fun MetaFormatFactory.parse(str: String): Meta = invoke().parse(str) - -fun MetaFormat.fromBytes(packet: ByteReadPacket): Meta { - return packet.readObject() -} +fun MetaFormatFactory.parse(str: String, formatMeta: Meta): Meta = invoke(formatMeta).parse(str) diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt index cce3eade..0e18fa35 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaggedEnvelopeFormat.kt @@ -7,43 +7,51 @@ import hep.dataforge.meta.string import hep.dataforge.names.Name import hep.dataforge.names.plus import hep.dataforge.names.toName -import kotlinx.io.charsets.Charsets -import kotlinx.io.core.* +import kotlinx.io.* +import kotlinx.io.text.readRawString +import kotlinx.io.text.writeRawString -@ExperimentalUnsignedTypes +@ExperimentalIoApi class TaggedEnvelopeFormat( val io: IOPlugin, - val version: VERSION = TaggedEnvelopeFormat.VERSION.DF02 + val version: VERSION = VERSION.DF02 ) : EnvelopeFormat { // private val metaFormat = io.metaFormat(metaFormatKey) // ?: error("Meta format with key $metaFormatKey could not be resolved in $io") - private fun Tag.toBytes(): ByteReadPacket = buildPacket(24) { - writeText(START_SEQUENCE) - writeText(version.name) + private fun Tag.toBytes() = buildBytes(24) { + writeRawString(START_SEQUENCE) + writeRawString(version.name) writeShort(metaFormatKey) writeUInt(metaSize) when (version) { - TaggedEnvelopeFormat.VERSION.DF02 -> { + VERSION.DF02 -> { writeUInt(dataSize.toUInt()) } - TaggedEnvelopeFormat.VERSION.DF03 -> { + VERSION.DF03 -> { writeULong(dataSize) } } - writeText(END_SEQUENCE) + writeRawString(END_SEQUENCE) } override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) { val metaFormat = metaFormatFactory.invoke(formatMeta, io.context) val metaBytes = metaFormat.writeBytes(envelope.meta) - val tag = Tag(metaFormatFactory.key, metaBytes.size.toUInt() + 2u, envelope.data?.size ?: 0.toULong()) - writePacket(tag.toBytes()) - writeFully(metaBytes) - writeText("\r\n") - envelope.data?.read { copyTo(this@writeEnvelope) } + val actualSize: ULong = if (envelope.data == null) { + 0 + } else { + envelope.data?.size ?: Binary.INFINITE + }.toULong() + val tag = Tag(metaFormatFactory.key, metaBytes.size.toUInt() + 2u, actualSize) + writeBinary(tag.toBytes()) + writeBinary(metaBytes) + writeRawString("\r\n") + envelope.data?.let { + writeBinary(it) + } flush() } @@ -59,11 +67,15 @@ class TaggedEnvelopeFormat( val metaFormat = io.metaFormat(tag.metaFormatKey) ?: error("Meta format with key ${tag.metaFormatKey} not found") - val metaPacket = ByteReadPacket(readBytes(tag.metaSize.toInt())) - val dataBytes = readBytes(tag.dataSize.toInt()) + val meta: Meta = limit(tag.metaSize.toInt()).run { + metaFormat.run { + readObject() + } + } - val meta = metaFormat.run { metaPacket.readObject() } - return SimpleEnvelope(meta, ArrayBinary(dataBytes)) + val data = ByteArray(tag.dataSize.toInt()).also { readArray(it) }.asBinary() + + return SimpleEnvelope(meta, data) } override fun Input.readPartial(): PartialEnvelope { @@ -72,8 +84,11 @@ class TaggedEnvelopeFormat( val metaFormat = io.metaFormat(tag.metaFormatKey) ?: error("Meta format with key ${tag.metaFormatKey} not found") - val metaPacket = ByteReadPacket(readBytes(tag.metaSize.toInt())) - val meta = metaFormat.run { metaPacket.readObject() } + val meta: Meta = limit(tag.metaSize.toInt()).run { + metaFormat.run { + readObject() + } + } return PartialEnvelope(meta, version.tagSize + tag.metaSize, tag.dataSize) } @@ -99,16 +114,16 @@ class TaggedEnvelopeFormat( val io = context.io val metaFormatName = meta["name"].string?.toName() ?: JsonMetaFormat.name - val metaFormatFactory = io.metaFormatFactories.find { it.name == metaFormatName } - ?: error("Meta format could not be resolved") + //Check if appropriate factory exists + io.metaFormatFactories.find { it.name == metaFormatName } ?: error("Meta format could not be resolved") return TaggedEnvelopeFormat(io) } private fun Input.readTag(version: VERSION): Tag { - val start = readTextExactBytes(2, charset = Charsets.ISO_8859_1) + val start = readRawString(2) if (start != START_SEQUENCE) error("The input is not an envelope") - val versionString = readTextExactBytes(4, charset = Charsets.ISO_8859_1) + val versionString = readRawString(4) if (version.name != versionString) error("Wrong version of DataForge: expected $version but found $versionString") val metaFormatKey = readShort() val metaLength = readUInt() @@ -116,14 +131,14 @@ class TaggedEnvelopeFormat( VERSION.DF02 -> readUInt().toULong() VERSION.DF03 -> readULong() } - val end = readTextExactBytes(4, charset = Charsets.ISO_8859_1) + val end = readRawString(4) if (end != END_SEQUENCE) error("The input is not an envelope") return Tag(metaFormatKey, metaLength, dataLength) } override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? { return try { - val header = input.readTextExactBytes(6) + val header = input.readRawString(6) when (header.substring(2..5)) { VERSION.DF02.name -> TaggedEnvelopeFormat(io, VERSION.DF02) VERSION.DF03.name -> TaggedEnvelopeFormat(io, VERSION.DF03) @@ -134,7 +149,16 @@ class TaggedEnvelopeFormat( } } - val default by lazy { invoke() } + private val default by lazy { invoke() } + + override fun Input.readPartial(): PartialEnvelope = + default.run { readPartial() } + + override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) = + default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) } + + override fun Input.readObject(): Envelope = + default.run { readObject() } } } \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt index 14d871db..7023ce1a 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TaglessEnvelopeFormat.kt @@ -3,9 +3,13 @@ package hep.dataforge.io import hep.dataforge.context.Context import hep.dataforge.meta.* import hep.dataforge.names.asName -import kotlinx.io.core.* -import kotlinx.serialization.toUtf8Bytes +import kotlinx.io.* +import kotlinx.io.text.readRawString +import kotlinx.io.text.readUtf8Line +import kotlinx.io.text.writeRawString +import kotlinx.io.text.writeUtf8String +@ExperimentalIoApi class TaglessEnvelopeFormat( val io: IOPlugin, meta: Meta = EmptyMeta @@ -15,40 +19,46 @@ class TaglessEnvelopeFormat( private val dataStart = meta[DATA_START_PROPERTY].string ?: DEFAULT_DATA_START private fun Output.writeProperty(key: String, value: Any) { - writeText("#? $key: $value;\r\n") + writeUtf8String("#? $key: $value;\r\n") } override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) { val metaFormat = metaFormatFactory(formatMeta, io.context) //printing header - writeText(TAGLESS_ENVELOPE_HEADER + "\r\n") + writeRawString(TAGLESS_ENVELOPE_HEADER + "\r\n") //printing all properties - writeProperty(META_TYPE_PROPERTY, metaFormatFactory.type) + writeProperty(META_TYPE_PROPERTY, metaFormatFactory.shortName) //TODO add optional metaFormat properties - writeProperty(DATA_LENGTH_PROPERTY, envelope.data?.size ?: 0) + val actualSize: Int = if (envelope.data == null) { + 0 + } else { + envelope.data?.size ?: Binary.INFINITE + } + + writeProperty(DATA_LENGTH_PROPERTY, actualSize) //Printing meta if (!envelope.meta.isEmpty()) { val metaBytes = metaFormat.writeBytes(envelope.meta) - writeProperty(META_LENGTH_PROPERTY, metaBytes.size) - writeText(metaStart + "\r\n") - writeFully(metaBytes) - writeText("\r\n") + writeProperty(META_LENGTH_PROPERTY, metaBytes.size + 2) + writeUtf8String(metaStart + "\r\n") + writeBinary(metaBytes) + writeRawString("\r\n") } //Printing data envelope.data?.let { data -> - writeText(dataStart + "\r\n") - writeFully(data.toBytes()) + writeUtf8String(dataStart + "\r\n") + writeBinary(data) } } override fun Input.readObject(): Envelope { - var line: String = "" + var line: String do { - line = readUTF8Line() ?: error("Input does not contain tagless envelope header") + line = readUtf8Line() // ?: error("Input does not contain tagless envelope header") } while (!line.startsWith(TAGLESS_ENVELOPE_HEADER)) val properties = HashMap<String, String>() @@ -60,19 +70,20 @@ class TaglessEnvelopeFormat( val (key, value) = match.destructured properties[key] = value } - line = readUTF8Line() ?: return SimpleEnvelope(Meta.empty, null) + //If can't read line, return envelope without data + if (eof()) return SimpleEnvelope(Meta.EMPTY, null) + line = readUtf8Line() } var meta: Meta = EmptyMeta if (line.startsWith(metaStart)) { - val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat.default - val metaSize = properties.get(META_LENGTH_PROPERTY)?.toInt() + val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat + val metaSize = properties[META_LENGTH_PROPERTY]?.toInt() meta = if (metaSize != null) { - val metaPacket = buildPacket { - writeFully(readBytes(metaSize)) + limit(metaSize).run { + metaFormat.run { readObject() } } - metaFormat.run { metaPacket.readObject() } } else { metaFormat.run { readObject() @@ -81,17 +92,22 @@ class TaglessEnvelopeFormat( } do { - line = readUTF8Line() ?: return SimpleEnvelope(meta, null) - //returning an Envelope without data if end of input is reached + try { + line = readUtf8Line() + } catch (ex: EOFException) { + //returning an Envelope without data if end of input is reached + return SimpleEnvelope(meta, null) + } } while (!line.startsWith(dataStart)) val data: Binary? = if (properties.containsKey(DATA_LENGTH_PROPERTY)) { val bytes = ByteArray(properties[DATA_LENGTH_PROPERTY]!!.toInt()) - readFully(bytes) + readArray(bytes) bytes.asBinary() } else { - val bytes = readBytes() - bytes.asBinary() + ArrayBinary.write { + writeInput(this@readObject) + } } return SimpleEnvelope(meta, data) @@ -99,10 +115,10 @@ class TaglessEnvelopeFormat( override fun Input.readPartial(): PartialEnvelope { var offset = 0u - var line: String = "" + var line: String do { - line = readUTF8Line() ?: error("Input does not contain tagless envelope header") - offset += line.toUtf8Bytes().size.toUInt() + line = readUtf8Line()// ?: error("Input does not contain tagless envelope header") + offset += line.encodeToByteArray().size.toUInt() } while (!line.startsWith(TAGLESS_ENVELOPE_HEADER)) val properties = HashMap<String, String>() @@ -114,30 +130,32 @@ class TaglessEnvelopeFormat( val (key, value) = match.destructured properties[key] = value } - line = readUTF8Line() ?: return PartialEnvelope(Meta.empty, offset.toUInt(), 0.toULong()) - offset += line.toUtf8Bytes().size.toUInt() + try { + line = readUtf8Line() + offset += line.encodeToByteArray().size.toUInt() + } catch (ex: EOFException) { + return PartialEnvelope(Meta.EMPTY, offset.toUInt(), 0.toULong()) + } } var meta: Meta = EmptyMeta if (line.startsWith(metaStart)) { - val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat.default - - val metaSize = properties.get(META_LENGTH_PROPERTY)?.toInt() + val metaFormat = properties[META_TYPE_PROPERTY]?.let { io.metaFormat(it) } ?: JsonMetaFormat + val metaSize = properties[META_LENGTH_PROPERTY]?.toInt() meta = if (metaSize != null) { - val metaPacket = buildPacket { - writeFully(readBytes(metaSize)) - } offset += metaSize.toUInt() - metaFormat.run { metaPacket.readObject() } + limit(metaSize).run { + metaFormat.run { readObject() } + } } else { error("Can't partially read an envelope with undefined meta size") } } do { - line = readUTF8Line() ?: return PartialEnvelope(Meta.empty, offset.toUInt(), 0.toULong()) - offset += line.toUtf8Bytes().size.toUInt() + line = readUtf8Line() //?: return PartialEnvelope(Meta.EMPTY, offset.toUInt(), 0.toULong()) + offset += line.encodeToByteArray().size.toUInt() //returning an Envelope without data if end of input is reached } while (!line.startsWith(dataStart)) @@ -170,13 +188,21 @@ class TaglessEnvelopeFormat( return TaglessEnvelopeFormat(context.io, meta) } - val default by lazy { invoke() } + private val default by lazy { invoke() } + + override fun Input.readPartial(): PartialEnvelope = + default.run { readPartial() } + + override fun Output.writeEnvelope(envelope: Envelope, metaFormatFactory: MetaFormatFactory, formatMeta: Meta) = + default.run { writeEnvelope(envelope, metaFormatFactory, formatMeta) } + + override fun Input.readObject(): Envelope = + default.run { readObject() } override fun peekFormat(io: IOPlugin, input: Input): EnvelopeFormat? { return try { - val buffer = ByteArray(TAGLESS_ENVELOPE_HEADER.length) - input.readFully(buffer) - return if (buffer.toString() == TAGLESS_ENVELOPE_HEADER) { + val string = input.readRawString(TAGLESS_ENVELOPE_HEADER.length) + return if (string == TAGLESS_ENVELOPE_HEADER) { TaglessEnvelopeFormat(io) } else { null diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt index 7c294891..1df4b42d 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionClient.kt @@ -6,6 +6,7 @@ import hep.dataforge.io.* import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.int +import hep.dataforge.meta.scheme.int import kotlin.reflect.KClass class RemoteFunctionClient(override val context: Context, val responder: Responder) : FunctionServer, ContextAware { diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt index 8252b1d3..efd4351e 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/functions/RemoteFunctionServer.kt @@ -8,6 +8,7 @@ import hep.dataforge.io.Responder import hep.dataforge.io.type import hep.dataforge.meta.get import hep.dataforge.meta.int +import hep.dataforge.meta.scheme.int class RemoteFunctionServer( override val context: Context, diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt deleted file mode 100644 index b22fed4a..00000000 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/MetaSerializer.kt +++ /dev/null @@ -1,141 +0,0 @@ -package hep.dataforge.io.serialization - -import hep.dataforge.io.toJson -import hep.dataforge.io.toMeta -import hep.dataforge.meta.* -import hep.dataforge.names.NameToken -import hep.dataforge.values.* -import kotlinx.serialization.* -import kotlinx.serialization.internal.* -import kotlinx.serialization.json.JsonInput -import kotlinx.serialization.json.JsonObjectSerializer -import kotlinx.serialization.json.JsonOutput - - -@Serializer(Value::class) -object ValueSerializer : KSerializer<Value> { - private val valueTypeSerializer = EnumSerializer(ValueType::class) - private val listSerializer by lazy { ArrayListSerializer(ValueSerializer) } - - override val descriptor: SerialDescriptor = descriptor("hep.dataforge.values.Value") { - boolean("isList") - enum<ValueType>("valueType") - element("value", null) - } - - private fun Decoder.decodeValue(): Value { - return when (decode(valueTypeSerializer)) { - ValueType.NULL -> Null - ValueType.NUMBER -> decodeDouble().asValue() //TODO differentiate? - ValueType.BOOLEAN -> decodeBoolean().asValue() - ValueType.STRING -> decodeString().asValue() - else -> decodeString().parseValue() - } - } - - - override fun deserialize(decoder: Decoder): Value { - val isList = decoder.decodeBoolean() - return if (isList) { - listSerializer.deserialize(decoder).asValue() - } else { - decoder.decodeValue() - } - } - - private fun Encoder.encodeValue(value: Value) { - encode(valueTypeSerializer, value.type) - when (value.type) { - ValueType.NULL -> { - // do nothing - } - ValueType.NUMBER -> encodeDouble(value.double) - ValueType.BOOLEAN -> encodeBoolean(value.boolean) - ValueType.STRING -> encodeString(value.string) - else -> encodeString(value.string) - } - } - - override fun serialize(encoder: Encoder, obj: Value) { - encoder.encodeBoolean(obj.isList()) - if (obj.isList()) { - listSerializer.serialize(encoder, obj.list) - } else { - encoder.encodeValue(obj) - } - } -} - -@Serializer(MetaItem::class) -object MetaItemSerializer : KSerializer<MetaItem<*>> { - override val descriptor: SerialDescriptor = descriptor("MetaItem") { - boolean("isNode") - element("value", null) - } - - - override fun deserialize(decoder: Decoder): MetaItem<*> { - val isNode = decoder.decodeBoolean() - return if (isNode) { - MetaItem.NodeItem(decoder.decode(MetaSerializer)) - } else { - MetaItem.ValueItem(decoder.decode(ValueSerializer)) - } - } - - override fun serialize(encoder: Encoder, obj: MetaItem<*>) { - encoder.encodeBoolean(obj is MetaItem.NodeItem) - when (obj) { - is MetaItem.NodeItem -> MetaSerializer.serialize(encoder, obj.node) - is MetaItem.ValueItem -> ValueSerializer.serialize(encoder, obj.value) - } - } -} - -private class DeserializedMeta(override val items: Map<NameToken, MetaItem<*>>) : MetaBase() - -/** - * Serialized for meta - */ -@Serializer(Meta::class) -object MetaSerializer : KSerializer<Meta> { - private val mapSerializer = HashMapSerializer( - StringSerializer, - MetaItemSerializer - ) - - override val descriptor: SerialDescriptor = NamedMapClassDescriptor( - "hep.dataforge.meta.Meta", - StringSerializer.descriptor, - MetaItemSerializer.descriptor - ) - - override fun deserialize(decoder: Decoder): Meta { - return if (decoder is JsonInput) { - JsonObjectSerializer.deserialize(decoder).toMeta() - } else { - DeserializedMeta(mapSerializer.deserialize(decoder).mapKeys { NameToken(it.key) }) - } - } - - override fun serialize(encoder: Encoder, obj: Meta) { - if (encoder is JsonOutput) { - JsonObjectSerializer.serialize(encoder, obj.toJson()) - } else { - mapSerializer.serialize(encoder, obj.items.mapKeys { it.key.toString() }) - } - } -} - -@Serializer(Config::class) -object ConfigSerializer : KSerializer<Config> { - override val descriptor: SerialDescriptor = MetaSerializer.descriptor - - override fun deserialize(decoder: Decoder): Config { - return MetaSerializer.deserialize(decoder).toConfig() - } - - override fun serialize(encoder: Encoder, obj: Config) { - MetaSerializer.serialize(encoder, obj) - } -} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt deleted file mode 100644 index c12a6c19..00000000 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/nameSerializers.kt +++ /dev/null @@ -1,33 +0,0 @@ -package hep.dataforge.io.serialization - -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import hep.dataforge.names.toName -import kotlinx.serialization.* -import kotlinx.serialization.internal.StringDescriptor - -@Serializer(Name::class) -object NameSerializer : KSerializer<Name> { - override val descriptor: SerialDescriptor = StringDescriptor.withName("Name") - - override fun deserialize(decoder: Decoder): Name { - return decoder.decodeString().toName() - } - - override fun serialize(encoder: Encoder, obj: Name) { - encoder.encodeString(obj.toString()) - } -} - -@Serializer(NameToken::class) -object NameTokenSerializer : KSerializer<NameToken> { - override val descriptor: SerialDescriptor = StringDescriptor.withName("NameToken") - - override fun deserialize(decoder: Decoder): NameToken { - return decoder.decodeString().toName().first()!! - } - - override fun serialize(encoder: Encoder, obj: NameToken) { - encoder.encodeString(obj.toString()) - } -} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt deleted file mode 100644 index 09d17054..00000000 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/serialization/serializationUtils.kt +++ /dev/null @@ -1,80 +0,0 @@ -package hep.dataforge.io.serialization - -import kotlinx.serialization.CompositeDecoder -import kotlinx.serialization.Decoder -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerialDescriptor -import kotlinx.serialization.internal.* - -/** - * A convenience builder for serial descriptors - */ -inline class SerialDescriptorBuilder(private val impl: SerialClassDescImpl) { - fun element( - name: String, - descriptor: SerialDescriptor?, - isOptional: Boolean = false, - vararg annotations: Annotation - ) { - impl.addElement(name, isOptional) - descriptor?.let { impl.pushDescriptor(descriptor) } - annotations.forEach { - impl.pushAnnotation(it) - } - } - - fun element( - name: String, - isOptional: Boolean = false, - vararg annotations: Annotation, - block: SerialDescriptorBuilder.() -> Unit - ) { - impl.addElement(name, isOptional) - impl.pushDescriptor(SerialDescriptorBuilder(SerialClassDescImpl(name)).apply(block).build()) - annotations.forEach { - impl.pushAnnotation(it) - } - } - - fun boolean(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, BooleanDescriptor, isOptional, *annotations) - - fun string(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, StringDescriptor, isOptional, *annotations) - - fun int(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, IntDescriptor, isOptional, *annotations) - - fun double(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, DoubleDescriptor, isOptional, *annotations) - - fun float(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, FloatDescriptor, isOptional, *annotations) - - fun long(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, LongDescriptor, isOptional, *annotations) - - fun doubleArray(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, DoubleArraySerializer.descriptor, isOptional, *annotations) - - inline fun <reified E : Enum<E>> enum(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = - element(name, EnumSerializer(E::class).descriptor, isOptional, *annotations) - - fun classAnnotation(a: Annotation) = impl.pushClassAnnotation(a) - - fun build(): SerialDescriptor = impl -} - -inline fun <reified T : Any> KSerializer<T>.descriptor( - name: String, - block: SerialDescriptorBuilder.() -> Unit -): SerialDescriptor = - SerialDescriptorBuilder(SerialClassDescImpl(name)).apply(block).build() - -fun Decoder.decodeStructure( - desc: SerialDescriptor, - vararg typeParams: KSerializer<*> = emptyArray(), - block: CompositeDecoder.() -> Unit -) { - beginStructure(desc, *typeParams).apply(block).endStructure(desc) -} \ No newline at end of file diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt index 29e60f2f..0851d6df 100644 --- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt +++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/EnvelopeFormatTest.kt @@ -1,5 +1,7 @@ package hep.dataforge.io +import kotlinx.io.readDouble +import kotlinx.io.writeDouble import kotlin.test.Test import kotlin.test.assertEquals @@ -12,16 +14,18 @@ class EnvelopeFormatTest { } data{ writeDouble(22.2) +// repeat(2000){ +// writeInt(it) +// } } } - @ExperimentalStdlibApi @Test fun testTaggedFormat(){ - TaggedEnvelopeFormat.default.run { - val bytes = writeBytes(envelope) - println(bytes.decodeToString()) - val res = readBytes(bytes) + TaggedEnvelopeFormat.run { + val byteArray = this.writeByteArray(envelope) + //println(byteArray.decodeToString()) + val res = readByteArray(byteArray) assertEquals(envelope.meta,res.meta) val double = res.data?.read { readDouble() @@ -32,10 +36,10 @@ class EnvelopeFormatTest { @Test fun testTaglessFormat(){ - TaglessEnvelopeFormat.default.run { - val bytes = writeBytes(envelope) - println(bytes.decodeToString()) - val res = readBytes(bytes) + TaglessEnvelopeFormat.run { + val byteArray = writeByteArray(envelope) + //println(byteArray.decodeToString()) + val res = readByteArray(byteArray) assertEquals(envelope.meta,res.meta) val double = res.data?.read { readDouble() diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt index 02180dbc..6fc801ec 100644 --- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt +++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt @@ -1,16 +1,26 @@ package hep.dataforge.io import hep.dataforge.meta.* +import kotlinx.io.Bytes +import kotlinx.io.buildBytes import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.json import kotlinx.serialization.json.jsonArray import kotlin.test.Test import kotlin.test.assertEquals +fun Meta.toBytes(format: MetaFormat = JsonMetaFormat): Bytes = buildBytes { + format.run { writeObject(this@toBytes) } +} + +fun MetaFormat.fromBytes(packet: Bytes): Meta { + return packet.read { readObject() } +} + class MetaFormatTest { @Test fun testBinaryMetaFormat() { - val meta = buildMeta { + val meta = Meta { "a" put 22 "node" put { "b" put "DDD" @@ -25,7 +35,7 @@ class MetaFormatTest { @Test fun testJsonMetaFormat() { - val meta = buildMeta { + val meta = Meta { "a" put 22 "node" put { "b" put "DDD" diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt index 7a8447c0..86a569a6 100644 --- a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt +++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaSerializerTest.kt @@ -1,29 +1,27 @@ package hep.dataforge.io -import hep.dataforge.io.serialization.MetaItemSerializer -import hep.dataforge.io.serialization.MetaSerializer -import hep.dataforge.io.serialization.NameSerializer -import hep.dataforge.meta.buildMeta +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.MetaSerializer +import hep.dataforge.names.Name import hep.dataforge.names.toName -import kotlinx.io.charsets.Charsets -import kotlinx.io.core.String import kotlinx.serialization.cbor.Cbor import kotlinx.serialization.json.Json import kotlin.test.Test import kotlin.test.assertEquals class MetaSerializerTest { + val meta = Meta { + "a" put 22 + "node" put { + "b" put "DDD" + "c" put 11.1 + "array" put doubleArrayOf(1.0, 2.0, 3.0) + } + } + @Test fun testMetaSerialization() { - val meta = buildMeta { - "a" put 22 - "node" put { - "b" put "DDD" - "c" put 11.1 - "array" put doubleArrayOf(1.0, 2.0, 3.0) - } - } - val string = Json.indented.stringify(MetaSerializer, meta) val restored = Json.plain.parse(MetaSerializer, string) assertEquals(restored, meta) @@ -31,17 +29,8 @@ class MetaSerializerTest { @Test fun testCborSerialization() { - val meta = buildMeta { - "a" put 22 - "node" put { - "b" put "DDD" - "c" put 11.1 - "array" put doubleArrayOf(1.0, 2.0, 3.0) - } - } - val bytes = Cbor.dump(MetaSerializer, meta) - println(String(bytes, charset = Charsets.ISO_8859_1)) + println(bytes.contentToString()) val restored = Cbor.load(MetaSerializer, bytes) assertEquals(restored, meta) } @@ -49,13 +38,13 @@ class MetaSerializerTest { @Test fun testNameSerialization() { val name = "a.b.c".toName() - val string = Json.indented.stringify(NameSerializer, name) - val restored = Json.plain.parse(NameSerializer, string) + val string = Json.indented.stringify(Name.serializer(), name) + val restored = Json.plain.parse(Name.serializer(), string) assertEquals(restored, name) } @Test - fun testMetaItemDescriptor(){ - val descriptor = MetaItemSerializer.descriptor.getElementDescriptor(0) + fun testMetaItemDescriptor() { + val descriptor = MetaItem.serializer(MetaSerializer).descriptor.getElementDescriptor(0) } } \ No newline at end of file diff --git a/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt new file mode 100644 index 00000000..04d097aa --- /dev/null +++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MultipartTest.kt @@ -0,0 +1,48 @@ +package hep.dataforge.io + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.meta.get +import hep.dataforge.meta.int +import hep.dataforge.meta.scheme.int +import kotlinx.io.text.writeRawString +import kotlinx.io.text.writeUtf8String + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@DFExperimental +class MultipartTest { + val envelopes = (0..5).map { + Envelope { + meta { + "value" put it + } + data { + writeUtf8String("Hello World $it") + repeat(300) { + writeRawString("$it ") + } + } + } + } + + val partsEnvelope = Envelope { + multipart(envelopes, TaggedEnvelopeFormat) + } + + @Test + fun testParts() { + TaggedEnvelopeFormat.run { + val singleEnvelopeData = writeBytes(envelopes[0]) + val singleEnvelopeSize = singleEnvelopeData.size + val bytes = writeBytes(partsEnvelope) + assertTrue(5*singleEnvelopeSize < bytes.size) + val reconstructed = bytes.readWith(this) + val parts = reconstructed.parts()?.toList() ?: emptyList() + assertEquals(2, parts[2].meta["value"].int) + println(reconstructed.data!!.size) + } + } + +} \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt deleted file mode 100644 index aa90a638..00000000 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileBinary.kt +++ /dev/null @@ -1,31 +0,0 @@ -package hep.dataforge.io - -import kotlinx.io.core.Input -import kotlinx.io.core.buildPacket -import java.nio.channels.FileChannel -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import kotlin.math.min - -@ExperimentalUnsignedTypes -class FileBinary(val path: Path, private val offset: UInt = 0u, size: ULong? = null) : RandomAccessBinary { - - override val size: ULong = size ?: (Files.size(path).toULong() - offset).toULong() - - init { - if( size != null && Files.size(path) < offset.toLong() + size.toLong()){ - error("Can't read binary from file. File is to short.") - } - } - - override fun <R> read(from: UInt, size: UInt, block: Input.() -> R): R { - FileChannel.open(path, StandardOpenOption.READ).use { - val theSize: UInt = min(size, Files.size(path).toUInt() - offset) - val buffer = it.map(FileChannel.MapMode.READ_ONLY, (from + offset).toLong(), theSize.toLong()) - return buildPacket { writeFully(buffer) }.block() - } - } -} - -fun Path.asBinary(offset: UInt = 0u, size: ULong? = null): FileBinary = FileBinary(this, offset, size) \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt index 3187cd54..21cca102 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/FileEnvelope.kt @@ -1,52 +1,22 @@ package hep.dataforge.io -import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta -import kotlinx.io.nio.asInput -import kotlinx.io.nio.asOutput -import java.nio.file.Files +import kotlinx.io.Binary +import kotlinx.io.ExperimentalIoApi +import kotlinx.io.FileBinary +import kotlinx.io.read import java.nio.file.Path -import java.nio.file.StandardOpenOption +@ExperimentalIoApi class FileEnvelope internal constructor(val path: Path, val format: EnvelopeFormat) : Envelope { //TODO do not like this constructor. Hope to replace it later - private val partialEnvelope: PartialEnvelope - - init { - val input = Files.newByteChannel(path, StandardOpenOption.READ).asInput() - partialEnvelope = format.run { input.use { it.readPartial()} } + private val partialEnvelope: PartialEnvelope = path.read { + format.run { readPartial() } } override val meta: Meta get() = partialEnvelope.meta - override val data: Binary? = FileBinary(path, partialEnvelope.dataOffset, partialEnvelope.dataSize) -} - -fun IOPlugin.readEnvelopeFile( - path: Path, - formatFactory: EnvelopeFormatFactory = TaggedEnvelopeFormat, - formatMeta: Meta = EmptyMeta -): FileEnvelope { - val format = formatFactory(formatMeta, context) - return FileEnvelope(path, format) -} - -fun IOPlugin.writeEnvelopeFile( - path: Path, - envelope: Envelope, - formatFactory: EnvelopeFormatFactory = TaggedEnvelopeFormat, - formatMeta: Meta = EmptyMeta -) { - val output = Files.newByteChannel( - path, - StandardOpenOption.WRITE, - StandardOpenOption.CREATE, - StandardOpenOption.TRUNCATE_EXISTING - ).asOutput() - - with(formatFactory(formatMeta, context)) { - output.writeObject(envelope) - } + override val data: Binary? = FileBinary(path, partialEnvelope.dataOffset.toInt(), partialEnvelope.dataSize?.toInt()) } diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt new file mode 100644 index 00000000..c9b4f1bf --- /dev/null +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/fileIO.kt @@ -0,0 +1,193 @@ +package hep.dataforge.io + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.isEmpty +import kotlinx.io.* +import java.nio.file.Files +import java.nio.file.Path +import kotlin.reflect.full.isSuperclassOf +import kotlin.streams.asSequence + +/** + * Resolve IOFormat based on type + */ +@Suppress("UNCHECKED_CAST") +@DFExperimental +inline fun <reified T : Any> IOPlugin.resolveIOFormat(): IOFormat<T>? { + return ioFormats.values.find { it.type.isSuperclassOf(T::class) } as IOFormat<T>? +} + +/** + * Read file containing meta using given [formatOverride] or file extension to infer meta type. + * If [path] is a directory search for file starting with `meta` in it + */ +fun IOPlugin.readMetaFile(path: Path, formatOverride: MetaFormat? = null, descriptor: NodeDescriptor? = null): Meta { + if (!Files.exists(path)) error("Meta file $path does not exist") + + val actualPath: Path = if (Files.isDirectory(path)) { + Files.list(path).asSequence().find { it.fileName.startsWith("meta") } + ?: error("The directory $path does not contain meta file") + } else { + path + } + val extension = actualPath.fileName.toString().substringAfterLast('.') + + val metaFormat = formatOverride ?: metaFormat(extension) ?: error("Can't resolve meta format $extension") + return metaFormat.run { + actualPath.read{ + readMeta(descriptor) + } + } +} + +/** + * Write meta to file using [metaFormat]. If [path] is a directory, write a file with name equals name of [metaFormat]. + * Like "meta.json" + */ +fun IOPlugin.writeMetaFile( + path: Path, + meta: Meta, + metaFormat: MetaFormatFactory = JsonMetaFormat, + descriptor: NodeDescriptor? = null +) { + val actualPath = if (Files.isDirectory(path)) { + path.resolve("@" + metaFormat.name.toString()) + } else { + path + } + metaFormat.run { + actualPath.write{ + writeMeta(meta, descriptor) + } + } +} + +/** + * Return inferred [EnvelopeFormat] if only one format could read given file. If no format accepts file, return null. If + * multiple formats accepts file, throw an error. + */ +fun IOPlugin.peekBinaryFormat(path: Path): EnvelopeFormat? { + val binary = path.asBinary() + val formats = envelopeFormatFactories.mapNotNull { factory -> + binary.read { + factory.peekFormat(this@peekBinaryFormat, this@read) + } + } + + return when (formats.size) { + 0 -> null + 1 -> formats.first() + else -> error("Envelope format binary recognition clash") + } +} + +val IOPlugin.Companion.META_FILE_NAME: String get() = "@meta" +val IOPlugin.Companion.DATA_FILE_NAME: String get() = "@data" + +/** + * Read and envelope from file if the file exists, return null if file does not exist. + * + * If file is directory, then expect two files inside: + * * **meta.<format name>** for meta + * * **data** for data + * + * If the file is envelope read it using [EnvelopeFormatFactory.peekFormat] functionality to infer format. + * + * If the file is not an envelope and [readNonEnvelopes] is true, return an Envelope without meta, using file as binary. + * + * Return null otherwise. + */ +@DFExperimental +fun IOPlugin.readEnvelopeFile( + path: Path, + readNonEnvelopes: Boolean = false, + formatPeeker: IOPlugin.(Path) -> EnvelopeFormat? = IOPlugin::peekBinaryFormat +): Envelope? { + if (!Files.exists(path)) return null + + //read two-files directory + if (Files.isDirectory(path)) { + val metaFile = Files.list(path).asSequence() + .singleOrNull { it.fileName.toString().startsWith(IOPlugin.META_FILE_NAME) } + + val meta = if (metaFile == null) { + EmptyMeta + } else { + readMetaFile(metaFile) + } + + val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME) + + val data: Binary? = if (Files.exists(dataFile)) { + dataFile.asBinary() + } else { + null + } + + return SimpleEnvelope(meta, data) + } + + return formatPeeker(path)?.let { format -> + FileEnvelope(path, format) + } ?: if (readNonEnvelopes) { // if no format accepts file, read it as binary + SimpleEnvelope(Meta.EMPTY, path.asBinary()) + } else null +} + +/** + * Write a binary into file. Throws an error if file already exists + */ +fun <T : Any> IOFormat<T>.writeToFile(path: Path, obj: T) { + path.write { + writeObject(obj) + } +} + +/** + * Write envelope file to given [path] using [envelopeFormat] and optional [metaFormat] + */ +@DFExperimental +fun IOPlugin.writeEnvelopeFile( + path: Path, + envelope: Envelope, + envelopeFormat: EnvelopeFormat = TaggedEnvelopeFormat, + metaFormat: MetaFormatFactory? = null +) { + path.write { + with(envelopeFormat) { + writeEnvelope(envelope, metaFormat ?: envelopeFormat.defaultMetaFormat) + } + } +} + +/** + * Write separate meta and data files to given directory [path] + */ +@DFExperimental +fun IOPlugin.writeEnvelopeDirectory( + path: Path, + envelope: Envelope, + metaFormat: MetaFormatFactory = JsonMetaFormat +) { + if (!Files.exists(path)) { + Files.createDirectories(path) + } + if (!Files.isDirectory(path)) { + error("Can't write envelope directory to file") + } + if (!envelope.meta.isEmpty()) { + writeMetaFile(path, envelope.meta, metaFormat) + } + val dataFile = path.resolve(IOPlugin.DATA_FILE_NAME) + dataFile.write { + envelope.data?.read { + val copied = writeInput(this) + if (envelope.data?.size != Binary.INFINITE && copied != envelope.data?.size) { + error("The number of copied bytes does not equal data size") + } + } + } +} \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt new file mode 100644 index 00000000..ffb924ef --- /dev/null +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/functionsJVM.kt @@ -0,0 +1,28 @@ +package hep.dataforge.io + +import hep.dataforge.io.functions.FunctionServer +import hep.dataforge.io.functions.function +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import kotlin.reflect.KClass +import kotlin.reflect.full.isSuperclassOf + + +fun IOPlugin.resolveIOFormatName(type: KClass<*>): Name { + return ioFormats.entries.find { it.value.type.isSuperclassOf(type) }?.key + ?: error("Can't resolve IOFormat for type $type") +} + +inline fun <reified T : Any, reified R : Any> IOPlugin.generateFunctionMeta(functionName: String): Meta = Meta { + FunctionServer.FUNCTION_NAME_KEY put functionName + FunctionServer.INPUT_FORMAT_KEY put resolveIOFormatName(T::class).toString() + FunctionServer.OUTPUT_FORMAT_KEY put resolveIOFormatName(R::class).toString() +} + +inline fun <reified T : Any, reified R : Any> FunctionServer.function( + functionName: String +): (suspend (T) -> R) { + val plugin = context.plugins.get<IOPlugin>() ?: error("IO plugin not loaded") + val meta = plugin.generateFunctionMeta<T, R>(functionName) + return function(meta) +} \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt deleted file mode 100644 index c926d07a..00000000 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ioFormatsJVM.kt +++ /dev/null @@ -1,51 +0,0 @@ -package hep.dataforge.io - -import hep.dataforge.descriptors.NodeDescriptor -import hep.dataforge.io.functions.FunctionServer -import hep.dataforge.io.functions.FunctionServer.Companion.FUNCTION_NAME_KEY -import hep.dataforge.io.functions.FunctionServer.Companion.INPUT_FORMAT_KEY -import hep.dataforge.io.functions.FunctionServer.Companion.OUTPUT_FORMAT_KEY -import hep.dataforge.io.functions.function -import hep.dataforge.meta.Meta -import hep.dataforge.meta.buildMeta -import hep.dataforge.names.Name -import kotlinx.io.nio.asOutput -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import kotlin.reflect.KClass -import kotlin.reflect.full.isSuperclassOf - -inline fun <reified T : Any> IOPlugin.resolveIOFormat(): IOFormat<T>? { - return ioFormats.values.find { it.type.isSuperclassOf(T::class) } as IOFormat<T>? -} - -fun IOPlugin.resolveIOFormatName(type: KClass<*>): Name { - return ioFormats.entries.find { it.value.type.isSuperclassOf(type) }?.key - ?: error("Can't resolve IOFormat for type $type") -} - -inline fun <reified T : Any, reified R : Any> IOPlugin.generateFunctionMeta(functionName: String): Meta = buildMeta { - FUNCTION_NAME_KEY put functionName - INPUT_FORMAT_KEY put resolveIOFormatName(T::class).toString() - OUTPUT_FORMAT_KEY put resolveIOFormatName(R::class).toString() -} - -inline fun <reified T : Any, reified R : Any> FunctionServer.function( - functionName: String -): (suspend (T) -> R) { - val plugin = context.plugins.get<IOPlugin>() ?: error("IO plugin not loaded") - val meta = plugin.generateFunctionMeta<T, R>(functionName) - return function(meta) -} - -/** - * Write meta to file in a given [format] - */ -fun Meta.write(path: Path, format: MetaFormat, descriptor: NodeDescriptor? = null) { - format.run { - Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW) - .asOutput() - .writeMeta(this@write, descriptor) - } -} \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt index b6b85101..9562d146 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeClient.kt @@ -7,7 +7,6 @@ import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.withContext -import kotlinx.io.streams.writePacket import java.net.Socket import java.util.concurrent.Executors import kotlin.time.ExperimentalTime @@ -39,7 +38,7 @@ class EnvelopeClient( suspend fun close() { try { respond( - Envelope.invoke { + Envelope { type = EnvelopeServer.SHUTDOWN_ENVELOPE_TYPE } ) @@ -52,14 +51,14 @@ class EnvelopeClient( override suspend fun respond(request: Envelope): Envelope = withContext(dispatcher) { //val address = InetSocketAddress(host,port) val socket = Socket(host, port) - val input = socket.getInputStream().asInput() - val output = socket.getOutputStream() + val inputStream = socket.getInputStream() + val outputStream = socket.getOutputStream() format.run { - output.writePacket { + outputStream.write { writeObject(request) } logger.debug { "Sent request with type ${request.type} to ${socket.remoteSocketAddress}" } - val res = input.readObject() + val res = inputStream.readBlocking { readObject() } logger.debug { "Received response with type ${res.type} from ${socket.remoteSocketAddress}" } return@withContext res } diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt index b733aedd..10c5b712 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/EnvelopeServer.kt @@ -9,7 +9,6 @@ import hep.dataforge.io.type import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta import kotlinx.coroutines.* -import kotlinx.io.streams.writePacket import java.net.ServerSocket import java.net.Socket import kotlin.concurrent.thread @@ -71,14 +70,17 @@ class EnvelopeServer( private fun readSocket(socket: Socket) { thread { - val input = socket.getInputStream().asInput() + val inputStream = socket.getInputStream() val outputStream = socket.getOutputStream() format.run { while (socket.isConnected) { - val request = input.readObject() + val request = inputStream.readBlocking { readObject() } logger.debug { "Accepted request with type ${request.type} from ${socket.remoteSocketAddress}" } if (request.type == SHUTDOWN_ENVELOPE_TYPE) { //Echo shutdown command + outputStream.write{ + writeObject(request) + } logger.info { "Accepted graceful shutdown signal from ${socket.inetAddress}" } socket.close() return@thread @@ -86,7 +88,7 @@ class EnvelopeServer( } runBlocking { val response = responder.respond(request) - outputStream.writePacket { + outputStream.write { writeObject(response) } logger.debug { "Sent response with type ${response.type} to ${socket.remoteSocketAddress}" } diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt deleted file mode 100644 index 1c711be0..00000000 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/InputStreamAsInput.kt +++ /dev/null @@ -1,33 +0,0 @@ -package hep.dataforge.io.tcp - -import kotlinx.io.core.AbstractInput -import kotlinx.io.core.Input -import kotlinx.io.core.IoBuffer -import kotlinx.io.core.IoBuffer.Companion.NoPool -import kotlinx.io.core.writePacket -import kotlinx.io.streams.readPacketAtMost -import java.io.InputStream - -/** - * Modified version of InputStream to Input converter that supports waiting for input - */ -internal class InputStreamAsInput( - private val stream: InputStream -) : AbstractInput(pool = NoPool) { - - - override fun fill(): IoBuffer? { - val packet = stream.readPacketAtMost(4096) - return pool.borrow().apply { - resetForWrite(4096) - writePacket(packet) - } - } - - override fun closeSource() { - stream.close() - } -} - -fun InputStream.asInput(): Input = - InputStreamAsInput(this) diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt new file mode 100644 index 00000000..2c240f77 --- /dev/null +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/tcp/streams.kt @@ -0,0 +1,62 @@ +package hep.dataforge.io.tcp + +import kotlinx.io.Input +import kotlinx.io.Output +import kotlinx.io.asBinary +import kotlinx.io.buffer.Buffer +import kotlinx.io.buffer.get +import kotlinx.io.buffer.set +import java.io.InputStream +import java.io.OutputStream + +private class InputStreamInput(val source: InputStream, val waitForInput: Boolean = false) : Input() { + override fun closeSource() { + source.close() + } + + override fun fill(buffer: Buffer): Int { + if (waitForInput) { + while (source.available() == 0) { + //block until input is available + } + } + var bufferPos = 0 + do { + val byte = source.read() + buffer[bufferPos] = byte.toByte() + bufferPos++ + } while (byte > 0 && bufferPos < buffer.size && source.available() > 0) + return bufferPos + } +} + +private class OutputStreamOutput(val out: OutputStream) : Output() { + override fun flush(source: Buffer, length: Int) { + for (i in 0..length) { + out.write(source[i].toInt()) + } + out.flush() + } + + override fun closeSource() { + out.flush() + out.close() + } +} + + +fun <R> InputStream.read(size: Int, block: Input.() -> R): R { + val buffer = ByteArray(size) + read(buffer) + return buffer.asBinary().read(block) +} + +fun <R> InputStream.read(block: Input.() -> R): R = + InputStreamInput(this, false).block() + +fun <R> InputStream.readBlocking(block: Input.() -> R): R = + InputStreamInput(this, true).block() + +fun OutputStream.write(block: Output.() -> Unit) { + OutputStreamOutput(this).block() +} \ No newline at end of file diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt index 94403dcd..685342cf 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileBinaryTest.kt @@ -1,6 +1,9 @@ package hep.dataforge.io import hep.dataforge.context.Global +import kotlinx.io.asBinary +import kotlinx.io.toByteArray +import kotlinx.io.writeDouble import java.nio.file.Files import kotlin.test.Test import kotlin.test.assertEquals @@ -21,11 +24,11 @@ class FileBinaryTest { @Test fun testSize() { val binary = envelope.data - assertEquals(binary?.size?.toInt(), binary?.toBytes()?.size) + assertEquals(binary?.size?.toInt(), binary?.toByteArray()?.size) } @Test - fun testFileData(){ + fun testFileData() { val dataFile = Files.createTempFile("dataforge_test_bin", ".bin") dataFile.toFile().writeText("This is my binary") val envelopeFromFile = Envelope { @@ -34,12 +37,12 @@ class FileBinaryTest { "b" put 22.2 } dataType = "hep.dataforge.satellite" - dataID = "cellDepositTest" // добавил только что + dataID = "cellDepositTest" data = dataFile.asBinary() } val binary = envelopeFromFile.data!! - println(binary.toBytes().size) - assertEquals(binary.size.toInt(), binary.toBytes().size) + println(binary.toByteArray().size) + assertEquals(binary.size.toInt(), binary.toByteArray().size) } @@ -49,8 +52,7 @@ class FileBinaryTest { val tmpPath = Files.createTempFile("dataforge_test", ".df") Global.io.writeEnvelopeFile(tmpPath, envelope) - val binary = Global.io.readEnvelopeFile(tmpPath).data!! - assertEquals(binary.size.toInt(), binary.toBytes().size) + val binary = Global.io.readEnvelopeFile(tmpPath)?.data!! + assertEquals(binary.size.toInt(), binary.toByteArray().size) } - } \ No newline at end of file diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt index ba7f7cc5..edee906b 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/FileEnvelopeTest.kt @@ -1,6 +1,7 @@ package hep.dataforge.io import hep.dataforge.context.Global +import kotlinx.io.writeDouble import java.nio.file.Files import kotlin.test.Test import kotlin.test.assertTrue @@ -22,10 +23,23 @@ class FileEnvelopeTest { @Test fun testFileWriteRead() { - val tmpPath = Files.createTempFile("dataforge_test", ".df") - Global.io.writeEnvelopeFile(tmpPath,envelope) - println(tmpPath.toUri()) - val restored: Envelope = Global.io.readEnvelopeFile(tmpPath) - assertTrue { envelope.contentEquals(restored) } + Global.io.run { + val tmpPath = Files.createTempFile("dataforge_test", ".df") + writeEnvelopeFile(tmpPath, envelope) + println(tmpPath.toUri()) + val restored: Envelope = readEnvelopeFile(tmpPath)!! + assertTrue { envelope.contentEquals(restored) } + } + } + + @Test + fun testFileWriteReadTagless() { + Global.io.run { + val tmpPath = Files.createTempFile("dataforge_test_tagless", ".df") + writeEnvelopeFile(tmpPath, envelope, envelopeFormat = TaglessEnvelopeFormat) + println(tmpPath.toUri()) + val restored: Envelope = readEnvelopeFile(tmpPath)!! + assertTrue { envelope.contentEquals(restored) } + } } } \ No newline at end of file diff --git a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt index 37c35efc..de1d35ff 100644 --- a/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt +++ b/dataforge-io/src/jvmTest/kotlin/hep/dataforge/io/tcp/EnvelopeServerTest.kt @@ -4,19 +4,20 @@ import hep.dataforge.context.Global import hep.dataforge.io.Envelope import hep.dataforge.io.Responder import hep.dataforge.io.TaggedEnvelopeFormat -import hep.dataforge.io.writeBytes +import hep.dataforge.io.writeByteArray import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.runBlocking +import kotlinx.io.writeDouble import org.junit.AfterClass import org.junit.BeforeClass -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.time.ExperimentalTime @ExperimentalStdlibApi object EchoResponder : Responder { override suspend fun respond(request: Envelope): Envelope { - val string = TaggedEnvelopeFormat().run { writeBytes(request).decodeToString() } + val string = TaggedEnvelopeFormat().run { writeByteArray(request).decodeToString() } println("ECHO:") println(string) return request @@ -43,7 +44,7 @@ class EnvelopeServerTest { } } - @Test + @Test(timeout = 1000) fun doEchoTest() { val request = Envelope.invoke { type = "test.echo" diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts index 6f2a5160..fea7ecd7 100644 --- a/dataforge-meta/build.gradle.kts +++ b/dataforge-meta/build.gradle.kts @@ -1,5 +1,9 @@ +import scientifik.serialization + plugins { id("scientifik.mpp") } +serialization() + description = "Meta definition and basic operations on meta" \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt deleted file mode 100644 index f355c828..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt +++ /dev/null @@ -1,29 +0,0 @@ -package hep.dataforge.descriptors - -import hep.dataforge.descriptors.Described.Companion.DESCRIPTOR_NODE -import hep.dataforge.meta.Meta -import hep.dataforge.meta.get -import hep.dataforge.meta.node - -/** - * An object which provides its descriptor - */ -interface Described { - val descriptor: NodeDescriptor - - companion object { - const val DESCRIPTOR_NODE = "@descriptor" - } -} - -/** - * If meta node supplies explicit descriptor, return it, otherwise try to use descriptor node from meta itself - */ -val Meta.descriptor: NodeDescriptor? - get() { - return if (this is Described) { - descriptor - } else { - get(DESCRIPTOR_NODE).node?.let { NodeDescriptor.wrap(it) } - } - } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt deleted file mode 100644 index d65a0e79..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright 2018 Alexander Nozik. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package hep.dataforge.descriptors - -import hep.dataforge.values.ValueType -import kotlin.reflect.KClass - -@Target(AnnotationTarget.PROPERTY) -@MustBeDocumented -annotation class ValueDef( - val key: String, - val type: Array<ValueType> = arrayOf(ValueType.STRING), - val multiple: Boolean = false, - val def: String = "", - val info: String = "", - val required: Boolean = true, - val allowed: Array<String> = emptyArray(), - val enumeration: KClass<*> = Any::class, - val tags: Array<String> = emptyArray() -) - -@MustBeDocumented -annotation class NodeDef( - val key: String, - val info: String = "", - val multiple: Boolean = false, - val required: Boolean = false, - val tags: Array<String> = emptyArray(), - /** - * A list of child value descriptors - */ - val values: Array<ValueDef> = emptyArray(), - /** - * A target class for this node to describe - * @return - */ - val type: KClass<*> = Any::class, - /** - * The DataForge path to the resource containing the description. Following targets are supported: - * - * 1. resource - * 1. file - * 1. class - * 1. method - * 1. property - * - * - * Does not work if [type] is provided - * - * @return - */ - val descriptor: String = "" -) - -/** - * Description text for meta property, node or whole object - */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Description(val value: String) - -/** - * Annotation for value property which states that lists are expected - */ -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Multiple - -/** - * Descriptor target - * The DataForge path to the resource containing the description. Following targets are supported: - * 1. resource - * 1. file - * 1. class - * 1. method - * 1. property - * - * - * Does not work if [type] is provided - */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class Descriptor(val value: String) - - -/** - * Aggregator class for descriptor nodes - */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class DescriptorNodes(vararg val nodes: NodeDef) - -/** - * Aggregator class for descriptor values - */ -@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class DescriptorValues(vararg val nodes: ValueDef) - -/** - * Alternative name for property descriptor declaration - */ -@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class DescriptorName(val name: String) - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class DescriptorValue(val def: ValueDef) -//TODO enter fields directly? - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class ValueProperty( - val name: String = "", - val type: Array<ValueType> = arrayOf(ValueType.STRING), - val multiple: Boolean = false, - val def: String = "", - val enumeration: KClass<*> = Any::class, - val tags: Array<String> = emptyArray() -) - - -@Target(AnnotationTarget.PROPERTY) -@Retention(AnnotationRetention.RUNTIME) -@MustBeDocumented -annotation class NodeProperty(val name: String = "") diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt index f47d3bcd..53a2e184 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt @@ -4,6 +4,7 @@ import hep.dataforge.names.Name import hep.dataforge.names.NameToken import hep.dataforge.names.asName import hep.dataforge.names.plus +import kotlinx.serialization.* //TODO add validator to configuration @@ -12,10 +13,16 @@ data class MetaListener( val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit ) +interface ObservableMeta : Meta { + fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) + fun removeListener(owner: Any?) +} + /** * Mutable meta representing object state */ -class Config : AbstractMutableMeta<Config>() { +@Serializable +class Config : AbstractMutableMeta<Config>(), ObservableMeta { private val listeners = HashSet<MetaListener>() @@ -26,21 +33,21 @@ class Config : AbstractMutableMeta<Config>() { /** * 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?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) { + override fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) { listeners.add(MetaListener(owner, action)) } /** * Remove all listeners belonging to given owner */ - fun removeListener(owner: Any?) { + override fun removeListener(owner: Any?) { listeners.removeAll { it.owner === owner } } override fun replaceItem(key: NameToken, oldItem: MetaItem<Config>?, newItem: MetaItem<Config>?) { if (newItem == null) { _items.remove(key) - if(oldItem!= null && oldItem is MetaItem.NodeItem<Config>) { + if (oldItem != null && oldItem is MetaItem.NodeItem<Config>) { oldItem.node.removeListener(this) } } else { @@ -57,33 +64,34 @@ class Config : AbstractMutableMeta<Config>() { /** * Attach configuration node instead of creating one */ - override fun wrapNode(meta: Meta): Config = meta.toConfig() + override fun wrapNode(meta: Meta): Config = meta.asConfig() override fun empty(): Config = Config() - companion object { + @Serializer(Config::class) + companion object ConfigSerializer : KSerializer<Config> { + fun empty(): Config = Config() + override val descriptor: SerialDescriptor get() = MetaSerializer.descriptor + + override fun deserialize(decoder: Decoder): Config { + return MetaSerializer.deserialize(decoder).asConfig() + } + + override fun serialize(encoder: Encoder, value: Config) { + MetaSerializer.serialize(encoder, value) + } } } operator fun Config.get(token: NameToken): MetaItem<Config>? = items[token] -fun Meta.toConfig(): Config = this as? Config ?: Config().also { builder -> +fun Meta.asConfig(): Config = this as? Config ?: Config().also { builder -> this.items.mapValues { entry -> val item = entry.value builder[entry.key.asName()] = when (item) { is MetaItem.ValueItem -> item.value - is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.toConfig()) + is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.asConfig()) } } -} - -interface Configurable { - val config: Config -} - -fun <T : Configurable> T.configure(meta: Meta): T = this.apply { config.update(meta) } - -fun <T : Configurable> T.configure(action: MetaBuilder.() -> Unit): T = configure(buildMeta(action)) - -open class SimpleConfigurable(override val config: Config) : Configurable \ No newline at end of file +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt new file mode 100644 index 00000000..ee906008 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/JsonMeta.kt @@ -0,0 +1,132 @@ +package hep.dataforge.meta + +import hep.dataforge.meta.descriptors.ItemDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.ValueDescriptor +import hep.dataforge.names.NameToken +import hep.dataforge.names.toName +import hep.dataforge.values.* +import kotlinx.serialization.json.* + + +/** + * @param descriptor reserved for custom serialization in future + */ +fun Value.toJson(descriptor: ValueDescriptor? = null): JsonElement { + return if (isList()) { + JsonArray(list.map { it.toJson() }) + } else { + when (type) { + ValueType.NUMBER -> JsonPrimitive(number) + ValueType.STRING -> JsonPrimitive(string) + ValueType.BOOLEAN -> JsonPrimitive(boolean) + ValueType.NULL -> JsonNull + } + } +} + +//Use these methods to customize JSON key mapping +private fun NameToken.toJsonKey(descriptor: ItemDescriptor?) = toString() + +//private fun NodeDescriptor?.getDescriptor(key: String) = this?.items?.get(key) + +fun Meta.toJson(descriptor: NodeDescriptor? = null): JsonObject { + + //TODO search for same name siblings and arrange them into arrays + val map = this.items.entries.associate { (name, item) -> + val itemDescriptor = descriptor?.items?.get(name.body) + val key = name.toJsonKey(itemDescriptor) + val value = when (item) { + is MetaItem.ValueItem -> { + item.value.toJson(itemDescriptor as? ValueDescriptor) + } + is MetaItem.NodeItem -> { + item.node.toJson(itemDescriptor as? NodeDescriptor) + } + } + key to value + } + return JsonObject(map) +} + +fun JsonObject.toMeta(descriptor: NodeDescriptor? = null): Meta = JsonMeta(this, descriptor) + +fun JsonPrimitive.toValue(descriptor: ValueDescriptor?): Value { + return when (this) { + JsonNull -> Null + else -> this.content.parseValue() // Optimize number and boolean parsing + } +} + +fun JsonElement.toMetaItem(descriptor: ItemDescriptor? = null): MetaItem<JsonMeta> = when (this) { + is JsonPrimitive -> { + val value = this.toValue(descriptor as? ValueDescriptor) + MetaItem.ValueItem(value) + } + is JsonObject -> { + val meta = JsonMeta(this, descriptor as? NodeDescriptor) + MetaItem.NodeItem(meta) + } + is JsonArray -> { + if (this.all { it is JsonPrimitive }) { + val value = if (isEmpty()) { + Null + } else { + ListValue( + map<JsonElement, Value> { + //We already checked that all values are primitives + (it as JsonPrimitive).toValue(descriptor as? ValueDescriptor) + } + ) + } + MetaItem.ValueItem(value) + } else { + json { + "@value" to this@toMetaItem + }.toMetaItem(descriptor) + } + } +} + +class JsonMeta(val json: JsonObject, val descriptor: NodeDescriptor? = null) : MetaBase() { + + @Suppress("UNCHECKED_CAST") + private operator fun MutableMap<String, MetaItem<JsonMeta>>.set(key: String, value: JsonElement): Unit { + val itemDescriptor = descriptor?.items?.get(key) + return when (value) { + is JsonPrimitive -> { + this[key] = MetaItem.ValueItem(value.toValue(itemDescriptor as? ValueDescriptor)) as MetaItem<JsonMeta> + } + is JsonObject -> { + this[key] = MetaItem.NodeItem( + JsonMeta( + value, + itemDescriptor as? NodeDescriptor + ) + ) + } + is JsonArray -> { + when { + value.all { it is JsonPrimitive } -> { + val listValue = ListValue( + value.map { + //We already checked that all values are primitives + (it as JsonPrimitive).toValue(itemDescriptor as? ValueDescriptor) + } + ) + this[key] = MetaItem.ValueItem(listValue) as MetaItem<JsonMeta> + } + else -> value.forEachIndexed { index, jsonElement -> + this["$key[$index]"] = jsonElement.toMetaItem(itemDescriptor) + } + } + } + } + } + + override val items: Map<NameToken, MetaItem<JsonMeta>> by lazy { + val map = HashMap<String, MetaItem<JsonMeta>>() + json.forEach { (key, value) -> map[key] = value } + map.mapKeys { it.key.toName().first()!! } + } +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt index b403544c..7dec0790 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt @@ -1,9 +1,11 @@ package hep.dataforge.meta +import hep.dataforge.names.Name import hep.dataforge.names.NameToken /** - * A meta laminate consisting of multiple immutable meta layers. For mutable front layer, use [Styled]. + * A meta laminate consisting of multiple immutable meta layers. For mutable front layer, use [Scheme]. + * If [layers] list contains a [Laminate] it is flat-mapped. */ class Laminate(layers: List<Meta>) : MetaBase() { @@ -17,10 +19,11 @@ class Laminate(layers: List<Meta>) : MetaBase() { constructor(vararg layers: Meta?) : this(layers.filterNotNull()) - override val items: Map<NameToken, MetaItem<Meta>> - get() = layers.map { it.items.keys }.flatten().associateWith { key -> + override val items: Map<NameToken, MetaItem<Meta>> by lazy { + layers.map { it.items.keys }.flatten().associateWith { key -> layers.asSequence().map { it.items[key] }.filterNotNull().let(replaceRule) } + } /** * Generate sealed meta using [mergeRule] @@ -61,7 +64,7 @@ class Laminate(layers: List<Meta>) : MetaBase() { } else -> map { when (it) { - is MetaItem.ValueItem -> MetaItem.NodeItem(buildMeta { Meta.VALUE_KEY put it.value }) + is MetaItem.ValueItem -> MetaItem.NodeItem(Meta { Meta.VALUE_KEY put it.value }) is MetaItem.NodeItem -> it } }.merge() @@ -77,6 +80,16 @@ class Laminate(layers: List<Meta>) : MetaBase() { } } +/** + * Performance optimized version of get method + */ +fun Laminate.getFirst(name: Name): MetaItem<*>? { + layers.forEach { layer -> + layer[name]?.let { return it } + } + return null +} + /** * Create a new [Laminate] adding given layer to the top */ 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 e5aa0c45..672a1922 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt @@ -4,9 +4,8 @@ import hep.dataforge.meta.Meta.Companion.VALUE_KEY import hep.dataforge.meta.MetaItem.NodeItem import hep.dataforge.meta.MetaItem.ValueItem import hep.dataforge.names.* -import hep.dataforge.values.EnumValue -import hep.dataforge.values.Value -import hep.dataforge.values.boolean +import hep.dataforge.values.* +import kotlinx.serialization.* /** @@ -14,13 +13,51 @@ import hep.dataforge.values.boolean * * a [ValueItem] (leaf) * * a [NodeItem] (node) */ +@Serializable sealed class MetaItem<out M : Meta> { + + @Serializable data class ValueItem(val value: Value) : MetaItem<Nothing>() { override fun toString(): String = value.toString() + + @Serializer(ValueItem::class) + companion object : KSerializer<ValueItem> { + override val descriptor: SerialDescriptor get() = ValueSerializer.descriptor + + override fun deserialize(decoder: Decoder): ValueItem = ValueItem(ValueSerializer.deserialize(decoder)) + + override fun serialize(encoder: Encoder, value: ValueItem) { + ValueSerializer.serialize(encoder, value.value) + } + } } - data class NodeItem<M : Meta>(val node: M) : MetaItem<M>() { + @Serializable + data class NodeItem<M : Meta>(@Serializable(MetaSerializer::class) val node: M) : MetaItem<M>() { + //Fixing serializer for node could cause class cast problems, but it should not since Meta descendants are not serializeable override fun toString(): String = node.toString() + + @Serializer(NodeItem::class) + companion object : KSerializer<NodeItem<out Meta>> { + override val descriptor: SerialDescriptor get() = MetaSerializer.descriptor + + override fun deserialize(decoder: Decoder): NodeItem<*> = NodeItem(MetaSerializer.deserialize(decoder)) + + override fun serialize(encoder: Encoder, value: NodeItem<*>) { + MetaSerializer.serialize(encoder, value.node) + } + } + } + + companion object { + fun of(arg: Any?): MetaItem<*> { + return when (arg) { + null -> ValueItem(Null) + is MetaItem<*> -> arg + is Meta -> NodeItem(arg) + else -> ValueItem(Value.of(arg)) + } + } } } @@ -45,7 +82,7 @@ interface Meta : MetaRepr { */ val items: Map<NameToken, MetaItem<*>> - override fun toMeta(): Meta = this + override fun toMeta(): Meta = seal() override fun equals(other: Any?): Boolean @@ -55,19 +92,26 @@ interface Meta : MetaRepr { companion object { const val TYPE = "meta" + /** * A key for single value node */ const val VALUE_KEY = "@value" - val empty: EmptyMeta = EmptyMeta + val EMPTY: EmptyMeta = EmptyMeta } } /* Get operations*/ +/** + * Perform recursive item search using given [name]. Each [NameToken] is treated as a name in [Meta.items] of a parent node. + * + * If [name] is empty return current [Meta] as a [NodeItem] + */ operator fun Meta?.get(name: Name): MetaItem<*>? { if (this == null) return null + if (name.isEmpty()) return NodeItem(this) return name.first()?.let { token -> val tail = name.cutFirst() when (tail.length) { @@ -78,6 +122,10 @@ operator fun Meta?.get(name: Name): MetaItem<*>? { } operator fun Meta?.get(token: NameToken): MetaItem<*>? = this?.items?.get(token) + +/** + * Parse [Name] from [key] using full name notation and pass it to [Meta.get] + */ operator fun Meta?.get(key: String): MetaItem<*>? = get(key.toName()) /** @@ -113,32 +161,19 @@ operator fun Meta.iterator(): Iterator<Pair<Name, MetaItem<*>>> = sequence().ite /** * A meta node that ensures that all of its descendants has at least the same type */ -interface MetaNode<M : MetaNode<M>> : Meta { +interface MetaNode<out M : MetaNode<M>> : Meta { override val items: Map<NameToken, MetaItem<M>> } -operator fun <M : MetaNode<M>> MetaNode<M>?.get(name: Name): MetaItem<M>? { - if (this == null) return null - return name.first()?.let { token -> - val tail = name.cutFirst() - when (tail.length) { - 0 -> items[token] - else -> items[token]?.node?.get(tail) - } - } -} +/** + * The same as [Meta.get], but with specific node type + */ +@Suppress("UNCHECKED_CAST") +operator fun <M : MetaNode<M>> M?.get(name: Name): MetaItem<M>? = (this as Meta)[name] as MetaItem<M>? -operator fun <M : MetaNode<M>> MetaNode<M>?.get(key: String): MetaItem<M>? = if (this == null) { - null -} else { - this[key.toName()] -} +operator fun <M : MetaNode<M>> M?.get(key: String): MetaItem<M>? = this[key.toName()] -operator fun <M : MetaNode<M>> MetaNode<M>?.get(key: NameToken): MetaItem<M>? = if (this == null) { - null -} else { - this[key.asName()] -} +operator fun <M : MetaNode<M>> M?.get(key: NameToken): MetaItem<M>? = this[key.asName()] /** * Equals, hashcode and to string for any meta @@ -153,7 +188,7 @@ abstract class MetaBase : Meta { override fun hashCode(): Int = items.hashCode() - override fun toString(): String = items.toString() + override fun toString(): String = toJson().toString() } /** @@ -166,8 +201,9 @@ abstract class AbstractMetaNode<M : MetaNode<M>> : MetaNode<M>, MetaBase() * * If the argument is possibly mutable node, it is copied on creation */ -class SealedMeta internal constructor(override val items: Map<NameToken, MetaItem<SealedMeta>>) : - AbstractMetaNode<SealedMeta>() +class SealedMeta internal constructor( + override val items: Map<NameToken, MetaItem<SealedMeta>> +) : AbstractMetaNode<SealedMeta>() /** * Generate sealed node from [this]. If it is already sealed return it as is @@ -200,7 +236,7 @@ val MetaItem<*>?.int get() = number?.toInt() val MetaItem<*>?.long get() = number?.toLong() val MetaItem<*>?.short get() = number?.toShort() -inline fun <reified E : Enum<E>> MetaItem<*>?.enum() = if (this is ValueItem && this.value is EnumValue<*>) { +inline fun <reified E : Enum<E>> MetaItem<*>?.enum(): E? = if (this is ValueItem && this.value is EnumValue<*>) { this.value.value as E } else { string?.let { enumValueOf<E>(it) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt index c5ad3831..da1bafad 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt @@ -15,6 +15,10 @@ class MetaBuilder : AbstractMutableMeta<MetaBuilder>() { override fun wrapNode(meta: Meta): MetaBuilder = if (meta is MetaBuilder) meta else meta.builder() override fun empty(): MetaBuilder = MetaBuilder() + infix fun String.put(item: MetaItem<*>?) { + set(this, item) + } + infix fun String.put(value: Value?) { set(this, value) } @@ -141,9 +145,11 @@ fun Meta.edit(builder: MetaBuilder.() -> Unit): MetaBuilder = builder().apply(bu /** * Build a [MetaBuilder] using given transformation */ +@Deprecated("To be replaced with fake constructor", ReplaceWith("Meta")) fun buildMeta(builder: MetaBuilder.() -> Unit): MetaBuilder = MetaBuilder().apply(builder) /** - * Build meta using given source meta as a base + * Build a [MetaBuilder] using given transformation */ -fun buildMeta(source: Meta, builder: MetaBuilder.() -> Unit): MetaBuilder = source.builder().apply(builder) \ No newline at end of file +@Suppress("FunctionName") +fun Meta(builder: MetaBuilder.() -> Unit): MetaBuilder = MetaBuilder().apply(builder) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt new file mode 100644 index 00000000..50764a7c --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaDelegate.kt @@ -0,0 +1,98 @@ +package hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.values.Value +import kotlin.jvm.JvmName +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + +/* Meta delegates */ + +open class MetaDelegate( + open val owner: Meta, + val key: Name? = null, + open val default: MetaItem<*>? = null +) : ReadOnlyProperty<Any?, MetaItem<*>?> { + override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? { + return owner[key ?: property.name.asName()] ?: default + } +} + +class LazyMetaDelegate( + owner: Meta, + key: Name? = null, + defaultProvider: () -> MetaItem<*>? = { null } +) : MetaDelegate(owner, key) { + override val default by lazy(defaultProvider) +} + +class DelegateWrapper<T, R>( + val delegate: ReadOnlyProperty<Any?, T>, + val reader: (T) -> R +) : ReadOnlyProperty<Any?, R> { + override fun getValue(thisRef: Any?, property: KProperty<*>): R { + return reader(delegate.getValue(thisRef, property)) + } +} + +fun <T, R> ReadOnlyProperty<Any?, T>.map(reader: (T) -> R): DelegateWrapper<T, R> = + DelegateWrapper(this, reader) + + +fun Meta.item(default: Any? = null, key: Name? = null): MetaDelegate = + MetaDelegate(this, key, default?.let { MetaItem.of(it) }) + +fun Meta.lazyItem(key: Name? = null, defaultProvider: () -> Any?): LazyMetaDelegate = + LazyMetaDelegate(this, key) { defaultProvider()?.let { MetaItem.of(it) } } + +//TODO add caching for sealed nodes + + +//Read-only delegates for Metas + +/** + * A property delegate that uses custom key + */ +fun Meta.value(default: Value? = null, key: Name? = null) = + item(default, key).map { it.value } + +fun Meta.string(default: String? = null, key: Name? = null) = + item(default, key).map { it.string } + +fun Meta.boolean(default: Boolean? = null, key: Name? = null) = + item(default, key).map { it.boolean } + +fun Meta.number(default: Number? = null, key: Name? = null) = + item(default, key).map { it.number } + +fun Meta.node(key: Name? = null) = + item(key).map { it.node } + +@JvmName("safeString") +fun Meta.string(default: String, key: Name? = null) = + item(default, key).map { it.string!! } + +@JvmName("safeBoolean") +fun Meta.boolean(default: Boolean, key: Name? = null) = + item(default, key).map { it.boolean!! } + +@JvmName("safeNumber") +fun Meta.number(default: Number, key: Name? = null) = + item(default, key).map { it.number!! } + +@JvmName("lazyString") +fun Meta.string(key: Name? = null, default: () -> String) = + lazyItem(key, default).map { it.string!! } + +@JvmName("lazyBoolean") +fun Meta.boolean(key: Name? = null, default: () -> Boolean) = + lazyItem(key, default).map { it.boolean!! } + +@JvmName("lazyNumber") +fun Meta.number(key: Name? = null, default: () -> Number) = + lazyItem(key, default).map { it.number!! } + + +inline fun <reified E : Enum<E>> Meta.enum(default: E, key: Name? = null) = + item(default, key).map { it.enum<E>()!! } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt new file mode 100644 index 00000000..6f0db59e --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaSerializer.kt @@ -0,0 +1,40 @@ +package hep.dataforge.meta + +import hep.dataforge.names.NameToken +import kotlinx.serialization.* +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.json.JsonInput +import kotlinx.serialization.json.JsonObjectSerializer +import kotlinx.serialization.json.JsonOutput + + +/** + * Serialized for meta + */ +@Serializer(Meta::class) +object MetaSerializer : KSerializer<Meta> { + private val mapSerializer = MapSerializer( + NameToken.serializer(), + MetaItem.serializer(MetaSerializer) + ) + + override val descriptor: SerialDescriptor get() = mapSerializer.descriptor + + override fun deserialize(decoder: Decoder): Meta { + return if (decoder is JsonInput) { + JsonObjectSerializer.deserialize(decoder).toMeta() + } else { + object : MetaBase() { + override val items: Map<NameToken, MetaItem<*>> = mapSerializer.deserialize(decoder) + } + } + } + + override fun serialize(encoder: Encoder, value: Meta) { + if (encoder is JsonOutput) { + JsonObjectSerializer.serialize(encoder, value.toJson()) + } else { + mapSerializer.serialize(encoder, value.items) + } + } +} \ No newline at end of file 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 4da08ce4..043283e3 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt @@ -1,9 +1,10 @@ package hep.dataforge.meta +import hep.dataforge.meta.scheme.Configurable import hep.dataforge.names.* import hep.dataforge.values.Value -interface MutableMeta<M : MutableMeta<M>> : MetaNode<M> { +interface MutableMeta<out M : MutableMeta<M>> : MetaNode<M> { override val items: Map<NameToken, MetaItem<M>> operator fun set(name: Name, item: MetaItem<*>?) // fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) @@ -54,13 +55,14 @@ abstract class AbstractMutableMeta<M : MutableMeta<M>> : AbstractMetaNode<M>(), 0 -> error("Can't setValue meta item for empty name") 1 -> { val token = name.first()!! - replaceItem(token, get(name), wrapItem(item)) + @Suppress("UNCHECKED_CAST") val oldItem: MetaItem<M>? = get(name) as? MetaItem<M> + replaceItem(token, oldItem, wrapItem(item)) } else -> { val token = name.first()!! //get existing or create new node. Query is ignored for new node - if(items[token] == null){ - replaceItem(token,null, MetaItem.NodeItem(empty())) + if (items[token] == null) { + replaceItem(token, null, MetaItem.NodeItem(empty())) } items[token]?.node!![name.cutFirst()] = item } @@ -71,6 +73,7 @@ abstract class AbstractMutableMeta<M : MutableMeta<M>> : AbstractMetaNode<M>(), @Suppress("NOTHING_TO_INLINE") inline fun MutableMeta<*>.remove(name: Name) = set(name, null) + @Suppress("NOTHING_TO_INLINE") inline fun MutableMeta<*>.remove(name: String) = remove(name.toName()) @@ -103,7 +106,7 @@ operator fun MutableMeta<*>.set(name: Name, value: Any?) { null -> remove(name) is MetaItem<*> -> setItem(name, value) is Meta -> setNode(name, value) - is Specific -> setNode(name, value.config) + is Configurable -> setNode(name, value.config) else -> setValue(name, Value.of(value)) } } @@ -112,6 +115,8 @@ operator fun MutableMeta<*>.set(name: NameToken, value: Any?) = set(name.asName( operator fun MutableMeta<*>.set(key: String, value: Any?) = set(key.toName(), value) +operator fun MutableMeta<*>.set(key: String, index: String, value: Any?) = set(key.toName().withIndex(index), value) + /** * Update existing mutable node with another node. The rules are following: * * value replaces anything @@ -158,7 +163,7 @@ operator fun MutableMeta<*>.set(name: String, metas: Iterable<Meta>): Unit = set /** * Append the node with a same-name-sibling, automatically generating numerical index */ -fun MutableMeta<*>.append(name: Name, value: Any?) { +fun <M : MutableMeta<M>> M.append(name: Name, value: Any?) { require(!name.isEmpty()) { "Name could not be empty for append operation" } val newIndex = name.last()!!.index if (newIndex.isNotEmpty()) { @@ -169,4 +174,4 @@ fun MutableMeta<*>.append(name: Name, value: Any?) { } } -fun MutableMeta<*>.append(name: String, value: Any?) = append(name.toName(), value) \ No newline at end of file +fun <M : MutableMeta<M>> M.append(name: String, value: Any?) = append(name.toName(), value) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt new file mode 100644 index 00000000..3fcdc9da --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMetaDelegate.kt @@ -0,0 +1,118 @@ +package hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.values.Value +import kotlin.jvm.JvmName +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/* Read-write delegates */ + +open class MutableMetaDelegate<M : MutableMeta<M>>( + override val owner: M, + key: Name? = null, + default: MetaItem<*>? = null +) : MetaDelegate(owner, key, default), ReadWriteProperty<Any?, MetaItem<*>?> { + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) { + val name = key ?: property.name.asName() + owner.setItem(name, value) + } +} + +class LazyMutableMetaDelegate<M : MutableMeta<M>>( + owner: M, + key: Name? = null, + defaultProvider: () -> MetaItem<*>? = { null } +) : MutableMetaDelegate<M>(owner, key) { + override val default by lazy(defaultProvider) +} + +class ReadWriteDelegateWrapper<T, R>( + val delegate: ReadWriteProperty<Any?, T>, + val reader: (T) -> R, + val writer: (R) -> T +) : ReadWriteProperty<Any?, R> { + override fun getValue(thisRef: Any?, property: KProperty<*>): R { + return reader(delegate.getValue(thisRef, property)) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: R) { + delegate.setValue(thisRef, property, writer(value)) + } +} + +fun <T, R> ReadWriteProperty<Any?, T>.map(reader: (T) -> R, writer: (R) -> T): ReadWriteDelegateWrapper<T, R> = + ReadWriteDelegateWrapper(this, reader, writer) + +fun <R> ReadWriteProperty<Any?, MetaItem<*>?>.transform(reader: (MetaItem<*>?) -> R): ReadWriteProperty<Any?, R> = + map(reader = reader, writer = { MetaItem.of(it) }) + +fun <R> ReadWriteProperty<Any?, Value?>.transform(reader: (Value?) -> R): ReadWriteDelegateWrapper<Value?, R> = + map(reader = reader, writer = { Value.of(it) }) + +/** + * A delegate that throws + */ +fun <R : Any> ReadWriteProperty<Any?, R?>.notNull(default: () -> R): ReadWriteProperty<Any?, R> { + return ReadWriteDelegateWrapper(this, + reader = { it ?: default() }, + writer = { it } + ) +} + + +fun <M : MutableMeta<M>> M.item(default: Any? = null, key: Name? = null): MutableMetaDelegate<M> = + MutableMetaDelegate(this, key, default?.let { MetaItem.of(it) }) + +fun <M : MutableMeta<M>> M.lazyItem(key: Name? = null, defaultProvider: () -> Any?): LazyMutableMetaDelegate<M> = + LazyMutableMetaDelegate(this, key) { defaultProvider()?.let { MetaItem.of(it) } } + +//Read-write delegates + +/** + * A property delegate that uses custom key + */ +fun <M : MutableMeta<M>> M.value(default: Value? = null, key: Name? = null): ReadWriteProperty<Any?, Value?> = + item(default, key).transform { it.value } + +fun <M : MutableMeta<M>> M.string(default: String? = null, key: Name? = null): ReadWriteProperty<Any?, String?> = + item(default, key).transform { it.string } + +fun <M : MutableMeta<M>> M.boolean(default: Boolean? = null, key: Name? = null): ReadWriteProperty<Any?, Boolean?> = + item(default, key).transform { it.boolean } + +fun <M : MutableMeta<M>> M.number(default: Number? = null, key: Name? = null): ReadWriteProperty<Any?, Number?> = + item(default, key).transform { it.number } + +inline fun <reified M : MutableMeta<M>> M.node(key: Name? = null) = + item(this, key).transform { it.node as? M } + +@JvmName("safeString") +fun <M : MutableMeta<M>> M.string(default: String, key: Name? = null) = + item(default, key).transform { it.string!! } + +@JvmName("safeBoolean") +fun <M : MutableMeta<M>> M.boolean(default: Boolean, key: Name? = null) = + item(default, key).transform { it.boolean!! } + +@JvmName("safeNumber") +fun <M : MutableMeta<M>> M.number(default: Number, key: Name? = null) = + item(default, key).transform { it.number!! } + +@JvmName("lazyString") +fun <M : MutableMeta<M>> M.string(key: Name? = null, default: () -> String) = + lazyItem(key, default).transform { it.string!! } + +@JvmName("safeBoolean") +fun <M : MutableMeta<M>> M.boolean(key: Name? = null, default: () -> Boolean) = + lazyItem(key, default).transform { it.boolean!! } + +@JvmName("safeNumber") +fun <M : MutableMeta<M>> M.number(key: Name? = null, default: () -> Number) = + lazyItem(key, default).transform { it.number!! } + + +inline fun <M : MutableMeta<M>, reified E : Enum<E>> M.enum(default: E, key: Name? = null) = + item(default, key).transform { it.enum<E>()!! } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt deleted file mode 100644 index d3ec418a..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt +++ /dev/null @@ -1,73 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import kotlin.jvm.JvmName - -/** - * Marker interface for classes with specifications - */ -interface Specific : Configurable - -//TODO separate mutable config from immutable meta to allow free wrapping of meta - -operator fun Specific.get(name: String): MetaItem<*>? = config[name] - -/** - * Allows to apply custom configuration in a type safe way to simple untyped configuration. - * By convention [Specific] companion should inherit this class - * - */ -interface Specification<T : Specific> { - /** - * Update given configuration using given type as a builder - */ - fun update(config: Config, action: T.() -> Unit): T { - return wrap(config).apply(action) - } - - fun build(action: T.() -> Unit) = update(Config(), action) - - fun empty() = build { } - - /** - * Wrap generic configuration producing instance of desired type - */ - fun wrap(config: Config): T - - //TODO replace by free wrapper - fun wrap(meta: Meta): T = wrap(meta.toConfig()) -} - -fun <T : Specific> specification(wrapper: (Config) -> T): Specification<T> = - object : Specification<T> { - override fun wrap(config: Config): T = wrapper(config) - } - -/** - * Apply specified configuration to configurable - */ -fun <T : Configurable, C : Specific, S : Specification<C>> T.configure(spec: S, action: C.() -> Unit) = - apply { spec.update(config, action) } - -/** - * Update configuration using given specification - */ -fun <C : Specific, S : Specification<C>> Specific.update(spec: S, action: C.() -> Unit) = - apply { spec.update(config, action) } - -/** - * Create a style based on given specification - */ -fun <C : Specific, S : Specification<C>> S.createStyle(action: C.() -> Unit): Meta = - Config().also { update(it, action) } - - -fun <C : Specific> Specific.spec( - spec: Specification<C>, - key: Name? = null -): MutableMorphDelegate<Config, C> = MutableMorphDelegate(config, key) { spec.wrap(it) } - -fun <T: Specific> MetaItem<*>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it)} - -@JvmName("configSpec") -fun <T: Specific> MetaItem<Config>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it)} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt deleted file mode 100644 index 55d652aa..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt +++ /dev/null @@ -1,72 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - - -/** - * A meta object with read-only meta base and changeable configuration on top of it - * @param base - unchangeable base - * @param style - the style - */ -class Styled(val base: Meta, val style: Config = Config().empty()) : AbstractMutableMeta<Styled>() { - override fun wrapNode(meta: Meta): Styled = Styled(meta) - - override fun empty(): Styled = Styled(EmptyMeta) - - override val items: Map<NameToken, MetaItem<Styled>> - get() = (base.items.keys + style.items.keys).associate { key -> - val value = base.items[key] - val styleValue = style[key] - val item: MetaItem<Styled> = when (value) { - null -> when (styleValue) { - null -> error("Should be unreachable") - is MetaItem.NodeItem -> MetaItem.NodeItem(Styled(style.empty(), styleValue.node)) - is MetaItem.ValueItem -> styleValue - } - is MetaItem.ValueItem -> value - is MetaItem.NodeItem -> MetaItem.NodeItem( - Styled(value.node, styleValue?.node ?: Config.empty()) - ) - } - key to item - } - - override fun set(name: Name, item: MetaItem<*>?) { - if (item == null) { - style.remove(name) - } else { - style[name] = item - } - } - - fun onChange(owner: Any?, action: (Name, before: MetaItem<*>?, after: MetaItem<*>?) -> Unit) { - //TODO test correct behavior - style.onChange(owner) { name, before, after -> action(name, before ?: base[name], after ?: base[name]) } - } - - fun removeListener(owner: Any?) { - style.removeListener(owner) - } -} - -fun Styled.configure(meta: Meta) = apply { style.update(meta) } - -fun Meta.withStyle(style: Meta = EmptyMeta) = if (this is Styled) { - this.apply { this.configure(style) } -} else { - Styled(this, style.toConfig()) -} - -class StyledNodeDelegate(val owner: Styled, val key: String?) : ReadWriteProperty<Any?, Meta> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Meta { - return owner[key ?: property.name]?.node ?: EmptyMeta - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Meta) { - owner.style[key ?: property.name] = value - } - -} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt index 865ef800..2085bc5d 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/annotations.kt @@ -6,6 +6,6 @@ package hep.dataforge.meta @DslMarker annotation class DFBuilder -@Experimental(level = Experimental.Level.WARNING) +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) @Retention(AnnotationRetention.BINARY) annotation class DFExperimental \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt deleted file mode 100644 index 34fa0e71..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/configDelegates.kt +++ /dev/null @@ -1,137 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.values.DoubleArrayValue -import hep.dataforge.values.Null -import hep.dataforge.values.Value -import kotlin.jvm.JvmName - - -//Configurable delegates - -/** - * A property delegate that uses custom key - */ -fun Configurable.value(default: Any = Null, key: Name? = null): MutableValueDelegate<Config> = - MutableValueDelegate(config, key, Value.of(default)) - -fun <T> Configurable.value( - default: T? = null, - key: Name? = null, - writer: (T) -> Value = { Value.of(it) }, - reader: (Value?) -> T -): ReadWriteDelegateWrapper<Value?, T> = - MutableValueDelegate(config, key, default?.let { Value.of(it) }).transform(reader = reader, writer = writer) - -fun Configurable.string(default: String? = null, key: Name? = null): MutableStringDelegate<Config> = - MutableStringDelegate(config, key, default) - -fun Configurable.boolean(default: Boolean? = null, key: Name? = null): MutableBooleanDelegate<Config> = - MutableBooleanDelegate(config, key, default) - -fun Configurable.number(default: Number? = null, key: Name? = null): MutableNumberDelegate<Config> = - MutableNumberDelegate(config, key, default) - -/* Number delegates*/ - -fun Configurable.int(default: Int? = null, key: Name? = null) = - number(default, key).int - -fun Configurable.double(default: Double? = null, key: Name? = null) = - number(default, key).double - -fun Configurable.long(default: Long? = null, key: Name? = null) = - number(default, key).long - -fun Configurable.short(default: Short? = null, key: Name? = null) = - number(default, key).short - -fun Configurable.float(default: Float? = null, key: Name? = null) = - number(default, key).float - - -@JvmName("safeString") -fun Configurable.string(default: String, key: Name? = null) = - MutableSafeStringDelegate(config, key) { default } - -@JvmName("safeBoolean") -fun Configurable.boolean(default: Boolean, key: Name? = null) = - MutableSafeBooleanDelegate(config, key) { default } - -@JvmName("safeNumber") -fun Configurable.number(default: Number, key: Name? = null) = - MutableSafeNumberDelegate(config, key) { default } - -@JvmName("safeString") -fun Configurable.string(key: Name? = null, default: () -> String) = - MutableSafeStringDelegate(config, key, default) - -@JvmName("safeBoolean") -fun Configurable.boolean(key: Name? = null, default: () -> Boolean) = - MutableSafeBooleanDelegate(config, key, default) - -@JvmName("safeNumber") -fun Configurable.number(key: Name? = null, default: () -> Number) = - MutableSafeNumberDelegate(config, key, default) - - -/* Safe number delegates*/ - -@JvmName("safeInt") -fun Configurable.int(default: Int, key: Name? = null) = - number(default, key).int - -@JvmName("safeDouble") -fun Configurable.double(default: Double, key: Name? = null) = - number(default, key).double - -@JvmName("safeLong") -fun Configurable.long(default: Long, key: Name? = null) = - number(default, key).long - -@JvmName("safeShort") -fun Configurable.short(default: Short, key: Name? = null) = - number(default, key).short - -@JvmName("safeFloat") -fun Configurable.float(default: Float, key: Name? = null) = - number(default, key).float - -/** - * Enum delegate - */ -inline fun <reified E : Enum<E>> Configurable.enum(default: E, key: Name? = null) = - MutableSafeEnumvDelegate(config, key, default) { enumValueOf(it) } - -/* Node delegates */ - -fun Configurable.node(key: Name? = null): MutableNodeDelegate<Config> = MutableNodeDelegate(config, key) - -fun <T : Specific> Configurable.spec(spec: Specification<T>, key: Name? = null) = - MutableMorphDelegate(config, key) { spec.wrap(it) } - -fun <T : Specific> Configurable.spec(builder: (Config) -> T, key: Name? = null) = - MutableMorphDelegate(config, key) { specification(builder).wrap(it) } - -/* - * Extra delegates for special cases - */ - -fun Configurable.stringList(vararg strings: String, key: Name? = null): ReadWriteDelegateWrapper<Value?, List<String>> = - value(strings.asList(), key) { it?.list?.map { value -> value.string } ?: emptyList() } - -fun Configurable.numberList(vararg numbers: Number, key: Name? = null): ReadWriteDelegateWrapper<Value?, List<Number>> = - value(numbers.asList(), key) { it?.list?.map { value -> value.number } ?: emptyList() } - -/** - * A special delegate for double arrays - */ -fun Configurable.doubleArray(key: Name? = null): ReadWriteDelegateWrapper<Value?, DoubleArray> = - value(doubleArrayOf(), key) { - (it as? DoubleArrayValue)?.value - ?: it?.list?.map { value -> value.number.toDouble() }?.toDoubleArray() - ?: doubleArrayOf() - } - -fun <T : Configurable> Configurable.node(key: Name? = null, converter: (Meta) -> T) = - MutableMorphDelegate(config, key, converter) diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt new file mode 100644 index 00000000..0a09a0fb --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/Described.kt @@ -0,0 +1,24 @@ +package hep.dataforge.meta.descriptors + +/** + * An object which provides its descriptor + */ +interface Described { + val descriptor: NodeDescriptor? + + companion object { + const val DESCRIPTOR_NODE = "@descriptor" + } +} + +///** +// * If meta node supplies explicit descriptor, return it, otherwise try to use descriptor node from meta itself +// */ +//val MetaRepr.descriptor: NodeDescriptor? +// get() { +// return if (this is Described) { +// descriptor +// } else { +// toMeta()[DESCRIPTOR_NODE].node?.let { NodeDescriptor.wrap(it) } +// } +// } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt new file mode 100644 index 00000000..f95069d6 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/DescriptorMeta.kt @@ -0,0 +1,28 @@ +package hep.dataforge.meta.descriptors + +import hep.dataforge.meta.MetaBase +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.NameToken +import hep.dataforge.values.Null + +class DescriptorMeta(val descriptor: NodeDescriptor) : MetaBase() { + override val items: Map<NameToken, MetaItem<*>> + get() = descriptor.items.entries.associate { entry -> + NameToken(entry.key) to entry.value.defaultItem() + } +} + +fun NodeDescriptor.defaultItem(): MetaItem.NodeItem<*> = + MetaItem.NodeItem(default ?: DescriptorMeta(this)) + +fun ValueDescriptor.defaultItem(): MetaItem.ValueItem = MetaItem.ValueItem(default ?: Null) + +/** + * Build a default [MetaItem] from descriptor. + */ +fun ItemDescriptor.defaultItem(): MetaItem<*> { + return when (this) { + is ValueDescriptor -> defaultItem() + is NodeDescriptor -> defaultItem() + } +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt similarity index 62% rename from dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt index 0658dfd8..84d0d5c0 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ItemDescriptor.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/descriptors/ItemDescriptor.kt @@ -1,21 +1,17 @@ -package hep.dataforge.descriptors +package hep.dataforge.meta.descriptors import hep.dataforge.meta.* +import hep.dataforge.meta.scheme.* +import hep.dataforge.names.Name import hep.dataforge.names.NameToken -import hep.dataforge.names.toName +import hep.dataforge.names.asName +import hep.dataforge.names.isEmpty import hep.dataforge.values.False import hep.dataforge.values.True import hep.dataforge.values.Value import hep.dataforge.values.ValueType -sealed class ItemDescriptor(override val config: Config) : Specific { - - /** - * The name of this item - * - * @return - */ - var name: String by string { error("Anonymous descriptors are not allowed") } +sealed class ItemDescriptor : Scheme() { /** * True if same name siblings with this name are allowed @@ -36,7 +32,7 @@ sealed class ItemDescriptor(override val config: Config) : Specific { * * @return */ - var attributes by node() + var attributes by config() /** * True if the item is required @@ -46,13 +42,33 @@ sealed class ItemDescriptor(override val config: Config) : Specific { abstract var required: Boolean } +/** + * Configure attributes of the descriptor + */ +fun ItemDescriptor.attributes(block: Config.() -> Unit) { + (attributes ?: Config().also { this.config = it }).apply(block) +} + +/** + * Check if given item suits the descriptor + */ +fun ItemDescriptor.validateItem(item: MetaItem<*>?): Boolean { + return when (this) { + is ValueDescriptor -> isAllowedValue(item.value ?: return false) + is NodeDescriptor -> items.all { (key, d) -> + d.validateItem(item.node[key]) + } + } +} + /** * Descriptor for meta node. Could contain additional information for viewing * and editing. * * @author Alexander Nozik */ -class NodeDescriptor(config: Config) : ItemDescriptor(config) { +@DFBuilder +class NodeDescriptor : ItemDescriptor() { /** * True if the node is required @@ -66,65 +82,103 @@ class NodeDescriptor(config: Config) : ItemDescriptor(config) { * * @return */ - var default: Config? by node() - - /** - * The list of value descriptors - */ - val values: Map<String, ValueDescriptor> - get() = config.getIndexed(VALUE_KEY.toName()).entries.associate { (name, node) -> - name to ValueDescriptor.wrap(node.node ?: error("Value descriptor must be a node")) - } - - fun value(name: String, descriptor: ValueDescriptor) { - if (items.keys.contains(name)) error("The key $name already exists in descriptor") - val token = NameToken(VALUE_KEY, name) - config[token] = descriptor.config - } - - /** - * Add a value descriptor using block for - */ - fun value(name: String, block: ValueDescriptor.() -> Unit) { - value(name, ValueDescriptor.build { this.name = name }.apply(block)) - } + var default by node() /** * The map of children node descriptors */ val nodes: Map<String, NodeDescriptor> - get() = config.getIndexed(NODE_KEY.toName()).entries.associate { (name, node) -> + get() = config.getIndexed(NODE_KEY.asName()).entries.associate { (name, node) -> name to wrap(node.node ?: error("Node descriptor must be a node")) } - - fun node(name: String, descriptor: NodeDescriptor) { + /** + * Define a child item descriptor for this node + */ + fun defineItem(name: String, descriptor: ItemDescriptor) { if (items.keys.contains(name)) error("The key $name already exists in descriptor") - val token = NameToken(NODE_KEY, name) + val token = when (descriptor) { + is NodeDescriptor -> NameToken(NODE_KEY, name) + is ValueDescriptor -> NameToken(VALUE_KEY, name) + } config[token] = descriptor.config + } - fun node(name: String, block: NodeDescriptor.() -> Unit) { - node(name, build { this.name = name }.apply(block)) + + fun defineNode(name: String, block: NodeDescriptor.() -> Unit) { + val token = NameToken(NODE_KEY, name) + if (config[token] == null) { + config[token] = NodeDescriptor(block) + } else { + NodeDescriptor.update(config[token].node ?: error("Node expected"), block) + } + } + + private fun buildNode(name: Name): NodeDescriptor { + return when (name.length) { + 0 -> this + 1 -> { + val token = NameToken(NODE_KEY, name.toString()) + val config: Config = config[token].node ?: Config().also { config[token] = it } + wrap(config) + } + else -> buildNode(name.first()?.asName()!!).buildNode(name.cutFirst()) + } + } + + fun defineNode(name: Name, block: NodeDescriptor.() -> Unit) { + buildNode(name).apply(block) + } + + /** + * The list of value descriptors + */ + val values: Map<String, ValueDescriptor> + get() = config.getIndexed(VALUE_KEY.asName()).entries.associate { (name, node) -> + name to ValueDescriptor.wrap(node.node ?: error("Value descriptor must be a node")) + } + + + /** + * Add a value descriptor using block for + */ + fun defineValue(name: String, block: ValueDescriptor.() -> Unit) { + defineItem(name, ValueDescriptor(block)) + } + + fun defineValue(name: Name, block: ValueDescriptor.() -> Unit) { + require(name.length >= 1) { "Name length for value descriptor must be non-empty" } + buildNode(name.cutLast()).defineValue(name.last().toString(), block) } val items: Map<String, ItemDescriptor> get() = nodes + values - //override val descriptor: NodeDescriptor = empty("descriptor") +//override val descriptor: NodeDescriptor = empty("descriptor") - companion object : Specification<NodeDescriptor> { + companion object : SchemeSpec<NodeDescriptor>(::NodeDescriptor) { // const val ITEM_KEY = "item" const val NODE_KEY = "node" const val VALUE_KEY = "value" - override fun wrap(config: Config): NodeDescriptor = NodeDescriptor(config) + //override fun wrap(config: Config): NodeDescriptor = NodeDescriptor(config) //TODO infer descriptor from spec } } +/** + * Get a descriptor item associated with given name or null if item for given name not provided + */ +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()) + } +} /** * A descriptor for meta value @@ -133,7 +187,7 @@ class NodeDescriptor(config: Config) : ItemDescriptor(config) { * * @author Alexander Nozik */ -class ValueDescriptor(config: Config) : ItemDescriptor(config) { +class ValueDescriptor : ItemDescriptor() { /** @@ -159,8 +213,8 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) { * * @return */ - var type: List<ValueType> by value { - it?.list?.map { v -> ValueType.valueOf(v.string) } ?: emptyList() + var type: List<ValueType> by item { + it?.value?.list?.map { v -> ValueType.valueOf(v.string) } ?: emptyList() } fun type(vararg t: ValueType) { @@ -201,16 +255,11 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) { this.allowedValues = v.map { Value.of(it) } } - companion object : Specification<ValueDescriptor> { - - override fun wrap(config: Config): ValueDescriptor = ValueDescriptor(config) - - inline fun <reified E : Enum<E>> enum(name: String) = - build { - this.name = name - type(ValueType.STRING) - this.allowedValues = enumValues<E>().map { Value.of(it.name) } - } + companion object : SchemeSpec<ValueDescriptor>(::ValueDescriptor) { +// inline fun <reified E : Enum<E>> enum(name: String) = ValueDescriptor { +// type(ValueType.STRING) +// this.allowedValues = enumValues<E>().map { Value.of(it.name) } +// } // /** // * Build a value descriptor from annotation @@ -270,4 +319,4 @@ class ValueDescriptor(config: Config) : ItemDescriptor(config) { // return ValueDescriptor(Laminate(primary.meta, secondary.meta)) // } } -} +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt index b0d8162c..df3e97a5 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/mapMeta.kt @@ -1,16 +1,8 @@ package hep.dataforge.meta -import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.values.Value -///** -// * Find all elements with given body -// */ -//private fun Meta.byBody(body: String): Map<String, MetaItem<*>> = -// items.filter { it.key.body == body }.mapKeys { it.key.index } -// -//private fun Meta.distinctNames() = items.keys.map { it.body }.distinct() - /** * Convert meta to map of maps */ @@ -28,7 +20,7 @@ fun Meta.toMap(descriptor: NodeDescriptor? = null): Map<String, Any?> { * Convert map of maps to meta */ @DFExperimental -fun Map<String, Any?>.toMeta(descriptor: NodeDescriptor? = null): Meta = buildMeta { +fun Map<String, Any?>.toMeta(descriptor: NodeDescriptor? = null): Meta = Meta { entries.forEach { (key, value) -> @Suppress("UNCHECKED_CAST") when (value) { diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt deleted file mode 100644 index 65e49da2..00000000 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaDelegates.kt +++ /dev/null @@ -1,432 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.names.asName -import hep.dataforge.values.Null -import hep.dataforge.values.Value -import hep.dataforge.values.asValue -import kotlin.jvm.JvmName -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -/* Meta delegates */ - -//TODO add caching for sealed nodes - -class ValueDelegate(val meta: Meta, private val key: String? = null, private val default: Value? = null) : - ReadOnlyProperty<Any?, Value?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Value? { - return meta[key ?: property.name]?.value ?: default - } -} - -class StringDelegate(val meta: Meta, private val key: String? = null, private val default: String? = null) : - ReadOnlyProperty<Any?, String?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): String? { - return meta[key ?: property.name]?.string ?: default - } -} - -class BooleanDelegate( - val meta: Meta, - private val key: String? = null, - private val default: Boolean? = null -) : ReadOnlyProperty<Any?, Boolean?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? { - return meta[key ?: property.name]?.boolean ?: default - } -} - -class NumberDelegate( - val meta: Meta, - private val key: String? = null, - private val default: Number? = null -) : ReadOnlyProperty<Any?, Number?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Number? { - return meta[key ?: property.name]?.number ?: default - } - - //delegates for number transformation - - val double get() = DelegateWrapper(this) { it?.toDouble() } - val int get() = DelegateWrapper(this) { it?.toInt() } - val short get() = DelegateWrapper(this) { it?.toShort() } - val long get() = DelegateWrapper(this) { it?.toLong() } -} - -class DelegateWrapper<T, R>(val delegate: ReadOnlyProperty<Any?, T>, val reader: (T) -> R) : - ReadOnlyProperty<Any?, R> { - override fun getValue(thisRef: Any?, property: KProperty<*>): R { - return reader(delegate.getValue(thisRef, property)) - } -} - -//Delegates with non-null values - -class SafeStringDelegate( - val meta: Meta, - private val key: String? = null, - default: () -> String -) : ReadOnlyProperty<Any?, String> { - - private val default: String by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): String { - return meta[key ?: property.name]?.string ?: default - } -} - -class SafeBooleanDelegate( - val meta: Meta, - private val key: String? = null, - default: () -> Boolean -) : ReadOnlyProperty<Any?, Boolean> { - - private val default: Boolean by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { - return meta[key ?: property.name]?.boolean ?: default - } -} - -class SafeNumberDelegate( - val meta: Meta, - private val key: String? = null, - default: () -> Number -) : ReadOnlyProperty<Any?, Number> { - - private val default: Number by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): Number { - return meta[key ?: property.name]?.number ?: default - } - - val double get() = DelegateWrapper(this) { it.toDouble() } - val int get() = DelegateWrapper(this) { it.toInt() } - val short get() = DelegateWrapper(this) { it.toShort() } - val long get() = DelegateWrapper(this) { it.toLong() } -} - -class SafeEnumDelegate<E : Enum<E>>( - val meta: Meta, - private val key: String? = null, - private val default: E, - private val resolver: (String) -> E -) : ReadOnlyProperty<Any?, E> { - override fun getValue(thisRef: Any?, property: KProperty<*>): E { - return (meta[key ?: property.name]?.string)?.let { resolver(it) } ?: default - } -} - -//Child node delegate - -class ChildDelegate<T>( - val meta: Meta, - private val key: String? = null, - private val converter: (Meta) -> T -) : ReadOnlyProperty<Any?, T?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): T? { - return meta[key ?: property.name]?.node?.let { converter(it) } - } -} - -//Read-only delegates for Metas - -/** - * A property delegate that uses custom key - */ -fun Meta.value(default: Value = Null, key: String? = null) = ValueDelegate(this, key, default) - -fun Meta.string(default: String? = null, key: String? = null) = StringDelegate(this, key, default) - -fun Meta.boolean(default: Boolean? = null, key: String? = null) = BooleanDelegate(this, key, default) - -fun Meta.number(default: Number? = null, key: String? = null) = NumberDelegate(this, key, default) - -fun Meta.node(key: String? = null) = ChildDelegate(this, key) { it } - -@JvmName("safeString") -fun Meta.string(default: String, key: String? = null) = - SafeStringDelegate(this, key) { default } - -@JvmName("safeBoolean") -fun Meta.boolean(default: Boolean, key: String? = null) = - SafeBooleanDelegate(this, key) { default } - -@JvmName("safeNumber") -fun Meta.number(default: Number, key: String? = null) = - SafeNumberDelegate(this, key) { default } - -@JvmName("safeString") -fun Meta.string(key: String? = null, default: () -> String) = - SafeStringDelegate(this, key, default) - -@JvmName("safeBoolean") -fun Meta.boolean(key: String? = null, default: () -> Boolean) = - SafeBooleanDelegate(this, key, default) - -@JvmName("safeNumber") -fun Meta.number(key: String? = null, default: () -> Number) = - SafeNumberDelegate(this, key, default) - - -inline fun <reified E : Enum<E>> Meta.enum(default: E, key: String? = null) = - SafeEnumDelegate(this, key, default) { enumValueOf(it) } - -/* Read-write delegates */ - -class MutableValueDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - private val default: Value? = null -) : ReadWriteProperty<Any?, Value?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Value? { - return meta[key ?: property.name.asName()]?.value ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Value?) { - val name = key ?: property.name.asName() - if (value == null) { - meta.remove(name) - } else { - meta.setValue(name, value) - } - } - - fun <T> transform(writer: (T) -> Value? = { Value.of(it) }, reader: (Value?) -> T) = - ReadWriteDelegateWrapper(this, reader, writer) -} - -class MutableStringDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - private val default: String? = null -) : ReadWriteProperty<Any?, String?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): String? { - return meta[key ?: property.name.asName()]?.string ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { - val name = key ?: property.name.asName() - if (value == null) { - meta.remove(name) - } else { - meta.setValue(name, value.asValue()) - } - } -} - -class MutableBooleanDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - private val default: Boolean? = null -) : ReadWriteProperty<Any?, Boolean?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? { - return meta[key ?: property.name.asName()]?.boolean ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { - val name = key ?: property.name.asName() - if (value == null) { - meta.remove(name) - } else { - meta.setValue(name, value.asValue()) - } - } -} - -class MutableNumberDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - private val default: Number? = null -) : ReadWriteProperty<Any?, Number?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): Number? { - return meta[key ?: property.name.asName()]?.number ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number?) { - val name = key ?: property.name.asName() - if (value == null) { - meta.remove(name) - } else { - meta.setValue(name, value.asValue()) - } - } - - val double get() = ReadWriteDelegateWrapper(this, reader = { it?.toDouble() }, writer = { it }) - val float get() = ReadWriteDelegateWrapper(this, reader = { it?.toFloat() }, writer = { it }) - val int get() = ReadWriteDelegateWrapper(this, reader = { it?.toInt() }, writer = { it }) - val short get() = ReadWriteDelegateWrapper(this, reader = { it?.toShort() }, writer = { it }) - val long get() = ReadWriteDelegateWrapper(this, reader = { it?.toLong() }, writer = { it }) -} - -//Delegates with non-null values - -class MutableSafeStringDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - default: () -> String -) : ReadWriteProperty<Any?, String> { - - private val default: String by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): String { - return meta[key ?: property.name.asName()]?.string ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { - meta.setValue(key ?: property.name.asName(), value.asValue()) - } -} - -class MutableSafeBooleanDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - default: () -> Boolean -) : ReadWriteProperty<Any?, Boolean> { - - private val default: Boolean by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { - return meta[key ?: property.name.asName()]?.boolean ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { - meta.setValue(key ?: property.name.asName(), value.asValue()) - } -} - -class MutableSafeNumberDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null, - default: () -> Number -) : ReadWriteProperty<Any?, Number> { - - private val default: Number by lazy(default) - - override fun getValue(thisRef: Any?, property: KProperty<*>): Number { - return meta[key ?: property.name.asName()]?.number ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number) { - meta.setValue(key ?: property.name.asName(), value.asValue()) - } - - val double get() = ReadWriteDelegateWrapper(this, reader = { it.toDouble() }, writer = { it }) - val float get() = ReadWriteDelegateWrapper(this, reader = { it.toFloat() }, writer = { it }) - val int get() = ReadWriteDelegateWrapper(this, reader = { it.toInt() }, writer = { it }) - val short get() = ReadWriteDelegateWrapper(this, reader = { it.toShort() }, writer = { it }) - val long get() = ReadWriteDelegateWrapper(this, reader = { it.toLong() }, writer = { it }) -} - -class MutableSafeEnumvDelegate<M : MutableMeta<M>, E : Enum<E>>( - val meta: M, - private val key: Name? = null, - private val default: E, - private val resolver: (String) -> E -) : ReadWriteProperty<Any?, E> { - override fun getValue(thisRef: Any?, property: KProperty<*>): E { - return (meta[key ?: property.name.asName()]?.string)?.let { resolver(it) } ?: default - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: E) { - meta.setValue(key ?: property.name.asName(), value.name.asValue()) - } -} - -//Child node delegate - -class MutableNodeDelegate<M : MutableMeta<M>>( - val meta: M, - private val key: Name? = null -) : ReadWriteProperty<Any?, M?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): M? { - return meta[key ?: property.name.asName()]?.node - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: M?) { - meta[key ?: property.name.asName()] = value - } -} - -class MutableMorphDelegate<M : MutableMeta<M>, T : Configurable>( - val meta: M, - private val key: Name? = null, - private val converter: (Meta) -> T -) : ReadWriteProperty<Any?, T?> { - override fun getValue(thisRef: Any?, property: KProperty<*>): T? { - return meta[key ?: property.name.asName()]?.node?.let(converter) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { - if (value == null) { - meta.remove(key ?: property.name.asName()) - } else { - meta[key ?: property.name.asName()] = value.config - } - } -} - -class ReadWriteDelegateWrapper<T, R>( - val delegate: ReadWriteProperty<Any?, T>, - val reader: (T) -> R, - val writer: (R) -> T -) : ReadWriteProperty<Any?, R> { - override fun getValue(thisRef: Any?, property: KProperty<*>): R { - return reader(delegate.getValue(thisRef, property)) - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: R) { - delegate.setValue(thisRef, property, writer(value)) - } -} - - -//Read-write delegates - -/** - * A property delegate that uses custom key - */ -fun <M : MutableMeta<M>> M.value(default: Value = Null, key: Name? = null) = - MutableValueDelegate(this, key, default) - -fun <M : MutableMeta<M>> M.string(default: String? = null, key: Name? = null) = - MutableStringDelegate(this, key, default) - -fun <M : MutableMeta<M>> M.boolean(default: Boolean? = null, key: Name? = null) = - MutableBooleanDelegate(this, key, default) - -fun <M : MutableMeta<M>> M.number(default: Number? = null, key: Name? = null) = - MutableNumberDelegate(this, key, default) - -fun <M : MutableMeta<M>> M.node(key: Name? = null) = - MutableNodeDelegate(this, key) - -@JvmName("safeString") -fun <M : MutableMeta<M>> M.string(default: String, key: Name? = null) = - MutableSafeStringDelegate(this, key) { default } - -@JvmName("safeBoolean") -fun <M : MutableMeta<M>> M.boolean(default: Boolean, key: Name? = null) = - MutableSafeBooleanDelegate(this, key) { default } - -@JvmName("safeNumber") -fun <M : MutableMeta<M>> M.number(default: Number, key: Name? = null) = - MutableSafeNumberDelegate(this, key) { default } - -@JvmName("safeString") -fun <M : MutableMeta<M>> M.string(key: Name? = null, default: () -> String) = - MutableSafeStringDelegate(this, key, default) - -@JvmName("safeBoolean") -fun <M : MutableMeta<M>> M.boolean(key: Name? = null, default: () -> Boolean) = - MutableSafeBooleanDelegate(this, key, default) - -@JvmName("safeNumber") -fun <M : MutableMeta<M>> M.number(key: Name? = null, default: () -> Number) = - MutableSafeNumberDelegate(this, key, default) - - -inline fun <M : MutableMeta<M>, reified E : Enum<E>> M.enum(default: E, key: Name? = null) = - MutableSafeEnumvDelegate(this, key, default) { enumValueOf(it) } \ No newline at end of file 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 6f16f537..263483a2 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/metaMatcher.kt @@ -26,26 +26,13 @@ fun Meta.getIndexed(name: Name): Map<String, MetaItem<*>> { @DFExperimental fun Meta.getIndexed(name: String): Map<String, MetaItem<*>> = this@getIndexed.getIndexed(name.toName()) - /** * Get all items matching given name. */ +@Suppress("UNCHECKED_CAST") @DFExperimental -fun <M : MetaNode<M>> M.getIndexed(name: Name): Map<String, MetaItem<M>> { - val root: MetaNode<M>? = when (name.length) { - 0 -> error("Can't use empty name for that") - 1 -> this - else -> (this[name.cutLast()] as? MetaItem.NodeItem<M>)?.node - } - - val (body, index) = name.last()!! - val regex = index.toRegex() - - return root?.items - ?.filter { it.key.body == body && (index.isEmpty() || regex.matches(it.key.index)) } - ?.mapKeys { it.key.index } - ?: emptyMap() -} +fun <M : MetaNode<M>> M.getIndexed(name: Name): Map<String, MetaItem<M>> = + (this as Meta).getIndexed(name) as Map<String, MetaItem<M>> @DFExperimental fun <M : MetaNode<M>> M.getIndexed(name: String): Map<String, MetaItem<M>> = getIndexed(name.toName()) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt new file mode 100644 index 00000000..4ae5e180 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Configurable.kt @@ -0,0 +1,68 @@ +package hep.dataforge.meta.scheme + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.* +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.values.Value + +/** + * A container that holds a [Config] and a default item provider. + * Default item provider could be use for example to reference parent configuration. + * It is not possible to know if some property is declared by provider just by looking on [Configurable], + * this information should be provided externally. + */ +interface Configurable : Described { + /** + * Backing config + */ + val config: Config + + /** + * Default meta item provider + */ + fun getDefaultItem(name: Name): MetaItem<*>? = null + + /** + * Check if property with given [name] could be assigned to [value] + */ + fun validateItem(name: Name, item: MetaItem<*>?): Boolean { + val descriptor = descriptor?.get(name) + return descriptor?.validateItem(item) ?: true + } + + override val descriptor: NodeDescriptor? get() = null + + /** + * Get a property with default + */ + fun getProperty(name: Name): MetaItem<*>? = + config[name] ?: getDefaultItem(name) ?: descriptor?.get(name)?.defaultItem() + + /** + * Set a configurable property + */ + fun setProperty(name: Name, item: MetaItem<*>?) { + if (validateItem(name, item)) { + config[name] = item + } else { + error("Validation failed for property $name with value $item") + } + } +} + +fun Configurable.getProperty(key: String) = getProperty(key.toName()) + +fun Configurable.setProperty(name: Name, value: Value?) = setProperty(name, value?.let { MetaItem.ValueItem(value) }) +fun Configurable.setProperty(name: Name, meta: Meta?) = setProperty(name, meta?.let { MetaItem.NodeItem(meta) }) + +fun Configurable.setProperty(key: String, item: MetaItem<*>?) { + setProperty(key.toName(), item) +} + +fun Configurable.setProperty(key: String, value: Value?) = setProperty(key, value?.let { MetaItem.ValueItem(value) }) +fun Configurable.setProperty(key: String, meta: Meta?) = setProperty(key, meta?.let { MetaItem.NodeItem(meta) }) + +fun <T : Configurable> T.configure(meta: Meta): T = this.apply { config.update(meta) } + +inline fun <T : Configurable> T.configure(action: Config.() -> Unit): T = apply { config.apply(action) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt new file mode 100644 index 00000000..c582282b --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/ConfigurableDelegate.kt @@ -0,0 +1,236 @@ +package hep.dataforge.meta.scheme + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.values.* +import kotlin.jvm.JvmName +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + + +//delegates + +/** + * A delegate that uses a [Configurable] object and delegate read and write operations to its properties + */ +open class ConfigurableDelegate( + val owner: Configurable, + val key: Name? = null, + open val default: MetaItem<*>? = null +) : ReadWriteProperty<Any?, MetaItem<*>?> { + + override fun getValue(thisRef: Any?, property: KProperty<*>): MetaItem<*>? { + val name = key ?: property.name.asName() + return owner.getProperty(name) ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MetaItem<*>?) { + val name = key ?: property.name.asName() + owner.setProperty(name, value) + } +} + +class LazyConfigurableDelegate( + configurable: Configurable, + key: Name? = null, + defaultProvider: () -> MetaItem<*>? = { null } +) : ConfigurableDelegate(configurable, key) { + override val default by lazy(defaultProvider) +} + +/** + * A property delegate that uses custom key + */ +fun Configurable.item(default: Any? = null, key: Name? = null): ConfigurableDelegate = + ConfigurableDelegate( + this, + key, + default?.let { MetaItem.of(it) }) + +/** + * Generation of item delegate with lazy default. + * Lazy default could be used also for validation + */ +fun Configurable.lazyItem(key: Name? = null, default: () -> Any?): ConfigurableDelegate = + LazyConfigurableDelegate(this, key) { + default()?.let { + MetaItem.of(it) + } + } + +fun <T> Configurable.item( + default: T? = null, + key: Name? = null, + writer: (T) -> MetaItem<*>? = { + MetaItem.of(it) + }, + reader: (MetaItem<*>?) -> T +): ReadWriteProperty<Any?, T> = + ConfigurableDelegate( + this, + key, + default?.let { MetaItem.of(it) }).map(reader = reader, writer = writer) + +fun Configurable.value(default: Any? = null, key: Name? = null): ReadWriteProperty<Any?, Value?> = + item(default, key).transform { it.value } + +fun <T> Configurable.value( + default: T? = null, + key: Name? = null, + writer: (T) -> Value? = { Value.of(it) }, + reader: (Value?) -> T +): ReadWriteProperty<Any?, T> = + ConfigurableDelegate( + this, + key, + default?.let { MetaItem.of(it) } + ).map( + reader = { reader(it.value) }, + writer = { value -> writer(value)?.let { MetaItem.ValueItem(it) } } + ) + +fun Configurable.string(default: String? = null, key: Name? = null): ReadWriteProperty<Any?, String?> = + item(default, key).transform { it.value?.string } + +fun Configurable.boolean(default: Boolean? = null, key: Name? = null): ReadWriteProperty<Any?, Boolean?> = + item(default, key).transform { it.value?.boolean } + +fun Configurable.number(default: Number? = null, key: Name? = null): ReadWriteProperty<Any?, Number?> = + item(default, key).transform { it.value?.number } + +/* Number delegates*/ + +fun Configurable.int(default: Int? = null, key: Name? = null): ReadWriteProperty<Any?, Int?> = + item(default, key).transform { it.value?.int } + +fun Configurable.double(default: Double? = null, key: Name? = null): ReadWriteProperty<Any?, Double?> = + item(default, key).transform { it.value?.double } + +fun Configurable.long(default: Long? = null, key: Name? = null): ReadWriteProperty<Any?, Long?> = + item(default, key).transform { it.value?.long } + +fun Configurable.short(default: Short? = null, key: Name? = null): ReadWriteProperty<Any?, Short?> = + item(default, key).transform { it.value?.short } + +fun Configurable.float(default: Float? = null, key: Name? = null): ReadWriteProperty<Any?, Float?> = + item(default, key).transform { it.value?.float } + + +@JvmName("safeString") +fun Configurable.string(default: String, key: Name? = null): ReadWriteProperty<Any?, String> = + item(default, key).transform { it.value!!.string } + +@JvmName("safeBoolean") +fun Configurable.boolean(default: Boolean, key: Name? = null): ReadWriteProperty<Any?, Boolean> = + item(default, key).transform { it.value!!.boolean } + +@JvmName("safeNumber") +fun Configurable.number(default: Number, key: Name? = null): ReadWriteProperty<Any?, Number> = + item(default, key).transform { it.value!!.number } + +/* Lazy initializers for values */ + +@JvmName("lazyString") +fun Configurable.string(key: Name? = null, default: () -> String): ReadWriteProperty<Any?, String> = + lazyItem(key, default).transform { it.value!!.string } + +@JvmName("lazyBoolean") +fun Configurable.boolean(key: Name? = null, default: () -> Boolean): ReadWriteProperty<Any?, Boolean> = + lazyItem(key, default).transform { it.value!!.boolean } + +@JvmName("lazyNumber") +fun Configurable.number(key: Name? = null, default: () -> Number): ReadWriteProperty<Any?, Number> = + lazyItem(key, default).transform { it.value!!.number } + +/* Safe number delegates*/ + +@JvmName("safeInt") +fun Configurable.int(default: Int, key: Name? = null): ReadWriteProperty<Any?, Int> = + item(default, key).transform { it.value!!.int } + +@JvmName("safeDouble") +fun Configurable.double(default: Double, key: Name? = null): ReadWriteProperty<Any?, Double> = + item(default, key).transform { it.value!!.double } + +@JvmName("safeLong") +fun Configurable.long(default: Long, key: Name? = null): ReadWriteProperty<Any?, Long> = + item(default, key).transform { it.value!!.long } + +@JvmName("safeShort") +fun Configurable.short(default: Short, key: Name? = null): ReadWriteProperty<Any?, Short> = + item(default, key).transform { it.value!!.short } + +@JvmName("safeFloat") +fun Configurable.float(default: Float, key: Name? = null): ReadWriteProperty<Any?, Float> = + item(default, key).transform { it.value!!.float } + +/** + * Enum delegate + */ +fun <E : Enum<E>> Configurable.enum( + default: E, key: Name? = null, resolve: MetaItem<*>.() -> E? +): ReadWriteProperty<Any?, E> = item(default, key).transform { it?.resolve() ?: default } + +/* + * Extra delegates for special cases + */ +fun Configurable.stringList(vararg strings: String, key: Name? = null): ReadWriteProperty<Any?, List<String>> = + item(listOf(*strings), key) { + it?.value?.stringList ?: emptyList() + } + +fun Configurable.numberList(vararg numbers: Number, key: Name? = null): ReadWriteProperty<Any?, List<Number>> = + item(listOf(*numbers), key) { item -> + item?.value?.list?.map { it.number } ?: emptyList() + } + +/** + * A special delegate for double arrays + */ +fun Configurable.doubleArray(vararg doubles: Double, key: Name? = null): ReadWriteProperty<Any?, DoubleArray> = + item(doubleArrayOf(*doubles), key) { + (it.value as? DoubleArrayValue)?.value + ?: it?.value?.list?.map { value -> value.number.toDouble() }?.toDoubleArray() + ?: doubleArrayOf() + } + + +/* Node delegates */ + +fun Configurable.config(key: Name? = null): ReadWriteProperty<Any?, Config?> = + config.node(key) + +fun Configurable.node(key: Name? = null): ReadWriteProperty<Any?, Meta?> = item(key).map( + reader = { it.node }, + writer = { it?.let { MetaItem.NodeItem(it) } } +) + +fun <T : Configurable> Configurable.spec( + spec: Specification<T>, key: Name? = null +): ReadWriteProperty<Any?, T?> = object : ReadWriteProperty<Any?, T?> { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + val name = key ?: property.name.asName() + return config[name].node?.let { spec.wrap(it) } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + val name = key ?: property.name.asName() + config[name] = value?.config + } +} + +fun <T : Configurable> Configurable.spec( + spec: Specification<T>, default: T, key: Name? = null +): ReadWriteProperty<Any?, T> = object : ReadWriteProperty<Any?, T> { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + val name = key ?: property.name.asName() + return config[name].node?.let { spec.wrap(it) }?:default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + val name = key ?: property.name.asName() + config[name] = value.config + } +} + diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt new file mode 100644 index 00000000..b8d6257f --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Scheme.kt @@ -0,0 +1,90 @@ +package hep.dataforge.meta.scheme + +import hep.dataforge.meta.* +import hep.dataforge.meta.descriptors.* +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.plus + +/** + * A base for delegate-based or descriptor-based scheme. [Scheme] has an empty constructor to simplify usage from [Specification]. + */ +open class Scheme() : Configurable, Described, MetaRepr { + constructor(config: Config, defaultProvider: (Name) -> MetaItem<*>?) : this() { + this.config = config + this.defaultProvider = defaultProvider + } + + //constructor(config: Config, default: Meta) : this(config, { default[it] }) + constructor(config: Config) : this(config, { null }) + + final override var config: Config = Config() + internal set + + var defaultProvider: (Name) -> MetaItem<*>? = { null } + internal set + + final override var descriptor: NodeDescriptor? = null + internal set + + override fun getDefaultItem(name: Name): MetaItem<*>? { + return defaultProvider(name) ?: descriptor?.get(name)?.defaultItem() + } + + /** + * Provide a default layer which returns items from [defaultProvider] and falls back to descriptor + * values if default value is unavailable. + * Values from [defaultProvider] completely replace + */ + open val defaultLayer: Meta get() = DefaultLayer(Name.EMPTY) + + override fun toMeta(): Laminate = Laminate(config, defaultLayer) + + private inner class DefaultLayer(val path: Name) : MetaBase() { + override val items: Map<NameToken, MetaItem<*>> = + (descriptor?.get(path) as? NodeDescriptor)?.items?.entries?.associate { (key, descriptor) -> + val token = NameToken(key) + val fullName = path + token + val item: MetaItem<*> = when (descriptor) { + is ValueDescriptor -> getDefaultItem(fullName) ?: descriptor.defaultItem() + is NodeDescriptor -> MetaItem.NodeItem(DefaultLayer(fullName)) + } + token to item + } ?: emptyMap() + } +} + +inline operator fun <T : Scheme> T.invoke(block: T.() -> Unit) = apply(block) + +/** + * A specification for simplified generation of wrappers + */ +open class SchemeSpec<T : Scheme>(val builder: () -> T) : Specification<T> { + override fun wrap(config: Config, defaultProvider: (Name) -> MetaItem<*>?): T { + return builder().apply { + this.config = config + this.defaultProvider = defaultProvider + } + } +} + +/** + * A scheme that uses [Meta] as a default layer + */ +open class MetaScheme( + val meta: Meta, + descriptor: NodeDescriptor? = null, + config: Config = Config() +) : Scheme(config, meta::get) { + init { + this.descriptor = descriptor + } + + override val defaultLayer: Meta + get() = Laminate(meta, descriptor?.defaultItem().node) +} + +fun Meta.asScheme() = + MetaScheme(this) + +fun <T : Configurable> Meta.toScheme(spec: Specification<T>, block: T.() -> Unit) = spec.wrap(this).apply(block) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt new file mode 100644 index 00000000..1783e841 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/scheme/Specification.kt @@ -0,0 +1,67 @@ +package hep.dataforge.meta.scheme + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import kotlin.jvm.JvmName + +/** + * Allows to apply custom configuration in a type safe way to simple untyped configuration. + * By convention [Scheme] companion should inherit this class + * + */ +interface Specification<T : Configurable> { + /** + * Update given configuration using given type as a builder + */ + fun update(config: Config, action: T.() -> Unit): T { + return wrap(config).apply(action) + } + + operator fun invoke(action: T.() -> Unit) = update(Config(), action) + + fun empty() = wrap() + + /** + * Wrap generic configuration producing instance of desired type + */ + fun wrap(config: Config = Config(), defaultProvider: (Name) -> MetaItem<*>? = { null }): T + + /** + * Wrap a configuration using static meta as default + */ + fun wrap(config: Config = Config(), default: Meta): T = wrap(config) { default[it] } + + /** + * Wrap a configuration using static meta as default + */ + fun wrap(default: Meta): T = wrap( + Config() + ) { default[it] } +} + +/** + * Apply specified configuration to configurable + */ +fun <T : Configurable, C : Configurable, S : Specification<C>> T.configure(spec: S, action: C.() -> Unit) = + apply { spec.update(config, action) } + +/** + * Update configuration using given specification + */ +fun <C : Configurable, S : Specification<C>> Configurable.update(spec: S, action: C.() -> Unit) = + apply { spec.update(config, action) } + +/** + * Create a style based on given specification + */ +fun <C : Configurable, S : Specification<C>> S.createStyle(action: C.() -> Unit): Meta = + Config().also { update(it, action) } + +fun <T : Configurable> MetaItem<*>.spec(spec: Specification<T>): T? = node?.let { + spec.wrap( + Config(), it + ) +} + +@JvmName("configSpec") +fun <T : Configurable> MetaItem<Config>.spec(spec: Specification<T>): T? = node?.let { spec.wrap(it) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt new file mode 100644 index 00000000..16c58bdc --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/serializationUtils.kt @@ -0,0 +1,65 @@ +package hep.dataforge.meta + +import kotlinx.serialization.* +import kotlinx.serialization.builtins.DoubleArraySerializer +import kotlinx.serialization.builtins.serializer + +fun SerialDescriptorBuilder.boolean(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, Boolean.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.string(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, String.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.int(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, Int.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.double(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, Double.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.float(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, Float.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.long(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, Long.serializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +fun SerialDescriptorBuilder.doubleArray(name: String, isOptional: Boolean = false, vararg annotations: Annotation) = + element(name, DoubleArraySerializer().descriptor, isOptional = isOptional, annotations = annotations.toList()) + +@OptIn(InternalSerializationApi::class) +inline fun <reified E : Enum<E>> SerialDescriptorBuilder.enum( + name: String, + isOptional: Boolean = false, + vararg annotations: Annotation +) { + val enumDescriptor = SerialDescriptor(serialName, UnionKind.ENUM_KIND) { + enumValues<E>().forEach { + val fqn = "$serialName.${it.name}" + val enumMemberDescriptor = SerialDescriptor(fqn, StructureKind.OBJECT) + element(it.name, enumMemberDescriptor) + } + } + element(name, enumDescriptor, isOptional = isOptional, annotations = annotations.toList()) +} + +@DFExperimental +inline fun <R> Decoder.decodeStructure( + desc: SerialDescriptor, + vararg typeParams: KSerializer<*> = emptyArray(), + crossinline block: CompositeDecoder.() -> R +): R { + val decoder = beginStructure(desc, *typeParams) + val res = decoder.block() + decoder.endStructure(desc) + return res +} + +@DFExperimental +inline fun Encoder.encodeStructure( + desc: SerialDescriptor, + vararg typeParams: KSerializer<*> = emptyArray(), + block: CompositeEncoder.() -> Unit +) { + val encoder = beginStructure(desc, *typeParams) + encoder.block() + encoder.endStructure(desc) +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt new file mode 100644 index 00000000..21785980 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaCaster.kt @@ -0,0 +1,75 @@ +package hep.dataforge.meta.transformations + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaItem +import hep.dataforge.meta.get +import hep.dataforge.meta.value +import hep.dataforge.values.* + +/** + * A converter of generic object to and from [MetaItem] + */ +interface MetaCaster<T : Any> { + fun itemToObject(item: MetaItem<*>): T + fun objectToMetaItem(obj: T): MetaItem<*> + + companion object { + + val meta = object : MetaCaster<Meta> { + override fun itemToObject(item: MetaItem<*>): Meta = when (item) { + is MetaItem.NodeItem -> item.node + is MetaItem.ValueItem -> item.value.toMeta() + } + + override fun objectToMetaItem(obj: Meta): MetaItem<*> = MetaItem.NodeItem(obj) + } + + val value = object : MetaCaster<Value> { + override fun itemToObject(item: MetaItem<*>): Value = when (item) { + is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value") + is MetaItem.ValueItem -> item.value + } + + override fun objectToMetaItem(obj: Value): MetaItem<*> = MetaItem.ValueItem(obj) + } + + val string = object : MetaCaster<String> { + override fun itemToObject(item: MetaItem<*>): String = when (item) { + is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value") + is MetaItem.ValueItem -> item.value + }.string + + override fun objectToMetaItem(obj: String): MetaItem<*> = MetaItem.ValueItem(obj.asValue()) + } + + val boolean = object : MetaCaster<Boolean> { + override fun itemToObject(item: MetaItem<*>): Boolean = when (item) { + is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value") + is MetaItem.ValueItem -> item.value + }.boolean + + override fun objectToMetaItem(obj: Boolean): MetaItem<*> = MetaItem.ValueItem(obj.asValue()) + } + + val double = object : MetaCaster<Double> { + override fun itemToObject(item: MetaItem<*>): Double = when (item) { + is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value") + is MetaItem.ValueItem -> item.value + }.double + + override fun objectToMetaItem(obj: Double): MetaItem<*> = MetaItem.ValueItem(obj.asValue()) + } + + val int = object : MetaCaster<Int> { + override fun itemToObject(item: MetaItem<*>): Int = when (item) { + is MetaItem.NodeItem -> item.node[Meta.VALUE_KEY].value ?: error("Can't convert node to a value") + is MetaItem.ValueItem -> item.value + }.int + + override fun objectToMetaItem(obj: Int): MetaItem<*> = MetaItem.ValueItem(obj.asValue()) + } + } +} + +fun <T : Any> MetaCaster<T>.metaToObject(meta: Meta): T = itemToObject(MetaItem.NodeItem(meta)) +fun <T : Any> MetaCaster<T>.valueToObject(value: Value): T = itemToObject(MetaItem.ValueItem(value)) diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt similarity index 83% rename from dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt index 8deada19..d6f3bedf 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaTransformation.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/transformations/MetaTransformation.kt @@ -1,5 +1,6 @@ -package hep.dataforge.meta +package hep.dataforge.meta.transformations +import hep.dataforge.meta.* import hep.dataforge.names.Name /** @@ -8,7 +9,7 @@ import hep.dataforge.names.Name interface TransformationRule { /** - * Check if this transformation + * Check if this transformation should be applied to a node with given name and value */ fun matches(name: Name, item: MetaItem<*>?): Boolean @@ -29,7 +30,8 @@ interface TransformationRule { /** * A transformation which keeps all elements, matching [selector] unchanged. */ -data class KeepTransformationRule(val selector: (Name) -> Boolean) : TransformationRule { +data class KeepTransformationRule(val selector: (Name) -> Boolean) : + TransformationRule { override fun matches(name: Name, item: MetaItem<*>?): Boolean { return selector(name) } @@ -87,25 +89,27 @@ inline class MetaTransformation(val transformations: Collection<TransformationRu /** * Produce new meta using only those items that match transformation rules */ - fun transform(source: Meta): Meta = buildMeta { - transformations.forEach { rule -> - rule.selectItems(source).forEach { name -> - rule.transformItem(name, source[name], this) + fun transform(source: Meta): Meta = + Meta { + transformations.forEach { rule -> + rule.selectItems(source).forEach { name -> + rule.transformItem(name, source[name], this) + } } } - } /** * Transform a meta, replacing all elements found in rules with transformed entries */ - fun apply(source: Meta): Meta = buildMeta(source) { - transformations.forEach { rule -> - rule.selectItems(source).forEach { name -> - remove(name) - rule.transformItem(name, source[name], this) + fun apply(source: Meta): Meta = + source.edit { + transformations.forEach { rule -> + rule.selectItems(source).forEach { name -> + remove(name) + rule.transformItem(name, source[name], this) + } } } - } /** * Listens for changes in the source node and translates them into second node if transformation set contains a corresponding rule. @@ -150,9 +154,10 @@ class MetaTransformationBuilder { * Keep nodes by regex */ fun keep(regex: String) { - transformations.add(RegexItemTransformationRule(regex.toRegex()) { name, _, metaItem -> - setItem(name, metaItem) - }) + transformations.add( + RegexItemTransformationRule(regex.toRegex()) { name, _, metaItem -> + setItem(name, metaItem) + }) } /** 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 41680eb0..2b8908ed 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt @@ -1,11 +1,14 @@ package hep.dataforge.names +import kotlinx.serialization.* + /** * 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 index in square brackets. */ +@Serializable class Name(val tokens: List<NameToken>) { val length get() = tokens.size @@ -50,9 +53,21 @@ class Name(val tokens: List<NameToken>) { } } - - companion object { + @Serializer(Name::class) + companion object : KSerializer<Name> { const val NAME_SEPARATOR = "." + + val EMPTY = Name(emptyList()) + + override val descriptor: SerialDescriptor = PrimitiveDescriptor("hep.dataforge.names.Name", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Name { + return decoder.decodeString().toName() + } + + override fun serialize(encoder: Encoder, value: Name) { + encoder.encodeString(value.toString()) + } } } @@ -61,6 +76,7 @@ class Name(val tokens: List<NameToken>) { * 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 = "") { init { @@ -80,6 +96,19 @@ data class NameToken(val body: String, val index: String = "") { } fun hasIndex() = index.isNotEmpty() + + @Serializer(NameToken::class) + companion object : KSerializer<NameToken> { + override val descriptor: SerialDescriptor = PrimitiveDescriptor("hep.dataforge.names.NameToken", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): NameToken { + return decoder.decodeString().toName().first()!! + } + + override fun serialize(encoder: Encoder, value: NameToken) { + encoder.encodeString(value.toString()) + } + } } /** @@ -87,7 +116,7 @@ data class NameToken(val body: String, val index: String = "") { * This operation is rather heavy so it should be used with care in high performance code. */ fun String.toName(): Name { - if (isBlank()) return EmptyName + if (isBlank()) return Name.EMPTY val tokens = sequence { var bodyBuilder = StringBuilder() var queryBuilder = StringBuilder() @@ -139,7 +168,7 @@ 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()) EmptyName else NameToken(this).asName() +fun String.asName(): Name = if (isBlank()) Name.EMPTY else NameToken(this).asName() operator fun NameToken.plus(other: Name): Name = Name(listOf(this) + other.tokens) @@ -153,8 +182,6 @@ fun Name.appendLeft(other: String): Name = NameToken(other) + this fun NameToken.asName() = Name(listOf(this)) -val EmptyName = Name(emptyList()) - fun Name.isEmpty(): Boolean = this.length == 0 /** @@ -182,6 +209,8 @@ fun Name.startsWith(token: NameToken): Boolean = first() == token fun Name.endsWith(token: NameToken): Boolean = last() == token -fun Name.startsWith(name: Name): Boolean = tokens.subList(0, name.length) == name.tokens +fun Name.startsWith(name: Name): Boolean = + this.length >= name.length && tokens.subList(0, name.length) == name.tokens -fun Name.endsWith(name: Name): Boolean = tokens.subList(length - name.length, length) == name.tokens \ No newline at end of file +fun Name.endsWith(name: Name): Boolean = + this.length >= name.length && tokens.subList(length - name.length, length) == name.tokens \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt index f044b018..28a5838c 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt @@ -1,5 +1,7 @@ package hep.dataforge.values +import kotlinx.serialization.Serializable + /** * The list of supported Value types. @@ -7,6 +9,7 @@ package hep.dataforge.values * Time value and binary value are represented by string * */ +@Serializable enum class ValueType { NUMBER, STRING, BOOLEAN, NULL } @@ -41,7 +44,7 @@ interface Value { * get this value represented as List */ val list: List<Value> - get() = if(this == Null) emptyList() else listOf(this) + get() = if (this == Null) emptyList() else listOf(this) override fun equals(other: Any?): Boolean @@ -228,6 +231,8 @@ fun FloatArray.asValue(): Value = if (isEmpty()) Null else ListValue(map { Numbe fun ByteArray.asValue(): Value = if (isEmpty()) Null else ListValue(map { NumberValue(it) }) +fun <E : Enum<E>> E.asValue(): Value = EnumValue(this) + /** * Create Value from String using closest match conversion diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt new file mode 100644 index 00000000..8055b554 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/ValueSerializer.kt @@ -0,0 +1,59 @@ +package hep.dataforge.values + +import hep.dataforge.meta.boolean +import hep.dataforge.meta.enum +import hep.dataforge.meta.string +import kotlinx.serialization.* +import kotlinx.serialization.builtins.list + +@Serializer(Value::class) +object ValueSerializer : KSerializer<Value> { + private val listSerializer by lazy { ValueSerializer.list } + + override val descriptor: SerialDescriptor = + SerialDescriptor("hep.dataforge.values.Value") { + boolean("isList") + enum<ValueType>("valueType") + string("value") + } + + private fun Decoder.decodeValue(): Value { + return when (decode(ValueType.serializer())) { + ValueType.NULL -> Null + ValueType.NUMBER -> decodeDouble().asValue() //TODO differentiate? + ValueType.BOOLEAN -> decodeBoolean().asValue() + ValueType.STRING -> decodeString().asValue() + } + } + + + override fun deserialize(decoder: Decoder): Value { + val isList = decoder.decodeBoolean() + return if (isList) { + listSerializer.deserialize(decoder).asValue() + } else { + decoder.decodeValue() + } + } + + private fun Encoder.encodeValue(value: Value) { + encode(ValueType.serializer(), value.type) + when (value.type) { + ValueType.NULL -> { + // do nothing + } + ValueType.NUMBER -> encodeDouble(value.double) + ValueType.BOOLEAN -> encodeBoolean(value.boolean) + ValueType.STRING -> encodeString(value.string) + } + } + + override fun serialize(encoder: Encoder, value: Value) { + encoder.encodeBoolean(value.isList()) + if (value.isList()) { + listSerializer.serialize(encoder, value.list) + } else { + encoder.encodeValue(value) + } + } +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt index a63d5ec1..3767e2fb 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/valueExtensions.kt @@ -1,7 +1,6 @@ package hep.dataforge.values import hep.dataforge.meta.Meta -import hep.dataforge.meta.buildMeta /** * Check if value is null @@ -22,6 +21,7 @@ val Value.boolean val Value.int get() = number.toInt() val Value.double get() = number.toDouble() val Value.float get() = number.toFloat() +val Value.short get() = number.toShort() val Value.long get() = number.toLong() val Value.stringList: List<String> get() = list.map { it.string } @@ -34,4 +34,4 @@ val Value.doubleArray: DoubleArray } -fun Value.toMeta() = buildMeta { Meta.VALUE_KEY put this } \ No newline at end of file +fun Value.toMeta() = Meta { Meta.VALUE_KEY put this } \ No newline at end of file diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt index 997a13e3..277c2a6c 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt @@ -1,5 +1,6 @@ package hep.dataforge.meta +import hep.dataforge.meta.scheme.* import kotlin.test.Test import kotlin.test.assertEquals @@ -10,26 +11,29 @@ class MetaDelegateTest { NO } + class InnerSpec : Scheme() { + var innerValue by string() + + companion object : SchemeSpec<InnerSpec>(::InnerSpec) + } + + class TestScheme : Scheme() { + var myValue by string() + var safeValue by double(2.2) + var enumValue by enum(TestEnum.YES) { enum<TestEnum>() } + var inner by spec(InnerSpec) + + companion object : SchemeSpec<TestScheme>(::TestScheme) + } + @Test fun delegateTest() { - class InnerSpec(override val config: Config) : Specific { - var innerValue by string() - } - - val innerSpec = specification(::InnerSpec) - - val testObject = object : Specific { - override val config: Config = Config() - var myValue by string() - var safeValue by double(2.2) - var enumValue by enum(TestEnum.YES) - var inner by spec(innerSpec) - } + val testObject = TestScheme.empty() testObject.config["myValue"] = "theString" testObject.enumValue = TestEnum.NO - testObject.inner = innerSpec.build { innerValue = "ddd"} + testObject.inner = InnerSpec { innerValue = "ddd" } assertEquals("theString", testObject.myValue) assertEquals(TestEnum.NO, testObject.enumValue) diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt index f2fffd19..0f4c19be 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaExtensionTest.kt @@ -11,12 +11,12 @@ class MetaExtensionTest { @Test fun testEnum(){ - val meta = buildMeta{"enum" put TestEnum.test} + val meta = Meta{"enum" put TestEnum.test} meta["enum"].enum<TestEnum>() } @Test fun testEnumByString(){ - val meta = buildMeta{"enum" put TestEnum.test.name} + val meta = Meta{"enum" put TestEnum.test.name} println(meta["enum"].enum<TestEnum>()) } diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt index fb424116..c55cd4ad 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt @@ -16,13 +16,13 @@ class MetaTest { @Test fun metaEqualityTest() { - val meta1 = buildMeta { + val meta1 = Meta { "a" put 22 "b" put { "c" put "ddd" } } - val meta2 = buildMeta { + val meta2 = Meta { "b" put { "c" put "ddd" } @@ -33,13 +33,13 @@ class MetaTest { @Test fun metaToMap(){ - val meta = buildMeta { + val meta = Meta { "a" put 22 "b" put { "c" put "ddd" } "list" put (0..4).map { - buildMeta { + Meta { "value" put it } } diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt index 9057782f..ba44edec 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MutableMetaTest.kt @@ -6,7 +6,7 @@ import kotlin.test.assertEquals class MutableMetaTest{ @Test fun testRemove(){ - val meta = buildMeta { + val meta = Meta { "aNode" put { "innerNode" put { "innerValue" put true @@ -14,7 +14,7 @@ class MutableMetaTest{ "b" put 22 "c" put "StringValue" } - }.toConfig() + }.asConfig() meta.remove("aNode.c") assertEquals(meta["aNode.c"], null) diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt similarity index 63% rename from dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt index a4cbe18e..bcebedc6 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SchemeTest.kt @@ -1,22 +1,27 @@ package hep.dataforge.meta +import hep.dataforge.meta.scheme.asScheme +import hep.dataforge.meta.scheme.getProperty import kotlin.test.Test import kotlin.test.assertEquals -class StyledTest{ +class SchemeTest{ @Test - fun testSNS(){ - val meta = buildMeta { + fun testMetaScheme(){ + val styled = Meta { repeat(10){ "b.a[$it]" put { "d" put it } } - }.seal().withStyle() + }.asScheme() + + val meta = styled.toMeta() + assertEquals(10, meta.values().count()) - val bNode = meta["b"].node + val bNode = styled.getProperty("b").node val aNodes = bNode?.getIndexed("a") diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt index 9098cf18..6d99101a 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/SpecificationTest.kt @@ -1,21 +1,29 @@ package hep.dataforge.meta +import hep.dataforge.meta.scheme.Scheme +import hep.dataforge.meta.scheme.Specification +import hep.dataforge.meta.scheme.numberList +import hep.dataforge.names.Name import kotlin.test.Test import kotlin.test.assertEquals class SpecificationTest { - class TestSpecific(override val config: Config) : Specific { + class TestStyled(config: Config, defaultProvider: (Name) -> MetaItem<*>?) : + Scheme(config, defaultProvider) { var list by numberList(1, 2, 3) - companion object : Specification<TestSpecific> { - override fun wrap(config: Config): TestSpecific = TestSpecific(config) + companion object : Specification<TestStyled> { + override fun wrap( + config: Config, + defaultProvider: (Name) -> MetaItem<*>? + ): TestStyled = TestStyled(config, defaultProvider) } } @Test - fun testSpecific(){ - val testObject = TestSpecific.build { + fun testSpecific() { + val testObject = TestStyled { list = emptyList() } assertEquals(emptyList(), testObject.list) diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt similarity index 72% rename from dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt index 79800ec5..1fa07382 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/descriptors/DescriptorTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/descriptors/DescriptorTest.kt @@ -1,4 +1,4 @@ -package hep.dataforge.descriptors +package hep.dataforge.meta.descriptors import hep.dataforge.values.ValueType import kotlin.test.Test @@ -6,15 +6,15 @@ import kotlin.test.assertEquals class DescriptorTest { - val descriptor = NodeDescriptor.build { - node("aNode") { + val descriptor = NodeDescriptor { + defineNode("aNode") { info = "A root demo node" - value("b") { + defineValue("b") { info = "b number value" type(ValueType.NUMBER) } - node("otherNode") { - value("otherValue") { + defineNode("otherNode") { + defineValue("otherValue") { type(ValueType.BOOLEAN) default(false) info = "default value" diff --git a/dataforge-output-html/build.gradle.kts b/dataforge-output/dataforge-output-html/build.gradle.kts similarity index 100% rename from dataforge-output-html/build.gradle.kts rename to dataforge-output/dataforge-output-html/build.gradle.kts diff --git a/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt b/dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt similarity index 86% rename from dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt rename to dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt index b54b7eb7..c0aeaaab 100644 --- a/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt +++ b/dataforge-output/dataforge-output-html/src/commonMain/kotlin/hep/dataforge/output/html/HtmlRenderer.kt @@ -3,7 +3,8 @@ package hep.dataforge.output.html import hep.dataforge.context.Context import hep.dataforge.meta.Meta import hep.dataforge.output.Output -import hep.dataforge.output.TextRenderer +import hep.dataforge.output.Renderer +import hep.dataforge.output.TextFormat import hep.dataforge.output.html.HtmlBuilder.Companion.HTML_CONVERTER_TYPE import hep.dataforge.provider.Type import hep.dataforge.provider.top @@ -14,11 +15,11 @@ import kotlinx.html.p import kotlin.reflect.KClass -class HtmlOutput<T : Any>(override val context: Context, private val consumer: TagConsumer<*>) : Output<T> { +class HtmlRenderer<T : Any>(override val context: Context, private val consumer: TagConsumer<*>) : Renderer<T> { private val cache = HashMap<KClass<*>, HtmlBuilder<*>>() /** - * Find the first [TextRenderer] matching the given object type. + * Find the first [TextFormat] matching the given object type. */ override fun render(obj: T, meta: Meta) { @@ -47,7 +48,7 @@ class HtmlOutput<T : Any>(override val context: Context, private val consumer: T } /** - * A text or binary renderer based on [kotlinx.io.core.Output] + * A text or binary renderer based on [Renderer] */ @Type(HTML_CONVERTER_TYPE) interface HtmlBuilder<T : Any> { diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt index d9f7c1b2..e88b29a5 100644 --- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt @@ -1,13 +1,9 @@ package hep.dataforge.output -import hep.dataforge.context.AbstractPlugin -import hep.dataforge.context.Context -import hep.dataforge.context.PluginFactory -import hep.dataforge.context.PluginTag +import hep.dataforge.context.* import hep.dataforge.context.PluginTag.Companion.DATAFORGE_GROUP import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta -import hep.dataforge.names.EmptyName import hep.dataforge.names.Name import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers @@ -22,14 +18,14 @@ interface OutputManager { * Get an output specialized for given type, name and stage. * @param stage represents the node or directory for the output. Empty means root node. * @param name represents the name inside the node. - * @param meta configuration for [Output] (not for rendered object) + * @param meta configuration for [Renderer] (not for rendered object) */ operator fun <T : Any> get( type: KClass<out T>, name: Name, - stage: Name = EmptyName, + stage: Name = Name.EMPTY, meta: Meta = EmptyMeta - ): Output<T> + ): Renderer<T> } /** @@ -42,28 +38,35 @@ val Context.output: OutputManager get() = plugins.get() ?: ConsoleOutputManager( */ inline operator fun <reified T : Any> OutputManager.get( name: Name, - stage: Name = EmptyName, + stage: Name = Name.EMPTY, meta: Meta = EmptyMeta -): Output<T> { +): Renderer<T> { return get(T::class, name, stage, meta) } /** * Directly render an object using the most suitable renderer */ -fun OutputManager.render(obj: Any, name: Name, stage: Name = EmptyName, meta: Meta = EmptyMeta) = +fun OutputManager.render(obj: Any, name: Name, stage: Name = Name.EMPTY, meta: Meta = EmptyMeta) = get(obj::class, name, stage).render(obj, meta) /** * System console output. - * The [ConsoleOutput] is used when no other [OutputManager] is provided. + * The [CONSOLE_RENDERER] is used when no other [OutputManager] is provided. */ -expect val ConsoleOutput: Output<Any> +val CONSOLE_RENDERER: Renderer<Any> = object : Renderer<Any> { + override fun render(obj: Any, meta: Meta) { + println(obj) + } + + override val context: Context get() = Global + +} class ConsoleOutputManager : AbstractPlugin(), OutputManager { override val tag: PluginTag get() = ConsoleOutputManager.tag - override fun <T : Any> get(type: KClass<out T>, name: Name, stage: Name, meta: Meta): Output<T> = ConsoleOutput + override fun <T : Any> get(type: KClass<out T>, name: Name, stage: Name, meta: Meta): Renderer<T> = CONSOLE_RENDERER companion object : PluginFactory<ConsoleOutputManager> { override val tag = PluginTag("output.console", group = DATAFORGE_GROUP) diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt similarity index 86% rename from dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt index 091cb999..c1bcf6e5 100644 --- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Renderer.kt @@ -7,11 +7,11 @@ import hep.dataforge.meta.Meta /** * A generic way to render any object in the output. * - * An object could be rendered either in append or overlay mode. The mode is decided by the [Output] + * An object could be rendered either in append or overlay mode. The mode is decided by the [Renderer] * based on its configuration and provided meta * */ -interface Output<in T : Any> : ContextAware { +interface Renderer<in T : Any> : ContextAware { /** * Render specific object with configuration. * diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt similarity index 56% rename from dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt index 91aa5024..cc9d24f3 100644 --- a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextRenderer.kt @@ -2,48 +2,50 @@ package hep.dataforge.output import hep.dataforge.context.Context import hep.dataforge.meta.Meta -import hep.dataforge.output.TextRenderer.Companion.TEXT_RENDERER_TYPE +import hep.dataforge.output.TextFormat.Companion.TEXT_RENDERER_TYPE import hep.dataforge.provider.Type import hep.dataforge.provider.top import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.io.Output +import kotlinx.io.text.writeUtf8String import kotlin.reflect.KClass -class TextOutput(override val context: Context, private val output: kotlinx.io.core.Output) : Output<Any> { - private val cache = HashMap<KClass<*>, TextRenderer>() +class TextRenderer(override val context: Context, private val output: Output) : Renderer<Any> { + private val cache = HashMap<KClass<*>, TextFormat>() /** - * Find the first [TextRenderer] matching the given object type. + * Find the first [TextFormat] matching the given object type. */ override fun render(obj: Any, meta: Meta) { - val renderer: TextRenderer = if (obj is CharSequence) { - DefaultTextRenderer + val format: TextFormat = if (obj is CharSequence) { + DefaultTextFormat } else { val value = cache[obj::class] if (value == null) { val answer = - context.top<TextRenderer>(TEXT_RENDERER_TYPE).values.firstOrNull { it.type.isInstance(obj) } + context.top<TextFormat>(TEXT_RENDERER_TYPE).values.firstOrNull { it.type.isInstance(obj) } if (answer != null) { cache[obj::class] = answer answer } else { - DefaultTextRenderer + DefaultTextFormat } } else { value } } context.launch(Dispatchers.Output) { - renderer.run { output.render(obj) } + format.run { output.render(obj) } } } } /** - * A text or binary renderer based on [kotlinx.io.core.Output] + * A text or binary renderer based on [Output] */ @Type(TEXT_RENDERER_TYPE) -interface TextRenderer { +interface TextFormat { /** * The priority of this renderer compared to other renderers */ @@ -53,19 +55,18 @@ interface TextRenderer { */ val type: KClass<*> - suspend fun kotlinx.io.core.Output.render(obj: Any) + suspend fun Output.render(obj: Any) companion object { const val TEXT_RENDERER_TYPE = "dataforge.textRenderer" } } -object DefaultTextRenderer : TextRenderer { +object DefaultTextFormat : TextFormat { override val priority: Int = Int.MAX_VALUE override val type: KClass<*> = Any::class - override suspend fun kotlinx.io.core.Output.render(obj: Any) { - append(obj.toString()) - append('\n') + override suspend fun Output.render(obj: Any) { + writeUtf8String(obj.toString() + "\n") } } \ No newline at end of file diff --git a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt deleted file mode 100644 index b927a386..00000000 --- a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt +++ /dev/null @@ -1,22 +0,0 @@ -package hep.dataforge.output - -import hep.dataforge.context.Context -import hep.dataforge.context.Global -import hep.dataforge.meta.Meta -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.Dispatchers - -/** - * System console output. - * The [ConsoleOutput] is used when no other [OutputManager] is provided. - */ -actual val ConsoleOutput: Output<Any> = object : Output<Any> { - override fun render(obj: Any, meta: Meta) { - println(obj) - } - - override val context: Context get() = Global - -} - -actual val Dispatchers.Output: CoroutineDispatcher get() = Dispatchers.Default \ No newline at end of file diff --git a/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt new file mode 100644 index 00000000..18a71f07 --- /dev/null +++ b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/outputJS.kt @@ -0,0 +1,7 @@ +package hep.dataforge.output + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers + + +actual val Dispatchers.Output: CoroutineDispatcher get() = Dispatchers.Default \ No newline at end of file diff --git a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt deleted file mode 100644 index 57ae4294..00000000 --- a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt +++ /dev/null @@ -1,13 +0,0 @@ -package hep.dataforge.output - -import hep.dataforge.context.Global -import kotlinx.coroutines.Dispatchers -import kotlinx.io.streams.asOutput - -/** - * System console output. - * The [ConsoleOutput] is used when no other [OutputManager] is provided. - */ -actual val ConsoleOutput: Output<Any> = TextOutput(Global, System.out.asOutput()) - -actual val Dispatchers.Output get() = Dispatchers.IO \ No newline at end of file diff --git a/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt new file mode 100644 index 00000000..ea7c416c --- /dev/null +++ b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/outputJVM.kt @@ -0,0 +1,5 @@ +package hep.dataforge.output + +import kotlinx.coroutines.Dispatchers + +actual val Dispatchers.Output get() = Dispatchers.IO \ No newline at end of file diff --git a/dataforge-scripting/build.gradle.kts b/dataforge-scripting/build.gradle.kts index 757f0c33..c848c1b1 100644 --- a/dataforge-scripting/build.gradle.kts +++ b/dataforge-scripting/build.gradle.kts @@ -19,8 +19,6 @@ kotlin { } val jvmTest by getting { dependencies { - implementation(kotlin("test")) - implementation(kotlin("test-junit")) implementation("ch.qos.logback:logback-classic:1.2.3") } } diff --git a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt index 5a9ba56d..9fb1c919 100644 --- a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt +++ b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt @@ -3,10 +3,11 @@ package hep.dataforge.scripting import hep.dataforge.context.Global import hep.dataforge.meta.get import hep.dataforge.meta.int +import hep.dataforge.meta.scheme.int import hep.dataforge.workspace.SimpleWorkspaceBuilder import hep.dataforge.workspace.context import hep.dataforge.workspace.target -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals diff --git a/dataforge-tables/build.gradle b/dataforge-tables/build.gradle deleted file mode 100644 index a0011494..00000000 --- a/dataforge-tables/build.gradle +++ /dev/null @@ -1,25 +0,0 @@ -plugins { - id "org.jetbrains.kotlin.multiplatform" -} - -repositories { - jcenter() -} - -kotlin { - targets { - fromPreset(presets.jvm, 'jvm') - //fromPreset(presets.js, 'js') - // For ARM, preset should be changed to presets.iosArm32 or presets.iosArm64 - // For Linux, preset should be changed to e.g. presets.linuxX64 - // For MacOS, preset should be changed to e.g. presets.macosX64 - //fromPreset(presets.iosX64, 'ios') - } - sourceSets { - commonMain { - dependencies { - api project(":dataforge-context") - } - } - } -} \ No newline at end of file diff --git a/dataforge-tables/build.gradle.kts b/dataforge-tables/build.gradle.kts new file mode 100644 index 00000000..6c0a1e53 --- /dev/null +++ b/dataforge-tables/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id("scientifik.mpp") +} + +kotlin { + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-context")) + api(project(":dataforge-io")) + } + } + } +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt new file mode 100644 index 00000000..2df51d41 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnDef.kt @@ -0,0 +1,10 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlin.reflect.KClass + +data class ColumnDef<out T : Any>( + override val name: String, + override val type: KClass<out T>, + override val meta: Meta +): ColumnHeader<T> \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt new file mode 100644 index 00000000..2023d11b --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnHeader.kt @@ -0,0 +1,36 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.int +import hep.dataforge.meta.string +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType +import kotlin.reflect.KClass + +typealias TableHeader<C> = List<ColumnHeader<C>> + +typealias ValueTableHeader = List<ColumnHeader<Value>> + +interface ColumnHeader<out T : Any> { + val name: String + val type: KClass<out T> + val meta: Meta +} + +data class SimpleColumnHeader<T : Any>( + override val name: String, + override val type: KClass<out T>, + override val meta: Meta +) : ColumnHeader<T> + +val ColumnHeader<Value>.valueType: ValueType? get() = meta["valueType"].string?.let { ValueType.valueOf(it) } + +val ColumnHeader<Value>.textWidth: Int + get() = meta["columnWidth"].int ?: when (valueType) { + ValueType.NUMBER -> 8 + ValueType.STRING -> 16 + ValueType.BOOLEAN -> 5 + ValueType.NULL -> 5 + null -> 16 + } diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt new file mode 100644 index 00000000..7d364784 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnScheme.kt @@ -0,0 +1,18 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.enum +import hep.dataforge.meta.scheme.Scheme +import hep.dataforge.meta.scheme.SchemeSpec +import hep.dataforge.meta.scheme.enum +import hep.dataforge.meta.scheme.string +import hep.dataforge.values.ValueType + +open class ColumnScheme : Scheme() { + var title by string() + + companion object : SchemeSpec<ColumnScheme>(::ColumnScheme) +} + +class ValueColumnScheme : ColumnScheme() { + var valueType by enum(ValueType.STRING){enum<ValueType>()} +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt new file mode 100644 index 00000000..dbb90cf2 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ColumnTable.kt @@ -0,0 +1,31 @@ +package hep.dataforge.tables + +import kotlin.reflect.KClass + +/** + * @param C bottom type for all columns in the table + */ +class ColumnTable<C : Any>(override val columns: Collection<Column<C>>) : Table<C> { + private val rowsNum = columns.first().size + + init { + require(columns.all { it.size == rowsNum }) { "All columns must be of the same size" } + } + + override val rows: List<Row<C>> + get() = (0 until rowsNum).map { VirtualRow(this, it) } + + override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? { + val value = columns[column]?.get(row) + return type.cast(value) + } +} + +internal class VirtualRow<C : Any>(val table: Table<C>, val index: Int) : Row<C> { + override fun <T : C> getValue(column: String, type: KClass<out T>): T? = table.getValue(index, column, type) + +// override fun <T : C> get(columnHeader: ColumnHeader<T>): T? { +// return table.co[columnHeader][index] +// } +} + diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt new file mode 100644 index 00000000..fc7f03ea --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/ListColumn.kt @@ -0,0 +1,35 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlin.reflect.KClass + +class ListColumn<T : Any>( + override val name: String, + private val data: List<T?>, + override val type: KClass<out T>, + override val meta: Meta +) : Column<T> { + override val size: Int get() = data.size + + override fun get(index: Int): T? = data[index] + + companion object { + inline operator fun <reified T : Any> invoke( + name: String, + def: ColumnScheme, + data: List<T?> + ): ListColumn<T> = ListColumn(name, data, T::class, def.toMeta()) + + inline operator fun <reified T : Any> invoke( + name: String, + def: ColumnScheme, + size: Int, + dataBuilder: (Int) -> T? + ): ListColumn<T> = invoke(name, def, List(size, dataBuilder)) + } +} + +inline fun <T : Any, reified R : Any> Column<T>.map(meta: Meta = this.meta, noinline block: (T?) -> R): Column<R> { + val data = List(size) { block(get(it)) } + return ListColumn(name, data, R::class, meta) +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt new file mode 100644 index 00000000..04c7d3a4 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MapRow.kt @@ -0,0 +1,10 @@ +package hep.dataforge.tables + +import kotlin.reflect.KClass + +inline class MapRow<C: Any>(val values: Map<String, C?>) : Row<C> { + override fun <T : C> getValue(column: String, type: KClass<out T>): T? { + val value = values[column] + return type.cast(value) + } +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt new file mode 100644 index 00000000..8a9b06bc --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableColumnTable.kt @@ -0,0 +1,61 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlin.reflect.KClass + +/** + * Mutable table with a fixed size, but dynamic columns + */ +class MutableColumnTable<C: Any>(val size: Int) : Table<C> { + private val _columns = ArrayList<Column<C>>() + + override val columns: List<Column<C>> get() = _columns + override val rows: List<Row<C>> get() = (0 until size).map { + VirtualRow(this, it) + } + + override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? { + val value = columns[column]?.get(row) + return type.cast(value) + } + + /** + * Add a fixed column to the end of the table + */ + fun add(column: Column<C>) { + require(column.size == this.size) { "Required column size $size, but found ${column.size}" } + _columns.add(column) + } + + /** + * Insert a column at [index] + */ + fun insert(index: Int, column: Column<C>) { + require(column.size == this.size) { "Required column size $size, but found ${column.size}" } + _columns.add(index, column) + } +} + +class MapColumn<T : Any, R : Any>( + val source: Column<T>, + override val type: KClass<out R>, + override val name: String, + override val meta: Meta = source.meta, + val mapper: (T?) -> R? +) : Column<R> { + override val size: Int get() = source.size + + override fun get(index: Int): R? = mapper(source[index]) +} + +class CachedMapColumn<T : Any, R : Any>( + val source: Column<T>, + override val type: KClass<out R>, + override val name: String, + override val meta: Meta = source.meta, + val mapper: (T?) -> R? +) : Column<R> { + override val size: Int get() = source.size + private val values: HashMap<Int, R?> = HashMap() + override fun get(index: Int): R? = values.getOrPut(index) { mapper(source[index]) } +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt new file mode 100644 index 00000000..aa2d9abf --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/MutableTable.kt @@ -0,0 +1,40 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import hep.dataforge.values.Value +import kotlin.reflect.KClass + +class MutableTable<C : Any>( + override val rows: MutableList<Row<C>>, + override val header: MutableList<ColumnHeader<C>> +) : RowTable<C>(rows, header) { + + fun <T : C> column(name: String, type: KClass<out T>, meta: Meta): ColumnHeader<T> { + val column = SimpleColumnHeader(name, type, meta) + header.add(column) + return column + } + + inline fun <reified T : C> column( + name: String, + noinline columnMetaBuilder: ColumnScheme.() -> Unit = {} + ): ColumnHeader<T> { + return column(name, T::class, ColumnScheme(columnMetaBuilder).toMeta()) + } + + fun row(map: Map<String, C?>): Row<C> { + val row = MapRow(map) + rows.add(row) + return row + } + + fun <T : C> row(vararg pairs: Pair<ColumnHeader<T>, T>): Row<C> = + row(pairs.associate { it.first.name to it.second }) +} + +fun MutableTable<Value>.row(vararg pairs: Pair<ColumnHeader<Value>, Any?>): Row<Value> = + row(pairs.associate { it.first.name to Value.of(it.second) }) + +fun <C : Any> Table<C>.edit(block: MutableTable<C>.() -> Unit): Table<C> { + return MutableTable(rows.toMutableList(), header.toMutableList()).apply(block) +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt new file mode 100644 index 00000000..1bd8d6a3 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/NumberColumn.kt @@ -0,0 +1,94 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlin.reflect.KClass + +//interface NumberColumn<N : Number> : Column<N> + +data class RealColumn( + override val name: String, + val data: DoubleArray, + override val meta: Meta = Meta.EMPTY +) : Column<Double> { + override val type: KClass<out Double> get() = Double::class + + override val size: Int get() = data.size + + @Suppress("OVERRIDE_BY_INLINE", "NOTHING_TO_INLINE") + override inline fun get(index: Int): Double = data[index] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is RealColumn) return false + + if (name != other.name) return false + if (!data.contentEquals(other.data)) return false + if (meta != other.meta) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + data.contentHashCode() + result = 31 * result + meta.hashCode() + return result + } + + companion object { + inline operator fun <reified T : Any> invoke( + name: String, + data: DoubleArray, + noinline metaBuilder: ColumnScheme.() -> Unit + ): RealColumn = RealColumn(name, data, ColumnScheme(metaBuilder).toMeta()) + } +} + +fun <T : Any> Column<T>.map(meta: Meta = this.meta, block: (T?) -> Double): RealColumn { + val data = DoubleArray(size) { block(get(it)) } + return RealColumn(name, data, meta) +} + +data class IntColumn( + override val name: String, + val data: IntArray, + override val meta: Meta = Meta.EMPTY +) : Column<Int> { + override val type: KClass<out Int> get() = Int::class + + override val size: Int get() = data.size + + @Suppress("OVERRIDE_BY_INLINE", "NOTHING_TO_INLINE") + override inline fun get(index: Int): Int = data[index] + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is IntColumn) return false + + if (name != other.name) return false + if (!data.contentEquals(other.data)) return false + if (meta != other.meta) return false + + return true + } + + override fun hashCode(): Int { + var result = name.hashCode() + result = 31 * result + data.contentHashCode() + result = 31 * result + meta.hashCode() + return result + } + + companion object { + inline operator fun <reified T : Any> invoke( + name: String, + data: IntArray, + noinline metaBuilder: ColumnScheme.() -> Unit + ): IntColumn = IntColumn(name, data, ColumnScheme(metaBuilder).toMeta()) + } +} + +fun <T : Any> Column<T>.map(meta: Meta = this.meta, block: (T?) -> Int): IntColumn { + val data = IntArray(size) { block(get(it)) } + return IntColumn(name, data, meta) +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt new file mode 100644 index 00000000..c565d9cd --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/RowTable.kt @@ -0,0 +1,23 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlinx.coroutines.flow.toList +import kotlin.reflect.KClass + +internal class RowTableColumn<C : Any, T : C>(val table: Table<C>, val header: ColumnHeader<T>) : Column<T> { + override val name: String get() = header.name + override val type: KClass<out T> get() = header.type + override val meta: Meta get() = header.meta + override val size: Int get() = table.rows.size + + override fun get(index: Int): T? = table.rows[index].getValue(name, type) +} + +open class RowTable<C : Any>(override val rows: List<Row<C>>, override val header: List<ColumnHeader<C>>) : Table<C> { + override fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? = + rows[row].getValue(column, type) + + override val columns: List<Column<C>> get() = header.map { RowTableColumn(this, it) } +} + +suspend fun <C : Any> Rows<C>.collect(): Table<C> = this as? Table<C> ?: RowTable(rowFlow().toList(), header) \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt new file mode 100644 index 00000000..bba90d38 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/Table.kt @@ -0,0 +1,68 @@ +package hep.dataforge.tables + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlin.reflect.KClass + +//TODO to be removed in 1.3.70 +@Suppress("UNCHECKED_CAST") +internal fun <T : Any> KClass<T>.cast(value: Any?): T? { + return when { + value == null -> null + !isInstance(value) -> error("Expected type is $this, but found ${value::class}") + else -> value as T + } +} + +/** + * Finite or infinite row set. Rows are produced in a lazy suspendable [Flow]. + * Each row must contain at least all the fields mentioned in [header]. + */ +interface Rows<C : Any> { + val header: TableHeader<C> + fun rowFlow(): Flow<Row<C>> +} + +interface Table<C : Any> : Rows<C> { + fun <T : C> getValue(row: Int, column: String, type: KClass<out T>): T? + val columns: Collection<Column<C>> + override val header: TableHeader<C> get() = columns.toList() + val rows: List<Row<C>> + override fun rowFlow(): Flow<Row<C>> = rows.asFlow() + + /** + * Apply typed query to this table and return lazy [Flow] of resulting rows. The flow could be empty. + */ + //fun select(query: Any): Flow<Row> = error("Query of type ${query::class} is not supported by this table") + companion object { + inline operator fun <T : Any> invoke(block: MutableTable<T>.() -> Unit): Table<T> = + MutableTable<T>(arrayListOf(), arrayListOf()).apply(block) + } +} + +operator fun Collection<Column<*>>.get(name: String): Column<*>? = find { it.name == name } + +inline operator fun <C : Any, reified T : C> Table<C>.get(row: Int, column: String): T? = + getValue(row, column, T::class) + +operator fun <C : Any, T : C> Table<C>.get(row: Int, column: ColumnHeader<T>): T? = getValue(row, column.name, column.type) + +interface Column<T : Any> : ColumnHeader<T> { + val size: Int + operator fun get(index: Int): T? +} + +val Column<*>.indices get() = (0 until size) + +operator fun <T : Any> Column<T>.iterator() = iterator { + for (i in indices) { + yield(get(i)) + } +} + +interface Row<C: Any> { + fun <T : C> getValue(column: String, type: KClass<out T>): T? +} + +inline operator fun <C : Any, reified T : C> Row<C>.get(column: String): T? = getValue(column, T::class) +operator fun <C : Any, T : C> Row<C>.get(column: ColumnHeader<T>): T? = getValue(column.name, column.type) \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt new file mode 100644 index 00000000..ae3c386e --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/TextRows.kt @@ -0,0 +1,146 @@ +package hep.dataforge.tables.io + +import hep.dataforge.tables.* +import hep.dataforge.values.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.toList +import kotlinx.io.Binary +import kotlinx.io.ExperimentalIoApi +import kotlinx.io.Output +import kotlinx.io.RandomAccessBinary +import kotlinx.io.text.forEachUtf8Line +import kotlinx.io.text.readUtf8Line +import kotlinx.io.text.readUtf8StringUntilDelimiter +import kotlinx.io.text.writeUtf8String +import kotlin.reflect.KClass + +/** + * Read a lin as a fixed width [Row] + */ +private fun readLine(header: ValueTableHeader, line: String): Row<Value> { + val values = line.trim().split("\\s+".toRegex()).map { it.lazyParseValue() } + + if (values.size == header.size) { + val map = header.map { it.name }.zip(values).toMap() + return MapRow(map) + } else { + error("Can't read line \"$line\". Expected ${header.size} values in a line, but found ${values.size}") + } +} + +/** + * Finite or infinite [Rows] created from a fixed width text binary + */ +@ExperimentalIoApi +class TextRows(override val header: ValueTableHeader, val binary: Binary) : Rows<Value> { + + /** + * A flow of indexes of string start offsets ignoring empty strings + */ + fun indexFlow(): Flow<Int> = binary.read { + var counter: Int = 0 + flow { + val string = readUtf8StringUntilDelimiter('\n') + counter += string.length + if (!string.isBlank()) { + emit(counter) + } + } + } + + override fun rowFlow(): Flow<Row<Value>> = binary.read { + flow { + forEachUtf8Line { line -> + if (line.isNotBlank()) { + val row = readLine(header, line) + emit(row) + } + } + } + } + + companion object +} + +/** + * Create a row offset index for [TextRows] + */ +@ExperimentalIoApi +suspend fun TextRows.buildRowIndex(): List<Int> = indexFlow().toList() + +/** + * Finite table created from [RandomAccessBinary] with fixed width text table + */ +@ExperimentalIoApi +class TextTable( + override val header: ValueTableHeader, + val binary: RandomAccessBinary, + val index: List<Int> +) : Table<Value> { + + override val columns: Collection<Column<Value>> get() = header.map { RowTableColumn(this, it) } + + override val rows: List<Row<Value>> get() = index.map { readAt(it) } + + override fun rowFlow(): Flow<Row<Value>> = TextRows(header, binary).rowFlow() + + private fun readAt(offset: Int): Row<Value> { + return binary.read(offset) { + val line = readUtf8Line() + return@read readLine(header, line) + } + } + + override fun <T : Value> getValue(row: Int, column: String, type: KClass<out T>): T? { + val offset = index[row] + return type.cast(readAt(offset)[column]) + } + + companion object { + suspend operator fun invoke(header: ValueTableHeader, binary: RandomAccessBinary): TextTable { + val index = TextRows(header, binary).buildRowIndex() + return TextTable(header, binary, index) + } + } +} + + +/** + * Write a fixed width value to the output + */ +private fun Output.writeValue(value: Value, width: Int, left: Boolean = true) { + require(width > 5) { "Width could not be less than 5" } + val str: String = when (value.type) { + ValueType.NUMBER -> value.number.toString() //TODO apply decimal format + ValueType.STRING -> value.string.take(width) + ValueType.BOOLEAN -> if (value.boolean) { + "true" + } else { + "false" + } + ValueType.NULL -> "@null" + } + val padded = if (left) { + str.padEnd(width) + } else { + str.padStart(width) + } + writeUtf8String(padded) +} + +/** + * Write rows without header to the output + */ +suspend fun Output.writeRows(rows: Rows<Value>) { + val widths: List<Int> = rows.header.map { + it.textWidth + } + rows.rowFlow().collect { row -> + rows.header.forEachIndexed { index, columnHeader -> + writeValue(row[columnHeader] ?: Null, widths[index]) + } + writeUtf8String("\r\n") + } +} \ No newline at end of file diff --git a/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt new file mode 100644 index 00000000..1180ca23 --- /dev/null +++ b/dataforge-tables/src/commonMain/kotlin/hep/dataforge/tables/io/textTableEnvelope.kt @@ -0,0 +1,42 @@ +package hep.dataforge.tables.io + +import hep.dataforge.io.Envelope +import hep.dataforge.meta.* +import hep.dataforge.tables.SimpleColumnHeader +import hep.dataforge.tables.Table +import hep.dataforge.values.Value +import kotlinx.io.ByteArrayOutput +import kotlinx.io.EmptyBinary +import kotlinx.io.ExperimentalIoApi +import kotlinx.io.asBinary + + +@ExperimentalIoApi +suspend fun Table<Value>.wrap(): Envelope = Envelope { + meta { + header.forEachIndexed { index, columnHeader -> + set("column", index.toString(), Meta { + "name" put columnHeader.name + if (!columnHeader.meta.isEmpty()) { + "meta" put columnHeader.meta + } + }) + } + } + + type = "table.value" + dataID = "valueTable[${this@wrap.hashCode()}]" + + data = ByteArrayOutput().apply { writeRows(this@wrap) }.toByteArray().asBinary() +} + +@DFExperimental +@ExperimentalIoApi +fun TextRows.Companion.readEnvelope(envelope: Envelope): TextRows { + val header = envelope.meta.getIndexed("column") + .entries.sortedBy { it.key.toInt() } + .map { (_, item) -> + SimpleColumnHeader(item.node["name"].string!!, Value::class, item.node["meta"].node ?: Meta.EMPTY) + } + return TextRows(header, envelope.data ?: EmptyBinary) +} \ No newline at end of file diff --git a/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt b/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt new file mode 100644 index 00000000..a0bcb75e --- /dev/null +++ b/dataforge-tables/src/jvmMain/kotlin/hep/dataforge/tables/CastColumn.kt @@ -0,0 +1,36 @@ +package hep.dataforge.tables + +import hep.dataforge.meta.Meta +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.full.cast +import kotlin.reflect.full.isSubclassOf + +@Suppress("UNCHECKED_CAST") +fun <T : Any> Column<*>.cast(type: KClass<out T>): Column<T> { + return if (type.isSubclassOf(this.type)) { + this as Column<T> + } else { + CastColumn(this, type) + } +} + +class CastColumn<T : Any>(val origin: Column<*>, override val type: KClass<out T>) : Column<T> { + override val name: String get() = origin.name + override val meta: Meta get() = origin.meta + override val size: Int get() = origin.size + + + override fun get(index: Int): T? = type.cast(origin[index]) +} + +class ColumnProperty<C: Any, T : C>(val table: Table<C>, val type: KClass<T>) : ReadOnlyProperty<Any?, Column<T>> { + override fun getValue(thisRef: Any?, property: KProperty<*>): Column<T> { + val name = property.name + return (table.columns[name] ?: error("Column with name $name not found in the table")).cast(type) + } +} + +operator fun <C: Any, T : C> Collection<Column<C>>.get(header: ColumnHeader<T>): Column<T>? = + find { it.name == header.name }?.cast(header.type) diff --git a/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt b/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt new file mode 100644 index 00000000..02d97caf --- /dev/null +++ b/dataforge-tables/src/jvmTest/kotlin/hep/dataforge/tables/io/TextRowsTest.kt @@ -0,0 +1,39 @@ +package hep.dataforge.tables.io + +import hep.dataforge.meta.DFExperimental +import hep.dataforge.tables.Table +import hep.dataforge.tables.get +import hep.dataforge.tables.row +import hep.dataforge.values.Value +import hep.dataforge.values.int +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.runBlocking +import kotlinx.io.ExperimentalIoApi +import kotlinx.io.toByteArray +import kotlin.test.Test +import kotlin.test.assertEquals + + +@DFExperimental +@ExperimentalIoApi +class TextRowsTest { + val table = Table<Value> { + val a = column<Value>("a") + val b = column<Value>("b") + row(a to 1, b to "b1") + row(a to 2, b to "b2") + } + + @Test + fun testTableWriteRead() { + runBlocking { + val envelope = table.wrap() + val string = envelope.data!!.toByteArray().decodeToString() + println(string) + val table = TextRows.readEnvelope(envelope) + val rows = table.rowFlow().toList() + assertEquals(1, rows[0]["a"]?.int) + assertEquals("b2", rows[1]["b"]?.string) + } + } +} \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt index ddb53d5d..ed5ab7f0 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt @@ -5,8 +5,10 @@ import hep.dataforge.data.DataNode import hep.dataforge.data.filter import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaRepr -import hep.dataforge.meta.buildMeta -import hep.dataforge.names.* +import hep.dataforge.names.Name +import hep.dataforge.names.asName +import hep.dataforge.names.isEmpty +import hep.dataforge.names.plus /** * A dependency of the task which allows to lazily create a data tree for single dependency @@ -15,7 +17,7 @@ sealed class Dependency : MetaRepr { abstract fun apply(workspace: Workspace): DataNode<Any> } -class DataDependency(val filter: DataFilter, val placement: Name = EmptyName) : Dependency() { +class DataDependency(val filter: DataFilter, val placement: Name = Name.EMPTY) : Dependency() { override fun apply(workspace: Workspace): DataNode<Any> { val result = workspace.data.filter(filter) return if (placement.isEmpty()) { @@ -25,20 +27,20 @@ class DataDependency(val filter: DataFilter, val placement: Name = EmptyName) : } } - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "data" put filter.config "to" put placement.toString() } } -class AllDataDependency(val placement: Name = EmptyName) : Dependency() { +class AllDataDependency(val placement: Name = Name.EMPTY) : Dependency() { override fun apply(workspace: Workspace): DataNode<Any> = if (placement.isEmpty()) { workspace.data } else { DataNode.invoke(Any::class) { this[placement] = workspace.data } } - override fun toMeta() = buildMeta { + override fun toMeta() = Meta { "data" put "@all" "to" put placement.toString() } @@ -46,7 +48,7 @@ class AllDataDependency(val placement: Name = EmptyName) : Dependency() { abstract class TaskDependency<out T : Any>( val meta: Meta, - val placement: Name = EmptyName + val placement: Name = Name.EMPTY ) : Dependency() { abstract fun resolveTask(workspace: Workspace): Task<T> @@ -66,7 +68,7 @@ abstract class TaskDependency<out T : Any>( } } - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "task" put name.toString() "meta" put meta "to" put placement.toString() diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt index ff499888..4e0ca715 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt @@ -1,7 +1,7 @@ package hep.dataforge.workspace import hep.dataforge.data.DataNode -import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.node diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt index 0b11a7d5..7511bda9 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt @@ -2,7 +2,7 @@ package hep.dataforge.workspace import hep.dataforge.context.Named import hep.dataforge.data.DataNode -import hep.dataforge.descriptors.Described +import hep.dataforge.meta.descriptors.Described import hep.dataforge.meta.Meta import hep.dataforge.provider.Type import hep.dataforge.workspace.Task.Companion.TYPE diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt index 80d89e24..7f359914 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt @@ -2,18 +2,18 @@ package hep.dataforge.workspace import hep.dataforge.context.Context import hep.dataforge.data.* -import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.descriptors.NodeDescriptor +import hep.dataforge.meta.DFBuilder import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.string -import hep.dataforge.names.EmptyName import hep.dataforge.names.Name import hep.dataforge.names.isEmpty import hep.dataforge.names.toName import kotlin.jvm.JvmName import kotlin.reflect.KClass -@TaskBuildScope +@DFBuilder class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) { private var modelTransform: TaskModelBuilder.(Meta) -> Unit = { allData() } // private val additionalDependencies = HashSet<Dependency>() @@ -56,7 +56,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) { block: TaskEnv.(DataNode<*>) -> DataNode<R> ) { dataTransforms += DataTransformation(from, to) { context, model, data -> - val env = TaskEnv(EmptyName, model.meta, context, data) + val env = TaskEnv(Name.EMPTY, model.meta, context, data) env.block(data) } } @@ -69,7 +69,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) { ) { dataTransforms += DataTransformation(from, to) { context, model, data -> data.ensureType(inputType) - val env = TaskEnv(EmptyName, model.meta, context, data) + val env = TaskEnv(Name.EMPTY, model.meta, context, data) env.block(data.cast(inputType)) } } @@ -200,7 +200,7 @@ class TaskBuilder<R : Any>(val name: Name, val type: KClass<out R>) { * Use DSL to create a descriptor for this task */ fun description(transform: NodeDescriptor.() -> Unit) { - this.descriptor = NodeDescriptor.build(transform) + this.descriptor = NodeDescriptor(transform) } internal fun build(): GenericTask<R> { diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt index b4ccb7ae..a811f428 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt @@ -9,7 +9,6 @@ import hep.dataforge.data.DataFilter import hep.dataforge.data.DataTree import hep.dataforge.data.DataTreeBuilder import hep.dataforge.meta.* -import hep.dataforge.names.EmptyName import hep.dataforge.names.Name import hep.dataforge.names.asName import hep.dataforge.names.toName @@ -30,7 +29,7 @@ data class TaskModel( //TODO provide a way to get task descriptor //TODO add pre-run check of task result type? - override fun toMeta(): Meta = buildMeta { + override fun toMeta(): Meta = Meta { "name" put name.toString() "meta" put meta "dependsOn" put { @@ -58,9 +57,6 @@ fun TaskModel.buildInput(workspace: Workspace): DataTree<Any> { }.build() } -@DslMarker -annotation class TaskBuildScope - interface TaskDependencyContainer { val defaultMeta: Meta fun add(dependency: Dependency) @@ -71,21 +67,21 @@ interface TaskDependencyContainer { */ fun TaskDependencyContainer.dependsOn( name: Name, - placement: Name = EmptyName, + placement: Name = Name.EMPTY, meta: Meta = defaultMeta ): WorkspaceTaskDependency = WorkspaceTaskDependency(name, meta, placement).also { add(it) } fun TaskDependencyContainer.dependsOn( name: String, - placement: Name = EmptyName, + placement: Name = Name.EMPTY, meta: Meta = defaultMeta ): WorkspaceTaskDependency = dependsOn(name.toName(), placement, meta) fun <T : Any> TaskDependencyContainer.dependsOn( task: Task<T>, - placement: Name = EmptyName, + placement: Name = Name.EMPTY, meta: Meta = defaultMeta ): DirectTaskDependency<T> = DirectTaskDependency(task, meta, placement).also { add(it) } @@ -99,16 +95,16 @@ fun <T : Any> TaskDependencyContainer.dependsOn( fun <T : Any> TaskDependencyContainer.dependsOn( task: Task<T>, - placement: Name = EmptyName, + placement: Name = Name.EMPTY, metaBuilder: MetaBuilder.() -> Unit ): DirectTaskDependency<T> = - dependsOn(task, placement, buildMeta(metaBuilder)) + dependsOn(task, placement, Meta(metaBuilder)) /** * Add custom data dependency */ fun TaskDependencyContainer.data(action: DataFilter.() -> Unit): DataDependency = - DataDependency(DataFilter.build(action)).also { add(it) } + DataDependency(DataFilter(action)).also { add(it) } /** * User-friendly way to add data dependency @@ -123,7 +119,7 @@ fun TaskDependencyContainer.data(pattern: String? = null, from: String? = null, /** * Add all data as root node */ -fun TaskDependencyContainer.allData(to: Name = EmptyName) = AllDataDependency(to).also { add(it) } +fun TaskDependencyContainer.allData(to: Name = Name.EMPTY) = AllDataDependency(to).also { add(it) } /** * A builder for [TaskModel] diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt index 31da6c56..ac2b1131 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt @@ -8,7 +8,6 @@ import hep.dataforge.data.DataNode import hep.dataforge.data.dataSequence import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.buildMeta import hep.dataforge.names.Name import hep.dataforge.names.toName import hep.dataforge.provider.Provider @@ -76,7 +75,7 @@ fun Workspace.run(task: String, meta: Meta) = tasks[task.toName()]?.let { run(it, meta) } ?: error("Task with name $task not found") fun Workspace.run(task: String, block: MetaBuilder.() -> Unit = {}) = - run(task, buildMeta(block)) + run(task, Meta(block)) fun <T: Any> Workspace.run(task: Task<T>, metaBuilder: MetaBuilder.() -> Unit = {}): DataNode<T> = - run(task, buildMeta(metaBuilder)) \ No newline at end of file + run(task, Meta(metaBuilder)) \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt index 2f717f78..3bc1ffcf 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt @@ -5,14 +5,13 @@ import hep.dataforge.context.ContextBuilder import hep.dataforge.data.DataNode import hep.dataforge.data.DataTreeBuilder import hep.dataforge.meta.* -import hep.dataforge.names.EmptyName import hep.dataforge.names.Name import hep.dataforge.names.isEmpty import hep.dataforge.names.toName import kotlin.jvm.JvmName import kotlin.reflect.KClass -@TaskBuildScope +@DFBuilder interface WorkspaceBuilder { val parentContext: Context var context: Context @@ -32,7 +31,7 @@ fun WorkspaceBuilder.context(name: String = "WORKSPACE", block: ContextBuilder.( } inline fun <reified T : Any> WorkspaceBuilder.data( - name: Name = EmptyName, + name: Name = Name.EMPTY, noinline block: DataTreeBuilder<T>.() -> Unit ): DataNode<T> { val node = DataTreeBuilder(T::class).apply(block) @@ -47,13 +46,13 @@ inline fun <reified T : Any> WorkspaceBuilder.data( @JvmName("rawData") fun WorkspaceBuilder.data( - name: Name = EmptyName, + name: Name = Name.EMPTY, block: DataTreeBuilder<Any>.() -> Unit ): DataNode<Any> = data<Any>(name, block) fun WorkspaceBuilder.target(name: String, block: MetaBuilder.() -> Unit) { - targets[name] = buildMeta(block).seal() + targets[name] = Meta(block).seal() } /** diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt similarity index 59% rename from dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt rename to dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt index f6d27774..d378726f 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/dataUtils.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/envelopeData.kt @@ -1,9 +1,12 @@ package hep.dataforge.workspace import hep.dataforge.data.Data +import hep.dataforge.data.await import hep.dataforge.io.Envelope import hep.dataforge.io.IOFormat +import hep.dataforge.io.SimpleEnvelope import hep.dataforge.io.readWith +import kotlinx.io.ArrayBinary import kotlin.reflect.KClass /** @@ -11,4 +14,12 @@ import kotlin.reflect.KClass */ fun <T : Any> Envelope.toData(type: KClass<out T>, format: IOFormat<T>): Data<T> = Data(type, meta) { data?.readWith(format) ?: error("Can't convert envelope without data to Data") +} + +suspend fun <T : Any> Data<T>.toEnvelope(format: IOFormat<T>): Envelope { + val obj = await() + val binary = ArrayBinary.write { + format.run { writeObject(obj) } + } + return SimpleEnvelope(meta, binary) } \ No newline at end of file diff --git a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt index a483c78b..2b6d5454 100644 --- a/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt +++ b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/fileData.kt @@ -1,38 +1,26 @@ package hep.dataforge.workspace -import hep.dataforge.data.Data -import hep.dataforge.data.DataNode -import hep.dataforge.data.DataTreeBuilder -import hep.dataforge.data.datum -import hep.dataforge.descriptors.NodeDescriptor +//import jdk.nio.zipfs.ZipFileSystemProvider +import hep.dataforge.data.* import hep.dataforge.io.* -import hep.dataforge.meta.EmptyMeta -import hep.dataforge.meta.Meta +import hep.dataforge.meta.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import kotlinx.io.nio.asInput -import kotlinx.io.nio.asOutput +import java.nio.file.FileSystem import java.nio.file.Files import java.nio.file.Path -import java.nio.file.StandardOpenOption +import java.nio.file.StandardCopyOption +import java.nio.file.spi.FileSystemProvider import kotlin.reflect.KClass -/** - * Read meta from file in a given [MetaFormat] - */ -fun MetaFormat.readMetaFile(path: Path, descriptor: NodeDescriptor? = null): Meta { - return Files.newByteChannel(path, StandardOpenOption.READ) - .asInput() - .readMeta(descriptor) -} +typealias FileFormatResolver<T> = (Path, Meta) -> IOFormat<T> -/** - * Write meta to file using given [MetaFormat] - */ -fun MetaFormat.writeMetaFile(path: Path, meta: Meta, descriptor: NodeDescriptor? = null) { - return Files.newByteChannel(path, StandardOpenOption.WRITE, StandardOpenOption.CREATE_NEW) - .asOutput() - .writeMeta(meta, descriptor) +//private val zipFSProvider = ZipFileSystemProvider() + +private fun newZFS(path: Path): FileSystem { + val fsProvider = FileSystemProvider.installedProviders().find { it.scheme == "jar" } + ?: error("Zip file system provider not found") + return fsProvider.newFileSystem(path, mapOf("create" to "true")) } /** @@ -40,78 +28,156 @@ fun MetaFormat.writeMetaFile(path: Path, meta: Meta, descriptor: NodeDescriptor? * The operation is blocking since it must read meta header. The reading of envelope body is lazy * @param type explicit type of data read * @param dataFormat binary format - * @param envelopeFormatFactory the format of envelope. If null, file is read directly + * @param envelopeFormat the format of envelope. If null, file is read directly * @param metaFile the relative file for optional meta override * @param metaFileFormat the meta format for override */ -fun <T : Any> IOPlugin.readData( +@DFExperimental +fun <T : Any> IOPlugin.readDataFile( path: Path, type: KClass<out T>, - dataFormat: IOFormat<T>, - envelopeFormatFactory: EnvelopeFormatFactory? = null, - metaFile: Path = path.resolveSibling("${path.fileName}.meta"), - metaFileFormat: MetaFormat = JsonMetaFormat.default + formatResolver: FileFormatResolver<T> ): Data<T> { - val externalMeta = if (Files.exists(metaFile)) { - metaFileFormat.readMetaFile(metaFile) - } else { - null - } - return if (envelopeFormatFactory == null) { - Data(type, externalMeta ?: EmptyMeta) { - withContext(Dispatchers.IO) { - dataFormat.run { - Files.newByteChannel(path, StandardOpenOption.READ) - .asInput() - .readObject() - } - } - } - } else { - readEnvelopeFile(path, envelopeFormatFactory).let { - if (externalMeta == null) { - it - } else { - it.withMetaLayers(externalMeta) - } - }.toData(type, dataFormat) - } + val envelope = readEnvelopeFile(path, true) ?: error("Can't read data from $path") + val format = formatResolver(path, envelope.meta) + return envelope.toData(type, format) } -//TODO wants multi-receiver +@DFExperimental +inline fun <reified T : Any> IOPlugin.readDataFile(path: Path): Data<T> = + readDataFile(path, T::class) { _, _ -> + resolveIOFormat<T>() ?: error("Can't resolve IO format for ${T::class}") + } + +/** + * Add file/directory-based data tree item + */ +@DFExperimental fun <T : Any> DataTreeBuilder<T>.file( plugin: IOPlugin, path: Path, - dataFormat: IOFormat<T>, - envelopeFormatFactory: EnvelopeFormatFactory? = null + formatResolver: FileFormatResolver<T> ) { - plugin.run { - val data = readData(path, type, dataFormat, envelopeFormatFactory) - val name = path.fileName.toString().replace(".df", "") - datum(name, data) + //If path is a single file or a special directory, read it as single datum + if (!Files.isDirectory(path) || Files.list(path).allMatch { it.fileName.toString().startsWith("@") }) { + plugin.run { + val data = readDataFile(path, type, formatResolver) + val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string + ?: path.fileName.toString().replace(".df", "") + datum(name, data) + } + } else { + //otherwise, read as directory + plugin.run { + val data = readDataDirectory(path, type, formatResolver) + val name = data.meta[Envelope.ENVELOPE_NAME_KEY].string + ?: path.fileName.toString().replace(".df", "") + node(name, data) + } } } /** - * Read the directory as a data node + * Read the directory as a data node. If [path] is a zip archive, read it as directory */ -fun <T : Any> IOPlugin.readDataNode( +@DFExperimental +fun <T : Any> IOPlugin.readDataDirectory( path: Path, type: KClass<out T>, - dataFormat: IOFormat<T>, - envelopeFormatFactory: EnvelopeFormatFactory? = null + formatResolver: FileFormatResolver<T> ): DataNode<T> { - if (!Files.isDirectory(path)) error("Provided path $this is not a directory") + //read zipped data node + if (path.fileName != null && path.fileName.toString().endsWith(".zip")) { + //Using explicit Zip file system to avoid bizarre compatibility bugs + val fs = newZFS(path) + return readDataDirectory(fs.rootDirectories.first(), type, formatResolver) + } + if (!Files.isDirectory(path)) error("Provided path $path is not a directory") return DataNode(type) { Files.list(path).forEach { path -> - if (!path.fileName.toString().endsWith(".meta")) { - file(this@readDataNode,path, dataFormat, envelopeFormatFactory) + val fileName = path.fileName.toString() + if (fileName.startsWith(IOPlugin.META_FILE_NAME)) { + meta(readMetaFile(path)) + } else if (!fileName.startsWith("@")) { + file(this@readDataDirectory, path, formatResolver) + } + } + } +} + +@DFExperimental +inline fun <reified T : Any> IOPlugin.readDataDirectory(path: Path): DataNode<T> = + readDataDirectory(path, T::class) { _, _ -> + resolveIOFormat<T>() ?: error("Can't resolve IO format for ${T::class}") + } + +/** + * Write data tree to existing directory or create a new one using default [java.nio.file.FileSystem] provider + */ +@DFExperimental +suspend fun <T : Any> IOPlugin.writeDataDirectory( + path: Path, + node: DataNode<T>, + format: IOFormat<T>, + envelopeFormat: EnvelopeFormat? = null, + metaFormat: MetaFormatFactory? = null +) { + withContext(Dispatchers.IO) { + if (!Files.exists(path)) { + Files.createDirectories(path) + } else if (!Files.isDirectory(path)) { + error("Can't write a node into file") + } + node.items.forEach { (token, item) -> + val childPath = path.resolve(token.toString()) + when (item) { + is DataItem.Node -> { + writeDataDirectory(childPath, item.node, format, envelopeFormat) + } + is DataItem.Leaf -> { + val envelope = item.data.toEnvelope(format) + if (envelopeFormat != null) { + writeEnvelopeFile(childPath, envelope, envelopeFormat, metaFormat) + } else { + writeEnvelopeDirectory(childPath, envelope, metaFormat ?: JsonMetaFormat) + } + } + } + } + if (!node.meta.isEmpty()) { + writeMetaFile(path, node.meta, metaFormat ?: JsonMetaFormat) + } + } +} + +suspend fun <T : Any> IOPlugin.writeZip( + path: Path, + node: DataNode<T>, + format: IOFormat<T>, + envelopeFormat: EnvelopeFormat? = null, + metaFormat: MetaFormatFactory? = null +) { + withContext(Dispatchers.IO) { + val actualFile = if (path.toString().endsWith(".zip")) { + path + } else { + path.resolveSibling(path.fileName.toString() + ".zip") + } + if (Files.exists(actualFile) && Files.size(path) == 0.toLong()) { + Files.delete(path) + } + //Files.createFile(actualFile) + newZFS(actualFile).use { zipfs -> + val internalTargetPath = zipfs.getPath("/") + Files.createDirectories(internalTargetPath) + val tmp = Files.createTempDirectory("df_zip") + writeDataDirectory(tmp, node, format, envelopeFormat, metaFormat) + Files.list(tmp).forEach { sourcePath -> + val targetPath = sourcePath.fileName.toString() + val internalTargetPath = internalTargetPath.resolve(targetPath) + Files.copy(sourcePath, internalTargetPath, StandardCopyOption.REPLACE_EXISTING) } } } } -//suspend fun <T : Any> Path.writeData( -// data: Data<T>, -// format: IOFormat<T>, -// ) \ No newline at end of file diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt index c449ffc3..083d3f57 100644 --- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/DataPropagationTest.kt @@ -6,8 +6,8 @@ import hep.dataforge.context.PluginTag import hep.dataforge.data.* import hep.dataforge.meta.Meta import hep.dataforge.names.asName -import org.junit.Test import kotlin.reflect.KClass +import kotlin.test.Test import kotlin.test.assertEquals diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt new file mode 100644 index 00000000..4fc9e9a4 --- /dev/null +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/FileDataTest.kt @@ -0,0 +1,72 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Global +import hep.dataforge.data.* +import hep.dataforge.io.IOFormat +import hep.dataforge.io.io +import hep.dataforge.meta.DFExperimental +import kotlinx.coroutines.runBlocking +import kotlinx.io.Input +import kotlinx.io.Output +import kotlinx.io.text.readUtf8String +import kotlinx.io.text.writeUtf8String +import java.nio.file.Files +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals + +class FileDataTest { + val dataNode = DataNode<String> { + node("dir") { + static("a", "Some string") { + "content" put "Some string" + } + } + static("b", "root data") + meta { + "content" put "This is root meta node" + } + } + + object StringIOFormat : IOFormat<String> { + override fun Output.writeObject(obj: String) { + writeUtf8String(obj) + } + + override fun Input.readObject(): String { + return readUtf8String() + } + + } + + @Test + @DFExperimental + fun testDataWriteRead() { + Global.io.run { + val dir = Files.createTempDirectory("df_data_node") + runBlocking { + writeDataDirectory(dir, dataNode, StringIOFormat) + } + println(dir.toUri().toString()) + val reconstructed = readDataDirectory(dir, String::class) { _, _ -> StringIOFormat } + assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta) + assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get()) + } + } + + + @Test + @Ignore + fun testZipWriteRead() { + Global.io.run { + val zip = Files.createTempFile("df_data_node", ".zip") + runBlocking { + writeZip(zip, dataNode, StringIOFormat) + } + println(zip.toUri().toString()) + val reconstructed = readDataDirectory(zip, String::class) { _, _ -> StringIOFormat } + assertEquals(dataNode["dir.a"]?.meta, reconstructed["dir.a"]?.meta) + assertEquals(dataNode["b"]?.data?.get(), reconstructed["b"]?.data?.get()) + } + } +} \ No newline at end of file diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt index 3a40e783..8bd02c35 100644 --- a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt @@ -6,8 +6,9 @@ import hep.dataforge.meta.boolean import hep.dataforge.meta.builder import hep.dataforge.meta.get import hep.dataforge.meta.int +import hep.dataforge.meta.scheme.int import hep.dataforge.names.plus -import org.junit.Test +import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf0..f3d88b1c 100644 Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7c4388a9..a2bf1313 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.2.2-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 index 83f2acfd..2fe81a7d --- a/gradlew +++ b/gradlew @@ -154,19 +154,19 @@ if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -175,14 +175,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 9618d8d9..62bd9b9c 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" diff --git a/settings.gradle.kts b/settings.gradle.kts index b486c03f..e0d250bf 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -28,7 +28,10 @@ include( ":dataforge-context", ":dataforge-data", ":dataforge-output", - ":dataforge-output-html", + ":dataforge-output:dataforge-output-html", + ":dataforge-tables", ":dataforge-workspace", ":dataforge-scripting" -) \ No newline at end of file +) + +//includeBuild("../kotlinx-io") \ No newline at end of file