diff --git a/.gitignore b/.gitignore index d482c058..94cef066 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,10 @@ .idea/ *.iws -out/ +*/out/** .gradle -/build/ +*/build/** !gradle-wrapper.jar - +gradle.properties \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..e69de29b diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 8f26a92f..00000000 --- a/build.gradle +++ /dev/null @@ -1,23 +0,0 @@ -plugins { - id 'kotlin-platform-common' version '1.2.70' -} - -description = "The basic interfaces for DataForge meta-data" - -group 'hep.dataforge' -version '0.1.1-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - compile "org.jetbrains.kotlin:kotlin-stdlib-common" - testCompile "org.jetbrains.kotlin:kotlin-test-annotations-common" - testCompile "org.jetbrains.kotlin:kotlin-test-common" -} -kotlin { - experimental { - coroutines "enable" - } -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..71c5da82 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,142 @@ +import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +buildscript { + val kotlinVersion: String by rootProject.extra("1.3.21") + val ioVersion: String by rootProject.extra("0.1.5") + val coroutinesVersion: String by rootProject.extra("1.1.1") + val atomicfuVersion: String by rootProject.extra("0.12.1") + val dokkaVersion: String by rootProject.extra("0.9.17") + val serializationVersion: String by rootProject.extra("0.10.0") + + repositories { + jcenter() + maven("https://dl.bintray.com/kotlin/kotlin-eap") + } + + dependencies { + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + classpath("org.jfrog.buildinfo:build-info-extractor-gradle:4+") + classpath("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4") + classpath("org.jetbrains.dokka:dokka-gradle-plugin:$dokkaVersion") + classpath("org.jetbrains.kotlin:kotlin-frontend-plugin:0.0.45") + classpath("org.openjfx:javafx-plugin:0.0.7") + } +} + +plugins { + id("com.jfrog.artifactory") version "4.8.1" apply false +// id("org.jetbrains.kotlin.multiplatform") apply false +} + +allprojects { + apply(plugin = "maven") + apply(plugin = "maven-publish") + apply(plugin = "com.jfrog.artifactory") + + repositories { + jcenter() + maven("https://kotlin.bintray.com/kotlinx") + } + + group = "hep.dataforge" + version = "0.1.1-dev-5" + + // apply bintray configuration + apply(from = "${rootProject.rootDir}/gradle/bintray.gradle") + + //apply artifactory configuration + apply(from = "${rootProject.rootDir}/gradle/artifactory.gradle") + +} + +subprojects { + + // dokka { +// outputFormat = "html" +// outputDirectory = javadoc.destinationDir +// } +// +// task dokkaJar (type: Jar, dependsOn: dokka) { +// from javadoc . destinationDir +// classifier = "javadoc" +// } + + // Create empty jar for sources classifier to satisfy maven requirements + val stubSources by tasks.registering(Jar::class) { + archiveClassifier.set("sources") + //from(sourceSets.main.get().allSource) + } + + // Create empty jar for javadoc classifier to satisfy maven requirements + val stubJavadoc by tasks.registering(Jar::class) { + archiveClassifier.set("javadoc") + } + + tasks.withType { + kotlinOptions{ + jvmTarget = "1.8" + } + } + + + afterEvaluate { + extensions.findByType()?.apply { + jvm { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + js { + compilations.all { + tasks.getByName(compileKotlinTaskName) { + kotlinOptions { + metaInfo = true + sourceMap = true + sourceMapEmbedSources = "always" + moduleKind = "umd" + } + } + } + + configure(listOf(compilations["main"])) { + tasks.getByName(compileKotlinTaskName) { + kotlinOptions { + main = "call" + } + } + } + } + + targets.all { + sourceSets.all { + languageSettings.progressiveMode = true + } + } + + configure { + + publications.filterIsInstance().forEach { publication -> + if (publication.name == "kotlinMultiplatform") { + // for our root metadata publication, set artifactId with a package and project name + publication.artifactId = project.name + } else { + // for targets, set artifactId with a package, project name and target name (e.g. iosX64) + publication.artifactId = "${project.name}-${publication.name}" + } + } + + targets.all { + val publication = publications.findByName(name) as MavenPublication + + // Patch publications with fake javadoc + publication.artifact(stubJavadoc.get()) + } + } + } + } + +} \ No newline at end of file diff --git a/dataforge-context/build.gradle.kts b/dataforge-context/build.gradle.kts new file mode 100644 index 00000000..58da79a7 --- /dev/null +++ b/dataforge-context/build.gradle.kts @@ -0,0 +1,35 @@ +plugins { + kotlin("multiplatform") +} + +description = "Context and provider definitions" + +val coroutinesVersion: String by rootProject.extra + +kotlin { + jvm() + js() + + sourceSets { + val commonMain by getting { + dependencies { + api(project(":dataforge-meta")) + api(kotlin("reflect")) + api("io.github.microutils:kotlin-logging-common:1.6.10") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") + } + } + val jvmMain by getting { + dependencies { + api("io.github.microutils:kotlin-logging:1.6.10") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + val jsMain by getting { + dependencies { + api("io.github.microutils:kotlin-logging-js:1.6.10") + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutinesVersion") + } + } + } +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt new file mode 100644 index 00000000..8864fae1 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt @@ -0,0 +1,27 @@ +package hep.dataforge.context + +import hep.dataforge.meta.Config +import hep.dataforge.names.Name + +abstract class AbstractPlugin : Plugin { + private var _context: Context? = null + + override val context: Context + get() = _context ?: error("Plugin $tag is not attached") + + override val config = Config() + + override fun attach(context: Context) { + this._context = context + } + + override fun detach() { + this._context = null + } + + //TODO make configuration activation-safe + + override fun provideTop(target: String, name: Name): Any? = null + + override fun listTop(target: String): Sequence = emptySequence() +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt new file mode 100644 index 00000000..d1ff801d --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt @@ -0,0 +1,181 @@ +package hep.dataforge.context + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.provider.Provider +import hep.dataforge.provider.provideAll +import hep.dataforge.values.Value +import kotlinx.coroutines.CoroutineScope +import mu.KLogger +import mu.KotlinLogging +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.jvm.JvmName + +/** + * The local environment for anything being done in DataForge framework. Contexts are organized into tree structure with [Global] at the top. + * Context has [properties] - equivalent for system environment values, but grouped into a tree and inherited from parent context. + * + * The main function of the Context is to provide [PluginManager] which stores the loaded plugins and works as a dependency injection point. + * The normal behaviour of the [PluginManager] is to search for a plugin in parent context if it is not found in a current one. It is possible to have + * different plugins with the same interface in different contexts in the hierarchy. The usual behaviour is to use nearest one, but it could + * be overridden by plugin implementation. + * + * Since plugins could contain mutable state, context has two states: active and inactive. No changes are allowed to active context. + * @author Alexander Nozik + */ +open class Context(final override val name: String, val parent: Context? = Global) : Named, MetaRepr, Provider, + CoroutineScope { + + private val config = Config() + + /** + * Context properties. Working as substitute for environment variables + */ + val properties: Meta = if (parent == null) { + config + } else { + Laminate(config, parent.properties) + } + + /** + * Context logger + */ + val logger: KLogger = KotlinLogging.logger(name) + + /** + * A [PluginManager] for current context + */ + val plugins: PluginManager by lazy { PluginManager(this) } + + private val activators = HashSet() + + /** + * Defines if context is used in any kind of active computations. Active context properties and plugins could not be changed + */ + val isActive: Boolean = activators.isNotEmpty() + + override val defaultTarget: String get() = Plugin.PLUGIN_TARGET + + override fun provideTop(target: String, name: Name): Any? { + return when (target) { + Plugin.PLUGIN_TARGET -> plugins[PluginTag.fromString(name.toString())] + Value.TYPE -> properties[name]?.value + else -> null + } + } + + override fun listTop(target: String): Sequence { + return when (target) { + Plugin.PLUGIN_TARGET -> plugins.asSequence().map { it.name.toName() } + Value.TYPE -> properties.asValueSequence().map { it.first } + else -> emptySequence() + } + } + + /** + * Mark context as active and used by [activator] + */ + fun activate(activator: Any) { + activators.add(activator) + } + + /** + * Mark context unused by [activator] + */ + fun deactivate(activator: Any) { + activators.remove(activator) + } + + /** + * Change the properties of the context. If active, throw an exception + */ + fun configure(action: Config.() -> Unit) { + if (isActive) error("Can't configure active context") + config.action() + } + + override val coroutineContext: CoroutineContext + get() = EmptyCoroutineContext + + /** + * Detach all plugins and terminate context + */ + open fun close() { + if (isActive) error("Can't close active context") + //detach all plugins + plugins.forEach { it.detach() } + } + + override fun toMeta(): Meta = buildMeta { + "parent" to parent?.name + "properties" to properties.seal() + "plugins" to plugins.map { it.toMeta() } + } +} + +/** + * A sequences of all objects provided by plugins with given target and type + */ +fun Context.members(target: String): Sequence = + plugins.asSequence().flatMap { it.provideAll(target) } + +@JvmName("typedMembers") +inline fun Context.members(target: String) = + members(target).filterIsInstance() + + +/** + * A global root context. Closing [Global] terminates the framework. + */ +object Global : Context("GLOBAL", null) { + /** + * Closing all contexts + * + * @throws Exception + */ + override fun close() { + logger.info { "Shutting down GLOBAL" } + for (ctx in contextRegistry.values) { + ctx.close() + } + super.close() + } + + private val contextRegistry = HashMap() + + /** + * Get previously builder context o builder a new one + * + * @param name + * @return + */ + fun getContext(name: String): Context { + return contextRegistry.getOrPut(name) { Context(name) } + } +} + + +/** + * The interface for something that encapsulated in context + * + * @author Alexander Nozik + * @version $Id: $Id + */ +interface ContextAware { + /** + * Get context for this object + * + * @return + */ + val context: Context + + val logger: KLogger + get() = if (this is Named) { + KotlinLogging.logger(context.name + "." + (this as Named).name) + } else { + context.logger + } + +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt new file mode 100644 index 00000000..52079854 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt @@ -0,0 +1,37 @@ +package hep.dataforge.context + +import hep.dataforge.meta.Config +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.configure + +/** + * A convenience builder for context + */ +class ContextBuilder(var name: String = "@anonimous", val parent: Context = Global) { + private val plugins = ArrayList() + private var meta = MetaBuilder() + + fun properties(action: MetaBuilder.() -> Unit) { + meta.action() + } + + fun plugin(plugin: Plugin) { + plugins.add(plugin) + } + + fun plugin(tag: PluginTag, action: Config.() -> Unit) { + plugins.add(PluginRepository.fetch(tag).configure(action)) + } + + fun plugin(name: String, group: String = "", version: String = "", action: Config.() -> Unit) { + plugin(PluginTag(name, group, version), action) + } + + fun build(): Context { + return Context(name, parent).apply { + this@ContextBuilder.plugins.forEach { + plugins.load(it) + } + } + } +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Named.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Named.kt new file mode 100644 index 00000000..1ac31702 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Named.kt @@ -0,0 +1,57 @@ +/* + * Copyright 2015 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.context + +/** + * Any object that have name + * + * @author Alexander Nozik + */ +interface Named { + + /** + * The name of this object instance + * + * @return + */ + val name: String + + companion object { + const val ANONYMOUS = "" + + /** + * Get the name of given object. If object is Named its name is used, + * otherwise, use Object.toString + * + * @param obj + * @return + */ + fun nameOf(obj: Any): String { + return if (obj is Named) { + obj.name + } else { + obj.toString() + } + } + } +} + +/** + * Check if this object has an empty name and therefore is anonymous. + * @return + */ +val Named.isAnonymous: Boolean + get() = this.name == Named.ANONYMOUS diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt new file mode 100644 index 00000000..1de7f6ca --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt @@ -0,0 +1,78 @@ +package hep.dataforge.context + +import hep.dataforge.meta.Configurable +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaRepr +import hep.dataforge.meta.buildMeta +import hep.dataforge.provider.Provider + +/** + * The interface to define a Context plugin. A plugin stores all runtime features of a context. + * The plugin is by default configurable and a Provider (both features could be ignored). + * The plugin must in most cases have an empty constructor in order to be able to load it from library. + * + * + * The plugin lifecycle is the following: + * + * + * create - configure - attach - detach - destroy + * + * + * Configuration of attached plugin is possible for a context which is not in a runtime mode, but it is not recommended. + * + * @author Alexander Nozik + */ +interface Plugin : Named, ContextAware, Provider, MetaRepr, Configurable { + + /** + * Get tag for this plugin + * + * @return + */ + val tag: PluginTag + + /** + * The name of this plugin ignoring version and group + * + * @return + */ + override val name: String + get() = tag.name + + /** + * Plugin dependencies which are required to attach this plugin. Plugin + * dependencies must be initialized and enabled in the Context before this + * plugin is enabled. + * + * @return + */ + fun dependsOn(): List = emptyList() + + /** + * Start this plugin and attach registration info to the context. This method + * should be called only via PluginManager to avoid dependency issues. + * + * @param context + */ + fun attach(context: Context) + + /** + * Stop this plugin and remove registration info from context and other + * plugins. This method should be called only via PluginManager to avoid + * dependency issues. + */ + fun detach() + + override fun toMeta(): Meta = buildMeta { + "context" to context.name + "type" to this::class.simpleName + "tag" to tag + "meta" to config + } + + companion object { + + const val PLUGIN_TARGET = "plugin" + } + +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt new file mode 100644 index 00000000..1f905093 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt @@ -0,0 +1,159 @@ +package hep.dataforge.context + +import hep.dataforge.meta.* +import kotlin.reflect.KClass + +/** + * The manager for plugin system. Should monitor plugin dependencies and locks. + * + * @property context A context for this plugin manager + * @author Alexander Nozik + */ +class PluginManager(override val context: Context) : ContextAware, Iterable { + + /** + * A set of loaded plugins + */ + private val plugins = HashSet() + + /** + * A [PluginManager] of parent context if it is present + */ + private val parent: PluginManager? = context.parent?.plugins + + + fun sequence(recursive: Boolean): Sequence { + return if (recursive && parent != null) { + plugins.asSequence() + parent.sequence(true) + } else { + plugins.asSequence() + } + } + + /** + * Get existing plugin or return null if not present. Only first matching plugin is returned. + * @param recursive search for parent [PluginManager] plugins + * @param predicate condition for the plugin + */ + fun get(recursive: Boolean = true, predicate: (Plugin) -> Boolean): Plugin? = sequence(recursive).find(predicate) + + + /** + * Find a loaded plugin via its tag + * + * @param tag + * @return + */ + operator fun get(tag: PluginTag, recursive: Boolean = true): Plugin? = get(recursive) { tag.matches(it.tag) } + + + /** + * Find a loaded plugin via its class + * + * @param tag + * @param type + * @param + * @return + */ + @Suppress("UNCHECKED_CAST") + operator fun get(type: KClass, recursive: Boolean = true): T? = + get(recursive) { type.isInstance(it) } as T? + + inline fun get(recursive: Boolean = true): T? = get(T::class, recursive) + + + /** + * Load given plugin into this manager and return loaded instance. + * Throw error if plugin of the same class already exists in manager + * + * @param plugin + * @return + */ + fun load(plugin: T): T { + if (context.isActive) error("Can't load plugin into active context") + + if (get(plugin::class, false) != null) { + throw RuntimeException("Plugin of type ${plugin::class} already exists in ${context.name}") + } else { + loadDependencies(plugin) + + logger.info { "Loading plugin ${plugin.name} into ${context.name}" } + plugin.attach(context) + plugins.add(plugin) + return plugin + } + } + + private fun loadDependencies(plugin: Plugin) { + for (tag in plugin.dependsOn()) { + load(tag) + } + } + + /** + * Remove a plugin from [PluginManager] + */ + fun remove(plugin: Plugin) { + if (context.isActive) error("Can't remove plugin from active context") + + if (plugins.contains(plugin)) { + logger.info { "Removing plugin ${plugin.name} from ${context.name}" } + plugin.detach() + plugins.remove(plugin) + } + } + + /** + * Get plugin instance via plugin resolver and load it. + * + * @param tag + * @return + */ + fun load(tag: PluginTag, meta: Meta = EmptyMeta): Plugin { + val loaded = get(tag, false) + return when { + loaded == null -> load(PluginRepository.fetch(tag)).configure(meta) + loaded.config == meta -> loaded // if meta is the same, return existing plugin + else -> throw RuntimeException("Can't load plugin with tag $tag. Plugin with this tag and different configuration already exists in context.") + } + } + + /** + * Load plugin by its class and meta. Ignore if plugin with this meta is already loaded. + * Throw an exception if there exists plugin with the same type, but different meta + */ + fun load(type: KClass, meta: Meta = EmptyMeta): T { + val loaded = get(type, false) + return when { + loaded == null -> { + val plugin = PluginRepository.list().first { it.type == type }.build(meta) + if (type.isInstance(plugin)) { + @Suppress("UNCHECKED_CAST") + load(plugin as T) + } else { + error("Corrupt type information in plugin repository") + } + } + loaded.config == meta -> loaded // if meta is the same, return existing plugin + else -> throw RuntimeException("Can't load plugin with type $type. Plugin with this type and different configuration already exists in context.") + } + } + + inline fun load(noinline metaBuilder: MetaBuilder.() -> Unit = {}): T { + return load(T::class, buildMeta(metaBuilder)) + } + + fun load(name: String, meta: Meta = EmptyMeta): Plugin { + return load(PluginTag.fromString(name), meta) + } + + override fun iterator(): Iterator = plugins.iterator() + + /** + * Get a plugin if it exists or load it with given meta if it is not. + */ + inline fun getOrLoad(noinline metaBuilder: MetaBuilder.() -> Unit = {}): T { + return get(true) ?: load(metaBuilder) + } + +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt new file mode 100644 index 00000000..43e51a07 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -0,0 +1,48 @@ +package hep.dataforge.context + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.configure +import kotlin.reflect.KClass + +interface PluginFactory { + val tag: PluginTag + val type: KClass + fun build(): Plugin +} + +fun PluginFactory.build(meta: Meta) = build().configure(meta) + + +expect object PluginRepository { + + fun register(factory: PluginFactory) + + /** + * List plugins available in the repository + */ + fun list(): Sequence + +} + +/** + * Fetch specific plugin and instantiate it with given meta + */ +fun PluginRepository.fetch(tag: PluginTag): Plugin = + PluginRepository.list().find { it.tag.matches(tag) }?.build() + ?: error("Plugin with tag $tag not found in the repository") + +fun PluginRepository.register(tag: PluginTag, type: KClass, constructor: () -> Plugin) { + val factory = object : PluginFactory { + override val tag: PluginTag = tag + override val type: KClass = type + + override fun build(): Plugin = constructor() + + } + PluginRepository.register(factory) +} + +inline fun PluginRepository.register(tag: PluginTag, noinline constructor: () -> T) = + register(tag, T::class, constructor) + +fun PluginRepository.register(plugin: Plugin) = register(plugin.tag, plugin::class) { plugin } \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt new file mode 100644 index 00000000..25164c72 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginTag.kt @@ -0,0 +1,64 @@ +package hep.dataforge.context + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaRepr +import hep.dataforge.meta.buildMeta + +/** + * The tag which contains information about name, group and version of some + * object. It also could contain any complex rule to define version ranges + * + * @author Alexander Nozik + */ +data class PluginTag( + val name: String, + val group: String = "", + val version: String = "" +) : MetaRepr { + + /** + * Check if given tag is compatible (in range) of this tag + * + * @param otherTag + * @return + */ + fun matches(otherTag: PluginTag): Boolean { + return matchesName(otherTag) && matchesGroup(otherTag) + } + + private fun matchesGroup(otherTag: PluginTag): Boolean { + return this.group.isEmpty() || this.group == otherTag.group + } + + private fun matchesName(otherTag: PluginTag): Boolean { + return this.name == otherTag.name + } + + override fun toString(): String = listOf(group, name, version).joinToString(separator = ":") + + override fun toMeta(): Meta = buildMeta { + "name" to name + "group" to group + "version" to version + } + + companion object { + + const val DATAFORGE_GROUP = "hep.dataforge" + + /** + * Build new PluginTag from standard string representation + * + * @param tag + * @return + */ + fun fromString(tag: String): PluginTag { + val sepIndex = tag.indexOf(":") + return if (sepIndex >= 0) { + PluginTag(group = tag.substring(0, sepIndex), name = tag.substring(sepIndex + 1)) + } else { + PluginTag(tag) + } + } + } +} \ No newline at end of file diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Path.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Path.kt new file mode 100644 index 00000000..06c36ab0 --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Path.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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.provider + +import hep.dataforge.names.Name +import hep.dataforge.names.toName + +/** + * + * + * Path interface. + * + * @author Alexander Nozik + * @version $Id: $Id + */ +inline class Path(val tokens: List) : Iterable { + + val head: PathToken? get() = tokens.firstOrNull() + + val length: Int get() = tokens.size + + /** + * Returns non-empty optional containing the chain without first segment in case of chain path. + * + * @return + */ + val tail: Path? get() = if (tokens.isEmpty()) null else Path(tokens.drop(1)) + + override fun iterator(): Iterator = tokens.iterator() + + companion object { + const val PATH_SEGMENT_SEPARATOR = "/" + + fun parse(path: String): Path { + val head = path.substringBefore(PATH_SEGMENT_SEPARATOR) + val tail = path.substringAfter(PATH_SEGMENT_SEPARATOR) + return PathToken.parse(head).toPath() + parse(tail) + } + } +} + +operator fun Path.plus(path: Path) = Path(this.tokens + path.tokens) + +data class PathToken(val name: Name, val target: String? = null) { + override fun toString(): String = if (target == null) { + name.toString() + } else { + "$target$TARGET_SEPARATOR$name" + } + + companion object { + const val TARGET_SEPARATOR = "::" + fun parse(token: String): PathToken { + val target = token.substringBefore(TARGET_SEPARATOR, "") + val name = token.substringAfter(TARGET_SEPARATOR).toName() + if (target.contains("[")) TODO("target separators in queries are not supported") + return PathToken(name, target) + } + } +} + +fun PathToken.toPath() = Path(listOf(this)) diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt new file mode 100644 index 00000000..79b94dce --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt @@ -0,0 +1,91 @@ +/* + * Copyright 2015 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.provider + +import hep.dataforge.names.Name +import hep.dataforge.names.toName + +/** + * A marker utility interface for providers. + * + * @author Alexander Nozik + */ +interface Provider { + + /** + * Default target for this provider + * + * @return + */ + val defaultTarget: String get() = "" + + /** + * Default target for next chain segment + * + * @return + */ + val defaultChainTarget: String get() = "" + + + /** + * Provide a top level element for this [Provider] or return null if element is not present + */ + fun provideTop(target: String, name: Name): Any? + + /** + * [Sequence] of available names with given target. Only top level names are listed, no chain path. + * + * @param target + * @return + */ + fun listTop(target: String): Sequence +} + +fun Provider.provide(path: Path, targetOverride: String? = null): Any? { + if (path.length == 0) throw IllegalArgumentException("Can't provide by empty path") + val first = path.first() + val top = provideTop(targetOverride ?: first.target ?: defaultTarget, first.name) + return when (path.length) { + 1 -> top + else -> { + when (top) { + null -> null + is Provider -> top.provide(path.tail!!, targetOverride = defaultChainTarget) + else -> throw IllegalStateException("Chain path not supported: child is not a provider") + } + } + } +} + +/** + * Type checked provide + */ +inline fun Provider.provide(path: String): T? { + return provide(Path.parse(path)) as? T +} + +inline fun Provider.provide(target: String, name: String): T? { + return provide(PathToken(name.toName(), target).toPath()) as? T +} + +/** + * [Sequence] of all elements with given target + */ +fun Provider.provideAll(target: String): Sequence { + return listTop(target).map { provideTop(target, it) ?: error("The element $it is declared but not provided") } +} + + diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Type.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Type.kt new file mode 100644 index 00000000..a31f1fdb --- /dev/null +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Type.kt @@ -0,0 +1,10 @@ +package hep.dataforge.provider + +/** + * A text label for internal DataForge type classification. Alternative for mime container type. + * + * The DataForge type notation presumes that type `A.B.C` is the subtype of `A.B` + */ +@MustBeDocumented +@Target(AnnotationTarget.CLASS) +annotation class Type(val id: String) diff --git a/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt new file mode 100644 index 00000000..4cbeb0d1 --- /dev/null +++ b/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -0,0 +1,16 @@ +package hep.dataforge.context + + +actual object PluginRepository { + + private val factories: MutableSet = HashSet() + + actual fun register(factory: PluginFactory) { + factories.add(factory) + } + + /** + * List plugins available in the repository + */ + actual fun list(): Sequence = factories.asSequence() +} \ No newline at end of file diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/ClassLoaderPlugin.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/ClassLoaderPlugin.kt new file mode 100644 index 00000000..6d401613 --- /dev/null +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/ClassLoaderPlugin.kt @@ -0,0 +1,118 @@ +/* + * 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.context + +import java.util.* +import kotlin.reflect.KClass +import kotlin.reflect.full.cast + +class ClassLoaderPlugin(val classLoader: ClassLoader) : AbstractPlugin() { + override val tag: PluginTag = PluginTag("classLoader", PluginTag.DATAFORGE_GROUP) + + private val serviceCache: MutableMap, ServiceLoader<*>> = HashMap() + + fun services(type: KClass): Sequence { + return serviceCache.getOrPut(type.java) { ServiceLoader.load(type.java, classLoader) }.asSequence() + .map { type.cast(it) } + } + + companion object { + val DEFAULT = ClassLoaderPlugin(Global::class.java.classLoader) + } +} + +val Context.classLoaderPlugin get() = this.plugins.get() ?: ClassLoaderPlugin.DEFAULT + +inline fun Context.services() = classLoaderPlugin.services(T::class) + + +//open class JVMContext( +// final override val name: String, +// final override val parent: JVMContext? = Global, +// classLoader: ClassLoader? = null, +// properties: Meta = EmptyMeta +//) : Context, AutoCloseable { +// +// override val properties: Meta = if (parent == null) { +// properties +// } else { +// Laminate(properties, parent.properties) +// } +// +// override val plugins: PluginManager by lazy { PluginManager(this) } +// override val logger: KLogger = KotlinLogging.logger(name) +// +// /** +// * A class loader for this context. Parent class loader is used by default +// */ +// open val classLoader: ClassLoader = classLoader ?: parent?.classLoader ?: Global.classLoader +// +// /** +// * A property showing that dispatch thread is started in the context +// */ +// private var started = false +// +// /** +// * A dispatch thread executor for current context +// * +// * @return +// */ +// val dispatcher: ExecutorService by lazy { +// logger.info("Initializing dispatch thread executor in {}", name) +// Executors.newSingleThreadExecutor { r -> +// Thread(r).apply { +// priority = 8 // slightly higher priority +// isDaemon = true +// name = this@JVMContext.name + "_dispatch" +// }.also { started = true } +// } +// } +// +// private val serviceCache: MutableMap, ServiceLoader<*>> = HashMap() +// +// fun services(type: KClass): Sequence { +// return serviceCache.getOrPut(type.java) { ServiceLoader.load(type.java, classLoader) }.asSequence() +// .map { type.cast(it) } +// } +// +// /** +// * Free up resources associated with this context +// * +// * @throws Exception +// */ +// override fun close() { +// if (isActive) error("Can't close active context") +// //detach all plugins +// plugins.forEach { it.detach() } +// +// if (started) { +// dispatcher.shutdown() +// } +// } +// +// private val activators = HashSet>() +// +// override val isActive: Boolean = activators.all { it.get() == null } +// +// override fun activate(activator: Any) { +// activators.add(WeakReference(activator)) +// } +// +// override fun deactivate(activator: Any) { +// activators.removeAll { it.get() == activator } +// } +//} +// diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt new file mode 100644 index 00000000..05b65ad9 --- /dev/null +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -0,0 +1,17 @@ +package hep.dataforge.context + +actual object PluginRepository { + + private val factories: MutableSet = HashSet() + + actual fun register(factory: PluginFactory) { + factories.add(factory) + } + + /** + * List plugins available in the repository + */ + actual fun list(): Sequence = + factories.asSequence() + Global.services() + +} \ No newline at end of file diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/provider/Types.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/provider/Types.kt new file mode 100644 index 00000000..15cfe2d1 --- /dev/null +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/provider/Types.kt @@ -0,0 +1,36 @@ +package hep.dataforge.provider + +import hep.dataforge.context.Context +import hep.dataforge.context.members +import kotlin.reflect.KClass +import kotlin.reflect.full.findAnnotation + + +object Types { + operator fun get(cl: KClass<*>): String { + return cl.findAnnotation()?.id ?: cl.simpleName ?: "" + } + + operator fun get(obj: Any): String { + return get(obj::class) + } +} + +/** + * Provide an object with given name inferring target from its type using [Type] annotation + */ +inline fun Provider.provideByType(name: String): T? { + val target = Types[T::class] + return provide(target, name) +} + +inline fun Provider.provideAllByType(): Sequence { + val target = Types[T::class] + return provideAll(target).filterIsInstance() +} + +/** + * A sequences of all objects provided by plugins with given target and type + */ +inline fun Context.members(): Sequence = members(Types[T::class]) + diff --git a/dataforge-data/build.gradle.kts b/dataforge-data/build.gradle.kts new file mode 100644 index 00000000..eb6b3669 --- /dev/null +++ b/dataforge-data/build.gradle.kts @@ -0,0 +1,30 @@ +plugins { + kotlin("multiplatform") +} + +val coroutinesVersion: String by rootProject.extra + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-meta")) + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") + } + } + + val jvmMain by getting{ + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") + } + } + + val jsMain by getting{ + dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:$coroutinesVersion") + } + } + } +} \ No newline at end of file diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt new file mode 100644 index 00000000..599101c0 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt @@ -0,0 +1,60 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name + +/** + * A simple data transformation on a data node + */ +interface Action { + /** + * Transform the data in the node, producing a new node. By default it is assumed that all calculations are lazy + * so not actual computation is started at this moment + */ + operator fun invoke(node: DataNode, meta: Meta): DataNode + + /** + * Terminal action is the one that could not be invoked lazily and requires some kind of blocking computation to invoke + */ + val isTerminal: Boolean get() = false +} + +/** + * Action composition. The result is terminal if one of parts is terminal + */ +infix fun Action.then(action: Action): Action { + return object : Action { + override fun invoke(node: DataNode, meta: Meta): DataNode { + return action(this@then.invoke(node, meta), meta) + } + + override val isTerminal: Boolean + get() = this@then.isTerminal || action.isTerminal + } +} + +/** + * An action that performs the same transformation on each of input data nodes. Null results are ignored. + */ +class PipeAction(val transform: (Name, Data, Meta) -> Data?) : Action { + override fun invoke(node: DataNode, meta: Meta): DataNode = DataNode.build { + node.dataSequence().forEach { (name, data) -> + val res = transform(name, data, meta) + if (res != null) { + set(name, res) + } + } + } + + companion object { + /** + * A simple pipe that performs transformation on the data and copies input meta into the output + */ + inline fun simple(noinline transform: suspend (Name, T, Meta) -> R) = + PipeAction { name, data: Data, meta -> + val goal = data.goal.pipe { transform(name, it, meta) } + return@PipeAction Data.of(goal, data.meta) + } + } +} + diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt new file mode 100644 index 00000000..6096a799 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt @@ -0,0 +1,56 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaRepr +import kotlin.coroutines.CoroutineContext +import kotlin.reflect.KClass + +/** + * A data element characterized by its meta + */ +interface Data : MetaRepr { + /** + * Type marker for the data. The type is known before the calculation takes place so it could be checked. + */ + val type: KClass + /** + * Meta for the data + */ + val meta: Meta + + /** + * Lazy data value + */ + val goal: Goal + + override fun toMeta(): Meta = meta + + companion object { + const val TYPE = "data" + + fun of(type: KClass, goal: Goal, meta: Meta): Data = DataImpl(type, goal, meta) + inline fun of(goal: Goal, meta: Meta): Data = of(T::class, goal, meta) + fun of(name: String, type: KClass, goal: Goal, meta: Meta): Data = + NamedData(name, of(type, goal, meta)) + + inline fun of(name: String, goal: Goal, meta: Meta): Data = + of(name, T::class, goal, meta) + + fun static(context: CoroutineContext, value: T, meta: Meta): Data = + DataImpl(value::class, Goal.static(context, value), meta) + } +} + +suspend fun Data.await(): T = goal.await() + +/** + * Generic Data implementation + */ +private class DataImpl( + override val type: KClass, + override val goal: Goal, + override val meta: Meta +) : Data + +class NamedData(val name: String, data: Data) : Data by data + diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt new file mode 100644 index 00000000..6f230c0f --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt @@ -0,0 +1,46 @@ +package hep.dataforge.data + +import hep.dataforge.meta.* +import hep.dataforge.names.toName + + +class DataFilter(override val config: Config) : Specification { + var from by string() + var to by string() + var pattern by string("*.") +// val prefix by string() +// val suffix by string() + + companion object : SpecificationCompanion { + override fun wrap(config: Config): DataFilter = DataFilter(config) + } +} + +/** + * Apply meta-based filter to given data node + */ +fun DataNode.filter(filter: DataFilter): DataNode { + val sourceNode = filter.from?.let { getNode(it.toName()) } ?: this@filter + val regex = filter.pattern.toRegex() + val targetNode = DataTreeBuilder().apply { + sourceNode.dataSequence().forEach { (name, data) -> + if (name.toString().matches(regex)) { + this[name] = data + } + } + } + return filter.to?.let { + DataTreeBuilder().apply { this[it.toName()] = targetNode }.build() + } ?: targetNode.build() +} + +/** + * Filter data using [DataFilter] specification + */ +fun DataNode.filter(filter: Meta): DataNode = filter(DataFilter.wrap(filter)) + +/** + * Filter data using [DataFilter] builder + */ +fun DataNode.filter(filterBuilder: DataFilter.() -> Unit): DataNode = + filter(DataFilter.build(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 new file mode 100644 index 00000000..bf8f6f81 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt @@ -0,0 +1,192 @@ +package hep.dataforge.data + +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.plus +import hep.dataforge.names.toName + +/** + * A tree-like data structure grouped into the node. All data inside the node must inherit its type + */ +interface DataNode { + /** + * Get the specific data if it exists + */ + operator fun get(name: Name): Data? + + /** + * Get a subnode with given name if it exists. + */ + fun getNode(name: Name): DataNode? + + /** + * Walk the tree upside down and provide all data nodes with full names + */ + fun dataSequence(): Sequence>> + + /** + * A sequence of all nodes in the tree walking upside down, excluding self + */ + fun nodeSequence(): Sequence>> + + operator fun iterator(): Iterator>> = dataSequence().iterator() + + companion object { + const val TYPE = "dataNode" + + fun build(block: DataTreeBuilder.() -> Unit) = DataTreeBuilder().apply(block).build() + } + +} + +internal sealed class DataTreeItem { + class Node(val tree: DataTree) : DataTreeItem() + class Value(val value: Data) : DataTreeItem() +} + +class DataTree internal constructor(private val items: Map>) : DataNode { + //TODO add node-level meta? + + override fun get(name: Name): Data? = when (name.length) { + 0 -> error("Empty name") + 1 -> (items[name.first()] as? DataTreeItem.Value)?.value + else -> getNode(name.first()!!.toName())?.get(name.cutFirst()) + } + + override fun getNode(name: Name): DataTree? = when (name.length) { + 0 -> this + 1 -> (items[name.first()] as? DataTreeItem.Node)?.tree + else -> getNode(name.first()!!.toName())?.getNode(name.cutFirst()) + } + + override fun dataSequence(): Sequence>> { + return sequence { + items.forEach { (head, tree) -> + when (tree) { + is DataTreeItem.Value -> yield(head.toName() to tree.value) + is DataTreeItem.Node -> { + val subSequence = + tree.tree.dataSequence().map { (name, data) -> (head.toName() + name) to data } + yieldAll(subSequence) + } + } + } + } + } + + override fun nodeSequence(): Sequence>> { + return sequence { + items.forEach { (head, tree) -> + if (tree is DataTreeItem.Node) { + yield(head.toName() to tree.tree) + val subSequence = + tree.tree.nodeSequence().map { (name, node) -> (head.toName() + name) to node } + yieldAll(subSequence) + } + } + } + } +} + +private sealed class DataTreeBuilderItem { + class Node(val tree: DataTreeBuilder) : DataTreeBuilderItem() + class Value(val value: Data) : DataTreeBuilderItem() +} + +/** + * A builder for a DataTree. + */ +class DataTreeBuilder { + private val map = HashMap>() + + operator fun set(token: NameToken, node: DataTreeBuilder) { + if (map.containsKey(token)) error("Tree entry with name $token is not empty") + map[token] = DataTreeBuilderItem.Node(node) + } + + operator fun set(token: NameToken, data: Data) { + if (map.containsKey(token)) error("Tree entry with name $token is not empty") + map[token] = DataTreeBuilderItem.Value(data) + } + + private fun buildNode(token: NameToken): DataTreeBuilder { + return if (!map.containsKey(token)) { + DataTreeBuilder().also { map[token] = DataTreeBuilderItem.Node(it) } + } else { + (map[token] as? DataTreeBuilderItem.Node ?: error("The node with name $token is occupied by leaf")).tree + } + } + + private fun buildNode(name: Name): DataTreeBuilder { + return when (name.length) { + 0 -> this + 1 -> buildNode(name.first()!!) + else -> buildNode(name.first()!!).buildNode(name.cutFirst()) + } + } + + operator fun set(name: Name, data: Data) { + when (name.length) { + 0 -> error("Can't add data with empty name") + 1 -> set(name.first()!!, data) + 2 -> buildNode(name.cutLast())[name.last()!!] = data + } + } + + operator fun set(name: Name, node: DataTreeBuilder) { + when (name.length) { + 0 -> error("Can't add data with empty name") + 1 -> set(name.first()!!, node) + 2 -> buildNode(name.cutLast())[name.last()!!] = node + } + } + + operator fun set(name: Name, node: DataNode) = set(name, node.builder()) + + /** + * Append data to node + */ + infix fun String.to(data: Data) = set(toName(), data) + + /** + * Append node + */ + infix fun String.to(node: DataNode) = set(toName(), node) + + /** + * Build and append node + */ + infix fun String.to(block: DataTreeBuilder.() -> Unit) = set(toName(), DataTreeBuilder().apply(block)) + + fun build(): DataTree { + val resMap = map.mapValues { (_, value) -> + when (value) { + is DataTreeBuilderItem.Value -> DataTreeItem.Value(value.value) + is DataTreeBuilderItem.Node -> DataTreeItem.Node(value.tree.build()) + } + } + return DataTree(resMap) + } +} + +/** + * Generate a mutable builder from this node. Node content is not changed + */ +fun DataNode.builder(): DataTreeBuilder = DataTreeBuilder().apply { + dataSequence().forEach { (name, data) -> this[name] = data } +} + +/** + * Start computation for all goals in data node + */ +fun DataNode<*>.startAll() = dataSequence().forEach { (_, data) -> data.goal.start() } + +fun DataNode.filter(predicate: (Name, Data) -> Boolean): DataNode = DataNode.build { + dataSequence().forEach { (name, data) -> + if (predicate(name, data)) { + this[name] = data + } + } +} + +//fun DataNode.filterIsInstance(type: KClass): DataNode = filter{_,data -> type.} \ No newline at end of file diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt new file mode 100644 index 00000000..104a9037 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt @@ -0,0 +1,106 @@ +package hep.dataforge.data + +import kotlinx.coroutines.* +import kotlin.coroutines.CoroutineContext + +/** + * A special deferred with explicit dependencies and some additional information like progress and unique id + */ +interface Goal : Deferred, CoroutineScope { + val dependencies: Collection> + + val status: String + + val totalWork: Double + val workDone: Double + + val progress: Double get() = workDone / totalWork + + companion object { + /** + * Create goal wrapping static value. This goal is always completed + */ + fun static(context: CoroutineContext, value: T): Goal = + StaticGoalImpl(context, CompletableDeferred(value)) + } +} + +/** + * A monitor of goal state that could be accessed only form inside the goal + */ +class GoalMonitor { + var totalWork: Double = 1.0 + var workDone: Double = 0.0 + var status: String = "" + + /** + * Mark the goal as started + */ + fun start() { + + } + + /** + * Mark the goal as completed + */ + fun finish() { + workDone = totalWork + } +} + +private class GoalImpl( + override val dependencies: Collection>, + val monitor: GoalMonitor, + deferred: Deferred +) : Goal, Deferred by deferred { + override val coroutineContext: CoroutineContext get() = this + override val totalWork: Double get() = dependencies.sumByDouble { totalWork } + monitor.totalWork + override val workDone: Double get() = dependencies.sumByDouble { workDone } + monitor.workDone + override val status: String get() = monitor.status +} + +private class StaticGoalImpl(val context: CoroutineContext, deferred: CompletableDeferred) : Goal, + Deferred by deferred { + override val dependencies: Collection> get() = emptyList() + override val status: String get() = "" + override val totalWork: Double get() = 0.0 + override val workDone: Double get() = 0.0 + override val coroutineContext: CoroutineContext get() = context +} + + +/** + * Create a new [Goal] with given [dependencies] and execution [block]. The block takes monitor as parameter. + * The goal block runs in a supervised scope, meaning that when it fails, it won't affect external scope. + * + * **Important:** Unlike regular deferred, the [Goal] is started lazily, so the actual calculation is called only when result is requested. + */ +fun CoroutineScope.createGoal(dependencies: Collection>, block: suspend GoalMonitor.() -> R): Goal { + val monitor = GoalMonitor() + val deferred = async(start = CoroutineStart.LAZY) { + dependencies.forEach { it.start() } + monitor.start() + return@async supervisorScope { monitor.block() } + }.also { + monitor.finish() + } + + return GoalImpl(dependencies, monitor, deferred) +} + +/** + * Create a one-to-one goal based on existing goal + */ +fun Goal.pipe(block: suspend GoalMonitor.(T) -> R): Goal = createGoal(listOf(this)) { block(await()) } + +/** + * Create a joining goal. + * @param scope the scope for resulting goal. By default use first goal in list + */ +fun Collection>.join( + scope: CoroutineScope = first(), + block: suspend GoalMonitor.(Collection) -> R +): Goal = + scope.createGoal(this) { + block(map { it.await() }) + } \ No newline at end of file diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/_Data.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/_Data.kt new file mode 100644 index 00000000..00c9e656 --- /dev/null +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/_Data.kt @@ -0,0 +1,8 @@ +package hep.dataforge.data + +import kotlinx.coroutines.runBlocking + +/** + * Block the thread and get data content + */ +fun Data.get(): T = runBlocking { await() } \ No newline at end of file diff --git a/dataforge-io/build.gradle.kts b/dataforge-io/build.gradle.kts new file mode 100644 index 00000000..1756f10d --- /dev/null +++ b/dataforge-io/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-context")) + api(project(":dataforge-meta-io")) + } + } + } +} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt new file mode 100644 index 00000000..e9d1800c --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt @@ -0,0 +1,22 @@ +package hep.dataforge.io + +import hep.dataforge.context.ContextAware +import hep.dataforge.meta.EmptyMeta +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] + * based on its configuration and provided meta + * + */ +interface Output : ContextAware { + /** + * Render specific object with configuration. + * + * By convention actual render is called in asynchronous mode, so this method should never + * block execution + */ + fun render(obj: T, meta: Meta = EmptyMeta) +} diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt new file mode 100644 index 00000000..3a96c458 --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt @@ -0,0 +1,63 @@ +package hep.dataforge.io + +import hep.dataforge.context.AbstractPlugin +import hep.dataforge.context.Plugin +import hep.dataforge.context.PluginTag +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 kotlin.reflect.KClass + +/** + * A manager for outputs + */ +interface OutputManager : Plugin { + /** + * Provide an output for given 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) + * + */ + operator fun get(name: Name, stage: Name = EmptyName, meta: Meta = EmptyMeta): Output + + /** + * Get an output specialized for giver ntype + */ + fun typed(type: KClass, name: Name, stage: Name = EmptyName, meta: Meta = EmptyMeta): Output + +} + +/** + * Get an output with given [name], [stage] and reified content type + */ +inline fun OutputManager.typed( + name: Name, + stage: Name = EmptyName, + meta: Meta = EmptyMeta +): Output { + return typed(T::class, name, stage, meta) +} + +/** + * System console output. + * The [ConsoleOutput] is used when no other [OutputManager] is provided. + */ +expect val ConsoleOutput: Output + +object ConsoleOutputManager : AbstractPlugin(), OutputManager { + override val tag: PluginTag = PluginTag("output.console", group = DATAFORGE_GROUP) + + override fun get(name: Name, stage: Name, meta: Meta): Output = ConsoleOutput + + override fun typed(type: KClass, name: Name, stage: Name, meta: Meta): Output = ConsoleOutput +} + +/** + * A dispatcher for output tasks. + */ +expect val OutputDispatcher : CoroutineDispatcher \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt new file mode 100644 index 00000000..446eed31 --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt @@ -0,0 +1,67 @@ +package hep.dataforge.io + +import hep.dataforge.context.Context +import hep.dataforge.io.TextRenderer.Companion.TEXT_RENDERER_TYPE +import hep.dataforge.meta.Meta +import hep.dataforge.provider.Type +import hep.dataforge.provider.provideAll +import kotlinx.coroutines.launch +import kotlin.reflect.KClass + +class TextOutput(override val context: Context, private val output: kotlinx.io.core.Output) : Output { + private val cache = HashMap, TextRenderer>() + + /** + * Find the first [TextRenderer] matching the given object type. + */ + override fun render(obj: Any, meta: Meta) { + val renderer: TextRenderer = if (obj is CharSequence) { + DefaultTextRenderer + } else { + val value = cache[obj::class] + if (value == null) { + val answer = context.provideAll(TEXT_RENDERER_TYPE).filterIsInstance() + .filter { it.type.isInstance(obj) }.firstOrNull() + if (answer != null) { + cache[obj::class] = answer + answer + } else { + DefaultTextRenderer + } + } else { + value + } + } + context.launch(OutputDispatcher) { + renderer.run { output.render(obj) } + } + } +} + +@Type(TEXT_RENDERER_TYPE) +interface TextRenderer { + /** + * The priority of this renderer compared to other renderers + */ + val priority: Int + /** + * The type of the content served by this renderer + */ + val type: KClass<*> + + suspend fun kotlinx.io.core.Output.render(obj: Any) + + companion object { + const val TEXT_RENDERER_TYPE = "dataforge.textRenderer" + } +} + +object DefaultTextRenderer : TextRenderer { + 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') + } +} \ No newline at end of file diff --git a/dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt b/dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt new file mode 100644 index 00000000..3aed2000 --- /dev/null +++ b/dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt @@ -0,0 +1,22 @@ +package hep.dataforge.io + +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 = object : Output { + override fun render(obj: Any, meta: Meta) { + println(obj) + } + + override val context: Context get() = Global + +} + +actual val OutputDispatcher: CoroutineDispatcher = Dispatchers.Default \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt new file mode 100644 index 00000000..b4f28887 --- /dev/null +++ b/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt @@ -0,0 +1,13 @@ +package hep.dataforge.io + +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 = TextOutput(Global, System.out.asOutput()) + +actual val OutputDispatcher = Dispatchers.IO \ No newline at end of file diff --git a/dataforge-meta-io/build.gradle.kts b/dataforge-meta-io/build.gradle.kts new file mode 100644 index 00000000..1b57774d --- /dev/null +++ b/dataforge-meta-io/build.gradle.kts @@ -0,0 +1,57 @@ +plugins { + kotlin("multiplatform") +} + +description = "IO for meta" + + +val ioVersion: String by rootProject.extra +val serializationVersion: String by rootProject.extra + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-meta")) + //implementation 'org.jetbrains.kotlin:kotlin-reflect' + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-common:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-io:$ioVersion") + } + } + val commonTest by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test-common") + implementation("org.jetbrains.kotlin:kotlin-test-annotations-common") + } + } + val jvmMain by getting { + dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-io-jvm:$ioVersion") + } + } + val jvmTest by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test") + implementation("org.jetbrains.kotlin:kotlin-test-junit") + } + } + val jsMain by getting { + dependencies { + api("org.jetbrains.kotlinx:kotlinx-serialization-runtime-js:$serializationVersion") + api("org.jetbrains.kotlinx:kotlinx-io-js:$ioVersion") + } + } + val jsTest by getting { + dependencies { + implementation("org.jetbrains.kotlin:kotlin-test-js") + } + } +// iosMain { +// } +// iosTest { +// } + } +} \ No newline at end of file diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Binary.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Binary.kt new file mode 100644 index 00000000..7ab032c2 --- /dev/null +++ b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Binary.kt @@ -0,0 +1,7 @@ +package hep.dataforge.meta.io + +import kotlinx.io.ByteBuffer +import kotlinx.io.core.Input + +//TODO replace by abstraction +typealias Binary = Input \ No newline at end of file diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/BinaryMetaFormat.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/BinaryMetaFormat.kt new file mode 100644 index 00000000..176435da --- /dev/null +++ b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/BinaryMetaFormat.kt @@ -0,0 +1,132 @@ +package hep.dataforge.meta.io + +import hep.dataforge.meta.* +import hep.dataforge.values.* +import kotlinx.io.core.Input +import kotlinx.io.core.Output +import kotlinx.io.core.readText +import kotlinx.io.core.writeText + +object BinaryMetaFormat : MetaFormat { + override fun write(meta: Meta, out: Output) { + out.writeMeta(meta) + } + + override fun read(input: Input): Meta { + return (input.readMetaItem() as MetaItem.NodeItem).node + } + + private fun Output.writeChar(char: Char) = writeByte(char.toByte()) + + private fun Output.writeString(str: String) { + writeInt(str.length) + writeText(str) + } + + private fun Output.writeValue(value: Value) { + if (value.isList()) { + writeChar('L') + writeInt(value.list.size) + value.list.forEach { + writeValue(it) + } + } else when (value.type) { + ValueType.NUMBER -> when (value.value) { + is Short -> { + writeChar('s') + writeShort(value.number.toShort()) + } + is Int -> { + writeChar('i') + writeInt(value.number.toInt()) + } + is Long -> { + writeChar('l') + writeLong(value.number.toLong()) + } + is Float -> { + writeChar('f') + writeFloat(value.number.toFloat()) + } + else -> { + writeChar('d') + writeDouble(value.number.toDouble()) + } + } + ValueType.STRING -> { + writeChar('S') + writeString(value.string) + } + ValueType.BOOLEAN -> { + if (value.boolean) { + writeChar('+') + } else { + writeChar('-') + } + } + ValueType.NULL -> { + writeChar('N') + } + } + } + + private fun Output.writeMeta(meta: Meta) { + writeChar('M') + writeInt(meta.items.size) + meta.items.forEach { (key, item) -> + writeString(key.toString()) + when (item) { + is MetaItem.ValueItem -> { + writeValue(item.value) + } + is MetaItem.NodeItem -> { + writeMeta(item.node) + } + } + } + } + + private fun Input.readString(): String { + val length = readInt() + return readText(max = length) + } + + private fun Input.readMetaItem(): MetaItem { + val keyChar = readByte().toChar() + return when (keyChar) { + 'S' -> MetaItem.ValueItem(StringValue(readString())) + 'N' -> MetaItem.ValueItem(Null) + '+' -> MetaItem.ValueItem(True) + '-' -> MetaItem.ValueItem(True) + 's' -> MetaItem.ValueItem(NumberValue(readShort())) + 'i' -> MetaItem.ValueItem(NumberValue(readInt())) + 'l' -> MetaItem.ValueItem(NumberValue(readInt())) + 'f' -> MetaItem.ValueItem(NumberValue(readFloat())) + 'd' -> MetaItem.ValueItem(NumberValue(readDouble())) + 'L' -> { + val length = readInt() + val list = (1..length).map { (readMetaItem() as MetaItem.ValueItem).value } + MetaItem.ValueItem(Value.of(list)) + } + 'M' -> { + val length = readInt() + val meta = buildMeta { + (1..length).forEach { _ -> + val name = readString() + val item = readMetaItem() + setItem(name, item) + } + } + MetaItem.NodeItem(meta) + } + else -> error("Unknown serialization key character: $keyChar") + } + } +} + +class BinaryMetaFormatFactory : MetaFormatFactory { + override val name: String = "bin" + override val key: Short = 0x4249//BI + + override fun build(): MetaFormat = BinaryMetaFormat +} \ No newline at end of file diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt new file mode 100644 index 00000000..ce01d506 --- /dev/null +++ b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt @@ -0,0 +1,50 @@ +package hep.dataforge.meta.io + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.string + +interface Envelope { + val meta: Meta + val data: Binary? + + companion object { +// /** +// * Property keys +// */ +// const val TYPE_PROPERTY = "type" +// const val META_TYPE_PROPERTY = "metaType" +// const val META_LENGTH_PROPERTY = "metaLength" +// const val DATA_LENGTH_PROPERTY = "dataLength" + + /** + * meta keys + */ + const val ENVELOPE_NODE = "@envelope" + const val ENVELOPE_TYPE_KEY = "$ENVELOPE_NODE.type" + const val ENVELOPE_DATA_TYPE_KEY = "$ENVELOPE_NODE.dataType" + const val ENVELOPE_DESCRIPTION_KEY = "$ENVELOPE_NODE.description" + //const val ENVELOPE_TIME_KEY = "@envelope.time" + } +} + +/** + * The purpose of the envelope + * + * @return + */ +val Envelope.type: String? get() = meta[Envelope.ENVELOPE_TYPE_KEY].string + +/** + * The type of data encoding + * + * @return + */ +val Envelope.dataType: String? get() = meta[Envelope.ENVELOPE_DATA_TYPE_KEY].string + +/** + * Textual user friendly description + * + * @return + */ +val Envelope.description: String? get() = meta[Envelope.ENVELOPE_DESCRIPTION_KEY].string \ No newline at end of file diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/JsonMetaFormat.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/JsonMetaFormat.kt new file mode 100644 index 00000000..2c82c085 --- /dev/null +++ b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/JsonMetaFormat.kt @@ -0,0 +1,89 @@ +package hep.dataforge.meta.io + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.NameToken +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.* + + +object JsonMetaFormat : MetaFormat { + + override fun write(meta: Meta, out: Output) { + val str = meta.toJson().toString() + out.writeText(str) + } + + override fun read(input: Input): Meta { + val str = input.readText() + val json = Json.plain.parseJson(str) + + if(json is JsonObject) { + return json.toMeta() + } else { + TODO("non-object root") + } + } +} + +fun Value.toJson(): JsonElement { + return when (type) { + ValueType.NUMBER -> JsonPrimitive(number) + ValueType.STRING -> JsonPrimitive(string) + ValueType.BOOLEAN -> JsonPrimitive(boolean) + ValueType.NULL -> JsonNull + } +} + +fun Meta.toJson(): JsonObject { + val map = this.items.mapValues { entry -> + val value = entry.value + when (value) { + is MetaItem.ValueItem -> value.value.toJson() + is MetaItem.NodeItem -> value.node.toJson() + } + }.mapKeys { it.key.toString() } + return JsonObject(map) +} + + +fun JsonElement.toMetaItem() = when (this) { + is JsonPrimitive -> MetaItem.ValueItem(this.toValue()) + is JsonObject -> MetaItem.NodeItem(this.toMeta()) + is JsonArray -> { + if (this.all { it is JsonPrimitive }) { + val value = ListValue(this.map { (it as JsonPrimitive).toValue() }) + MetaItem.ValueItem(value) + } else { + TODO("mixed nodes json") + } + } +} + +fun JsonObject.toMeta() = JsonMeta(this) + +private fun JsonPrimitive.toValue(): Value { + return when (this) { + JsonNull -> Null + else -> this.content.parseValue() // Optimize number and boolean parsing + } +} + +class JsonMeta(val json: JsonObject) : Meta { + override val items: Map> by lazy { + json.mapKeys { NameToken(it.key) }.mapValues { entry -> + entry.value.toMetaItem() + } + } +} + +class JsonMetaFormatFactory : MetaFormatFactory { + override val name: String = "json" + override val key: Short = 0x4a53//"JS" + + override fun build() = JsonMetaFormat +} \ No newline at end of file diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/MetaFormat.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/MetaFormat.kt new file mode 100644 index 00000000..2d7de871 --- /dev/null +++ b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/MetaFormat.kt @@ -0,0 +1,34 @@ +package hep.dataforge.meta.io + +import hep.dataforge.meta.Meta +import kotlinx.io.core.* + +/** + * A format for meta serialization + */ +interface MetaFormat { + fun write(meta: Meta, out: Output) + fun read(input: Input): Meta +} + +/** + * ServiceLoader compatible factory + */ +interface MetaFormatFactory { + val name: String + val key: Short + + fun build(): MetaFormat +} + +fun Meta.asString(format: MetaFormat = JsonMetaFormat): String { + val builder = BytePacketBuilder() + format.write(this, builder) + return builder.build().readText() +} + +fun MetaFormat.parse(str: String): Meta { + return read(ByteReadPacket(str.toByteArray())) +} + + diff --git a/dataforge-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt b/dataforge-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt new file mode 100644 index 00000000..78966dc8 --- /dev/null +++ b/dataforge-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt @@ -0,0 +1,36 @@ +package hep.dataforge.meta.io + +import hep.dataforge.meta.buildMeta +import kotlin.test.Test +import kotlin.test.assertEquals + +class MetaFormatTest { + @Test + fun testBinaryMetaFormat() { + val meta = buildMeta { + "a" to 22 + "node" to { + "b" to "DDD" + "c" to 11.1 + } + } + val string = meta.asString(BinaryMetaFormat) + val result = BinaryMetaFormat.parse(string) + assertEquals(meta, result) + } + + @Test + fun testJsonMetaFormat() { + val meta = buildMeta { + "a" to 22 + "node" to { + "b" to "DDD" + "c" to 11.1 + } + } + val string = meta.asString(JsonMetaFormat) + val result = JsonMetaFormat.parse(string) + assertEquals(meta, result) + } + +} \ No newline at end of file diff --git a/dataforge-meta-io/src/jvmMain/resources/META-INF/services/hep.dataforge.meta.io.MetaFormatFactory b/dataforge-meta-io/src/jvmMain/resources/META-INF/services/hep.dataforge.meta.io.MetaFormatFactory new file mode 100644 index 00000000..23bcfcd2 --- /dev/null +++ b/dataforge-meta-io/src/jvmMain/resources/META-INF/services/hep.dataforge.meta.io.MetaFormatFactory @@ -0,0 +1,2 @@ +hep.dataforge.meta.io.BinaryMetaFormatFactory +hep.dataforge.meta.io.JsonMetaFormatFactory \ No newline at end of file diff --git a/dataforge-meta-js/build.gradle b/dataforge-meta-js/build.gradle deleted file mode 100644 index 59da54d1..00000000 --- a/dataforge-meta-js/build.gradle +++ /dev/null @@ -1,17 +0,0 @@ -plugins { - id 'kotlin-platform-js' -} - -group 'hep.dataforge' -version '0.1.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - expectedBy rootProject - - compile "org.jetbrains.kotlin:kotlin-stdlib-js" - testCompile "org.jetbrains.kotlin:kotlin-test-js" -} diff --git a/dataforge-meta-jvm/build.gradle b/dataforge-meta-jvm/build.gradle deleted file mode 100644 index 99ac4a6d..00000000 --- a/dataforge-meta-jvm/build.gradle +++ /dev/null @@ -1,27 +0,0 @@ -plugins { - id 'kotlin-platform-jvm' -} - -group 'hep.dataforge' -version '0.1.0-SNAPSHOT' - -repositories { - mavenCentral() -} - -dependencies { - expectedBy rootProject - - compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8" - testCompile "junit:junit:4.12" - testCompile "org.jetbrains.kotlin:kotlin-test" - testCompile "org.jetbrains.kotlin:kotlin-test-junit" -} - -compileKotlin { - kotlinOptions.jvmTarget = "1.8" -} -compileTestKotlin { - kotlinOptions.jvmTarget = "1.8" -} -sourceCompatibility = "1.8" \ No newline at end of file diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts new file mode 100644 index 00000000..e754cf4b --- /dev/null +++ b/dataforge-meta/build.gradle.kts @@ -0,0 +1,60 @@ +plugins { + kotlin("multiplatform") +} + +description = "Meta definition and basic operations on meta" + +kotlin { + jvm() + js() + + sourceSets { + + val commonMain by getting { + dependencies { + api(kotlin("stdlib")) + } + } + val commonTest by getting { + dependencies { + implementation(kotlin("test-common")) + implementation(kotlin("test-annotations-common")) + } + } + val jvmMain by getting { + dependencies { + api(kotlin("stdlib-jdk8")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-junit")) + } + } + val jsMain by getting { + dependencies { + api(kotlin("stdlib-js")) + } + } + val jsTest by getting { + dependencies { + implementation(kotlin("test-js")) + } + } +// mingwMain { +// } +// mingwTest { +// } + } +} + +//tasks.withType{ +// kotlinOptions{ +// metaInfo = true +// outputFile = "${project.buildDir.path}/js/${project.name}.js" +// sourceMap = true +// moduleKind = "umd" +// main = "call" +// } +//} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Config.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt similarity index 72% rename from src/main/kotlin/hep/dataforge/meta/Config.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt index 1f53ba89..79407337 100644 --- a/src/main/kotlin/hep/dataforge/meta/Config.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt @@ -1,6 +1,8 @@ package hep.dataforge.meta import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.toName //TODO add validator to configuration @@ -15,15 +17,20 @@ open class Config : MutableMetaNode() { override fun wrap(name: Name, meta: Meta): Config = meta.toConfig() override fun empty(): Config = Config() + + companion object { + fun empty(): Config = Config() + } } +operator fun Config.get(token: NameToken): MetaItem? = items[token] + fun Meta.toConfig(): Config = this as? Config ?: Config().also { builder -> this.items.mapValues { entry -> val item = entry.value - builder[entry.key] = when (item) { + builder[entry.key.toName()] = when (item) { is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) - is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(item.node.toConfig()) - is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { it.toConfig() }) + is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.toConfig()) } } } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ConfigDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ConfigDelegates.kt new file mode 100644 index 00000000..581fa7ed --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ConfigDelegates.kt @@ -0,0 +1,42 @@ +package hep.dataforge.meta + +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: Value = Null, key: String? = null) = + ValueConfigDelegate(config, key, default) + +fun Configurable.string(default: String? = null, key: String? = null) = + StringConfigDelegate(config, key, default) + +fun Configurable.boolean(default: Boolean? = null, key: String? = null) = + BooleanConfigDelegate(config, key, default) + +fun Configurable.number(default: Number? = null, key: String? = null) = + NumberConfigDelegate(config, key, default) + +fun Configurable.child(key: String? = null) = MetaNodeDelegate(config, key) + +//fun Configurable.spec(spec: Specification, key: String? = null) = ChildConfigDelegate(key) { spec.wrap(this) } + +@JvmName("safeString") +fun Configurable.string(default: String, key: String? = null) = + SafeStringConfigDelegate(config, key, default) + +@JvmName("safeBoolean") +fun Configurable.boolean(default: Boolean, key: String? = null) = + SafeBooleanConfigDelegate(config, key, default) + +@JvmName("safeNumber") +fun Configurable.number(default: Number, key: String? = null) = + SafeNumberConfigDelegate(config, key, default) + +inline fun > Configurable.enum(default: E, key: String? = null) = + SafeEnumvConfigDelegate(config, key, default) { enumValueOf(it) } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt new file mode 100644 index 00000000..5672d2cf --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt @@ -0,0 +1,359 @@ +package hep.dataforge.meta + +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 { + 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 { + 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 { + override fun getValue(thisRef: Metoid, 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 { + 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(val delegate: ReadOnlyProperty, val reader: (T) -> R) : + ReadOnlyProperty { + 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, private val default: String) : + ReadOnlyProperty { + 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, private val default: Boolean) : + ReadOnlyProperty { + 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, private val default: Number) : + ReadOnlyProperty { + 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>( + val meta: Meta, + private val key: String? = null, + private val default: E, + private val resolver: (String) -> E +) : ReadOnlyProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): E { + return (meta[key ?: property.name]?.string)?.let { resolver(it) } ?: default + } +} + +//Child node delegate + +class ChildDelegate(val meta: Meta, private val key: String? = null, private val converter: (Meta) -> T) : + ReadOnlyProperty { + 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.child(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) + +inline fun > Meta.enum(default: E, key: String? = null) = + SafeEnumDelegate(this, key, default) { enumValueOf(it) } + + +/* Config delegates */ + +class ValueConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: Value? = null +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Value? { + return config[key ?: property.name]?.value ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Value?) { + val name = key ?: property.name + if (value == null) { + config.remove(name) + } else { + config.setValue(name, value) + } + } +} + +class StringConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: String? = null +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): String? { + return config[key ?: property.name]?.string ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String?) { + val name = key ?: property.name + if (value == null) { + config.remove(name) + } else { + config.setValue(name, value.asValue()) + } + } +} + +class BooleanConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: Boolean? = null +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean? { + return config[key ?: property.name]?.boolean ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean?) { + val name = key ?: property.name + if (value == null) { + config.remove(name) + } else { + config.setValue(name, value.asValue()) + } + } +} + +class NumberConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: Number? = null +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Number? { + return config[key ?: property.name]?.number ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number?) { + val name = key ?: property.name + if (value == null) { + config.remove(name) + } else { + config.setValue(name, value.asValue()) + } + } + + val double get() = ReadWriteDelegateWrapper(this, reader = { it?.toDouble() }, 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 SafeStringConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: String +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): String { + return config[key ?: property.name]?.string ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { + config.setValue(key ?: property.name, value.asValue()) + } +} + +class SafeBooleanConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: Boolean +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { + return config[key ?: property.name]?.boolean ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { + config.setValue(key ?: property.name, value.asValue()) + } +} + +class SafeNumberConfigDelegate>( + val config: M, + private val key: String? = null, + private val default: Number +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Number { + return config[key ?: property.name]?.number ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number) { + config.setValue(key ?: property.name, value.asValue()) + } + + val double get() = ReadWriteDelegateWrapper(this, reader = { it.toDouble() }, 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 SafeEnumvConfigDelegate, E : Enum>( + val config: M, + private val key: String? = null, + private val default: E, + private val resolver: (String) -> E +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): E { + return (config[key ?: property.name]?.string)?.let { resolver(it) } ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: E) { + config.setValue(key ?: property.name, value.name.asValue()) + } +} + +//Child node delegate + +class MetaNodeDelegate>( + val config: M, + private val key: String? = null +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Meta { + return config[key ?: property.name]?.node ?: EmptyMeta + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Meta) { + config[key ?: property.name] = value + } +} + +class ChildConfigDelegate, T : Configurable>( + val config: M, + private val key: String? = null, + private val converter: (Meta) -> T +) : + ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return converter(config[key ?: property.name]?.node ?: EmptyMeta) + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + config[key ?: property.name] = value.config + } +} + +class ReadWriteDelegateWrapper( + val delegate: ReadWriteProperty, + val reader: (T) -> R, + val writer: (R) -> T +) : ReadWriteProperty { + 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.value(default: Value = Null, key: String? = null) = + ValueConfigDelegate(this, key, default) + +fun > M.string(default: String? = null, key: String? = null) = + StringConfigDelegate(this, key, default) + +fun > M.boolean(default: Boolean? = null, key: String? = null) = + BooleanConfigDelegate(this, key, default) + +fun > M.number(default: Number? = null, key: String? = null) = + NumberConfigDelegate(this, key, default) + +fun > M.child(key: String? = null) = MetaNodeDelegate(this, key) + +//fun Configurable.spec(spec: Specification, key: String? = null) = ChildConfigDelegate(key) { spec.wrap(this) } + +@JvmName("safeString") +fun > M.string(default: String, key: String? = null) = + SafeStringConfigDelegate(this, key, default) + +@JvmName("safeBoolean") +fun > M.boolean(default: Boolean, key: String? = null) = + SafeBooleanConfigDelegate(this, key, default) + +@JvmName("safeNumber") +fun > M.number(default: Number, key: String? = null) = + SafeNumberConfigDelegate(this, key, default) + +inline fun , reified E : Enum> M.enum(default: E, key: String? = null) = + SafeEnumvConfigDelegate(this, key, default) { enumValueOf(it) } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt new file mode 100644 index 00000000..d0eaa54e --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt @@ -0,0 +1,36 @@ +package hep.dataforge.meta + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/* + * Extra delegates for special cases + */ + +/** + * A delegate for a string list + */ +class StringListConfigDelegate( + val config: Config, + private val key: String? = null, + private val default: List = emptyList() +) : + ReadWriteProperty> { + override fun getValue(thisRef: Any?, property: KProperty<*>): List { + return config[key ?: property.name]?.value?.list?.map { it.string } ?: default + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: List) { + val name = key ?: property.name + config[name] = value + } +} + +fun Configurable.stringList(vararg default: String = emptyArray(), key: String? = null) = + StringListConfigDelegate(config, key, default.toList()) + + +fun Metoid.child(key: String? = null, converter: (Meta) -> T) = ChildDelegate(meta, key, converter) + +fun Configurable.child(key: String? = null, converter: (Meta) -> T) = + ChildConfigDelegate(config, key, converter) diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt new file mode 100644 index 00000000..b16248ac --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Laminate.kt @@ -0,0 +1,82 @@ +package hep.dataforge.meta + +import hep.dataforge.names.NameToken + +/** + * A meta laminate consisting of multiple immutable meta layers. For mutable front layer, use [Styled]. + * + * + */ +class Laminate(layers: List) : Meta { + + val layers: List = layers.flatMap { + if (it is Laminate) { + it.layers + } else { + listOf(it) + } + } + + constructor(vararg layers: Meta) : this(layers.asList()) + + override val items: Map> + get() = layers.map { it.items.keys }.flatten().associateWith { key -> + layers.asSequence().map { it.items[key] }.filterNotNull().let(replaceRule) + } + + /** + * Generate sealed meta using [mergeRule] + */ + fun merge(): SealedMeta { + val items = layers.map { it.items.keys }.flatten().associateWith { key -> + layers.asSequence().map { it.items[key] }.filterNotNull().merge() + } + return SealedMeta(items) + } + + companion object { + + /** + * The default rule which always uses the first found item in sequence alongside with its children. + * + * TODO add picture + */ + val replaceRule: (Sequence>) -> MetaItem = { it.first().seal() } + + private fun Sequence>.merge(): MetaItem { + return when { + all { it is MetaItem.ValueItem } -> //If all items are values, take first + first().seal() + all { it is MetaItem.NodeItem } -> { + //list nodes in item + val nodes = map { (it as MetaItem.NodeItem).node } + //represent as key->value entries + val entries = nodes.flatMap { it.items.entries.asSequence() } + //group by keys + val groups = entries.groupBy { it.key } + // recursively apply the rule + val items = groups.mapValues { entry -> + entry.value.asSequence().map { it.value }.merge() + } + MetaItem.NodeItem(SealedMeta(items)) + + } + else -> map { + when (it) { + is MetaItem.ValueItem -> MetaItem.NodeItem(buildMeta { Meta.VALUE_KEY to it.value }) + is MetaItem.NodeItem -> it + } + }.merge() + } + } + + + /** + * The values a replaced but meta children are joined + * TODO add picture + */ + val mergeRule: (Sequence>) -> MetaItem = { it.merge() } + } +} + +//TODO add custom rules for Laminate merge diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt new file mode 100644 index 00000000..08b1ba09 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt @@ -0,0 +1,202 @@ +package hep.dataforge.meta + +import hep.dataforge.meta.Meta.Companion.VALUE_KEY +import hep.dataforge.meta.MetaItem.NodeItem +import hep.dataforge.meta.MetaItem.ValueItem +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.plus +import hep.dataforge.names.toName +import hep.dataforge.values.EnumValue +import hep.dataforge.values.Value +import hep.dataforge.values.boolean + + +/** + * A member of the meta tree. Could be represented as one of following: + * * a [ValueItem] (leaf) + * * a [NodeItem] (node) + */ +sealed class MetaItem { + data class ValueItem(val value: Value) : MetaItem() + data class NodeItem(val node: M) : MetaItem() +} + +/** + * The object that could be represented as [Meta]. Meta provided by [toMeta] method should fully represent object state. + * Meaning that two states with the same meta are equal. + */ +interface MetaRepr { + fun toMeta(): Meta +} + +/** + * Generic meta tree representation. Elements are [MetaItem] objects that could be represented by three different entities: + * * [MetaItem.ValueItem] (leaf) + * * [MetaItem.NodeItem] single node + * + * * Same name siblings are supported via elements with the same [Name] but different queries + */ +interface Meta : MetaRepr { + val items: Map> + + override fun toMeta(): Meta = this + + companion object { + const val TYPE = "meta" + /** + * A key for single value node + */ + const val VALUE_KEY = "@value" + } +} + +/* Get operations*/ + +/** + * Fast [String]-based accessor for item map + */ +operator fun Map.get(body: String, query: String = ""): T? = get(NameToken(body, query)) + +operator fun Meta?.get(name: Name): MetaItem? { + 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) + } + } +} + +operator fun Meta?.get(token: NameToken): MetaItem? = this?.items?.get(token) +operator fun Meta?.get(key: String): MetaItem? = get(key.toName()) + +/** + * Get all items matching given name. + */ +fun Meta.getAll(name: Name): Map> { + if (name.length == 0) error("Can't use empty name for that") + val (body, query) = name.last()!! + val regex = query.toRegex() + return (this[name.cutLast()] as? NodeItem<*>)?.node?.items + ?.filter { it.key.body == body && (query.isEmpty() || regex.matches(it.key.query)) } + ?.mapKeys { it.key.query } + ?: emptyMap() + +} + +/** + * Transform meta to sequence of [Name]-[Value] pairs + */ +fun Meta.asValueSequence(): Sequence> { + return items.asSequence().flatMap { entry -> + val item = entry.value + when (item) { + is ValueItem -> sequenceOf(entry.key.toName() to item.value) + is NodeItem -> item.node.asValueSequence().map { pair -> (entry.key.toName() + pair.first) to pair.second } + } + } +} + +operator fun Meta.iterator(): Iterator> = asValueSequence().iterator() + +/** + * A meta node that ensures that all of its descendants has at least the same type + */ +interface MetaNode> : Meta { + override val items: Map> +} + +operator fun > MetaNode.get(name: Name): MetaItem? { + return name.first()?.let { token -> + val tail = name.cutFirst() + when (tail.length) { + 0 -> items[token] + else -> items[token]?.node?.get(tail) + } + } +} + +operator fun > MetaNode.get(key: String): MetaItem? = get(key.toName()) + +/** + * Equals and hash code implementation for meta node + */ +abstract class AbstractMetaNode> : MetaNode { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Meta) return false + + return this.items == other.items + } + + override fun hashCode(): Int { + return items.hashCode() + } +} + +/** + * The meta implementation which is guaranteed to be immutable. + * + * If the argument is possibly mutable node, it is copied on creation + */ +class SealedMeta internal constructor(override val items: Map>) : + AbstractMetaNode() + +/** + * Generate sealed node from [this]. If it is already sealed return it as is + */ +fun Meta.seal(): SealedMeta = this as? SealedMeta ?: SealedMeta(items.mapValues { entry -> entry.value.seal() }) + +fun MetaItem<*>.seal(): MetaItem = when (this) { + is MetaItem.ValueItem -> MetaItem.ValueItem(value) + is MetaItem.NodeItem -> MetaItem.NodeItem(node.seal()) +} + +object EmptyMeta : Meta { + override val items: Map> = emptyMap() +} + +/** + * Unsafe methods to access values and nodes directly from [MetaItem] + */ + +val MetaItem<*>?.value + get() = (this as? MetaItem.ValueItem)?.value + ?: (this?.node?.get(VALUE_KEY) as? MetaItem.ValueItem)?.value + +val MetaItem<*>?.string get() = value?.string +val MetaItem<*>?.boolean get() = value?.boolean +val MetaItem<*>?.number get() = value?.number +val MetaItem<*>?.double get() = number?.toDouble() +val MetaItem<*>?.float get() = number?.toFloat() +val MetaItem<*>?.int get() = number?.toInt() +val MetaItem<*>?.long get() = number?.toLong() +val MetaItem<*>?.short get() = number?.toShort() + +inline fun > MetaItem<*>?.enum() = if (this is ValueItem && this.value is EnumValue<*>) { + this.value as E +} else { + string?.let { enumValueOf(it) } +} + +val MetaItem<*>?.stringList get() = value?.list?.map { it.string } ?: emptyList() + +val MetaItem?.node: M? + get() = when (this) { + null -> null + is MetaItem.ValueItem -> error("Trying to interpret value meta item as node item") + is MetaItem.NodeItem -> node + } + +/** + * Generic meta-holder object + */ +interface Metoid { + val meta: Meta +} + +fun Value.toMeta() = buildMeta { Meta.VALUE_KEY to this } + +fun Meta.isEmpty() = this === EmptyMeta || this.items.isEmpty() \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt similarity index 55% rename from src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt index 7182c82e..61310d67 100644 --- a/src/main/kotlin/hep/dataforge/meta/MetaBuilder.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt @@ -1,6 +1,8 @@ package hep.dataforge.meta import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.values.Value /** * DSL builder for meta. Is not intended to store mutable state @@ -10,9 +12,20 @@ class MetaBuilder : MutableMetaNode() { override fun empty(): MetaBuilder = MetaBuilder() infix fun String.to(value: Any) { + if (value is Meta) { + this@MetaBuilder[this] = value + } this@MetaBuilder[this] = Value.of(value) } + infix fun String.to(meta: Meta) { + this@MetaBuilder[this] = meta + } + + infix fun String.to(value: Iterable) { + this@MetaBuilder[this] = value.toList() + } + infix fun String.to(metaBuilder: MetaBuilder.() -> Unit) { this@MetaBuilder[this] = MetaBuilder().apply(metaBuilder) } @@ -25,13 +38,12 @@ fun Meta.builder(): MetaBuilder { return MetaBuilder().also { builder -> items.mapValues { entry -> val item = entry.value - builder[entry.key] = when (item) { - is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) - is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(item.node.builder()) - is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { it.builder() }) + builder[entry.key.toName()] = when (item) { + is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) + is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.builder()) } } } } -fun buildMeta(builder: MetaBuilder.() -> Unit): Meta = MetaBuilder().apply(builder) \ No newline at end of file +fun buildMeta(builder: MetaBuilder.() -> Unit): MetaBuilder = MetaBuilder().apply(builder) \ 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 new file mode 100644 index 00000000..788758fa --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt @@ -0,0 +1,172 @@ +package hep.dataforge.meta + +import hep.dataforge.names.Name +import hep.dataforge.names.NameToken +import hep.dataforge.names.plus +import hep.dataforge.names.toName +import hep.dataforge.values.Value + +internal data class MetaListener( + val owner: Any? = null, + val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit +) + + +interface MutableMeta> : MetaNode { + override val items: Map> + operator fun set(name: Name, item: MetaItem?) + fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) + fun removeListener(owner: Any? = null) +} + +/** + * A mutable meta node with attachable change listener. + * + * Changes in Meta are not thread safe. + */ +abstract class MutableMetaNode> : AbstractMetaNode(), MutableMeta { + private val listeners = HashSet() + + /** + * Add change listener to this meta. Owner is declared to be able to remove listeners later. Listener without owner could not be removed + */ + override fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) { + listeners.add(MetaListener(owner, action)) + } + + /** + * Remove all listeners belonging to given owner + */ + override fun removeListener(owner: Any?) { + listeners.removeAll { it.owner === owner } + } + + private val _items: MutableMap> = HashMap() + + override val items: Map> + get() = _items + + protected fun itemChanged(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) { + listeners.forEach { it.action(name, oldItem, newItem) } + } + + protected open fun replaceItem(key: NameToken, oldItem: MetaItem?, newItem: MetaItem?) { + if (newItem == null) { + _items.remove(key) + oldItem?.node?.removeListener(this) + } else { + _items[key] = newItem + if (newItem is MetaItem.NodeItem) { + newItem.node.onChange(this) { name, oldChild, newChild -> + itemChanged(key + name, oldChild, newChild) + } + } + } + itemChanged(key.toName(), oldItem, newItem) + } + + /** + * Transform given meta to node type of this meta tree + * @param name the name of the node where meta should be attached. Needed for correct assignment validators and styles + * @param meta the node itself + */ + internal abstract fun wrap(name: Name, meta: Meta): M + + /** + * Create empty node + */ + internal abstract fun empty(): M + + override operator fun set(name: Name, item: MetaItem?) { + when (name.length) { + 0 -> error("Can't setValue meta item for empty name") + 1 -> { + val token = name.first()!! + replaceItem(token, get(name), item) + } + else -> { + val token = name.first()!! + //get existing or create new node. Query is ignored for new node + val child = this.items[token]?.node + ?: empty().also { this[token.body.toName()] = MetaItem.NodeItem(it) } + child[name.cutFirst()] = item + } + } + } +} + +fun > MutableMeta.remove(name: Name) = set(name, null) +fun > MutableMeta.remove(name: String) = remove(name.toName()) + +fun > MutableMeta.setValue(name: Name, value: Value) = set(name, MetaItem.ValueItem(value)) +fun > MutableMeta.setItem(name: String, item: MetaItem) = set(name.toName(), item) +fun > MutableMeta.setValue(name: String, value: Value) = + set(name.toName(), MetaItem.ValueItem(value)) + +fun > MutableMeta.setItem(token: NameToken, item: MetaItem?) = set(token.toName(), item) + +fun > MutableMetaNode.setNode(name: Name, node: Meta) = + set(name, MetaItem.NodeItem(wrap(name, node))) + +fun > MutableMetaNode.setNode(name: String, node: Meta) = setNode(name.toName(), node) + +/** + * Universal set method + */ +operator fun > M.set(name: Name, value: Any?) { + when (value) { + null -> remove(name) + is MetaItem<*> -> when (value) { + is MetaItem.ValueItem<*> -> setValue(name, value.value) + is MetaItem.NodeItem<*> -> setNode(name, value.node) + } + is Meta -> setNode(name, value) + else -> setValue(name, Value.of(value)) + } +} + +operator fun > M.set(key: String, value: Any?) = set(key.toName(), value) + +/** + * Update existing mutable node with another node. The rules are following: + * * value replaces anything + * * node updates node and replaces anything but node + * * node list updates node list if number of nodes in the list is the same and replaces anything otherwise + */ +fun > M.update(meta: Meta) { + meta.items.forEach { entry -> + val value = entry.value + when (value) { + is MetaItem.ValueItem -> setValue(entry.key.toName(), value.value) + is MetaItem.NodeItem -> (this[entry.key.toName()] as? MetaItem.NodeItem)?.node?.update(value.node) + ?: run { setNode(entry.key.toName(), value.node) } + } + } +} + +/* Same name siblings generation */ + +fun > M.setIndexed( + name: Name, + items: Iterable>, + queryFactory: (Int) -> String = { it.toString() } +) { + val tokens = name.tokens.toMutableList() + val last = tokens.last() + items.forEachIndexed { index, meta -> + val indexedToken = NameToken(last.body, last.query + queryFactory(index)) + tokens[tokens.lastIndex] = indexedToken + set(Name(tokens), meta) + } +} + +fun > M.setIndexed( + name: Name, + metas: Iterable, + queryFactory: (Int) -> String = { it.toString() } +) { + setIndexed(name, metas.map { MetaItem.NodeItem(wrap(name, it)) }, queryFactory) +} + +operator fun > M.set(name: Name, metas: Iterable) = setIndexed(name, metas) +operator fun > M.set(name: String, metas: Iterable) = setIndexed(name.toName(), metas) diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt new file mode 100644 index 00000000..f1e2beaf --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt @@ -0,0 +1,62 @@ +package hep.dataforge.meta + +/** + * Marker interface for specifications + */ +interface Specification : Configurable { + operator fun get(name: String): MetaItem? = config[name] +} + +/** + * Allows to apply custom configuration in a type safe way to simple untyped configuration. + * By convention [Specification] companion should inherit this class + * + */ +interface SpecificationCompanion { + /** + * 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) + + /** + * Wrap generic configuration producing instance of desired type + */ + fun wrap(config: Config): T + + fun wrap(meta: Meta): T = wrap(meta.toConfig()) + +} + +fun specification(wrapper: (Config) -> T): SpecificationCompanion = + object : SpecificationCompanion { + override fun wrap(config: Config): T = wrapper(config) + } + +/** + * Apply specified configuration to configurable + */ +fun > T.configure(spec: S, action: C.() -> Unit) = + apply { spec.update(config, action) } + +/** + * Update configuration using given specification + */ +fun > Specification.update(spec: S, action: C.() -> Unit) = + apply { spec.update(config, action) } + +/** + * Create a style based on given specification + */ +fun > S.createStyle(action: C.() -> Unit): Meta = + Config().also { update(it, action) } + + +fun , C : Specification> Specification.spec( + spec: SpecificationCompanion, + key: String? = null +) = + ChildConfigDelegate(config, key) { spec.wrap(config) } \ 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 new file mode 100644 index 00000000..ba802f5f --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt @@ -0,0 +1,68 @@ +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()) : MutableMeta { + override val items: Map> + get() = (base.items.keys + style.items.keys).associate { key -> + val value = base.items[key] + val styleValue = style[key] + val item: MetaItem = when (value) { + null -> when (styleValue) { + null -> error("Should be unreachable") + is MetaItem.ValueItem -> MetaItem.ValueItem(styleValue.value) + is MetaItem.NodeItem -> MetaItem.NodeItem(Styled(style.empty(), styleValue.node)) + } + is MetaItem.ValueItem -> MetaItem.ValueItem(value.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 + } + } + + override 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]) } + } + + override fun removeListener(owner: Any?) { + style.removeListener(owner) + } +} + +fun Styled.configure(meta: Meta) = apply { style.update(style) } + +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 { + 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/src/main/kotlin/hep/dataforge/names/Name.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt similarity index 79% rename from src/main/kotlin/hep/dataforge/names/Name.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt index 6ac98d8d..78dba871 100644 --- a/src/main/kotlin/hep/dataforge/names/Name.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt @@ -1,13 +1,12 @@ package hep.dataforge.names -import kotlin.coroutines.experimental.buildSequence /** * The general interface for working with names. * The name is a dot separated list of strings like `token1.token2.token3`. * Each token could contain additional query in square brackets. */ -class Name internal constructor(val tokens: List) { +inline class Name constructor(val tokens: List) { val length get() = tokens.size @@ -23,12 +22,12 @@ class Name internal constructor(val tokens: List) { fun last(): NameToken? = tokens.lastOrNull() /** - * The reminder of the name after first element is cut + * The reminder of the name after first element is cut. For empty name return itself. */ fun cutFirst(): Name = Name(tokens.drop(1)) /** - * The reminder of the name after last element is cut + * The reminder of the name after last element is cut. For empty name return itself. */ fun cutLast(): Name = Name(tokens.dropLast(1)) @@ -36,19 +35,6 @@ class Name internal constructor(val tokens: List) { override fun toString(): String = tokens.joinToString(separator = NAME_SEPARATOR) { it.toString() } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Name) return false - - if (tokens != other.tokens) return false - - return true - } - - override fun hashCode(): Int { - return tokens.hashCode() - } - companion object { const val NAME_SEPARATOR = "." } @@ -59,7 +45,7 @@ class Name internal constructor(val tokens: List) { * Following symbols are prohibited in name tokens: `{}.:\`. * A name token could have appendix in square brackets called *query* */ -data class NameToken internal constructor(val body: String, val query: String) { +data class NameToken(val body: String, val query: String = "") { init { if (body.isEmpty()) error("Syntax error: Name token body is empty") @@ -75,13 +61,13 @@ data class NameToken internal constructor(val body: String, val query: String) { } fun String.toName(): Name { - val tokens = buildSequence { + val tokens = sequence { var bodyBuilder = StringBuilder() var queryBuilder = StringBuilder() var bracketCount: Int = 0 fun queryOn() = bracketCount > 0 - this@toName.asSequence().forEach { + asSequence().forEach { if (queryOn()) { when (it) { '[' -> bracketCount++ @@ -109,8 +95,14 @@ fun String.toName(): Name { return Name(tokens.toList()) } +operator fun NameToken.plus(other: Name): Name = Name(listOf(this) + other.tokens) + operator fun Name.plus(other: Name): Name = Name(this.tokens + other.tokens) operator fun Name.plus(other: String): Name = this + other.toName() -fun NameToken.toName() = Name(listOf(this)) \ No newline at end of file +fun NameToken.toName() = Name(listOf(this)) + +val EmptyName = Name(emptyList()) + +fun Name.isEmpty(): Boolean = this.length == 0 \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Value.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt similarity index 56% rename from src/main/kotlin/hep/dataforge/meta/Value.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt index 4bd6d55e..2f92bd60 100644 --- a/src/main/kotlin/hep/dataforge/meta/Value.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt @@ -1,4 +1,4 @@ -package hep.dataforge.meta +package hep.dataforge.values /** @@ -44,19 +44,22 @@ interface Value { get() = listOf(this) companion object { + const val TYPE = "value" + /** * Convert object to value */ fun of(value: Any?): Value { return when (value) { null -> Null + is Value -> value true -> True false -> False is Number -> NumberValue(value) is Iterable<*> -> ListValue(value.map { of(it) }) is Enum<*> -> EnumValue(value) is CharSequence -> StringValue(value.toString()) - else -> throw IllegalArgumentException("Unrecognized type of the object converted to Value") + else -> throw IllegalArgumentException("Unrecognized type of the object (${value::class}) converted to Value") } } } @@ -66,10 +69,12 @@ interface Value { * A singleton null value */ object Null : Value { - override val value: Any? = null - override val type: ValueType = ValueType.NULL - override val number: Number = Double.NaN - override val string: String = "@null" + override val value: Any? get() = null + override val type: ValueType get() = ValueType.NULL + override val number: Number get() = Double.NaN + override val string: String get() = "@null" + + override fun toString(): String = value.toString() } /** @@ -82,40 +87,75 @@ fun Value.isNull(): Boolean = this == Null * Singleton true value */ object True : Value { - override val value: Any? = true - override val type: ValueType = ValueType.BOOLEAN - override val number: Number = 1.0 - override val string: String = "+" + override val value: Any? get() = true + override val type: ValueType get() = ValueType.BOOLEAN + override val number: Number get() = 1.0 + override val string: String get() = "+" + + override fun toString(): String = value.toString() } /** * Singleton false value */ object False : Value { - override val value: Any? = false - override val type: ValueType = ValueType.BOOLEAN - override val number: Number = -1.0 - override val string: String = "-" + override val value: Any? get() = false + override val type: ValueType get() = ValueType.BOOLEAN + override val number: Number get() = -1.0 + override val string: String get() = "-" } val Value.boolean get() = this == True || this.list.firstOrNull() == True || (type == ValueType.STRING && string.toBoolean()) class NumberValue(override val number: Number) : Value { override val value: Any? get() = number - override val type: ValueType = ValueType.NUMBER + override val type: ValueType get() = ValueType.NUMBER override val string: String get() = number.toString() + + override fun equals(other: Any?): Boolean { + if (other !is Value) return false + return when (number) { + is Short -> number == other.number.toShort() + is Long -> number == other.number.toLong() + is Byte -> number == other.number.toByte() + is Int -> number == other.number.toInt() + is Float -> number == other.number.toFloat() + is Double -> number == other.number.toDouble() + else -> number.toString() == other.number.toString() + } + } + + override fun hashCode(): Int = number.hashCode() + + override fun toString(): String = value.toString() } class StringValue(override val string: String) : Value { override val value: Any? get() = string - override val type: ValueType = ValueType.STRING + override val type: ValueType get() = ValueType.STRING override val number: Number get() = string.toDouble() + + override fun equals(other: Any?): Boolean { + return this.string == (other as? Value)?.string + } + + override fun hashCode(): Int = string.hashCode() + + override fun toString(): String = value.toString() } class EnumValue>(override val value: E) : Value { - override val type: ValueType = ValueType.STRING - override val number: Number = value.ordinal - override val string: String = value.name + override val type: ValueType get() = ValueType.STRING + override val number: Number get() = value.ordinal + override val string: String get() = value.name + + override fun equals(other: Any?): Boolean { + return string == (other as? Value)?.string + } + + override fun hashCode(): Int = value.hashCode() + + override fun toString(): String = value.toString() } class ListValue(override val list: List) : Value { @@ -129,6 +169,8 @@ class ListValue(override val list: List) : Value { override val type: ValueType get() = list.first().type override val number: Number get() = list.first().number override val string: String get() = list.first().string + + override fun toString(): String = value.toString() } /** @@ -145,6 +187,7 @@ fun String.asValue(): Value = StringValue(this) fun Collection.asValue(): Value = ListValue(this.toList()) + /** * Create Value from String using closest match conversion */ @@ -178,4 +221,17 @@ fun String.parseValue(): Value { //Give up and return a StringValue return StringValue(this) +} + +class LazyParsedValue(override val string: String) : Value { + private val parsedValue by lazy { string.parseValue() } + + override val value: Any? + get() = parsedValue.value + override val type: ValueType + get() = parsedValue.type + override val number: Number + get() = parsedValue.number + + override fun toString(): String = value.toString() } \ No newline at end of file diff --git a/src/test/kotlin/hep/dataforge/meta/MetaBuilderTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt similarity index 79% rename from src/test/kotlin/hep/dataforge/meta/MetaBuilderTest.kt rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt index 26e10354..3f34882c 100644 --- a/src/test/kotlin/hep/dataforge/meta/MetaBuilderTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt @@ -1,15 +1,16 @@ package hep.dataforge.meta +import hep.dataforge.values.asValue import kotlin.test.Test import kotlin.test.assertEquals -class MetaBuilderTest{ +class MetaBuilderTest { @Test - fun testBuilder(){ + fun testBuilder() { val meta = buildMeta { "a" to 22 - "b" to listOf(1,2,3) + "b" to listOf(1, 2, 3) this["c"] = "myValue".asValue() "node" to { "e" to 12.2 diff --git a/src/test/kotlin/hep/dataforge/meta/MetaDelegateTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt similarity index 65% rename from src/test/kotlin/hep/dataforge/meta/MetaDelegateTest.kt rename to dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt index 30c30ed3..fbd93e62 100644 --- a/src/test/kotlin/hep/dataforge/meta/MetaDelegateTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt @@ -12,10 +12,11 @@ class MetaDelegateTest { @Test fun delegateTest() { - val testObject = object : SimpleConfigurable(Config()) { - var myValue by string() - var safeValue by number(2.2) - var enumValue by enum(TestEnum.YES) + val testObject = object : Specification { + override val config: Config = Config() + var myValue by config.string() + var safeValue by config.number(2.2) + var enumValue by config.enum(TestEnum.YES) } testObject.config["myValue"] = "theString" testObject.enumValue = TestEnum.NO diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt new file mode 100644 index 00000000..7f83b0b2 --- /dev/null +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaTest.kt @@ -0,0 +1,33 @@ +package hep.dataforge.meta + +import hep.dataforge.values.NumberValue +import hep.dataforge.values.True +import hep.dataforge.values.Value +import kotlin.test.Test +import kotlin.test.assertEquals + +class MetaTest { + @Test + fun valueEqualityTest() { + assertEquals(NumberValue(22), NumberValue(22)) + assertEquals(NumberValue(22.0), NumberValue(22)) + assertEquals(True, Value.of(true)) + } + + @Test + fun metaEqualityTest() { + val meta1 = buildMeta { + "a" to 22 + "b" to { + "c" to "ddd" + } + } + val meta2 = buildMeta { + "b" to { + "c" to "ddd" + } + "a" to 22 + }.seal() + assertEquals(meta1, meta2) + } +} \ No newline at end of file diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/names/NameTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/names/NameTest.kt new file mode 100644 index 00000000..1302777e --- /dev/null +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/names/NameTest.kt @@ -0,0 +1,19 @@ +package hep.dataforge.names + +import kotlin.test.Test +import kotlin.test.assertEquals + +class NameTest { + @Test + fun simpleName() { + val name = "token1.token2.token3".toName() + assertEquals("token2", name[1].toString()) + } + + @Test + fun equalityTest() { + val name1 = "token1.token2[2].token3".toName() + val name2 = "token1".toName() + "token2[2].token3" + assertEquals(name1, name2) + } +} \ No newline at end of file diff --git a/dataforge-scripting/build.gradle.kts b/dataforge-scripting/build.gradle.kts new file mode 100644 index 00000000..674ec25f --- /dev/null +++ b/dataforge-scripting/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm() + sourceSets { + val commonMain by getting { + dependencies { + api(project(":dataforge-workspace")) + implementation(kotlin("scripting-common")) + } + } + val jvmMain by getting { + dependencies { + implementation(kotlin("scripting-jvm-host-embeddable")) + implementation(kotlin("scripting-jvm")) + } + } + val jvmTest by getting { + dependencies { + implementation(kotlin("test")) + implementation(kotlin("test-junit")) + implementation("ch.qos.logback:logback-classic:1.2.3") + } + } + } +} \ No newline at end of file diff --git a/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt b/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt new file mode 100644 index 00000000..ee1af3df --- /dev/null +++ b/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt @@ -0,0 +1,49 @@ +package hep.dataforge.scripting + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.workspace.Workspace +import hep.dataforge.workspace.WorkspaceBuilder +import java.io.File +import kotlin.script.experimental.api.* +import kotlin.script.experimental.host.toScriptSource +import kotlin.script.experimental.jvm.dependenciesFromCurrentContext +import kotlin.script.experimental.jvm.jvm +import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost + +object Builders { + + fun buildWorkspace(source: SourceCode, context: Context = Global): Workspace { + val builder = WorkspaceBuilder(context) + + val workspaceScriptConfiguration = ScriptCompilationConfiguration { + baseClass(Any::class) + implicitReceivers(WorkspaceBuilder::class) + jvm { + dependenciesFromCurrentContext(wholeClasspath = true) + } + } + + val evaluationConfiguration = ScriptEvaluationConfiguration { + implicitReceivers(builder) + } + + BasicJvmScriptingHost().eval(source, workspaceScriptConfiguration, evaluationConfiguration).onFailure { + it.reports.forEach { scriptDiagnostic -> + when (scriptDiagnostic.severity) { + ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> + context.logger.error(scriptDiagnostic.exception) { scriptDiagnostic.toString() } + ScriptDiagnostic.Severity.WARNING -> context.logger.warn { scriptDiagnostic.toString() } + ScriptDiagnostic.Severity.INFO -> context.logger.info { scriptDiagnostic.toString() } + ScriptDiagnostic.Severity.DEBUG -> context.logger.debug { scriptDiagnostic.toString() } + } + } + } + + return builder.build() + } + + fun buildWorkspace(file: File): Workspace = buildWorkspace(file.toScriptSource()) + + fun buildWorkspace(string: String): Workspace = buildWorkspace(string.toScriptSource()) +} \ No newline at end of file diff --git a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt new file mode 100644 index 00000000..ef58de78 --- /dev/null +++ b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt @@ -0,0 +1,28 @@ +package hep.dataforge.scripting + +import hep.dataforge.meta.get +import hep.dataforge.meta.int +import org.junit.Test +import kotlin.test.assertEquals + + +class BuildersKtTest { + @Test + fun testWorkspaceBuilder() { + val script = """ + println("I am working") + + context{ + name = "test" + } + + target("testTarget"){ + "a" to 12 + } + """.trimIndent() + val workspace = Builders.buildWorkspace(script) + + val target = workspace.targets.getValue("testTarget") + assertEquals(12, target["a"]!!.int) + } +} \ No newline at end of file diff --git a/dataforge-tables/build.gradle b/dataforge-tables/build.gradle new file mode 100644 index 00000000..a0011494 --- /dev/null +++ b/dataforge-tables/build.gradle @@ -0,0 +1,25 @@ +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-workspace/build.gradle.kts b/dataforge-workspace/build.gradle.kts new file mode 100644 index 00000000..cb2ce82c --- /dev/null +++ b/dataforge-workspace/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + kotlin("multiplatform") +} + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-context")) + api(project(":dataforge-data")) + } + } + } +} \ 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 new file mode 100644 index 00000000..1f0f64f4 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt @@ -0,0 +1,48 @@ +package hep.dataforge.workspace + +import hep.dataforge.data.DataFilter +import hep.dataforge.data.DataNode +import hep.dataforge.data.DataTreeBuilder +import hep.dataforge.data.filter +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaRepr +import hep.dataforge.meta.buildMeta +import hep.dataforge.names.EmptyName +import hep.dataforge.names.Name +import hep.dataforge.names.isEmpty + +/** + * A dependency of the task which allows to lazily create a data tree for single dependency + */ +sealed class Dependency : MetaRepr { + abstract fun apply(workspace: Workspace): DataNode +} + +class DataDependency(val filter: DataFilter) : Dependency() { + override fun apply(workspace: Workspace): DataNode = + workspace.data.filter(filter) + + override fun toMeta(): Meta = filter.config + + companion object { + val all: DataDependency = DataDependency(DataFilter.build { }) + } +} + +class TaskModelDependency(val name: String, val meta: Meta, val placement: Name = EmptyName) : Dependency() { + override fun apply(workspace: Workspace): DataNode { + val task = workspace.tasks[name] ?: error("Task with name ${name} is not found in the workspace") + if (task.isTerminal) TODO("Support terminal task") + val result = with(workspace) { task(meta) } + return if (placement.isEmpty()) { + result + } else { + DataTreeBuilder().apply { this[placement] = result }.build() + } + } + + override fun toMeta(): Meta = buildMeta { + "name" to name + "meta" to meta + } +} \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt new file mode 100644 index 00000000..bb6d33f3 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt @@ -0,0 +1,52 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Named +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.provider.Type +import hep.dataforge.workspace.Task.Companion.TYPE +import kotlin.reflect.KClass + +@Type(TYPE) +interface Task : Named { + /** + * Terminal task is the one that could not build model lazily + */ + val isTerminal: Boolean get() = false + + /** + * The explicit type of the node returned by the task + */ + val type: KClass + + /** + * Build a model for this task + * + * @param workspace + * @param taskConfig + * @return + */ + fun build(workspace: Workspace, taskConfig: Meta): TaskModel + + /** + * Check if the model is valid and is acceptable by the task. Throw exception if not. + * + * @param model + */ + fun validate(model: TaskModel) { + if(this.name != model.name) error("The task $name could not be run with model from task ${model.name}") + } + + /** + * Run given task model. Type check expected to be performed before actual + * calculation. + * + * @param model + * @return + */ + fun run(model: TaskModel): DataNode + + companion object { + const val TYPE = "task" + } +} \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt new file mode 100644 index 00000000..8d0de0c0 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt @@ -0,0 +1,97 @@ +/* + * To change this license header, choose License Headers in Project Properties. + * To change this template file, choose Tools | Templates + * and open the template in the editor. + */ +package hep.dataforge.workspace + +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.toName + + +/** + * A model for task execution + * @param name the name of the task + * @param meta the meta for the task (not for the whole configuration) + * @param dependencies a list of direct dependencies for this task + */ +data class TaskModel( + val name: String, + val meta: Meta, + val dependencies: Collection +) : MetaRepr { + //TODO provide a way to get task descriptor + //TODO add pre-run check of task result type? + + override fun toMeta(): Meta = buildMeta { + "name" to name + "meta" to meta + "dependsOn" to { + val dataDependencies = dependencies.filterIsInstance() + val taskDependencies = dependencies.filterIsInstance() + setIndexed("data".toName(), dataDependencies.map { it.toMeta() }) + setIndexed("task".toName(), taskDependencies.map { it.toMeta() }) { taskDependencies[it].name } + //TODO ensure all dependencies are listed + } + } +} + +/** + * Build input for the task + */ +fun TaskModel.buildInput(workspace: Workspace): DataTree { + return DataTreeBuilder().apply { + dependencies.asSequence().flatMap { it.apply(workspace).dataSequence() }.forEach { (name, data) -> + //TODO add concise error on replacement + this[name] = data + } + }.build() +} + +/** + * A builder for [TaskModel] + */ +class TaskModelBuilder(val name: String, meta: Meta = EmptyMeta) { + /** + * Meta for current task. By default uses the whole input meta + */ + var meta: MetaBuilder = meta.builder() + val dependencies = HashSet() + + /** + * Add dependency for + */ + fun dependsOn(name: String, meta: Meta, placement: Name = EmptyName) { + dependencies.add(TaskModelDependency(name, meta, placement)) + } + + /** + * Add custom data dependency + */ + fun data(action: DataFilter.() -> Unit) { + dependencies.add(DataDependency(DataFilter.build(action))) + } + + /** + * User-friendly way to add data dependency + */ + fun data(pattern: String? = null, from: String? = null, to: String? = null) = data { + pattern?.let { this.pattern = it } + from?.let { this.from = it } + to?.let { this.to = it } + } + + /** + * Add all data as root node + */ + fun allData() { + dependencies.add(DataDependency.all) + } + + fun build(): TaskModel = TaskModel(name, meta.seal(), dependencies) +} diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt new file mode 100644 index 00000000..b0b1cf4d --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt @@ -0,0 +1,88 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextAware +import hep.dataforge.context.members +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.provider.Provider +import hep.dataforge.provider.Type + + +@Type(Workspace.TYPE) +interface Workspace : ContextAware, Provider { + /** + * The whole data node for current workspace + */ + val data: DataNode + + /** + * All targets associated with the workspace + */ + val targets: Map + + /** + * All tasks associated with the workspace + */ + val tasks: Map> + + override fun provideTop(target: String, name: Name): Any? { + return when (target) { + "target", Meta.TYPE -> targets[name.toString()] + Task.TYPE -> tasks[name.toString()] + Data.TYPE -> data[name] + DataNode.TYPE -> data.getNode(name) + else -> null + } + } + + override fun listTop(target: String): Sequence { + return when (target) { + "target", Meta.TYPE -> targets.keys.asSequence().map { it.toName() } + Task.TYPE -> tasks.keys.asSequence().map { it.toName() } + Data.TYPE -> data.dataSequence().map { it.first } + DataNode.TYPE -> data.nodeSequence().map { it.first } + else -> emptySequence() + } + } + + operator fun Task.invoke(config: Meta): DataNode { + context.activate(this) + try { + val model = build(this@Workspace, config) + validate(model) + return run(model) + } finally { + context.deactivate(this) + } + } + + /** + * Invoke a task in the workspace utilizing caching if possible + */ + operator fun Task.invoke(targetName: String): DataNode { + val target = targets[targetName] ?: error("A target with name $targetName not found in ${this@Workspace}") + return invoke(target) + } + + companion object { + const val TYPE = "workspace" + } +} + +class SimpleWorkspace( + override val context: Context, + override val data: DataNode, + override val targets: Map, + tasks: Collection> +) : Workspace { + + override val tasks: Map> by lazy { + (context.members>(Task.TYPE) + tasks).associate { it.name to it } + } + +} + diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt new file mode 100644 index 00000000..9aebdcd0 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt @@ -0,0 +1,40 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.ContextBuilder +import hep.dataforge.data.DataTreeBuilder +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta + +/** + * A builder for a workspace + */ +class WorkspaceBuilder(var context: Context) { + val data = DataTreeBuilder() + val targets = HashMap() + val tasks = HashSet>() + + fun context(action: ContextBuilder.() -> Unit) { + this.context = ContextBuilder().apply(action).build() + } + + fun data(action: DataTreeBuilder.() -> Unit) = data.apply(action) + + fun target(name: String, meta: Meta) { + targets[name] = meta + } + + fun target(name: String, action: MetaBuilder.() -> Unit) = target(name, buildMeta(action)) + + fun task(task: Task<*>) { + tasks.add(task) + } + + fun build(): Workspace = SimpleWorkspace( + context, + data.build(), + targets, + tasks + ) +} \ No newline at end of file diff --git a/gradle/artifactory.gradle b/gradle/artifactory.gradle new file mode 100644 index 00000000..12e59642 --- /dev/null +++ b/gradle/artifactory.gradle @@ -0,0 +1,31 @@ +apply plugin: "com.jfrog.artifactory" + +artifactory { + def artifactory_user = project.hasProperty('artifactoryUser') ? project.property('artifactoryUser') : "" + def artifactory_password = project.hasProperty('artifactoryPassword') ? project.property('artifactoryPassword') : "" + def artifactory_contextUrl = 'http://npm.mipt.ru:8081/artifactory' + + contextUrl = artifactory_contextUrl //The base Artifactory URL if not overridden by the publisher/resolver + publish { + repository { + repoKey = 'gradle-dev-local' + username = artifactory_user + password = artifactory_password + } + + defaults { + publications('jvm', 'js', 'kotlinMultiplatform', 'metadata') + publishBuildInfo = false + publishArtifacts = true + publishPom = true + publishIvy = false + } + } + resolve { + repository { + repoKey = 'gradle-dev' + username = artifactory_user + password = artifactory_password + } + } +} \ No newline at end of file diff --git a/gradle/bintray.gradle b/gradle/bintray.gradle new file mode 100644 index 00000000..8da83c86 --- /dev/null +++ b/gradle/bintray.gradle @@ -0,0 +1,85 @@ +apply plugin: 'com.jfrog.bintray' + +def vcs = "https://github.com/mipt-npm/kmath" + +def pomConfig = { + licenses { + license { + name "The Apache Software License, Version 2.0" + url "http://www.apache.org/licenses/LICENSE-2.0.txt" + distribution "repo" + } + } + developers { + developer { + id "MIPT-NPM" + name "MIPT nuclear physics methods laboratory" + organization "MIPT" + organizationUrl "http://npm.mipt.ru" + } + } + scm { + url vcs + } +} + +project.ext.configureMavenCentralMetadata = { pom -> + def root = asNode() + root.appendNode('name', project.name) + root.appendNode('description', project.description) + root.appendNode('url', vcs) + root.children().last() + pomConfig +} + +project.ext.configurePom = pomConfig + + +// Configure publishing +publishing { + repositories { + maven { + url = "https://bintray.com/mipt-npm/scientifik" + } + } + + // Process each publication we have in this project + publications.all { publication -> + // apply changes to pom.xml files, see pom.gradle + pom.withXml(configureMavenCentralMetadata) + + + } +} + +bintray { + user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER') + key = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_API_KEY') + publish = true + override = true // for multi-platform Kotlin/Native publishing + + pkg { + userOrg = "mipt-npm" + repo = "scientifik" + name = "scientifik.kmath" + issueTrackerUrl = "https://github.com/mipt-npm/kmath/issues" + licenses = ['Apache-2.0'] + vcsUrl = vcs + version { + name = project.version + vcsTag = project.version + released = new Date() + } + } +} + +bintrayUpload.dependsOn publishToMavenLocal + +// This is for easier debugging of bintray uploading problems +bintrayUpload.doFirst { + publications = project.publishing.publications.findAll { + !it.name.contains('-test') && it.name != 'kotlinMultiplatform' + }.collect { + println("Uploading artifact '$it.groupId:$it.artifactId:$it.version' from publication '$it.name'") + it.name//https://github.com/bintray/gradle-bintray-plugin/issues/256 + } +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..87b738cb Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..558870da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 00000000..af6708ff --- /dev/null +++ b/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((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" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +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 new file mode 100644 index 00000000..6d57edc7 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@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" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 8b0f8f86..00000000 --- a/settings.gradle +++ /dev/null @@ -1,21 +0,0 @@ -pluginManagement { - resolutionStrategy { - eachPlugin { - if (requested.id.id == "kotlin-platform-common") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") - } - - if (requested.id.id == "kotlin-platform-jvm") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") - } - - if (requested.id.id == "kotlin-platform-js") { - useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") - } - } - } -} -rootProject.name = 'dataforge-meta' - -include ":dataforge-meta-jvm" -include ":dataforge-meta-js" \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..c7ab404e --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,30 @@ +pluginManagement { + repositories { + jcenter() + gradlePluginPortal() + maven("https://dl.bintray.com/kotlin/kotlin-eap") + } + resolutionStrategy { + eachPlugin { + when (requested.id.id) { + "kotlinx-atomicfu" -> useModule("org.jetbrains.kotlinx:atomicfu-gradle-plugin:${requested.version}") + "kotlin-multiplatform" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") + "kotlin2js" -> useModule("org.jetbrains.kotlin:kotlin-gradle-plugin:${requested.version}") + "org.jetbrains.kotlin.frontend" -> useModule("org.jetbrains.kotlin:kotlin-frontend-plugin:0.0.45") + } + } + } +} + +enableFeaturePreview("GRADLE_METADATA") + +rootProject.name = "dataforge-core" +include( + ":dataforge-meta", + ":dataforge-meta-io", + ":dataforge-context", + ":dataforge-data", + ":dataforge-io", + ":dataforge-workspace", + ":dataforge-scripting" +) \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Delegates.kt b/src/main/kotlin/hep/dataforge/meta/Delegates.kt deleted file mode 100644 index bfb5eaae..00000000 --- a/src/main/kotlin/hep/dataforge/meta/Delegates.kt +++ /dev/null @@ -1,228 +0,0 @@ -package hep.dataforge.meta - -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(private val key: String? = null, private val default: Value? = null) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): Value? { - return thisRef.meta[key ?: property.name]?.value ?: default - } -} - -class StringDelegate(private val key: String? = null, private val default: String? = null) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): String? { - return thisRef.meta[key ?: property.name]?.string ?: default - } -} - -class BooleanDelegate(private val key: String? = null, private val default: Boolean? = null) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): Boolean? { - return thisRef.meta[key ?: property.name]?.boolean ?: default - } -} - -class NumberDelegate(private val key: String? = null, private val default: Number? = null) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): Number? { - return thisRef.meta[key ?: property.name]?.number ?: default - } -} - -//Delegates with non-null values - -class SafeStringDelegate(private val key: String? = null, private val default: String) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): String { - return thisRef.meta[key ?: property.name]?.string ?: default - } -} - -class SafeBooleanDelegate(private val key: String? = null, private val default: Boolean) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): Boolean { - return thisRef.meta[key ?: property.name]?.boolean ?: default - } -} - -class SafeNumberDelegate(private val key: String? = null, private val default: Number) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): Number { - return thisRef.meta[key ?: property.name]?.number ?: default - } -} - -class SafeEnumDelegate>(private val key: String? = null, private val default: E, private val resolver: (String) -> E) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): E { - return (thisRef.meta[key ?: property.name]?.string)?.let { resolver(it) } ?: default - } -} - -//Child node delegate - -class ChildDelegate(private val key: String? = null, private val converter: (Meta) -> T) : ReadOnlyProperty { - override fun getValue(thisRef: Metoid, property: KProperty<*>): T? { - return thisRef.meta[key ?: property.name]?.node?.let { converter(it)} - } -} - -//Read-only delegates - -/** - * A property delegate that uses custom key - */ -fun Metoid.value(default: Value = Null, key: String? = null) = ValueDelegate(key, default) - -fun Metoid.string(default: String? = null, key: String? = null) = StringDelegate(key, default) - -fun Metoid.boolean(default: Boolean? = null, key: String? = null) = BooleanDelegate(key, default) - -fun Metoid.number(default: Number? = null, key: String? = null) = NumberDelegate(key, default) - -fun Metoid.child(key: String? = null) = ChildDelegate(key) { it } - -fun Metoid.child(key: String? = null, converter: (Meta) -> T) = ChildDelegate(key, converter) - -@JvmName("safeString") -fun Metoid.string(default: String, key: String? = null) = SafeStringDelegate(key, default) - -@JvmName("safeBoolean") -fun Metoid.boolean(default: Boolean, key: String? = null) = SafeBooleanDelegate(key, default) - -@JvmName("safeNumber") -fun Metoid.number(default: Number, key: String? = null) = SafeNumberDelegate(key, default) - -inline fun > Metoid.enum(default: E, key: String? = null) = SafeEnumDelegate(key, default) { enumValueOf(it) } - -/* Config delegates */ - -class ValueConfigDelegate(private val key: String? = null, private val default: Value? = null) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): Value? { - return thisRef.config[key ?: property.name]?.value ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: Value?) { - val name = key ?: property.name - if(value == null){ - thisRef.config.remove(name) - } else { - thisRef.config[name] = value - } - } -} - -class StringConfigDelegate(private val key: String? = null, private val default: String? = null) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): String? { - return thisRef.config[key ?: property.name]?.string ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: String?) { - thisRef.config[key ?: property.name] = value - } -} - -class BooleanConfigDelegate(private val key: String? = null, private val default: Boolean? = null) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): Boolean? { - return thisRef.config[key ?: property.name]?.boolean ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: Boolean?) { - thisRef.config[key ?: property.name] = value - } -} - -class NumberConfigDelegate(private val key: String? = null, private val default: Number? = null) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): Number? { - return thisRef.config[key ?: property.name]?.number ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: Number?) { - thisRef.config[key ?: property.name] = value - } -} - -//Delegates with non-null values - -class SafeStringConfigDelegate(private val key: String? = null, private val default: String) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): String { - return thisRef.config[key ?: property.name]?.string ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: String) { - thisRef.config[key ?: property.name] = value - } -} - -class SafeBooleanConfigDelegate(private val key: String? = null, private val default: Boolean) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): Boolean { - return thisRef.config[key ?: property.name]?.boolean ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: Boolean) { - thisRef.config[key ?: property.name] = value - } -} - -class SafeNumberConfigDelegate(private val key: String? = null, private val default: Number) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): Number { - return thisRef.config[key ?: property.name]?.number ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: Number) { - thisRef.config[key ?: property.name] = value - } -} - -class SafeEnumvConfigDelegate>(private val key: String? = null, private val default: E, private val resolver: (String) -> E) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): E { - return (thisRef.config[key ?: property.name]?.string)?.let { resolver(it) } ?: default - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: E) { - thisRef.config[key ?: property.name] = value.name - } -} - -//Child node delegate - -class ChildConfigDelegate(private val key: String? = null, private val converter: (Config) -> T) : ReadWriteProperty { - override fun getValue(thisRef: Configurable, property: KProperty<*>): T { - return converter(thisRef.config[key ?: property.name]?.node ?: Config()) - } - - override fun setValue(thisRef: Configurable, property: KProperty<*>, value: T) { - thisRef.config[key ?: property.name] = value.config - } - -} - -//Read-write delegates - -/** - * A property delegate that uses custom key - */ -fun Configurable.value(default: Value = Null, key: String? = null) = ValueConfigDelegate(key, default) - -fun Configurable.string(default: String? = null, key: String? = null) = StringConfigDelegate(key, default) - -fun Configurable.boolean(default: Boolean? = null, key: String? = null) = BooleanConfigDelegate(key, default) - -fun Configurable.number(default: Number? = null, key: String? = null) = NumberConfigDelegate(key, default) - -fun Configurable.child(key: String? = null) = ChildConfigDelegate(key) { SimpleConfigurable(it) } - -fun Configurable.child(key: String? = null, converter: (Config) -> T) = ChildConfigDelegate(key, converter) - -//fun Configurable.spec(spec: Specification, key: String? = null) = ChildConfigDelegate(key) { spec.wrap(this) } - -@JvmName("safeString") -fun Configurable.string(default: String, key: String? = null) = SafeStringConfigDelegate(key, default) - -@JvmName("safeBoolean") -fun Configurable.boolean(default: Boolean, key: String? = null) = SafeBooleanConfigDelegate(key, default) - -@JvmName("safeNumber") -fun Configurable.number(default: Number, key: String? = null) = SafeNumberConfigDelegate(key, default) - -inline fun > Configurable.enum(default: E, key: String? = null) = SafeEnumvConfigDelegate(key, default) { enumValueOf(it) } \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Meta.kt b/src/main/kotlin/hep/dataforge/meta/Meta.kt deleted file mode 100644 index 4d9ded52..00000000 --- a/src/main/kotlin/hep/dataforge/meta/Meta.kt +++ /dev/null @@ -1,99 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.names.toName - -/** - * A member of the meta tree. Could be represented as one of following: - * * a value - * * a single node - * * a list of nodes - */ -sealed class MetaItem { - class ValueItem(val value: Value) : MetaItem() - class SingleNodeItem(val node: M) : MetaItem() - class MultiNodeItem(val nodes: List) : MetaItem() -} - -operator fun List.get(query: String): M? { - return if (query.isEmpty()) { - first() - } else { - //TODO add custom key queries - get(query.toInt()) - } -} - -/** - * Generic meta tree representation. Elements are [MetaItem] objects that could be represented by three different entities: - * * [MetaItem.ValueItem] (leaf) - * * [MetaItem.SingleNodeItem] single node - * * [MetaItem.MultiNodeItem] multi-value node - */ -interface Meta { - val items: Map> -} - -operator fun Meta.get(name: Name): MetaItem? { - return when (name.length) { - 0 -> error("Can't resolve element from empty name") - 1 -> items[name.first()!!.body] - else -> name.first()!!.let { token -> items[token.body]?.nodes?.get(token.query) }?.get(name.cutFirst()) - } -} - -//TODO create Java helper for meta operations -operator fun Meta.get(key: String): MetaItem? = get(key.toName()) - -/** - * A meta node that ensures that all of its descendants has at least the same type - */ -abstract class MetaNode> : Meta { - abstract override val items: Map> - - operator fun get(name: Name): MetaItem? { - return when (name.length) { - 0 -> error("Can't resolve element from empty name") - 1 -> items[name.first()!!.body] - else -> name.first()!!.let { token -> items[token.body]?.nodes?.get(token.query) }?.get(name.cutFirst()) - } - } - - operator fun get(key: String): MetaItem? = get(key.toName()) -} - -/** - * The meta implementation which is guaranteed to be immutable. - * - * If the argument is possibly mutable node, it is copied on creation - */ -class SealedMeta(meta: Meta) : MetaNode() { - override val items: Map> = if (meta is SealedMeta) { - meta.items - } else { - meta.items.mapValues { entry -> - val item = entry.value - when (item) { - is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) - is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(SealedMeta(item.node)) - is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(item.nodes.map { SealedMeta(it) }) - } - } - } -} - -/** - * Generate sealed node from [this]. If it is already sealed return it as is - */ -fun Meta.seal(): SealedMeta = this as? SealedMeta ?: SealedMeta(this) - -object EmptyMeta : Meta { - override val items: Map> = emptyMap() -} - -/** - * Generic meta-holder object - */ -interface Metoid { - val meta: Meta -} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt b/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt deleted file mode 100644 index ef8b8880..00000000 --- a/src/main/kotlin/hep/dataforge/meta/MetaUtils.kt +++ /dev/null @@ -1,40 +0,0 @@ -package hep.dataforge.meta - -/** - * Unsafe methods to access values and nodes directly from [MetaItem] - */ - -val MetaItem<*>.value - get() = (this as? MetaItem.ValueItem)?.value ?: error("Trying to interpret node meta item as value item") -val MetaItem<*>.string get() = value.string -val MetaItem<*>.boolean get() = value.boolean -val MetaItem<*>.number get() = value.number -val MetaItem<*>.double get() = number.toDouble() -val MetaItem<*>.int get() = number.toInt() -val MetaItem<*>.long get() = number.toLong() - -val MetaItem.node: M - get() = when (this) { - is MetaItem.ValueItem -> error("Trying to interpret value meta item as node item") - is MetaItem.SingleNodeItem -> node - is MetaItem.MultiNodeItem -> nodes.first() - } - -/** - * Utility method to access item content as list of nodes. - * Returns empty list if it is value item. - */ -val MetaItem.nodes: List - get() = when (this) { - is MetaItem.ValueItem -> emptyList()//error("Trying to interpret value meta item as node item") - is MetaItem.SingleNodeItem -> listOf(node) - is MetaItem.MultiNodeItem -> nodes - } - -fun MetaItem.indexOf(meta: M): Int { - return when (this) { - is MetaItem.ValueItem -> -1 - is MetaItem.SingleNodeItem -> if (node == meta) 0 else -1 - is MetaItem.MultiNodeItem -> nodes.indexOf(meta) - } -} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/MutableMetaNode.kt b/src/main/kotlin/hep/dataforge/meta/MutableMetaNode.kt deleted file mode 100644 index 4c7b40a4..00000000 --- a/src/main/kotlin/hep/dataforge/meta/MutableMetaNode.kt +++ /dev/null @@ -1,147 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.names.plus -import hep.dataforge.names.toName - -class MetaListener(val owner: Any? = null, val action: (name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) -> Unit) { - operator fun invoke(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) = action(name, oldItem, newItem) -} - - -interface MutableMeta> : Meta { - override val items: Map> - operator fun set(name: Name, item: MetaItem?) - fun onChange(owner: Any? = null, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) - fun removeListener(owner: Any) -} - -/** - * A mutable meta node with attachable change listener - */ -abstract class MutableMetaNode> : MetaNode(), MutableMeta { - private val listeners = HashSet() - - /** - * Add change listener to this meta. Owner is declared to be able to remove listeners later. Listener without owner could not be removed - */ - override fun onChange(owner: Any?, action: (Name, MetaItem<*>?, MetaItem<*>?) -> Unit) { - listeners.add(MetaListener(owner, action)) - } - - /** - * Remove all listeners belonging to given owner - */ - override fun removeListener(owner: Any) { - listeners.removeAll { it.owner === owner } - } - - private val _items: MutableMap> = HashMap() - - override val items: Map> - get() = _items - - protected fun itemChanged(name: Name, oldItem: MetaItem<*>?, newItem: MetaItem<*>?) { - listeners.forEach { it(name, oldItem, newItem) } - } - - protected open fun replaceItem(key: String, oldItem: MetaItem?, newItem: MetaItem?) { - if (newItem == null) { - _items.remove(key) - oldItem?.nodes?.forEach { - it.removeListener(this) - } - } else { - _items[key] = newItem - newItem.nodes.forEach { - it.onChange(this) { name, oldItem, newItem -> - itemChanged(key.toName() + name, oldItem, newItem) - } - } - } - itemChanged(key.toName(), oldItem, newItem) - } - - /** - * Transform given meta to node type of this meta tree - * @param name the name of the node where meta should be attached. Needed for correct assignment validators and styles - * @param meta the node itself - */ - abstract fun wrap(name: Name, meta: Meta): M - - /** - * Create empty node - */ - abstract fun empty(): M - - override operator fun set(name: Name, item: MetaItem?) { - when (name.length) { - 0 -> error("Can't set meta item for empty name") - 1 -> { - val token = name.first()!! - if (token.hasQuery()) TODO("Queries are not supported in set operations on meta") - replaceItem(token.body, get(name), item) - } - else -> { - val token = name.first()!! - //get existing or create new node. Query is ignored for new node - val child = this.items[token.body]?.nodes?.get(token.query) - ?: empty().also { this[token.body.toName()] = MetaItem.SingleNodeItem(it) } - child[name.cutFirst()] = item - } - } - } - - -} - -fun > M.remove(name: Name) = set(name, null) -fun > M.remove(name: String) = remove(name.toName()) - -operator fun > M.set(name: Name, value: Value) = set(name, MetaItem.ValueItem(value)) -operator fun > M.set(name: Name, meta: Meta) = set(name, MetaItem.SingleNodeItem(wrap(name, meta))) -operator fun > M.set(name: Name, metas: List) = set(name, MetaItem.MultiNodeItem(metas.map { wrap(name, it) })) - -operator fun > M.set(name: String, item: MetaItem) = set(name.toName(), item) -operator fun > M.set(name: String, value: Value) = set(name.toName(), MetaItem.ValueItem(value)) -operator fun > M.set(name: String, meta: Meta) = set(name.toName(), meta) -operator fun > M.set(name: String, metas: List) = set(name.toName(), metas) - - -/** - * Universal set method - */ -operator fun > M.set(key: String, value: Any?) { - when (value) { - null -> remove(key) - is Meta -> set(key, value) - else -> set(key, Value.of(value)) - } -} - -/** - * Update existing mutable node with another node. The rules are following: - * * value replaces anything - * * node updates node and replaces anything but node - * * node list updates node list if number of nodes in the list is the same and replaces anything otherwise - */ -fun > M.update(meta: Meta) { - meta.items.forEach { entry -> - val value = entry.value - when (value) { - is MetaItem.ValueItem -> this[entry.key] = value.value - is MetaItem.SingleNodeItem -> (this[entry.key] as? MetaItem.SingleNodeItem) - ?.node?.update(value.node) ?: kotlin.run { this[entry.key] = value.node } - is MetaItem.MultiNodeItem -> { - val existing = this[entry.key] - if (existing is MetaItem.MultiNodeItem && existing.nodes.size == value.nodes.size) { - existing.nodes.forEachIndexed { index, m -> - m.update(value.nodes[index]) - } - } else { - this[entry.key] = value.nodes - } - } - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Specification.kt b/src/main/kotlin/hep/dataforge/meta/Specification.kt deleted file mode 100644 index c44763dd..00000000 --- a/src/main/kotlin/hep/dataforge/meta/Specification.kt +++ /dev/null @@ -1,49 +0,0 @@ -package hep.dataforge.meta - -/** - * Marker interface for specifications - */ -interface Specification: Configurable{ - operator fun get(name: String): MetaItem? = config.get(name) -} - -/** - * Specification allows to apply custom configuration in a type safe way to simple untyped configuration - */ -interface SpecificationBuilder { - /** - * Update given configuration using given type as a builder - */ - fun update(config: Config, action: T.() -> Unit) { - wrap(config).apply(action) - } - - /** - * Wrap generic configuration producing instance of desired type - */ - fun wrap(config: Config): T - - fun wrap(meta: Meta): T = wrap(meta.toConfig()) -} - -fun specification(wrapper: (Config) -> T): SpecificationBuilder = object : SpecificationBuilder { - override fun wrap(config: Config): T = wrapper(config) -} - -/** - * Apply specified configuration to configurable - */ -fun > T.configure(spec: S, action: C.() -> Unit) = apply { spec.update(config, action) } - -/** - * Update configuration using given specification - */ -fun > Specification.update(spec: S, action: C.() -> Unit) = apply { spec.update(config, action) } - -/** - * Create a style based on given specification - */ -fun > S.createStyle(action: C.() -> Unit): Meta = Config().also { update(it, action) } - - -fun Specification.spec(spec: SpecificationBuilder, key: String? = null) = ChildConfigDelegate(key) { spec.wrap(config) } \ No newline at end of file diff --git a/src/main/kotlin/hep/dataforge/meta/Styleable.kt b/src/main/kotlin/hep/dataforge/meta/Styleable.kt deleted file mode 100644 index be8465e2..00000000 --- a/src/main/kotlin/hep/dataforge/meta/Styleable.kt +++ /dev/null @@ -1,72 +0,0 @@ -package hep.dataforge.meta - -import hep.dataforge.names.Name -import hep.dataforge.names.toName - -/** - * A configuration decorator with applied style - */ -class StyledConfig(val config: Config, style: Meta = EmptyMeta) : Config() { - - var style: Meta = style - set(value) { - field.items.forEach { - itemChanged(it.key.toName(), it.value, null) - } - field = value - value.items.forEach { - itemChanged(it.key.toName(), null, it.value) - } - } - - init { - config.onChange { name, oldItem, newItem -> this.itemChanged(name, oldItem, newItem) } - } - - override fun set(name: Name, item: MetaItem?) { - when (item) { - null -> config.remove(name) - is MetaItem.ValueItem -> config[name] = item.value - is MetaItem.SingleNodeItem -> config[name] = item.node - is MetaItem.MultiNodeItem -> config[name] = item.nodes - } - } - - override val items: Map> - get() = (config.items.keys + style.items.keys).associate { key -> - val value = config.items[key] - val styleValue = style[key] - val item: MetaItem = when (value) { - null -> when (styleValue) { - null -> error("Should be unreachable") - is MetaItem.ValueItem -> MetaItem.ValueItem(styleValue.value) - is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem(StyledConfig(config.empty(), styleValue.node)) - is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(styleValue.nodes.map { StyledConfig(config.empty(), it) }) - } - is MetaItem.ValueItem -> MetaItem.ValueItem(value.value) - is MetaItem.SingleNodeItem -> MetaItem.SingleNodeItem( - StyledConfig(value.node, styleValue?.node ?: EmptyMeta) - ) - is MetaItem.MultiNodeItem -> MetaItem.MultiNodeItem(value.nodes.map { - StyledConfig(it, styleValue?.node ?: EmptyMeta) - }) - } - key to item - } -} - -fun Config.withStyle(style: Meta = EmptyMeta) = if (this is StyledConfig) { - StyledConfig(this.config, style) -} else { - StyledConfig(this, style) -} - -interface Styleable : Configurable { - override val config: StyledConfig - - var style - get() = config.style - set(value) { - config.style = value - } -} \ No newline at end of file diff --git a/src/test/kotlin/hep/dataforge/names/NameTest.kt b/src/test/kotlin/hep/dataforge/names/NameTest.kt deleted file mode 100644 index 764d23c2..00000000 --- a/src/test/kotlin/hep/dataforge/names/NameTest.kt +++ /dev/null @@ -1,12 +0,0 @@ -package hep.dataforge.names - -import kotlin.test.Test -import kotlin.test.assertEquals - -class NameTest{ - @Test - fun simpleName(){ - val name = "token1.token2.token3".toName() - assertEquals("token2", name[1].toString()) - } -} \ No newline at end of file