diff --git a/.gitignore b/.gitignore index 94cef066..89cc712a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ .idea/ *.iws -*/out/** +out/ .gradle -*/build/** +build/ !gradle-wrapper.jar diff --git a/build.gradle.kts b/build.gradle.kts index 71c5da82..8494144e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,142 +1,18 @@ -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 -} +val dataforgeVersion by extra("0.1.2") 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") - + version = dataforgeVersion } 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()) - } - } - } - } - + if (name.startsWith("dataforge")) { + apply(plugin = "npm-bintray") + apply(plugin = "npm-artifactory") + } } \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 00000000..1ebbdf4d --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,20 @@ +plugins { + `kotlin-dsl` +} + +repositories { + gradlePluginPortal() + jcenter() +} + +val kotlinVersion = "1.3.31" + +// Add plugins used in buildSrc as dependencies, also we should specify version only here +dependencies { + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + implementation("org.jfrog.buildinfo:build-info-extractor-gradle:4.9.5") + implementation("com.jfrog.bintray.gradle:gradle-bintray-plugin:1.8.4") + implementation("org.jetbrains.dokka:dokka-gradle-plugin:0.9.18") + implementation("com.moowork.gradle:gradle-node-plugin:1.3.1") + implementation("org.openjfx:javafx-plugin:0.0.7") +} diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts new file mode 100644 index 00000000..e69de29b diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt new file mode 100644 index 00000000..883af120 --- /dev/null +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -0,0 +1,9 @@ +// Instead of defining runtime properties and use them dynamically +// define version in buildSrc and have autocompletion and compile-time check +// Also dependencies itself can be moved here +object Versions { + val ioVersion = "0.1.8" + val coroutinesVersion = "1.2.1" + val atomicfuVersion = "0.12.6" + val serializationVersion = "0.11.0" +} diff --git a/buildSrc/src/main/kotlin/dokka-publish.gradle.kts b/buildSrc/src/main/kotlin/dokka-publish.gradle.kts new file mode 100644 index 00000000..318e08ae --- /dev/null +++ b/buildSrc/src/main/kotlin/dokka-publish.gradle.kts @@ -0,0 +1,59 @@ +import org.jetbrains.dokka.gradle.DokkaTask + +plugins { + kotlin("multiplatform") + id("org.jetbrains.dokka") + `maven-publish` +} + +kotlin { + + val dokka by tasks.getting(DokkaTask::class) { + outputFormat = "html" + outputDirectory = "$buildDir/javadoc" + jdkVersion = 8 + + kotlinTasks { + // dokka fails to retrieve sources from MPP-tasks so we only define the jvm task + listOf(tasks.getByPath("compileKotlinJvm")) + } + sourceRoot { + // assuming only single source dir + path = sourceSets["commonMain"].kotlin.srcDirs.first().toString() + platforms = listOf("Common") + } + // although the JVM sources are now taken from the task, + // we still define the jvm source root to get the JVM marker in the generated html + sourceRoot { + // assuming only single source dir + path = sourceSets["jvmMain"].kotlin.srcDirs.first().toString() + platforms = listOf("JVM") + } + } + + val javadocJar by tasks.registering(Jar::class) { + dependsOn(dokka) + archiveClassifier.set("javadoc") + from("$buildDir/javadoc") + } + + publishing { + + // 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(javadocJar.get()) + } + } +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/js-test.gradle.kts b/buildSrc/src/main/kotlin/js-test.gradle.kts new file mode 100644 index 00000000..61759a28 --- /dev/null +++ b/buildSrc/src/main/kotlin/js-test.gradle.kts @@ -0,0 +1,44 @@ +import com.moowork.gradle.node.npm.NpmTask +import com.moowork.gradle.node.task.NodeTask +import org.gradle.kotlin.dsl.* +import org.jetbrains.kotlin.gradle.tasks.Kotlin2JsCompile + +plugins { + id("com.moowork.node") + kotlin("multiplatform") +} + +node { + nodeModulesDir = file("$buildDir/node_modules") +} + +val compileKotlinJs by tasks.getting(Kotlin2JsCompile::class) +val compileTestKotlinJs by tasks.getting(Kotlin2JsCompile::class) + +val populateNodeModules by tasks.registering(Copy::class) { + dependsOn(compileKotlinJs) + from(compileKotlinJs.destinationDir) + + kotlin.js().compilations["test"].runtimeDependencyFiles.forEach { + if (it.exists() && !it.isDirectory) { + from(zipTree(it.absolutePath).matching { include("*.js") }) + } + } + + into("$buildDir/node_modules") +} + +val installMocha by tasks.registering(NpmTask::class) { + setWorkingDir(buildDir) + setArgs(listOf("install", "mocha")) +} + +val runMocha by tasks.registering(NodeTask::class) { + dependsOn(compileTestKotlinJs, populateNodeModules, installMocha) + setScript(file("$buildDir/node_modules/mocha/bin/mocha")) + setArgs(listOf(compileTestKotlinJs.outputFile)) +} + +tasks["jsTest"].dependsOn(runMocha) + + diff --git a/buildSrc/src/main/kotlin/npm-artifactory.gradle.kts b/buildSrc/src/main/kotlin/npm-artifactory.gradle.kts new file mode 100644 index 00000000..d792dffb --- /dev/null +++ b/buildSrc/src/main/kotlin/npm-artifactory.gradle.kts @@ -0,0 +1,38 @@ +import groovy.lang.GroovyObject +import org.jfrog.gradle.plugin.artifactory.dsl.PublisherConfig +import org.jfrog.gradle.plugin.artifactory.dsl.ResolverConfig + +plugins { + id("com.jfrog.artifactory") +} + +artifactory { + val artifactoryUser: String? by project + val artifactoryPassword: String? by project + val artifactoryContextUrl = "http://npm.mipt.ru:8081/artifactory" + + setContextUrl(artifactoryContextUrl)//The base Artifactory URL if not overridden by the publisher/resolver + publish(delegateClosureOf { + repository(delegateClosureOf { + setProperty("repoKey", "gradle-dev-local") + setProperty("username", artifactoryUser) + setProperty("password", artifactoryPassword) + }) + + defaults(delegateClosureOf{ + invokeMethod("publications", arrayOf("jvm", "js", "kotlinMultiplatform", "metadata")) + //TODO: This property is not available for ArtifactoryTask + //setProperty("publishBuildInfo", false) + setProperty("publishArtifacts", true) + setProperty("publishPom", true) + setProperty("publishIvy", false) + }) + }) + resolve(delegateClosureOf { + repository(delegateClosureOf { + setProperty("repoKey", "gradle-dev") + setProperty("username", artifactoryUser) + setProperty("password", artifactoryPassword) + }) + }) +} diff --git a/buildSrc/src/main/kotlin/npm-bintray.gradle.kts b/buildSrc/src/main/kotlin/npm-bintray.gradle.kts new file mode 100644 index 00000000..b152d163 --- /dev/null +++ b/buildSrc/src/main/kotlin/npm-bintray.gradle.kts @@ -0,0 +1,97 @@ +@file:Suppress("UnstableApiUsage") + +import com.jfrog.bintray.gradle.BintrayExtension.PackageConfig +import com.jfrog.bintray.gradle.BintrayExtension.VersionConfig + +// Old bintray.gradle script converted to real Gradle plugin (precompiled script plugin) +// It now has own dependencies and support type safe accessors +// Syntax is pretty close to what we had in Groovy +// (excluding Property.set and bintray dynamic configs) + +plugins { + id("com.jfrog.bintray") + `maven-publish` +} + +val vcs = "https://github.com/mipt-npm/kmath" + +// Configure publishing +publishing { + repositories { + maven("https://bintray.com/mipt-npm/scientifik") + } + + // Process each publication we have in this project + publications.filterIsInstance().forEach { publication -> + + // use type safe pom config GSL insterad of old dynamic + publication.pom { + name.set(project.name) + description.set(project.description) + url.set(vcs) + + licenses { + license { + name.set("The Apache Software License, Version 2.0") + url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") + distribution.set("repo") + } + } + developers { + developer { + id.set("MIPT-NPM") + name.set("MIPT nuclear physics methods laboratory") + organization.set("MIPT") + organizationUrl.set("http://npm.mipt.ru") + } + + } + scm { + url.set(vcs) + } + } + + } +} + +bintray { + // delegates for runtime properties + val bintrayUser: String? by project + val bintrayApiKey: String? by project + user = bintrayUser ?: System.getenv("BINTRAY_USER") + key = bintrayApiKey ?: System.getenv("BINTRAY_API_KEY") + publish = true + override = true // for multi-platform Kotlin/Native publishing + + // We have to use delegateClosureOf because bintray supports only dynamic groovy syntax + // this is a problem of this plugin + pkg(delegateClosureOf { + userOrg = "mipt-npm" + repo = "scientifik" + name = "scientifik.kmath" + issueTrackerUrl = "https://github.com/mipt-npm/kmath/issues" + setLicenses("Apache-2.0") + vcsUrl = vcs + version(delegateClosureOf { + name = project.version.toString() + vcsTag = project.version.toString() + released = java.util.Date().toString() + }) + }) + + tasks { + bintrayUpload { + dependsOn(publishToMavenLocal) + doFirst { + setPublications(project.publishing.publications + .filterIsInstance() + .filter { !it.name.contains("-test") && it.name != "kotlinMultiplatform" } + .map { + println("""Uploading artifact "${it.groupId}:${it.artifactId}:${it.version}" from publication "${it.name}""") + it.name //https://github.com/bintray/gradle-bintray-plugin/issues/256 + }) + } + } + + } +} diff --git a/buildSrc/src/main/kotlin/npm-multiplatform.gradle.kts b/buildSrc/src/main/kotlin/npm-multiplatform.gradle.kts new file mode 100644 index 00000000..671986b8 --- /dev/null +++ b/buildSrc/src/main/kotlin/npm-multiplatform.gradle.kts @@ -0,0 +1,86 @@ +import org.gradle.kotlin.dsl.* + +plugins { + kotlin("multiplatform") + `maven-publish` +} + + +kotlin { + jvm { + compilations.all { + kotlinOptions { + jvmTarget = "1.8" + } + } + } + + js { + compilations.all { + kotlinOptions { + metaInfo = true + sourceMap = true + sourceMapEmbedSources = "always" + moduleKind = "commonjs" + } + } + + compilations.named("main") { + kotlinOptions { + main = "call" + } + } + } + + 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")) + } + } + } + + targets.all { + sourceSets.all { + languageSettings.progressiveMode = true + languageSettings.enableLanguageFeature("InlineClasses") + } + } + + apply(plugin = "dokka-publish") + + // Apply JS test configuration + val runJsTests by ext(false) + + if (runJsTests) { + apply(plugin = "js-test") + } + +} diff --git a/dataforge-context/build.gradle.kts b/dataforge-context/build.gradle.kts index 58da79a7..f454e2ef 100644 --- a/dataforge-context/build.gradle.kts +++ b/dataforge-context/build.gradle.kts @@ -1,10 +1,10 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } description = "Context and provider definitions" -val coroutinesVersion: String by rootProject.extra +val coroutinesVersion: String = Versions.coroutinesVersion kotlin { jvm() @@ -22,6 +22,7 @@ kotlin { val jvmMain by getting { dependencies { api("io.github.microutils:kotlin-logging:1.6.10") + api("ch.qos.logback:logback-classic:1.2.3") api("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion") } } diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt index 8864fae1..9a091ad1 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/AbstractPlugin.kt @@ -1,16 +1,15 @@ package hep.dataforge.context -import hep.dataforge.meta.Config +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta import hep.dataforge.names.Name -abstract class AbstractPlugin : Plugin { +abstract class AbstractPlugin(override val meta: Meta = EmptyMeta) : 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 } @@ -19,9 +18,7 @@ abstract class AbstractPlugin : Plugin { this._context = null } - //TODO make configuration activation-safe - override fun provideTop(target: String, name: Name): Any? = null - override fun listTop(target: String): Sequence = emptySequence() + override fun listNames(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 index d1ff801d..cf6bb662 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Context.kt @@ -2,9 +2,10 @@ package hep.dataforge.context import hep.dataforge.meta.* import hep.dataforge.names.Name +import hep.dataforge.names.appendLeft import hep.dataforge.names.toName import hep.dataforge.provider.Provider -import hep.dataforge.provider.provideAll +import hep.dataforge.provider.top import hep.dataforge.values.Value import kotlinx.coroutines.CoroutineScope import mu.KLogger @@ -66,10 +67,10 @@ open class Context(final override val name: String, val parent: Context? = Globa } } - override fun listTop(target: String): Sequence { + override fun listNames(target: String): Sequence { return when (target) { Plugin.PLUGIN_TARGET -> plugins.asSequence().map { it.name.toName() } - Value.TYPE -> properties.asValueSequence().map { it.first } + Value.TYPE -> properties.values().map { it.first } else -> emptySequence() } } @@ -118,12 +119,13 @@ open class Context(final override val name: String, val parent: Context? = Globa /** * 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) } +fun Context.content(target: String): Map = content(target) -@JvmName("typedMembers") -inline fun Context.members(target: String) = - members(target).filterIsInstance() +@JvmName("typedContent") +inline fun Context.content(target: String): Map = + plugins.flatMap { plugin -> + plugin.top(target).entries.map { (it.key.appendLeft(plugin.name)) to it.value } + }.associate { it } /** diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt index 52079854..203edd2c 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/ContextBuilder.kt @@ -1,8 +1,7 @@ package hep.dataforge.context -import hep.dataforge.meta.Config import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.configure +import hep.dataforge.meta.buildMeta /** * A convenience builder for context @@ -19,11 +18,11 @@ class ContextBuilder(var name: String = "@anonimous", val parent: Context = Glob plugins.add(plugin) } - fun plugin(tag: PluginTag, action: Config.() -> Unit) { - plugins.add(PluginRepository.fetch(tag).configure(action)) + fun plugin(tag: PluginTag, action: MetaBuilder.() -> Unit) { + plugins.add(PluginRepository.fetch(tag, buildMeta(action))) } - fun plugin(name: String, group: String = "", version: String = "", action: Config.() -> Unit) { + fun plugin(name: String, group: String = "", version: String = "", action: MetaBuilder.() -> Unit) { plugin(PluginTag(name, group, version), action) } diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt index 1de7f6ca..e06bf3f9 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/Plugin.kt @@ -1,6 +1,5 @@ package hep.dataforge.context -import hep.dataforge.meta.Configurable import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaRepr import hep.dataforge.meta.buildMeta @@ -22,7 +21,7 @@ import hep.dataforge.provider.Provider * * @author Alexander Nozik */ -interface Plugin : Named, ContextAware, Provider, MetaRepr, Configurable { +interface Plugin : Named, ContextAware, Provider, MetaRepr { /** * Get tag for this plugin @@ -31,13 +30,14 @@ interface Plugin : Named, ContextAware, Provider, MetaRepr, Configurable { */ val tag: PluginTag + val meta: Meta + /** * The name of this plugin ignoring version and group * * @return */ - override val name: String - get() = tag.name + override val name: String get() = tag.name /** * Plugin dependencies which are required to attach this plugin. Plugin @@ -46,7 +46,7 @@ interface Plugin : Named, ContextAware, Provider, MetaRepr, Configurable { * * @return */ - fun dependsOn(): List = emptyList() + fun dependsOn(): List> = emptyList() /** * Start this plugin and attach registration info to the context. This method @@ -67,7 +67,7 @@ interface Plugin : Named, ContextAware, Provider, MetaRepr, Configurable { "context" to context.name "type" to this::class.simpleName "tag" to tag - "meta" to config + "meta" to meta } companion object { diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt index 1f905093..bcce8eb6 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginManager.kt @@ -1,6 +1,9 @@ package hep.dataforge.context -import hep.dataforge.meta.* +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.buildMeta import kotlin.reflect.KClass /** @@ -112,12 +115,21 @@ class PluginManager(override val context: Context) : ContextAware, Iterable load(PluginRepository.fetch(tag)).configure(meta) - loaded.config == meta -> loaded // if meta is the same, return existing plugin + loaded == null -> load(PluginRepository.fetch(tag,meta)) + loaded.meta == 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.") } } + fun load(factory: PluginFactory<*>, meta: Meta = EmptyMeta): Plugin{ + val loaded = get(factory.tag, false) + return when { + loaded == null -> load(factory(meta)) + loaded.meta == meta -> loaded // if meta is the same, return existing plugin + else -> throw RuntimeException("Can't load plugin with tag ${factory.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 @@ -126,7 +138,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable { - val plugin = PluginRepository.list().first { it.type == type }.build(meta) + val plugin = PluginRepository.list().first { it.type == type }.invoke(meta) if (type.isInstance(plugin)) { @Suppress("UNCHECKED_CAST") load(plugin as T) @@ -134,7 +146,7 @@ class PluginManager(override val context: Context) : ContextAware, Iterable loaded // if meta is the same, return existing plugin + loaded.meta == 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.") } } diff --git a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt index 43e51a07..eb715f80 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -1,48 +1,49 @@ package hep.dataforge.context +import hep.dataforge.meta.EmptyMeta import hep.dataforge.meta.Meta -import hep.dataforge.meta.configure import kotlin.reflect.KClass -interface PluginFactory { +interface PluginFactory { val tag: PluginTag - val type: KClass - fun build(): Plugin + val type: KClass + operator fun invoke(meta: Meta = EmptyMeta): T } -fun PluginFactory.build(meta: Meta) = build().configure(meta) - - expect object PluginRepository { - fun register(factory: PluginFactory) + fun register(factory: PluginFactory<*>) /** * List plugins available in the repository */ - fun list(): Sequence + 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.fetch(tag: PluginTag, meta: Meta = EmptyMeta): Plugin = + list().find { it.tag.matches(tag) }?.invoke(meta) ?: error("Plugin with tag $tag not found in the repository") -fun PluginRepository.register(tag: PluginTag, type: KClass, constructor: () -> Plugin) { - val factory = object : PluginFactory { +fun PluginRepository.register( + tag: PluginTag, + type: KClass, + constructor: (Meta) -> T +): PluginFactory { + val factory = object : PluginFactory { override val tag: PluginTag = tag - override val type: KClass = type + override val type: KClass = type - override fun build(): Plugin = constructor() + override fun invoke(meta: Meta): T = constructor(meta) } - PluginRepository.register(factory) + register(factory) + return factory } -inline fun PluginRepository.register(tag: PluginTag, noinline constructor: () -> T) = +inline fun PluginRepository.register(tag: PluginTag, noinline constructor: (Meta) -> 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/provider/Provider.kt b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt index 79b94dce..657f272d 100644 --- a/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt +++ b/dataforge-context/src/commonMain/kotlin/hep/dataforge/provider/Provider.kt @@ -17,6 +17,7 @@ package hep.dataforge.provider import hep.dataforge.names.Name import hep.dataforge.names.toName +import kotlin.jvm.JvmName /** * A marker utility interface for providers. @@ -51,7 +52,7 @@ interface Provider { * @param target * @return */ - fun listTop(target: String): Sequence + fun listNames(target: String): Sequence } fun Provider.provide(path: Path, targetOverride: String? = null): Any? { @@ -77,15 +78,23 @@ 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 +inline fun Provider.provide(target: String, name: Name): T? { + return provide(PathToken(name, target).toPath()) as? T } +inline fun Provider.provide(target: String, name: String): T? = + provide(target, name.toName()) + /** - * [Sequence] of all elements with given target + * A top level content with names */ -fun Provider.provideAll(target: String): Sequence { - return listTop(target).map { provideTop(target, it) ?: error("The element $it is declared but not provided") } +fun Provider.top(target: String): Map = top(target) + +@JvmName("typedTop") +inline fun Provider.top(target: String): Map { + return listNames(target).associate { + it to (provideTop(target, it) as? T ?: error("The element $it is declared but not provided")) + } } diff --git a/dataforge-context/src/commonTest/kotlin/hep/dataforge/context/ContextTest.kt b/dataforge-context/src/commonTest/kotlin/hep/dataforge/context/ContextTest.kt new file mode 100644 index 00000000..1d37c69e --- /dev/null +++ b/dataforge-context/src/commonTest/kotlin/hep/dataforge/context/ContextTest.kt @@ -0,0 +1,40 @@ +package hep.dataforge.context + +import hep.dataforge.names.Name +import hep.dataforge.names.appendLeft +import hep.dataforge.names.toName +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + + +class ContextTest { + class DummyPlugin : AbstractPlugin() { + override val tag get() = PluginTag("test") + + override fun provideTop(target: String, name: Name): Any? { + return when (target) { + "test" -> return name + else -> super.provideTop(target, name) + } + } + + override fun listNames(target: String): Sequence { + return when (target) { + "test" -> sequenceOf("a", "b", "c.d").map { it.toName() } + else -> super.listNames(target) + } + } + } + + @Test + fun testPluginManager() { + Global.plugins.load(DummyPlugin()) + val members = Global.content("test") + assertEquals(3, members.count()) + members.forEach { + assertTrue{it.key == it.value.appendLeft("test")} + } + } + +} \ No newline at end of file diff --git a/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt index 4cbeb0d1..78a2952e 100644 --- a/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt +++ b/dataforge-context/src/jsMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -3,14 +3,14 @@ package hep.dataforge.context actual object PluginRepository { - private val factories: MutableSet = HashSet() + private val factories: MutableSet> = HashSet() - actual fun register(factory: PluginFactory) { + actual fun register(factory: PluginFactory<*>) { factories.add(factory) } /** * List plugins available in the repository */ - actual fun list(): Sequence = factories.asSequence() + actual fun list(): Sequence> = factories.asSequence() } \ No newline at end of file diff --git a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt index 05b65ad9..83455fd6 100644 --- a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/context/PluginRepository.kt @@ -2,16 +2,16 @@ package hep.dataforge.context actual object PluginRepository { - private val factories: MutableSet = HashSet() + private val factories: MutableSet> = HashSet() - actual fun register(factory: PluginFactory) { + actual fun register(factory: PluginFactory<*>) { factories.add(factory) } /** * List plugins available in the repository */ - actual fun list(): Sequence = + 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 index 15cfe2d1..d6ed723d 100644 --- a/dataforge-context/src/jvmMain/kotlin/hep/dataforge/provider/Types.kt +++ b/dataforge-context/src/jvmMain/kotlin/hep/dataforge/provider/Types.kt @@ -1,11 +1,14 @@ package hep.dataforge.provider import hep.dataforge.context.Context -import hep.dataforge.context.members +import hep.dataforge.context.content +import hep.dataforge.names.Name import kotlin.reflect.KClass import kotlin.reflect.full.findAnnotation - +/** + * + */ object Types { operator fun get(cl: KClass<*>): String { return cl.findAnnotation()?.id ?: cl.simpleName ?: "" @@ -24,13 +27,20 @@ inline fun Provider.provideByType(name: String): T? { return provide(target, name) } -inline fun Provider.provideAllByType(): Sequence { +inline fun Provider.provideByType(name: Name): T? { val target = Types[T::class] - return provideAll(target).filterIsInstance() + return provide(target, name) +} + +inline fun Provider.top(): Map { + val target = Types[T::class] + return listNames(target).associate { name -> + name to (provideByType(name) ?: error("The element $name is declared but not provided")) + } } /** * A sequences of all objects provided by plugins with given target and type */ -inline fun Context.members(): Sequence = members(Types[T::class]) +inline fun Context.content(): Map = content(Types[T::class]) diff --git a/dataforge-data/build.gradle.kts b/dataforge-data/build.gradle.kts index eb6b3669..7ebb46ce 100644 --- a/dataforge-data/build.gradle.kts +++ b/dataforge-data/build.gradle.kts @@ -1,8 +1,8 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } -val coroutinesVersion: String by rootProject.extra +val coroutinesVersion: String = Versions.coroutinesVersion kotlin { jvm() @@ -11,6 +11,7 @@ kotlin { val commonMain by getting{ dependencies { api(project(":dataforge-meta")) + api(kotlin("reflect")) api("org.jetbrains.kotlinx:kotlinx-coroutines-core-common:$coroutinesVersion") } } diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt index 599101c0..228522dc 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Action.kt @@ -20,9 +20,10 @@ interface Action { } /** - * Action composition. The result is terminal if one of parts is terminal + * Action composition. The result is terminal if one of its parts is terminal */ infix fun Action.then(action: Action): Action { + // TODO introduce composite action and add optimize by adding action to the list return object : Action { override fun invoke(node: DataNode, meta: Meta): DataNode { return action(this@then.invoke(node, meta), meta) @@ -33,28 +34,19 @@ infix fun Action.then(action: Action): A } } -/** - * 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) - } - } -} +///** +// * An action that performs the same transformation on each of input data nodes. Null results are ignored. +// * The transformation is non-suspending because it is lazy. +// */ +//class PipeAction(val transform: (Name, Data, Meta) -> Data?) : Action { +// override fun invoke(node: DataNode, meta: Meta): DataNode = DataNode.build { +// node.data().forEach { (name, data) -> +// val res = transform(name, data, meta) +// if (res != null) { +// set(name, res) +// } +// } +// } +//} diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt index 6096a799..20957824 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Data.kt @@ -2,7 +2,7 @@ package hep.dataforge.data import hep.dataforge.meta.Meta import hep.dataforge.meta.MetaRepr -import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope import kotlin.reflect.KClass /** @@ -29,19 +29,32 @@ interface Data : MetaRepr { 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) + fun static(scope: CoroutineScope, value: T, meta: Meta): Data = + DataImpl(value::class, Goal.static(scope, value), meta) } } -suspend fun Data.await(): T = goal.await() +/** + * Upcast a [Data] to a supertype + */ +inline fun Data.cast(): Data { + return Data.of(R::class, goal, meta) +} + +fun Data.cast(type: KClass): Data { + return Data.of(type, goal, meta) +} + +suspend fun Data.await(): T = goal.await() /** * Generic Data implementation diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt index 6f230c0f..6c920f57 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataFilter.kt @@ -4,14 +4,14 @@ import hep.dataforge.meta.* import hep.dataforge.names.toName -class DataFilter(override val config: Config) : Specification { +class DataFilter(override val config: Config) : Specific { var from by string() var to by string() var pattern by string("*.") // val prefix by string() // val suffix by string() - companion object : SpecificationCompanion { + companion object : Specification { override fun wrap(config: Config): DataFilter = DataFilter(config) } } @@ -22,15 +22,15 @@ class DataFilter(override val config: Config) : Specification { 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) -> + val targetNode = DataTreeBuilder(type).apply { + sourceNode.data().forEach { (name, data) -> if (name.toString().matches(regex)) { this[name] = data } } } return filter.to?.let { - DataTreeBuilder().apply { this[it.toName()] = targetNode }.build() + DataTreeBuilder(type).apply { this[it.toName()] = targetNode }.build() } ?: targetNode.build() } diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt index bf8f6f81..02fc6a9e 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/DataNode.kt @@ -1,14 +1,18 @@ package hep.dataforge.data -import hep.dataforge.names.Name -import hep.dataforge.names.NameToken -import hep.dataforge.names.plus -import hep.dataforge.names.toName +import hep.dataforge.names.* +import kotlin.reflect.KClass /** * A tree-like data structure grouped into the node. All data inside the node must inherit its type */ interface DataNode { + + /** + * The minimal common ancestor to all data in the node + */ + val type: KClass + /** * Get the specific data if it exists */ @@ -22,21 +26,23 @@ interface DataNode { /** * Walk the tree upside down and provide all data nodes with full names */ - fun dataSequence(): Sequence>> + fun data(): Sequence>> /** * A sequence of all nodes in the tree walking upside down, excluding self */ - fun nodeSequence(): Sequence>> + fun nodes(): Sequence>> - operator fun iterator(): Iterator>> = dataSequence().iterator() + operator fun iterator(): Iterator>> = data().iterator() companion object { const val TYPE = "dataNode" - fun build(block: DataTreeBuilder.() -> Unit) = DataTreeBuilder().apply(block).build() - } + fun build(type: KClass, block: DataTreeBuilder.() -> Unit) = + DataTreeBuilder(type).apply(block).build() + fun builder(type: KClass) = DataTreeBuilder(type) + } } internal sealed class DataTreeItem { @@ -44,29 +50,32 @@ internal sealed class DataTreeItem { class Value(val value: Data) : DataTreeItem() } -class DataTree internal constructor(private val items: Map>) : DataNode { +class DataTree internal constructor( + override val type: KClass, + 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()) + else -> getNode(name.first()!!.asName())?.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()) + else -> getNode(name.first()!!.asName())?.getNode(name.cutFirst()) } - override fun dataSequence(): Sequence>> { + override fun data(): Sequence>> { return sequence { items.forEach { (head, tree) -> when (tree) { - is DataTreeItem.Value -> yield(head.toName() to tree.value) + is DataTreeItem.Value -> yield(head.asName() to tree.value) is DataTreeItem.Node -> { val subSequence = - tree.tree.dataSequence().map { (name, data) -> (head.toName() + name) to data } + tree.tree.data().map { (name, data) -> (head.asName() + name) to data } yieldAll(subSequence) } } @@ -74,13 +83,13 @@ class DataTree internal constructor(private val items: Map>> { + override fun nodes(): Sequence>> { return sequence { items.forEach { (head, tree) -> if (tree is DataTreeItem.Node) { - yield(head.toName() to tree.tree) + yield(head.asName() to tree.tree) val subSequence = - tree.tree.nodeSequence().map { (name, node) -> (head.toName() + name) to node } + tree.tree.nodes().map { (name, node) -> (head.asName() + name) to node } yieldAll(subSequence) } } @@ -96,7 +105,7 @@ private sealed class DataTreeBuilderItem { /** * A builder for a DataTree. */ -class DataTreeBuilder { +class DataTreeBuilder(private val type: KClass) { private val map = HashMap>() operator fun set(token: NameToken, node: DataTreeBuilder) { @@ -111,7 +120,7 @@ class DataTreeBuilder { private fun buildNode(token: NameToken): DataTreeBuilder { return if (!map.containsKey(token)) { - DataTreeBuilder().also { map[token] = DataTreeBuilderItem.Node(it) } + DataTreeBuilder(type).also { map[token] = DataTreeBuilderItem.Node(it) } } else { (map[token] as? DataTreeBuilderItem.Node ?: error("The node with name $token is occupied by leaf")).tree } @@ -156,7 +165,15 @@ class DataTreeBuilder { /** * Build and append node */ - infix fun String.to(block: DataTreeBuilder.() -> Unit) = set(toName(), DataTreeBuilder().apply(block)) + infix fun String.to(block: DataTreeBuilder.() -> Unit) = set(toName(), DataTreeBuilder(type).apply(block)) + + + fun update(node: DataNode){ + node.data().forEach { + //TODO check if the place is occupied + this[it.first] = it.second + } + } fun build(): DataTree { val resMap = map.mapValues { (_, value) -> @@ -165,28 +182,35 @@ class DataTreeBuilder { is DataTreeBuilderItem.Node -> DataTreeItem.Node(value.tree.build()) } } - return DataTree(resMap) + return DataTree(type, 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 } +fun DataNode.builder(): DataTreeBuilder = DataTreeBuilder(type).apply { + data().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<*>.startAll() = data().forEach { (_, data) -> data.goal.start() } -fun DataNode.filter(predicate: (Name, Data) -> Boolean): DataNode = DataNode.build { - dataSequence().forEach { (name, data) -> +fun DataNode.filter(predicate: (Name, Data) -> Boolean): DataNode = DataNode.build(type) { + data().forEach { (name, data) -> if (predicate(name, data)) { this[name] = data } } } +fun DataNode.first(): Data = data().first().second + +/** + * Check that node is compatible with given type meaning that each element could be cast to the type + */ +expect fun DataNode<*>.checkType(type: KClass<*>) + //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 index 104a9037..991ddbdb 100644 --- a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/Goal.kt @@ -2,33 +2,37 @@ package hep.dataforge.data import kotlinx.coroutines.* import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext /** * A special deferred with explicit dependencies and some additional information like progress and unique id */ interface Goal : Deferred, CoroutineScope { + val scope: CoroutineScope + override val coroutineContext get() = scope.coroutineContext + val dependencies: Collection> - val status: String - - val totalWork: Double - val workDone: Double - + val totalWork: Double get() = dependencies.sumByDouble { totalWork } + (monitor?.totalWork ?: 0.0) + val workDone: Double get() = dependencies.sumByDouble { workDone } + (monitor?.workDone ?: 0.0) + val status: String get() = monitor?.status ?: "" 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)) + fun static(scope: CoroutineScope, value: T): Goal = + StaticGoalImpl(scope, CompletableDeferred(value)) } } /** * A monitor of goal state that could be accessed only form inside the goal */ -class GoalMonitor { +class GoalMonitor : CoroutineContext.Element { + override val key: CoroutineContext.Key<*> get() = GoalMonitor + var totalWork: Double = 1.0 var workDone: Double = 0.0 var status: String = "" @@ -46,26 +50,24 @@ class GoalMonitor { fun finish() { workDone = totalWork } + + companion object : CoroutineContext.Key } +val CoroutineScope.monitor: GoalMonitor? get() = coroutineContext[GoalMonitor] + private class GoalImpl( + override val scope: CoroutineScope, 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 -} +) : Goal, Deferred by deferred -private class StaticGoalImpl(val context: CoroutineContext, deferred: CompletableDeferred) : Goal, +private class StaticGoalImpl(override val scope: CoroutineScope, 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 } @@ -75,23 +77,32 @@ private class StaticGoalImpl(val context: CoroutineContext, deferred: Complet * * **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) { +fun CoroutineScope.createGoal( + dependencies: Collection>, + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.() -> R +): Goal { + val deferred = async(context + GoalMonitor(), start = CoroutineStart.LAZY) { dependencies.forEach { it.start() } - monitor.start() - return@async supervisorScope { monitor.block() } - }.also { - monitor.finish() + monitor?.start() + //Running in supervisor scope in order to allow manual error handling + return@async supervisorScope { + block().also { + monitor?.finish() + } + } } - return GoalImpl(dependencies, monitor, deferred) + return GoalImpl(this, dependencies, 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()) } +fun Goal.pipe( + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.(T) -> R +): Goal = createGoal(listOf(this), context) { block(await()) } /** * Create a joining goal. @@ -99,8 +110,22 @@ fun Goal.pipe(block: suspend GoalMonitor.(T) -> R): Goal = createGo */ 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 + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.(Collection) -> R +): Goal = scope.createGoal(this, context) { + block(map { it.await() }) +} + +/** + * A joining goal for a map + * @param K type of the map key + * @param T type of the input goal + * @param R type of the result goal + */ +fun Map>.join( + scope: CoroutineScope = values.first(), + context: CoroutineContext = EmptyCoroutineContext, + block: suspend CoroutineScope.(Map) -> R +): Goal = scope.createGoal(this.values, context) { + block(mapValues { it.value.await() }) +} \ No newline at end of file diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/GroupBuilder.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/GroupBuilder.kt new file mode 100644 index 00000000..0820c162 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/GroupBuilder.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.data + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.string + +interface GroupRule { + operator fun invoke(node: DataNode): Map> +} + +/** + * The class to builder groups of content with annotation defined rules + * + * @author Alexander Nozik + */ + +object GroupBuilder { + + /** + * Create grouping rule that creates groups for different values of value + * field with name [key] + * + * @param key + * @param defaultTagValue + * @return + */ + fun byValue(key: String, defaultTagValue: String): GroupRule = object : + GroupRule { + override fun invoke(node: DataNode): Map> { + val map = HashMap>() + + node.data().forEach { (name, data) -> + val tagValue = data.meta[key]?.string ?: defaultTagValue + map.getOrPut(tagValue) { DataNode.builder(node.type) }[name] = data + } + + return map.mapValues { it.value.build() } + } + } + + + // @ValueDef(key = "byValue", required = true, info = "The name of annotation value by which grouping should be made") +// @ValueDef( +// key = "defaultValue", +// def = "default", +// info = "Default value which should be used for content in which the grouping value is not presented" +// ) + fun byMeta(config: Meta): GroupRule { + //TODO expand grouping options + return config["byValue"]?.string?.let { + byValue( + it, + config["defaultValue"]?.string ?: "default" + ) + } + ?: object : GroupRule { + override fun invoke(node: DataNode): Map> = mapOf("" to node) + } + } +} diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/JoinAction.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/JoinAction.kt new file mode 100644 index 00000000..2f5979fe --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/JoinAction.kt @@ -0,0 +1,111 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.builder +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KClass + + +class JoinGroup(var name: String, internal val node: DataNode) { + + var meta: MetaBuilder = MetaBuilder() + + lateinit var result: suspend ActionEnv.(Map) -> R + + fun result(f: suspend ActionEnv.(Map) -> R) { + this.result = f; + } + +} + +class JoinGroupBuilder(val actionMeta: Meta) { + private val groupRules: MutableList<(DataNode) -> List>> = ArrayList(); + + /** + * introduce grouping by value name + */ + fun byValue(tag: String, defaultTag: String = "@default", action: JoinGroup.() -> Unit) { + groupRules += { node -> + GroupBuilder.byValue(tag, defaultTag).invoke(node).map { + JoinGroup(it.key, it.value).apply(action) + } + } + } + + /** + * Add a single fixed group to grouping rules + */ + fun group(groupName: String, filter: DataFilter, action: JoinGroup.() -> Unit) { + groupRules += { node -> + listOf( + JoinGroup(groupName, node.filter(filter)).apply(action) + ) + } + } + + fun group(groupName: String, filter: (Name, Data) -> Boolean, action: JoinGroup.() -> Unit) { + groupRules += { node -> + listOf( + JoinGroup(groupName, node.filter(filter)).apply(action) + ) + } + } + + /** + * Apply transformation to the whole node + */ + fun result(resultName: String, f: suspend ActionEnv.(Map) -> R) { + groupRules += { node -> + listOf(JoinGroup(resultName, node).apply { result(f) }) + } + } + + internal fun buildGroups(input: DataNode): List> { + return groupRules.flatMap { it.invoke(input) } + } + +} + + +/** + * The same rules as for KPipe + */ +class JoinAction( + val inputType: KClass, + val outputType: KClass, + val context: CoroutineContext = EmptyCoroutineContext, + private val action: JoinGroupBuilder.() -> Unit +) : Action { + + override fun invoke(node: DataNode, meta: Meta): DataNode { + node.checkType(inputType) + return DataNode.build(outputType) { + JoinGroupBuilder(meta).apply(action).buildGroups(node).forEach { group -> + + val laminate = Laminate(group.meta, meta) + + val goalMap: Map> = group.node + .data() + .associate { it.first to it.second.goal } + + val groupName: String = group.name; + + val env = ActionEnv(groupName.toName(), laminate.builder()) + + val goal = goalMap.join(context = context) { group.result.invoke(env, it) } + + val res = Data.of(outputType, goal, env.meta) + + set(env.name, res) + } + + } + } +} + +operator fun Map.get(name:String) = get(name.toName()) diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/PipeAction.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/PipeAction.kt new file mode 100644 index 00000000..f106df40 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/PipeAction.kt @@ -0,0 +1,65 @@ +package hep.dataforge.data + +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KClass + +class ActionEnv(val name: Name, val meta: Meta) + + +/** + * Action environment + */ +class PipeBuilder(var name: Name, var meta: MetaBuilder) { + lateinit var result: suspend ActionEnv.(T) -> R + + /** + * Calculate the result of goal + */ + fun result(f: suspend ActionEnv.(T) -> R) { + result = f; + } +} + + +class PipeAction( + val inputType: KClass, + val outputType: KClass, + val context: CoroutineContext = EmptyCoroutineContext, + private val block: PipeBuilder.() -> Unit +) : Action { + + override fun invoke(node: DataNode, meta: Meta): DataNode { + node.checkType(inputType) + + return DataNode.build(outputType) { + node.data().forEach { (name, data) -> + //merging data meta with action meta (data meta is primary) + val oldMeta = meta.builder().apply { update(data.meta) } + // creating environment from old meta and name + val env = ActionEnv(name, oldMeta) + //applying transformation from builder + val builder = PipeBuilder(name, oldMeta).apply(block) + //getting new name + val newName = builder.name + //getting new meta + val newMeta = builder.meta.seal() + //creating a goal with custom context if provided + val goal = data.goal.pipe(context) { builder.result(env, it) } + //setting the data node + this[newName] = Data.of(outputType, goal, newMeta) + } + } + } +} + +inline fun DataNode.pipe( + meta: Meta, + context: CoroutineContext = EmptyCoroutineContext, + noinline action: PipeBuilder.() -> Unit +): DataNode = PipeAction(T::class, R::class, context, action).invoke(this, meta) + + + diff --git a/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/SplitAction.kt b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/SplitAction.kt new file mode 100644 index 00000000..3aa08990 --- /dev/null +++ b/dataforge-data/src/commonMain/kotlin/hep/dataforge/data/SplitAction.kt @@ -0,0 +1,69 @@ +package hep.dataforge.data + +import hep.dataforge.meta.Laminate +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaBuilder +import hep.dataforge.meta.builder +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import kotlin.collections.set +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.reflect.KClass + + +class FragmentRule(val name: Name, var meta: MetaBuilder) { + lateinit var result: suspend (T) -> R + + fun result(f: suspend (T) -> R) { + result = f; + } +} + + +class SplitBuilder(val name: Name, val meta: Meta) { + internal val fragments: MutableMap.() -> Unit> = HashMap() + + /** + * Add new fragment building rule. If the framgent not defined, result won't be available even if it is present in the map + * @param name the name of a fragment + * @param rule the rule to transform fragment name and meta using + */ + fun fragment(name: String, rule: FragmentRule.() -> Unit) { + fragments[name.toName()] = rule + } +} + +class SplitAction( + val inputType: KClass, + val outputType: KClass, + val context: CoroutineContext = EmptyCoroutineContext, + private val action: SplitBuilder.() -> Unit +) : Action { + + override fun invoke(node: DataNode, meta: Meta): DataNode { + node.checkType(inputType) + + return DataNode.build(outputType) { + node.data().forEach { (name, data) -> + + val laminate = Laminate(data.meta, meta) + + val split = SplitBuilder(name, data.meta).apply(action) + + + // apply individual fragment rules to result + split.fragments.forEach { (fragmentName, rule) -> + val env = FragmentRule(fragmentName, laminate.builder()) + + rule(env) + + val goal = data.goal.pipe(context = context) { env.result(it) } + + val res = Data.of(outputType, goal, env.meta) + set(env.name, res) + } + } + } + } +} \ No newline at end of file diff --git a/dataforge-data/src/jsMain/kotlin/hep/dataforge/data/checkType.kt b/dataforge-data/src/jsMain/kotlin/hep/dataforge/data/checkType.kt new file mode 100644 index 00000000..56d1d0c9 --- /dev/null +++ b/dataforge-data/src/jsMain/kotlin/hep/dataforge/data/checkType.kt @@ -0,0 +1,10 @@ +package hep.dataforge.data + +import kotlin.reflect.KClass + +/** + * Check that node is compatible with given type meaning that each element could be cast to the type + */ +actual fun DataNode<*>.checkType(type: KClass<*>) { + //Not supported in js yet +} \ No newline at end of file diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/CastDataNode.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/CastDataNode.kt new file mode 100644 index 00000000..9ff277cd --- /dev/null +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/CastDataNode.kt @@ -0,0 +1,46 @@ +package hep.dataforge.data + +import hep.dataforge.names.Name +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + +fun Data.safeCast(type: KClass): Data? { + return if (type.isSubclassOf(type)) { + @Suppress("UNCHECKED_CAST") + Data.of(type, goal as Goal, meta) + } else { + null + } +} + +/** + * Filter a node by data and node type. Resulting node and its subnodes is guaranteed to have border type [type], + * but could contain empty nodes + */ +fun DataNode.cast(type: KClass): DataNode { + return if (this is CastDataNode) { + origin.cast(type) + } else { + CastDataNode(this, type) + } +} + +inline fun DataNode.cast(): DataNode = cast(R::class) + +class CastDataNode(val origin: DataNode, override val type: KClass) : DataNode { + + override fun get(name: Name): Data? = + origin[name]?.safeCast(type) + + override fun getNode(name: Name): DataNode? { + return origin.getNode(name)?.cast(type) + } + + override fun data(): Sequence>> = + origin.data().mapNotNull { pair -> + pair.second.safeCast(type)?.let { pair.first to it } + } + + override fun nodes(): Sequence>> = + origin.nodes().map { it.first to it.second.cast(type) } +} \ No newline at end of file diff --git a/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/checkType.kt b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/checkType.kt new file mode 100644 index 00000000..0b4c602f --- /dev/null +++ b/dataforge-data/src/jvmMain/kotlin/hep/dataforge/data/checkType.kt @@ -0,0 +1,13 @@ +package hep.dataforge.data + +import kotlin.reflect.KClass +import kotlin.reflect.full.isSuperclassOf + +/** + * Check that node is compatible with given type meaning that each element could be cast to the type + */ +actual fun DataNode<*>.checkType(type: KClass<*>) { + if (!type.isSuperclassOf(type)) { + error("$type expected, but $type received") + } +} \ No newline at end of file diff --git a/dataforge-io/build.gradle.kts b/dataforge-io/build.gradle.kts index 1756f10d..67ffe124 100644 --- a/dataforge-io/build.gradle.kts +++ b/dataforge-io/build.gradle.kts @@ -1,16 +1,57 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } +description = "IO for meta" + + +val ioVersion: String = Versions.ioVersion +val serializationVersion: String = Versions.serializationVersion + kotlin { jvm() js() sourceSets { val commonMain by getting{ dependencies { - api(project(":dataforge-context")) - api(project(":dataforge-meta-io")) + 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/BinaryMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt similarity index 97% rename from dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/BinaryMetaFormat.kt rename to dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt index 176435da..0e73934a 100644 --- a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/BinaryMetaFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/BinaryMetaFormat.kt @@ -1,4 +1,4 @@ -package hep.dataforge.meta.io +package hep.dataforge.io import hep.dataforge.meta.* import hep.dataforge.values.* @@ -8,8 +8,8 @@ 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 write(obj: Meta, out: Output) { + out.writeMeta(obj) } override fun read(input: Input): Meta { diff --git a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt similarity index 72% rename from dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt rename to dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt index ce01d506..1a9e58d7 100644 --- a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Envelope.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Envelope.kt @@ -1,21 +1,15 @@ -package hep.dataforge.meta.io +package hep.dataforge.io import hep.dataforge.meta.Meta import hep.dataforge.meta.get import hep.dataforge.meta.string +import kotlinx.io.core.Input interface Envelope { val meta: Meta - val data: Binary? + val data: Input? 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 @@ -25,9 +19,16 @@ interface Envelope { const val ENVELOPE_DATA_TYPE_KEY = "$ENVELOPE_NODE.dataType" const val ENVELOPE_DESCRIPTION_KEY = "$ENVELOPE_NODE.description" //const val ENVELOPE_TIME_KEY = "@envelope.time" + } } +class SimpleEnvelope(override val meta: Meta, val dataProvider: () -> Input?) : Envelope{ + override val data: Input? + get() = dataProvider() + +} + /** * The purpose of the envelope * @@ -47,4 +48,5 @@ val Envelope.dataType: String? get() = meta[Envelope.ENVELOPE_DATA_TYPE_KEY].str * * @return */ -val Envelope.description: String? get() = meta[Envelope.ENVELOPE_DESCRIPTION_KEY].string \ No newline at end of file +val Envelope.description: String? get() = meta[Envelope.ENVELOPE_DESCRIPTION_KEY].string + diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt new file mode 100644 index 00000000..ce58c05d --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/IOFormat.kt @@ -0,0 +1,10 @@ +package hep.dataforge.io + +import kotlinx.io.core.Input +import kotlinx.io.core.Output + + +interface IOFormat { + fun write(obj: T, out: Output) + fun read(input: Input): T +} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt new file mode 100644 index 00000000..9d7b5739 --- /dev/null +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/JsonMetaFormat.kt @@ -0,0 +1,107 @@ +package hep.dataforge.io + +import hep.dataforge.meta.Meta +import hep.dataforge.meta.MetaItem +import hep.dataforge.names.NameToken +import hep.dataforge.names.toName +import hep.dataforge.values.* +import kotlinx.io.core.Input +import kotlinx.io.core.Output +import kotlinx.io.core.readText +import kotlinx.io.core.writeText +import kotlinx.serialization.json.* + + +object JsonMetaFormat : MetaFormat { + + override fun write(obj: Meta, out: Output) { + val str = obj.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 not supported") + } + } +} + +fun Value.toJson(): JsonElement { + return if(isList()){ + JsonArray(list.map { it.toJson() }) + } else { + when (type) { + ValueType.NUMBER -> JsonPrimitive(number) + ValueType.STRING -> JsonPrimitive(string) + ValueType.BOOLEAN -> JsonPrimitive(boolean) + ValueType.NULL -> JsonNull + } + } +} + +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 JsonObject.toMeta() = JsonMeta(this) + +class JsonMeta(val json: JsonObject) : Meta { + + private fun JsonPrimitive.toValue(): Value { + return when (this) { + JsonNull -> Null + else -> this.content.parseValue() // Optimize number and boolean parsing + } + } + + private operator fun MutableMap>.set(key: String, value: JsonElement) = when (value) { + is JsonPrimitive -> this[key] = MetaItem.ValueItem(value.toValue()) + is JsonObject -> this[key] = MetaItem.NodeItem(value.toMeta()) + is JsonArray -> { + when { + value.all { it is JsonPrimitive } -> { + val listValue = ListValue( + value.map { + //We already checked that all values are primitives + (it as JsonPrimitive).toValue() + } + ) + this[key] = MetaItem.ValueItem(listValue) + } + else -> value.forEachIndexed { index, jsonElement -> + when (jsonElement) { + is JsonObject -> this["$key[$index]"] = MetaItem.NodeItem(JsonMeta(jsonElement)) + is JsonPrimitive -> this["$key[$index]"] = MetaItem.ValueItem(jsonElement.toValue()) + is JsonArray -> TODO("Nested arrays not supported") + } + } + } + } + } + + override val items: Map> by lazy { + val map = HashMap>() + json.forEach { (key, value) -> map[key] = value } + map.mapKeys { it.key.toName().first()!! } + } +} + +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-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt similarity index 73% rename from dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/MetaFormat.kt rename to dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt index 2d7de871..61969536 100644 --- a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/MetaFormat.kt +++ b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/MetaFormat.kt @@ -1,15 +1,14 @@ -package hep.dataforge.meta.io +package hep.dataforge.io import hep.dataforge.meta.Meta -import kotlinx.io.core.* +import kotlinx.io.core.BytePacketBuilder +import kotlinx.io.core.ByteReadPacket +import kotlinx.io.core.toByteArray /** * A format for meta serialization */ -interface MetaFormat { - fun write(meta: Meta, out: Output) - fun read(input: Input): Meta -} +interface MetaFormat: IOFormat /** * ServiceLoader compatible factory diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt b/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt deleted file mode 100644 index 3a96c458..00000000 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/OutputManager.kt +++ /dev/null @@ -1,63 +0,0 @@ -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-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt similarity index 90% rename from dataforge-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt rename to dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt index 78966dc8..e177e4d8 100644 --- a/dataforge-meta-io/src/commonTest/kotlin/hep/dataforge/meta/io/MetaFormatTest.kt +++ b/dataforge-io/src/commonTest/kotlin/hep/dataforge/io/MetaFormatTest.kt @@ -1,4 +1,4 @@ -package hep.dataforge.meta.io +package hep.dataforge.io import hep.dataforge.meta.buildMeta import kotlin.test.Test @@ -26,6 +26,7 @@ class MetaFormatTest { "node" to { "b" to "DDD" "c" to 11.1 + "array" to doubleArrayOf(1.0,2.0,3.0) } } val string = meta.asString(JsonMetaFormat) diff --git a/dataforge-io/src/jvmMain/resources/META-INF/services/hep.dataforge.io.MetaFormatFactory b/dataforge-io/src/jvmMain/resources/META-INF/services/hep.dataforge.io.MetaFormatFactory new file mode 100644 index 00000000..41eb3ce0 --- /dev/null +++ b/dataforge-io/src/jvmMain/resources/META-INF/services/hep.dataforge.io.MetaFormatFactory @@ -0,0 +1,2 @@ +hep.dataforge.io.BinaryMetaFormatFactory +hep.dataforge.io.JsonMetaFormatFactory \ No newline at end of file diff --git a/dataforge-meta-io/build.gradle.kts b/dataforge-meta-io/build.gradle.kts deleted file mode 100644 index 1b57774d..00000000 --- a/dataforge-meta-io/build.gradle.kts +++ /dev/null @@ -1,57 +0,0 @@ -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 deleted file mode 100644 index 7ab032c2..00000000 --- a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/Binary.kt +++ /dev/null @@ -1,7 +0,0 @@ -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/JsonMetaFormat.kt b/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/JsonMetaFormat.kt deleted file mode 100644 index 2c82c085..00000000 --- a/dataforge-meta-io/src/commonMain/kotlin/hep/dataforge/meta/io/JsonMetaFormat.kt +++ /dev/null @@ -1,89 +0,0 @@ -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/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 deleted file mode 100644 index 23bcfcd2..00000000 --- a/dataforge-meta-io/src/jvmMain/resources/META-INF/services/hep.dataforge.meta.io.MetaFormatFactory +++ /dev/null @@ -1,2 +0,0 @@ -hep.dataforge.meta.io.BinaryMetaFormatFactory -hep.dataforge.meta.io.JsonMetaFormatFactory \ No newline at end of file diff --git a/dataforge-meta/build.gradle.kts b/dataforge-meta/build.gradle.kts index e754cf4b..17aaebf8 100644 --- a/dataforge-meta/build.gradle.kts +++ b/dataforge-meta/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } description = "Meta definition and basic operations on meta" @@ -7,54 +7,4 @@ 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 +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt new file mode 100644 index 00000000..a0780a3e --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/Described.kt @@ -0,0 +1,5 @@ +package hep.dataforge.descriptors + +interface Described { + val descriptor: NodeDescriptor +} \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/NodeDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/NodeDescriptor.kt new file mode 100644 index 00000000..902c81a3 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/NodeDescriptor.kt @@ -0,0 +1,128 @@ +/* + * 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. + */ + +/* + * 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.descriptors + +import hep.dataforge.meta.* +import hep.dataforge.names.NameToken +import hep.dataforge.names.toName + +/** + * Descriptor for meta node. Could contain additional information for viewing + * and editing. + * + * @author Alexander Nozik + */ +class NodeDescriptor(override val config: Config) : Specific { + + /** + * The name of this node + * + * @return + */ + var name: String by string { error("Anonymous descriptors are not allowed") } + + + /** + * The default for this node. Null if there is no default. + * + * @return + */ + var default: Meta? by node() + + /** + * True if multiple children with this nodes name are allowed. Anonymous + * nodes are always single + * + * @return + */ + var multiple: Boolean by boolean(false) + + /** + * True if the node is required + * + * @return + */ + var required: Boolean by boolean { default == null } + + /** + * The node description + * + * @return + */ + var info: String? by string() + + /** + * A list of tags for this node. Tags used to customize node usage + * + * @return + */ + var tags: List by value{ value -> + value?.list?.map { it.string } ?: emptyList() + } + + /** + * The list of value descriptors + */ + val values: Map + get() = config.getAll("value".toName()).entries.associate { (name, node) -> + name to ValueDescriptor.wrap(node.node ?: error("Value descriptor must be a node")) + } + + fun value(name: String, descriptor: ValueDescriptor) { + val token = NameToken("value", name) + config[token] = descriptor.config + } + + /** + * Add a value descriptor using block for + */ + fun value(name: String, block: ValueDescriptor.() -> Unit) { + value(name, ValueDescriptor.build { this.name = name }.apply(block)) + } + + /** + * The map of children node descriptors + */ + val nodes: Map + get() = config.getAll("node".toName()).entries.associate { (name, node) -> + name to NodeDescriptor.wrap(node.node ?: error("Node descriptor must be a node")) + } + + + fun node(name: String, descriptor: NodeDescriptor) { + val token = NameToken("node", name) + config[token] = descriptor.config + } + + fun node(name: String, block: NodeDescriptor.() -> Unit) { + node(name, NodeDescriptor.build { this.name = name }.apply(block)) + } + + + //override val descriptor: NodeDescriptor = empty("descriptor") + + companion object : Specification { + + override fun wrap(config: Config): NodeDescriptor = NodeDescriptor(config) + + } +} diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ValueDescriptor.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ValueDescriptor.kt new file mode 100644 index 00000000..e3acd07b --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/ValueDescriptor.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package hep.dataforge.descriptors + +import hep.dataforge.meta.* +import hep.dataforge.values.False +import hep.dataforge.values.True +import hep.dataforge.values.Value +import hep.dataforge.values.ValueType + +/** + * A descriptor for meta value + * + * Descriptor can have non-atomic path. It is resolved when descriptor is added to the node + * + * @author Alexander Nozik + */ +class ValueDescriptor(override val config: Config) : Specific { + + /** + * The default for this value. Null if there is no default. + * + * @return + */ + var default: Value? by value() + + fun default(v: Any) { + this.default = Value.of(v) + } + + /** + * True if multiple values with this name are allowed. + * + * @return + */ + var multiple: Boolean by boolean(false) + + /** + * True if the value is required + * + * @return + */ + var required: Boolean by boolean { default == null } + + /** + * Value name + * + * @return + */ + var name: String by string { error("Anonymous descriptors are not allowed") } + + /** + * The value info + * + * @return + */ + var info: String? by string() + + /** + * A list of allowed ValueTypes. Empty if any value type allowed + * + * @return + */ + var type: List by value { + it?.list?.map { v -> ValueType.valueOf(v.string) } ?: emptyList() + } + + fun type(vararg t: ValueType) { + this.type = listOf(*t) + } + + var tags: List by value { value -> + value?.list?.map { it.string } ?: emptyList() + } + + /** + * Check if given value is allowed for here. The type should be allowed and + * if it is value should be within allowed values + * + * @param value + * @return + */ + fun isAllowedValue(value: Value): Boolean { + return (type.isEmpty() || type.contains(ValueType.STRING) || type.contains(value.type)) && (allowedValues.isEmpty() || allowedValues.contains( + value + )) + } + + /** + * A list of allowed values with descriptions. If empty than any value is + * allowed. + * + * @return + */ + var allowedValues: List by value { + it?.list ?: if (type.size == 1 && type[0] === ValueType.BOOLEAN) { + listOf(True, False) + } else { + emptyList() + } + } + + /** + * Allow given list of value and forbid others + */ + fun allow(vararg v: Any) { + this.allowedValues = v.map { Value.of(it) } + } + + companion object : Specification { + + override fun wrap(config: Config): ValueDescriptor = ValueDescriptor(config) + + inline fun > enum(name: String) = + ValueDescriptor.build { + this.name = name + type(ValueType.STRING) + this.allowedValues = enumValues().map { Value.of(it.name) } + } + +// /** +// * Build a value descriptor from annotation +// */ +// fun build(def: ValueDef): ValueDescriptor { +// val builder = MetaBuilder("value") +// .setValue("name", def.key) +// +// if (def.type.isNotEmpty()) { +// builder.setValue("type", def.type) +// } +// +// if (def.multiple) { +// builder.setValue("multiple", def.multiple) +// } +// +// if (!def.info.isEmpty()) { +// builder.setValue("info", def.info) +// } +// +// if (def.allowed.isNotEmpty()) { +// builder.setValue("allowedValues", def.allowed) +// } else if (def.enumeration != Any::class) { +// if (def.enumeration.java.isEnum) { +// val values = def.enumeration.java.enumConstants +// builder.setValue("allowedValues", values.map { it.toString() }) +// } else { +// throw RuntimeException("Only enumeration classes are allowed in 'enumeration' annotation property") +// } +// } +// +// if (def.def.isNotEmpty()) { +// builder.setValue("default", def.def) +// } else if (!def.required) { +// builder.setValue("required", def.required) +// } +// +// if (def.tags.isNotEmpty()) { +// builder.setValue("tags", def.tags) +// } +// return ValueDescriptor(builder) +// } +// +// /** +// * Build empty value descriptor +// */ +// fun empty(valueName: String): ValueDescriptor { +// val builder = MetaBuilder("value") +// .setValue("name", valueName) +// return ValueDescriptor(builder) +// } +// +// /** +// * Merge two separate value descriptors +// */ +// fun merge(primary: ValueDescriptor, secondary: ValueDescriptor): ValueDescriptor { +// return ValueDescriptor(Laminate(primary.meta, secondary.meta)) +// } + } +} diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt new file mode 100644 index 00000000..d65a0e79 --- /dev/null +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/descriptors/annotations.kt @@ -0,0 +1,149 @@ +/* + * Copyright 2018 Alexander Nozik. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package hep.dataforge.descriptors + +import hep.dataforge.values.ValueType +import kotlin.reflect.KClass + +@Target(AnnotationTarget.PROPERTY) +@MustBeDocumented +annotation class ValueDef( + val key: String, + val type: Array = arrayOf(ValueType.STRING), + val multiple: Boolean = false, + val def: String = "", + val info: String = "", + val required: Boolean = true, + val allowed: Array = emptyArray(), + val enumeration: KClass<*> = Any::class, + val tags: Array = emptyArray() +) + +@MustBeDocumented +annotation class NodeDef( + val key: String, + val info: String = "", + val multiple: Boolean = false, + val required: Boolean = false, + val tags: Array = emptyArray(), + /** + * A list of child value descriptors + */ + val values: Array = emptyArray(), + /** + * A target class for this node to describe + * @return + */ + val type: KClass<*> = Any::class, + /** + * The DataForge path to the resource containing the description. Following targets are supported: + * + * 1. resource + * 1. file + * 1. class + * 1. method + * 1. property + * + * + * Does not work if [type] is provided + * + * @return + */ + val descriptor: String = "" +) + +/** + * Description text for meta property, node or whole object + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Description(val value: String) + +/** + * Annotation for value property which states that lists are expected + */ +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Multiple + +/** + * Descriptor target + * The DataForge path to the resource containing the description. Following targets are supported: + * 1. resource + * 1. file + * 1. class + * 1. method + * 1. property + * + * + * Does not work if [type] is provided + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Descriptor(val value: String) + + +/** + * Aggregator class for descriptor nodes + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorNodes(vararg val nodes: NodeDef) + +/** + * Aggregator class for descriptor values + */ +@Target(AnnotationTarget.CLASS, AnnotationTarget.PROPERTY, AnnotationTarget.FUNCTION, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorValues(vararg val nodes: ValueDef) + +/** + * Alternative name for property descriptor declaration + */ +@Target(AnnotationTarget.PROPERTY, AnnotationTarget.VALUE_PARAMETER) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorName(val name: String) + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DescriptorValue(val def: ValueDef) +//TODO enter fields directly? + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class ValueProperty( + val name: String = "", + val type: Array = arrayOf(ValueType.STRING), + val multiple: Boolean = false, + val def: String = "", + val enumeration: KClass<*> = Any::class, + val tags: Array = emptyArray() +) + + +@Target(AnnotationTarget.PROPERTY) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class NodeProperty(val name: String = "") diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt index 79407337..1f9a3522 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Config.kt @@ -2,7 +2,7 @@ package hep.dataforge.meta import hep.dataforge.names.Name import hep.dataforge.names.NameToken -import hep.dataforge.names.toName +import hep.dataforge.names.asName //TODO add validator to configuration @@ -28,7 +28,7 @@ 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.toName()] = when (item) { + builder[entry.key.asName()] = when (item) { is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) 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 index 581fa7ed..e848ec31 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ConfigDelegates.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ConfigDelegates.kt @@ -10,33 +10,104 @@ import kotlin.jvm.JvmName /** * A property delegate that uses custom key */ -fun Configurable.value(default: Value = Null, key: String? = null) = - ValueConfigDelegate(config, key, default) +fun Configurable.value(default: Any = Null, key: String? = null) = + MutableValueDelegate(config, key, Value.of(default)) + +fun Configurable.value(default: T? = null, key: String? = null, transform: (Value?) -> T) = + MutableValueDelegate(config, key, Value.of(default)).transform(reader = transform) + +fun Configurable.stringList(key: String? = null) = + value(key) { it?.list?.map { value -> value.string } ?: emptyList() } + +fun Configurable.numberList(key: String? = null) = + value(key) { it?.list?.map { value -> value.number } ?: emptyList() } fun Configurable.string(default: String? = null, key: String? = null) = - StringConfigDelegate(config, key, default) + MutableStringDelegate(config, key, default) fun Configurable.boolean(default: Boolean? = null, key: String? = null) = - BooleanConfigDelegate(config, key, default) + MutableBooleanDelegate(config, key, default) fun Configurable.number(default: Number? = null, key: String? = null) = - NumberConfigDelegate(config, key, default) + MutableNumberDelegate(config, key, default) -fun Configurable.child(key: String? = null) = MetaNodeDelegate(config, key) +/* Number delegates*/ + +fun Configurable.int(default: Int? = null, key: String? = null) = + number(default, key).int + +fun Configurable.double(default: Double? = null, key: String? = null) = + number(default, key).double + +fun Configurable.long(default: Long? = null, key: String? = null) = + number(default, key).long + +fun Configurable.short(default: Short? = null, key: String? = null) = + number(default, key).short + +fun Configurable.float(default: Float? = null, key: String? = null) = + number(default, key).float -//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) + MutableSafeStringDelegate(config, key) { default } @JvmName("safeBoolean") fun Configurable.boolean(default: Boolean, key: String? = null) = - SafeBooleanConfigDelegate(config, key, default) + MutableSafeBooleanDelegate(config, key) { default } @JvmName("safeNumber") fun Configurable.number(default: Number, key: String? = null) = - SafeNumberConfigDelegate(config, key, default) + MutableSafeNumberDelegate(config, key) { default } +@JvmName("safeString") +fun Configurable.string(key: String? = null, default: () -> String) = + MutableSafeStringDelegate(config, key, default) + +@JvmName("safeBoolean") +fun Configurable.boolean(key: String? = null, default: () -> Boolean) = + MutableSafeBooleanDelegate(config, key, default) + +@JvmName("safeNumber") +fun Configurable.number(key: String? = null, default: () -> Number) = + MutableSafeNumberDelegate(config, key, default) + + +/* Safe number delegates*/ + +@JvmName("safeInt") +fun Configurable.int(default: Int, key: String? = null) = + number(default, key).int + +@JvmName("safeDouble") +fun Configurable.double(default: Double, key: String? = null) = + number(default, key).double + +@JvmName("safeLong") +fun Configurable.long(default: Long, key: String? = null) = + number(default, key).long + +@JvmName("safeShort") +fun Configurable.short(default: Short, key: String? = null) = + number(default, key).short + +@JvmName("safeFloat") +fun Configurable.float(default: Float, key: String? = null) = + number(default, key).float + +/** + * Enum delegate + */ inline fun > Configurable.enum(default: E, key: String? = null) = - SafeEnumvConfigDelegate(config, key, default) { enumValueOf(it) } \ No newline at end of file + MutableSafeEnumvDelegate(config, key, default) { enumValueOf(it) } + +/* Node delegates */ + +fun Configurable.node(key: String? = null) = MutableNodeDelegate(config, key) + +fun Configurable.spec(spec: Specification, key: String? = null) = + MutableMorphDelegate(config, key) { spec.wrap(it) } + +fun Configurable.spec(builder: (Config) -> T, key: String? = null) = + MutableMorphDelegate(config, key) { specification(builder).wrap(it) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt index 5672d2cf..377ec994 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Delegates.kt @@ -56,22 +56,40 @@ class DelegateWrapper(val delegate: ReadOnlyProperty, val reader: //Delegates with non-null values -class SafeStringDelegate(val meta: Meta, private val key: String? = null, private val default: String) : - ReadOnlyProperty { +class SafeStringDelegate( + val meta: Meta, + private val key: String? = null, + default: () -> String +) : ReadOnlyProperty { + + private val default: String by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): String { return meta[key ?: property.name]?.string ?: default } } -class SafeBooleanDelegate(val meta: Meta, private val key: String? = null, private val default: Boolean) : - ReadOnlyProperty { +class SafeBooleanDelegate( + val meta: Meta, + private val key: String? = null, + default: () -> Boolean +) : ReadOnlyProperty { + + private val default: Boolean by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { return meta[key ?: property.name]?.boolean ?: default } } -class SafeNumberDelegate(val meta: Meta, private val key: String? = null, private val default: Number) : - ReadOnlyProperty { +class SafeNumberDelegate( + val meta: Meta, + private val key: String? = null, + default: () -> Number +) : ReadOnlyProperty { + + private val default: Number by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): Number { return meta[key ?: property.name]?.number ?: default } @@ -118,96 +136,116 @@ fun Meta.number(default: Number? = null, key: String? = null) = NumberDelegate(t fun Meta.child(key: String? = null) = ChildDelegate(this, key) { it } @JvmName("safeString") -fun Meta.string(default: String, key: String? = null) = SafeStringDelegate(this, key, default) +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) +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) +fun Meta.number(default: Number, key: String? = null) = + SafeNumberDelegate(this, key) { default } + +@JvmName("safeString") +fun Meta.string(key: String? = null, default: () -> String) = + SafeStringDelegate(this, key, default) + +@JvmName("safeBoolean") +fun Meta.boolean(key: String? = null, default: () -> Boolean) = + SafeBooleanDelegate(this, key, default) + +@JvmName("safeNumber") +fun Meta.number(key: String? = null, default: () -> Number) = + SafeNumberDelegate(this, key, default) + inline fun > Meta.enum(default: E, key: String? = null) = SafeEnumDelegate(this, key, default) { enumValueOf(it) } -/* Config delegates */ +/* Read-write delegates */ -class ValueConfigDelegate>( - val config: M, +class MutableValueDelegate>( + val meta: 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 + return meta[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) + meta.remove(name) } else { - config.setValue(name, value) + meta.setValue(name, value) } } + + fun transform(writer: (T) -> Value? = { Value.of(it) }, reader: (Value?) -> T) = + ReadWriteDelegateWrapper(this, reader, writer) } -class StringConfigDelegate>( - val config: M, +class MutableStringDelegate>( + val meta: 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 + return meta[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) + meta.remove(name) } else { - config.setValue(name, value.asValue()) + meta.setValue(name, value.asValue()) } } } -class BooleanConfigDelegate>( - val config: M, +class MutableBooleanDelegate>( + val meta: 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 + return meta[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) + meta.remove(name) } else { - config.setValue(name, value.asValue()) + meta.setValue(name, value.asValue()) } } } -class NumberConfigDelegate>( - val config: M, +class MutableNumberDelegate>( + val meta: 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 + return meta[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) + meta.remove(name) } else { - config.setValue(name, value.asValue()) + meta.setValue(name, value.asValue()) } } val double get() = ReadWriteDelegateWrapper(this, reader = { it?.toDouble() }, writer = { it }) + val float get() = ReadWriteDelegateWrapper(this, reader = { it?.toFloat() }, writer = { it }) val int get() = ReadWriteDelegateWrapper(this, reader = { it?.toInt() }, writer = { it }) val short get() = ReadWriteDelegateWrapper(this, reader = { it?.toShort() }, writer = { it }) val long get() = ReadWriteDelegateWrapper(this, reader = { it?.toLong() }, writer = { it }) @@ -215,95 +253,108 @@ class NumberConfigDelegate>( //Delegates with non-null values -class SafeStringConfigDelegate>( - val config: M, +class MutableSafeStringDelegate>( + val meta: M, private val key: String? = null, - private val default: String + default: () -> String ) : ReadWriteProperty { + + private val default: String by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): String { - return config[key ?: property.name]?.string ?: default + return meta[key ?: property.name]?.string ?: default } override fun setValue(thisRef: Any?, property: KProperty<*>, value: String) { - config.setValue(key ?: property.name, value.asValue()) + meta.setValue(key ?: property.name, value.asValue()) } } -class SafeBooleanConfigDelegate>( - val config: M, +class MutableSafeBooleanDelegate>( + val meta: M, private val key: String? = null, - private val default: Boolean + default: () -> Boolean ) : ReadWriteProperty { + + private val default: Boolean by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): Boolean { - return config[key ?: property.name]?.boolean ?: default + return meta[key ?: property.name]?.boolean ?: default } override fun setValue(thisRef: Any?, property: KProperty<*>, value: Boolean) { - config.setValue(key ?: property.name, value.asValue()) + meta.setValue(key ?: property.name, value.asValue()) } } -class SafeNumberConfigDelegate>( - val config: M, +class MutableSafeNumberDelegate>( + val meta: M, private val key: String? = null, - private val default: Number + default: () -> Number ) : ReadWriteProperty { + + private val default: Number by lazy(default) + override fun getValue(thisRef: Any?, property: KProperty<*>): Number { - return config[key ?: property.name]?.number ?: default + return meta[key ?: property.name]?.number ?: default } override fun setValue(thisRef: Any?, property: KProperty<*>, value: Number) { - config.setValue(key ?: property.name, value.asValue()) + meta.setValue(key ?: property.name, value.asValue()) } val double get() = ReadWriteDelegateWrapper(this, reader = { it.toDouble() }, writer = { it }) + val float get() = ReadWriteDelegateWrapper(this, reader = { it.toFloat() }, writer = { it }) val int get() = ReadWriteDelegateWrapper(this, reader = { it.toInt() }, writer = { it }) val short get() = ReadWriteDelegateWrapper(this, reader = { it.toShort() }, writer = { it }) val long get() = ReadWriteDelegateWrapper(this, reader = { it.toLong() }, writer = { it }) } -class SafeEnumvConfigDelegate, E : Enum>( - val config: M, +class MutableSafeEnumvDelegate, E : Enum>( + val meta: 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 + return (meta[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()) + meta.setValue(key ?: property.name, value.name.asValue()) } } //Child node delegate -class MetaNodeDelegate>( - val config: M, +class MutableNodeDelegate>( + val meta: M, private val key: String? = null -) : ReadWriteProperty { - override fun getValue(thisRef: Any?, property: KProperty<*>): Meta { - return config[key ?: property.name]?.node ?: EmptyMeta +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): Meta? { + return meta[key ?: property.name]?.node } - override fun setValue(thisRef: Any?, property: KProperty<*>, value: Meta) { - config[key ?: property.name] = value + override fun setValue(thisRef: Any?, property: KProperty<*>, value: Meta?) { + meta[key ?: property.name] = value } } -class ChildConfigDelegate, T : Configurable>( - val config: M, +class MutableMorphDelegate, T : Configurable>( + val meta: 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) +) : ReadWriteProperty { + override fun getValue(thisRef: Any?, property: KProperty<*>): T? { + return meta[key ?: property.name]?.node?.let(converter) } - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { - config[key ?: property.name] = value.config + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T?) { + if (value == null) { + meta.remove(key ?: property.name) + } else { + meta[key ?: property.name] = value.config + } } } @@ -328,32 +379,43 @@ class ReadWriteDelegateWrapper( * A property delegate that uses custom key */ fun > M.value(default: Value = Null, key: String? = null) = - ValueConfigDelegate(this, key, default) + MutableValueDelegate(this, key, default) fun > M.string(default: String? = null, key: String? = null) = - StringConfigDelegate(this, key, default) + MutableStringDelegate(this, key, default) fun > M.boolean(default: Boolean? = null, key: String? = null) = - BooleanConfigDelegate(this, key, default) + MutableBooleanDelegate(this, key, default) fun > M.number(default: Number? = null, key: String? = null) = - NumberConfigDelegate(this, key, default) + MutableNumberDelegate(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) } +fun > M.node(key: String? = null) = MutableNodeDelegate(this, key) @JvmName("safeString") fun > M.string(default: String, key: String? = null) = - SafeStringConfigDelegate(this, key, default) + MutableSafeStringDelegate(this, key) { default } @JvmName("safeBoolean") fun > M.boolean(default: Boolean, key: String? = null) = - SafeBooleanConfigDelegate(this, key, default) + MutableSafeBooleanDelegate(this, key) { default } @JvmName("safeNumber") fun > M.number(default: Number, key: String? = null) = - SafeNumberConfigDelegate(this, key, default) + MutableSafeNumberDelegate(this, key) { default } + +@JvmName("safeString") +fun > M.string(key: String? = null, default: () -> String) = + MutableSafeStringDelegate(this, key, default) + +@JvmName("safeBoolean") +fun > M.boolean(key: String? = null, default: () -> Boolean) = + MutableSafeBooleanDelegate(this, key, default) + +@JvmName("safeNumber") +fun > M.number(key: String? = null, default: () -> Number) = + MutableSafeNumberDelegate(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 + MutableSafeEnumvDelegate(this, key, default) { enumValueOf(it) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt index d0eaa54e..6e24c6d0 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/ExtraMetaDelegates.kt @@ -33,4 +33,4 @@ fun Configurable.stringList(vararg default: String = emptyArray(), key: String? 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) + MutableMorphDelegate(config, key, converter) diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt index 08b1ba09..e62c3621 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Meta.kt @@ -3,10 +3,7 @@ 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.names.* import hep.dataforge.values.EnumValue import hep.dataforge.values.Value import hep.dataforge.values.boolean @@ -76,30 +73,37 @@ 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() + val root = when (name.length) { + 0 -> error("Can't use empty name for that") + 1 -> this + else -> (this[name.cutLast()] as? NodeItem<*>)?.node + } + val (body, index) = name.last()!! + val regex = index.toRegex() + + return root?.items + ?.filter { it.key.body == body && (index.isEmpty() || regex.matches(it.key.index)) } + ?.mapKeys { it.key.index } + ?: emptyMap() } +fun Meta.getAll(name: String): Map> = getAll(name.toName()) + /** - * Transform meta to sequence of [Name]-[Value] pairs + * Get a sequence of [Name]-[Value] pairs */ -fun Meta.asValueSequence(): Sequence> { +fun Meta.values(): 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 } + is ValueItem -> sequenceOf(entry.key.asName() to item.value) + is NodeItem -> item.node.values().map { pair -> (entry.key.asName() + pair.first) to pair.second } } } } -operator fun Meta.iterator(): Iterator> = asValueSequence().iterator() +operator fun Meta.iterator(): Iterator> = values().iterator() /** * A meta node that ensures that all of its descendants has at least the same type @@ -108,6 +112,27 @@ interface MetaNode> : Meta { override val items: Map> } +/** + * Get all items matching given name. + */ +fun > MetaNode.getAll(name: Name): Map> { + val root: MetaNode? = when (name.length) { + 0 -> error("Can't use empty name for that") + 1 -> this + else -> (this[name.cutLast()] as? NodeItem)?.node + } + + val (body, index) = name.last()!! + val regex = index.toRegex() + + return root?.items + ?.filter { it.key.body == body && (index.isEmpty() || regex.matches(it.key.index)) } + ?.mapKeys { it.key.index } + ?: emptyMap() +} + +fun > M.getAll(name: String): Map> = getAll(name.toName()) + operator fun > MetaNode.get(name: Name): MetaItem? { return name.first()?.let { token -> val tail = name.cutFirst() @@ -118,7 +143,7 @@ operator fun > MetaNode.get(name: Name): MetaItem? { } } -operator fun > MetaNode.get(key: String): MetaItem? = get(key.toName()) +operator fun > MetaNode?.get(key: String): MetaItem? = this?.let { get(key.toName()) } /** * Equals and hash code implementation for meta node diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt index 61310d67..fe6829d4 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MetaBuilder.kt @@ -1,7 +1,7 @@ package hep.dataforge.meta import hep.dataforge.names.Name -import hep.dataforge.names.toName +import hep.dataforge.names.asName import hep.dataforge.values.Value /** @@ -38,7 +38,7 @@ fun Meta.builder(): MetaBuilder { return MetaBuilder().also { builder -> items.mapValues { entry -> val item = entry.value - builder[entry.key.toName()] = when (item) { + builder[entry.key.asName()] = when (item) { is MetaItem.ValueItem -> MetaItem.ValueItem(item.value) is MetaItem.NodeItem -> MetaItem.NodeItem(item.node.builder()) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt index 788758fa..8182495b 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/MutableMeta.kt @@ -1,9 +1,6 @@ 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.names.* import hep.dataforge.values.Value internal data class MetaListener( @@ -62,7 +59,7 @@ abstract class MutableMetaNode> : AbstractMetaNode(), } } } - itemChanged(key.toName(), oldItem, newItem) + itemChanged(key.asName(), oldItem, newItem) } /** @@ -103,7 +100,7 @@ fun > MutableMeta.setItem(name: String, item: MetaItem) 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 > MutableMeta.setItem(token: NameToken, item: MetaItem?) = set(token.asName(), item) fun > MutableMetaNode.setNode(name: Name, node: Meta) = set(name, MetaItem.NodeItem(wrap(name, node))) @@ -121,10 +118,13 @@ operator fun > M.set(name: Name, value: Any?) { is MetaItem.NodeItem<*> -> setNode(name, value.node) } is Meta -> setNode(name, value) + is Specific -> setNode(name, value.config) else -> setValue(name, Value.of(value)) } } +operator fun > M.set(name: NameToken, value: Any?) = set(name.asName(), value) + operator fun > M.set(key: String, value: Any?) = set(key.toName(), value) /** @@ -137,9 +137,9 @@ 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) } + is MetaItem.ValueItem -> setValue(entry.key.asName(), value.value) + is MetaItem.NodeItem -> (this[entry.key.asName()] as? MetaItem.NodeItem)?.node?.update(value.node) + ?: run { setNode(entry.key.asName(), value.node) } } } } @@ -149,12 +149,12 @@ fun > M.update(meta: Meta) { fun > M.setIndexed( name: Name, items: Iterable>, - queryFactory: (Int) -> String = { it.toString() } + indexFactory: MetaItem.(index: 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)) + val indexedToken = NameToken(last.body, last.index + meta.indexFactory(index)) tokens[tokens.lastIndex] = indexedToken set(Name(tokens), meta) } @@ -163,10 +163,26 @@ fun > M.setIndexed( fun > M.setIndexed( name: Name, metas: Iterable, - queryFactory: (Int) -> String = { it.toString() } + indexFactory: MetaItem.(index: Int) -> String = { it.toString() } ) { - setIndexed(name, metas.map { MetaItem.NodeItem(wrap(name, it)) }, queryFactory) + setIndexed(name, metas.map { MetaItem.NodeItem(wrap(name, it)) }, indexFactory) } operator fun > M.set(name: Name, metas: Iterable) = setIndexed(name, metas) operator fun > M.set(name: String, metas: Iterable) = setIndexed(name.toName(), metas) + +/** + * Append the node with a same-name-sibling, automatically generating numerical index + */ +fun > M.append(name: Name, value: Any?) { + require(!name.isEmpty()) { "Name could not be empty for append operation" } + val newIndex = name.last()!!.index + if (newIndex.isNotEmpty()) { + set(name, value) + } else { + val index = (getAll(name).keys.mapNotNull { it.toIntOrNull() }.max() ?: -1) + 1 + set(name.withIndex(index.toString()), value) + } +} + +fun > M.append(name: String, value: Any?) = append(name.toName(), value) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt similarity index 57% rename from dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt rename to dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt index f1e2beaf..10210fb4 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specification.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Specific.kt @@ -3,16 +3,16 @@ package hep.dataforge.meta /** * Marker interface for specifications */ -interface Specification : Configurable { +interface Specific : 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 + * By convention [Specific] companion should inherit this class * */ -interface SpecificationCompanion { +interface Specification { /** * Update given configuration using given type as a builder */ @@ -22,41 +22,41 @@ interface SpecificationCompanion { fun build(action: T.() -> Unit) = update(Config(), action) + fun empty() = build { } + /** * 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 { +fun specification(wrapper: (Config) -> T): Specification = + object : Specification { override fun wrap(config: Config): T = wrapper(config) } /** * Apply specified configuration to configurable */ -fun > T.configure(spec: S, action: C.() -> Unit) = +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) = +fun > Specific.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 = +fun > S.createStyle(action: C.() -> Unit): Meta = Config().also { update(it, action) } -fun , C : Specification> Specification.spec( - spec: SpecificationCompanion, +fun Specific.spec( + spec: Specification, key: String? = null -) = - ChildConfigDelegate(config, key) { spec.wrap(config) } \ No newline at end of file +) = MutableMorphDelegate(config, key) { spec.wrap(it) } \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt index ba802f5f..dcc36a5a 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/meta/Styled.kt @@ -48,7 +48,7 @@ class Styled(val base: Meta, val style: Config = Config().empty()) : MutableMeta } } -fun Styled.configure(meta: Meta) = apply { style.update(style) } +fun Styled.configure(meta: Meta) = apply { style.update(meta) } fun Meta.withStyle(style: Meta = EmptyMeta) = if (this is Styled) { this.apply { this.configure(style) } diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt index 78dba871..d8513e9c 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/names/Name.kt @@ -4,7 +4,7 @@ package hep.dataforge.names /** * 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. + * Each token could contain additional index in square brackets. */ inline class Name constructor(val tokens: List) { @@ -43,24 +43,25 @@ inline class Name constructor(val tokens: List) { /** * A single name token. Body is not allowed to be empty. * Following symbols are prohibited in name tokens: `{}.:\`. - * A name token could have appendix in square brackets called *query* + * A name token could have appendix in square brackets called *index* */ -data class NameToken(val body: String, val query: String = "") { +data class NameToken(val body: String, val index: String = "") { init { if (body.isEmpty()) error("Syntax error: Name token body is empty") } - override fun toString(): String = if (hasQuery()) { - "$body[$query]" + override fun toString(): String = if (hasIndex()) { + "$body[$index]" } else { body } - fun hasQuery() = query.isNotEmpty() + fun hasIndex() = index.isNotEmpty() } fun String.toName(): Name { + if (isBlank()) return EmptyName val tokens = sequence { var bodyBuilder = StringBuilder() var queryBuilder = StringBuilder() @@ -84,7 +85,7 @@ fun String.toName(): Name { '[' -> bracketCount++ ']' -> error("Syntax error: closing bracket ] not have not matching open bracket") else -> { - if (queryBuilder.isNotEmpty()) error("Syntax error: only name end and name separator are allowed after query") + if (queryBuilder.isNotEmpty()) error("Syntax error: only name end and name separator are allowed after index") bodyBuilder.append(it) } } @@ -101,8 +102,24 @@ 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)) +fun Name.appendLeft(other: String): Name = NameToken(other) + this + +fun NameToken.asName() = Name(listOf(this)) val EmptyName = Name(emptyList()) -fun Name.isEmpty(): Boolean = this.length == 0 \ No newline at end of file +fun Name.isEmpty(): Boolean = this.length == 0 + +/** + * Set or replace last token index + */ +fun Name.withIndex(index: String): Name { + val tokens = ArrayList(tokens) + val last = NameToken(tokens.last().body, index) + tokens.removeAt(tokens.size - 1) + tokens.add(last) + return Name(tokens) +} + +operator fun Map.get(name: String) = get(name.toName()) +operator fun MutableMap.set(name: String, value: T) = set(name.toName(), value) \ No newline at end of file diff --git a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt index 2f92bd60..20f86779 100644 --- a/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt +++ b/dataforge-meta/src/commonMain/kotlin/hep/dataforge/values/Value.kt @@ -55,8 +55,15 @@ interface Value { is Value -> value true -> True false -> False - is Number -> NumberValue(value) + is Number -> value.asValue() is Iterable<*> -> ListValue(value.map { of(it) }) + is DoubleArray -> value.asValue() + is IntArray -> value.asValue() + is FloatArray -> value.asValue() + is ShortArray -> value.asValue() + is LongArray -> value.asValue() + is ByteArray -> value.asValue() + is Array<*> -> ListValue(value.map { of(it) }) is Enum<*> -> EnumValue(value) is CharSequence -> StringValue(value.toString()) else -> throw IllegalArgumentException("Unrecognized type of the object (${value::class}) converted to Value") @@ -171,6 +178,18 @@ class ListValue(override val list: List) : Value { override val string: String get() = list.first().string override fun toString(): String = value.toString() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Value) return false + return list == other.list + } + + override fun hashCode(): Int { + return list.hashCode() + } + + } /** @@ -185,7 +204,20 @@ fun Boolean.asValue(): Value = if (this) True else False fun String.asValue(): Value = StringValue(this) -fun Collection.asValue(): Value = ListValue(this.toList()) +fun Iterable.asValue(): Value = ListValue(this.toList()) + +//TODO maybe optimized storage performance +fun DoubleArray.asValue(): Value = ListValue(map{NumberValue(it)}) + +fun IntArray.asValue(): Value = ListValue(map{NumberValue(it)}) + +fun LongArray.asValue(): Value = ListValue(map{NumberValue(it)}) + +fun ShortArray.asValue(): Value = ListValue(map{NumberValue(it)}) + +fun FloatArray.asValue(): Value = ListValue(map{NumberValue(it)}) + +fun ByteArray.asValue(): Value = ListValue(map{NumberValue(it)}) /** diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt index 3f34882c..849a34fc 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaBuilderTest.kt @@ -23,4 +23,18 @@ class MetaBuilderTest { assertEquals(true, meta["node.childNode.f"]?.boolean) } + @Test + fun testSNS(){ + val meta = buildMeta { + repeat(10){ + "b.a[$it]" to it + } + }.seal() + assertEquals(10, meta.values().count()) + + val nodes = meta.getAll("b.a") + + assertEquals(3, nodes["3"]?.int) + } + } \ No newline at end of file diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt index fbd93e62..997a13e3 100644 --- a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/MetaDelegateTest.kt @@ -12,17 +12,30 @@ class MetaDelegateTest { @Test fun delegateTest() { - val testObject = object : Specification { + + class InnerSpec(override val config: Config) : Specific { + var innerValue by string() + } + + val innerSpec = specification(::InnerSpec) + + val testObject = object : Specific { override val config: Config = Config() - var myValue by config.string() - var safeValue by config.number(2.2) - var enumValue by config.enum(TestEnum.YES) + var myValue by string() + var safeValue by double(2.2) + var enumValue by enum(TestEnum.YES) + var inner by spec(innerSpec) } testObject.config["myValue"] = "theString" testObject.enumValue = TestEnum.NO + + testObject.inner = innerSpec.build { innerValue = "ddd"} + assertEquals("theString", testObject.myValue) assertEquals(TestEnum.NO, testObject.enumValue) assertEquals(2.2, testObject.safeValue) + assertEquals("ddd", testObject.inner?.innerValue) + } } \ No newline at end of file diff --git a/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt new file mode 100644 index 00000000..5a4699f9 --- /dev/null +++ b/dataforge-meta/src/commonTest/kotlin/hep/dataforge/meta/StyledTest.kt @@ -0,0 +1,29 @@ +package hep.dataforge.meta + +import kotlin.test.Test +import kotlin.test.assertEquals + + +class StyledTest{ + @Test + fun testSNS(){ + val meta = buildMeta { + repeat(10){ + "b.a[$it]" to { + "d" to it + } + } + }.seal().withStyle() + assertEquals(10, meta.values().count()) + + val bNode = meta["b"].node + + val aNodes = bNode?.getAll("a") + + val allNodes = meta.getAll("b.a") + + assertEquals(3, aNodes?.get("3").node["d"].int) + assertEquals(3, allNodes["3"].node["d"].int) + } + +} \ No newline at end of file diff --git a/dataforge-meta/src/jsMain/kotlin/hep/dataforge/meta/DynamicMeta.kt b/dataforge-meta/src/jsMain/kotlin/hep/dataforge/meta/DynamicMeta.kt new file mode 100644 index 00000000..4719831b --- /dev/null +++ b/dataforge-meta/src/jsMain/kotlin/hep/dataforge/meta/DynamicMeta.kt @@ -0,0 +1,63 @@ +package hep.dataforge.meta + +import hep.dataforge.names.NameToken +import hep.dataforge.values.Null +import hep.dataforge.values.Value + + +//TODO add Meta wrapper for dynamic + +/** + * Represent or copy this [Meta] to dynamic object to be passed to JS libraries + */ +fun Meta.toDynamic(): dynamic { + if(this is DynamicMeta) return this.obj + + fun MetaItem<*>.toDynamic(): dynamic = when (this) { + is MetaItem.ValueItem -> this.value.value.asDynamic() + is MetaItem.NodeItem -> this.node.toDynamic() + } + + val res = js("{}") + this.items.entries.groupBy { it.key.body }.forEach { (key, value) -> + val list = value.map { it.value } + res[key] = when (list.size) { + 1 -> list.first().toDynamic() + else -> list.map { it.toDynamic() } + } + } + return res +} + +class DynamicMeta(val obj: dynamic) : Meta { + private fun keys() = js("Object.keys(this.obj)") as Array + + private fun isArray(@Suppress("UNUSED_PARAMETER") obj: dynamic): Boolean = + js("Array.isArray(obj)") as Boolean + + private fun asItem(obj: dynamic): MetaItem? { + if (obj == null) return MetaItem.ValueItem(Null) + return when (jsTypeOf(obj)) { + "boolean" -> MetaItem.ValueItem(Value.of(obj as Boolean)) + "number" -> MetaItem.ValueItem(Value.of(obj as Number)) + "string" -> MetaItem.ValueItem(Value.of(obj as String)) + "object" -> MetaItem.NodeItem(DynamicMeta(obj)) + else -> null + } + } + + override val items: Map> + get() = keys().flatMap>> { key -> + val value = obj[key] ?: return@flatMap emptyList() + if (isArray(value)) { + return@flatMap (value as Array) + .mapIndexedNotNull() { index, it -> + val item = asItem(it) ?: return@mapIndexedNotNull null + NameToken(key, index.toString()) to item + } + } else { + val item = asItem(value) ?: return@flatMap emptyList() + listOf(NameToken(key) to item) + } + }.associate { it } +} \ No newline at end of file diff --git a/dataforge-meta/src/jsTest/kotlin/hep/dataforge/meta/DynamicMetaTest.kt b/dataforge-meta/src/jsTest/kotlin/hep/dataforge/meta/DynamicMetaTest.kt new file mode 100644 index 00000000..56033171 --- /dev/null +++ b/dataforge-meta/src/jsTest/kotlin/hep/dataforge/meta/DynamicMetaTest.kt @@ -0,0 +1,24 @@ +package hep.dataforge.meta + +import kotlin.test.Test +import kotlin.test.assertEquals + + +class DynamicMetaTest { + + @Test + fun testDynamicMeta() { + val d = js("{}") + d.a = 22 + d.array = arrayOf(1,2,3) + d.b = "myString" + d.ob = js("{}") + d.ob.childNode = 18 + d.ob.booleanNode = true + + val meta = DynamicMeta(d) + assertEquals(true, meta["ob.booleanNode"].boolean) + assertEquals(2,meta["array[1]"].int) + } + +} \ No newline at end of file diff --git a/dataforge-output/build.gradle.kts b/dataforge-output/build.gradle.kts new file mode 100644 index 00000000..36811267 --- /dev/null +++ b/dataforge-output/build.gradle.kts @@ -0,0 +1,16 @@ +plugins { + `npm-multiplatform` +} + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting{ + dependencies { + api(project(":dataforge-context")) + api(project(":dataforge-io")) + } + } + } +} \ No newline at end of file diff --git a/dataforge-output/dataforge-output-html/build.gradle.kts b/dataforge-output/dataforge-output-html/build.gradle.kts new file mode 100644 index 00000000..d1693bab --- /dev/null +++ b/dataforge-output/dataforge-output-html/build.gradle.kts @@ -0,0 +1,28 @@ +plugins { + `npm-multiplatform` +} + +val htmlVersion by rootProject.extra("0.6.12") + +kotlin { + jvm() + js() + sourceSets { + val commonMain by getting { + dependencies { + api(project(":dataforge-output")) + api("org.jetbrains.kotlinx:kotlinx-html-common:$htmlVersion") + } + } + val jsMain by getting { + dependencies { + api("org.jetbrains.kotlinx:kotlinx-html-js:$htmlVersion") + } + } + val jvmMain by getting{ + dependencies { + api("org.jetbrains.kotlinx:kotlinx-html-jvm:$htmlVersion") + } + } + } +} \ No newline at end of file diff --git a/dataforge-output/dataforge-output-html/src/jvmMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt b/dataforge-output/dataforge-output-html/src/jvmMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt new file mode 100644 index 00000000..bfbd74eb --- /dev/null +++ b/dataforge-output/dataforge-output-html/src/jvmMain/kotlin/hep/dataforge/output/html/HtmlOutput.kt @@ -0,0 +1,77 @@ +package hep.dataforge.output.html + +import hep.dataforge.context.Context +import hep.dataforge.meta.Meta +import hep.dataforge.output.Output +import hep.dataforge.output.TextRenderer +import hep.dataforge.output.html.HtmlBuilder.Companion.HTML_CONVERTER_TYPE +import hep.dataforge.provider.Type +import hep.dataforge.provider.top +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.html.TagConsumer +import kotlinx.html.p +import kotlin.reflect.KClass + + +class HtmlOutput(override val context: Context, private val consumer: TagConsumer<*>) : Output { + private val cache = HashMap, HtmlBuilder<*>>() + + /** + * Find the first [TextRenderer] matching the given object type. + */ + override fun render(obj: T, meta: Meta) { + + val builder: HtmlBuilder<*> = if (obj is CharSequence) { + DefaultHtmlBuilder + } else { + val value = cache[obj::class] + if (value == null) { + val answer = context.top>().values + .filter { it.type.isInstance(obj) }.firstOrNull() + if (answer != null) { + cache[obj::class] = answer + answer + } else { + DefaultHtmlBuilder + } + } else { + value + } + } + context.launch(Dispatchers.Output) { + (builder as HtmlBuilder).run { consumer.render(obj) } + } + } +} + +/** + * A text or binary renderer based on [kotlinx.io.core.Output] + */ +@Type(HTML_CONVERTER_TYPE) +interface HtmlBuilder { + /** + * 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 TagConsumer<*>.render(obj: T) + + companion object { + const val HTML_CONVERTER_TYPE = "dataforge.htmlBuilder" + } +} + +object DefaultHtmlBuilder : HtmlBuilder { + override val priority: Int = Int.MAX_VALUE + override val type: KClass = Any::class + + override suspend fun TagConsumer<*>.render(obj: Any) { + p { +obj.toString() } + } +} \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt similarity index 95% rename from dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt index e9d1800c..091cb999 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/Output.kt +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/Output.kt @@ -1,4 +1,4 @@ -package hep.dataforge.io +package hep.dataforge.output import hep.dataforge.context.ContextAware import hep.dataforge.meta.EmptyMeta diff --git a/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt new file mode 100644 index 00000000..47d9f59c --- /dev/null +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/OutputManager.kt @@ -0,0 +1,77 @@ +package hep.dataforge.output + +import hep.dataforge.context.* +import hep.dataforge.context.PluginTag.Companion.DATAFORGE_GROUP +import hep.dataforge.meta.EmptyMeta +import hep.dataforge.meta.Meta +import hep.dataforge.names.EmptyName +import hep.dataforge.names.Name +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlin.reflect.KClass + +/** + * A manager for outputs + */ +interface OutputManager : Plugin { + + /** + * Get an output specialized for given type, name and stage. + * @param stage represents the node or directory for the output. Empty means root node. + * @param name represents the name inside the node. + * @param meta configuration for [Output] (not for rendered object) + */ + operator fun get( + type: KClass, + name: Name, + stage: Name = EmptyName, + meta: Meta = EmptyMeta + ): Output +} + +/** + * Get an output manager for a context + */ +val Context.output: OutputManager get() = plugins.get() ?: ConsoleOutputManager() + +/** + * Get an output with given [name], [stage] and reified content type + */ +inline operator fun OutputManager.get( + name: Name, + stage: Name = EmptyName, + meta: Meta = EmptyMeta +): Output { + return get(T::class, name, stage, meta) +} + +/** + * Directly render an object using the most suitable renderer + */ +fun OutputManager.render(obj: Any, name: Name, stage: Name = EmptyName, meta: Meta = EmptyMeta) = + get(obj::class, name, stage).render(obj, meta) + +/** + * System console output. + * The [ConsoleOutput] is used when no other [OutputManager] is provided. + */ +expect val ConsoleOutput: Output + +class ConsoleOutputManager : AbstractPlugin(), OutputManager { + override val tag: PluginTag get() = ConsoleOutputManager.tag + + override fun get(type: KClass, name: Name, stage: Name, meta: Meta): Output = ConsoleOutput + + companion object : PluginFactory { + override val tag = PluginTag("output.console", group = DATAFORGE_GROUP) + + override val type = ConsoleOutputManager::class + + override fun invoke(meta:Meta) = ConsoleOutputManager() + } +} + +/** + * A dispatcher for output tasks. + */ +expect val Dispatchers.Output: CoroutineDispatcher \ No newline at end of file diff --git a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt similarity index 79% rename from dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt rename to dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt index 446eed31..91aa5024 100644 --- a/dataforge-io/src/commonMain/kotlin/hep/dataforge/io/TextOutput.kt +++ b/dataforge-output/src/commonMain/kotlin/hep/dataforge/output/TextOutput.kt @@ -1,10 +1,11 @@ -package hep.dataforge.io +package hep.dataforge.output import hep.dataforge.context.Context -import hep.dataforge.io.TextRenderer.Companion.TEXT_RENDERER_TYPE import hep.dataforge.meta.Meta +import hep.dataforge.output.TextRenderer.Companion.TEXT_RENDERER_TYPE import hep.dataforge.provider.Type -import hep.dataforge.provider.provideAll +import hep.dataforge.provider.top +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlin.reflect.KClass @@ -20,8 +21,8 @@ class TextOutput(override val context: Context, private val output: kotlinx.io.c } else { val value = cache[obj::class] if (value == null) { - val answer = context.provideAll(TEXT_RENDERER_TYPE).filterIsInstance() - .filter { it.type.isInstance(obj) }.firstOrNull() + val answer = + context.top(TEXT_RENDERER_TYPE).values.firstOrNull { it.type.isInstance(obj) } if (answer != null) { cache[obj::class] = answer answer @@ -32,12 +33,15 @@ class TextOutput(override val context: Context, private val output: kotlinx.io.c value } } - context.launch(OutputDispatcher) { + context.launch(Dispatchers.Output) { renderer.run { output.render(obj) } } } } +/** + * A text or binary renderer based on [kotlinx.io.core.Output] + */ @Type(TEXT_RENDERER_TYPE) interface TextRenderer { /** diff --git a/dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt similarity index 82% rename from dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt rename to dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt index 3aed2000..b927a386 100644 --- a/dataforge-io/src/jsMain/kotlin/hep/dataforge/io/ConsoleOutput.kt +++ b/dataforge-output/src/jsMain/kotlin/hep/dataforge/output/ConsoleOutput.kt @@ -1,4 +1,4 @@ -package hep.dataforge.io +package hep.dataforge.output import hep.dataforge.context.Context import hep.dataforge.context.Global @@ -19,4 +19,4 @@ actual val ConsoleOutput: Output = object : Output { } -actual val OutputDispatcher: CoroutineDispatcher = Dispatchers.Default \ No newline at end of file +actual val Dispatchers.Output: CoroutineDispatcher get() = Dispatchers.Default \ No newline at end of file diff --git a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt similarity index 78% rename from dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt rename to dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt index b4f28887..57ae4294 100644 --- a/dataforge-io/src/jvmMain/kotlin/hep/dataforge/io/ConsoleOutput.kt +++ b/dataforge-output/src/jvmMain/kotlin/hep/dataforge/output/ConsoleOutput.kt @@ -1,4 +1,4 @@ -package hep.dataforge.io +package hep.dataforge.output import hep.dataforge.context.Global import kotlinx.coroutines.Dispatchers @@ -10,4 +10,4 @@ import kotlinx.io.streams.asOutput */ actual val ConsoleOutput: Output = TextOutput(Global, System.out.asOutput()) -actual val OutputDispatcher = Dispatchers.IO \ No newline at end of file +actual val Dispatchers.Output get() = Dispatchers.IO \ No newline at end of file diff --git a/dataforge-scripting/build.gradle.kts b/dataforge-scripting/build.gradle.kts index 674ec25f..eb8f7742 100644 --- a/dataforge-scripting/build.gradle.kts +++ b/dataforge-scripting/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } kotlin { diff --git a/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt b/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt index ee1af3df..31f3a508 100644 --- a/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt +++ b/dataforge-scripting/src/jvmMain/kotlin/hep/dataforge/scripting/Builders.kt @@ -2,6 +2,7 @@ package hep.dataforge.scripting import hep.dataforge.context.Context import hep.dataforge.context.Global +import hep.dataforge.workspace.SimpleWorkspaceBuilder import hep.dataforge.workspace.Workspace import hep.dataforge.workspace.WorkspaceBuilder import java.io.File @@ -14,11 +15,12 @@ import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost object Builders { fun buildWorkspace(source: SourceCode, context: Context = Global): Workspace { - val builder = WorkspaceBuilder(context) + val builder = SimpleWorkspaceBuilder(context) val workspaceScriptConfiguration = ScriptCompilationConfiguration { baseClass(Any::class) implicitReceivers(WorkspaceBuilder::class) + defaultImports("hep.dataforge.workspace.*") jvm { dependenciesFromCurrentContext(wholeClasspath = true) } @@ -31,8 +33,10 @@ object Builders { BasicJvmScriptingHost().eval(source, workspaceScriptConfiguration, evaluationConfiguration).onFailure { it.reports.forEach { scriptDiagnostic -> when (scriptDiagnostic.severity) { - ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> + ScriptDiagnostic.Severity.FATAL, ScriptDiagnostic.Severity.ERROR -> { context.logger.error(scriptDiagnostic.exception) { scriptDiagnostic.toString() } + error(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() } diff --git a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt index ef58de78..bcc357d4 100644 --- a/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt +++ b/dataforge-scripting/src/jvmTest/kotlin/hep/dataforge/scripting/BuildersKtTest.kt @@ -1,20 +1,33 @@ package hep.dataforge.scripting +import hep.dataforge.context.Global import hep.dataforge.meta.get import hep.dataforge.meta.int +import hep.dataforge.workspace.* import org.junit.Test import kotlin.test.assertEquals class BuildersKtTest { + @Test + fun checkBuilder(){ + val workspace = SimpleWorkspaceBuilder(Global).apply { + println("I am working") + + context("test") + + target("testTarget"){ + "a" to 12 + } + } + } + @Test fun testWorkspaceBuilder() { val script = """ println("I am working") - context{ - name = "test" - } + context("test") target("testTarget"){ "a" to 12 diff --git a/dataforge-workspace/build.gradle.kts b/dataforge-workspace/build.gradle.kts index cb2ce82c..e6aa9dd0 100644 --- a/dataforge-workspace/build.gradle.kts +++ b/dataforge-workspace/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("multiplatform") + `npm-multiplatform` } kotlin { diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt index 1f0f64f4..24c012ea 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Dependency.kt @@ -2,13 +2,13 @@ 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.get import hep.dataforge.names.isEmpty /** @@ -18,31 +18,51 @@ 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() { +class DataDependency(val filter: DataFilter, 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) } + val result = workspace.data.filter(filter) return if (placement.isEmpty()) { result } else { - DataTreeBuilder().apply { this[placement] = result }.build() + DataNode.build(Any::class) { this[placement] = result } } } override fun toMeta(): Meta = buildMeta { - "name" to name + "data" to filter.config + "to" to placement + } +} + +class AllDataDependency(val placement: Name = EmptyName) : Dependency() { + override fun apply(workspace: Workspace): DataNode = if (placement.isEmpty()) { + workspace.data + } else { + DataNode.build(Any::class) { this[placement] = workspace.data } + } + + override fun toMeta() = buildMeta { + "data" to "*" + "to" to placement + } + +} + +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 = workspace.run(task, meta) + return if (placement.isEmpty()) { + result + } else { + DataNode.build(Any::class) { this[placement] = result } + } + } + + override fun toMeta(): Meta = buildMeta { + "task" to name "meta" to meta + "to" to placement } } \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt new file mode 100644 index 00000000..278098d1 --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/GenericTask.kt @@ -0,0 +1,61 @@ +package hep.dataforge.workspace + +import hep.dataforge.data.* +import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.node +import kotlin.reflect.KClass + +//data class TaskEnv(val workspace: Workspace, val model: TaskModel) + + +class GenericTask( + override val name: String, + override val type: KClass, + override val descriptor: NodeDescriptor, + private val modelTransform: TaskModelBuilder.(Meta) -> Unit, + private val dataTransform: Workspace.() -> TaskModel.(DataNode) -> DataNode +) : Task { + + private fun gather(workspace: Workspace, model: TaskModel): DataNode { + return DataNode.build(Any::class) { + model.dependencies.forEach { dep -> + update(dep.apply(workspace)) + } + } + } + + override fun run(workspace: Workspace, model: TaskModel): DataNode { + //validate model + validate(model) + + // gather data + val input = gather(workspace, model) + + //execute + workspace.context.logger.info{"Starting task '$name' on ${model.target} with meta: \n${model.meta}"} + val output = dataTransform(workspace).invoke(model, input) + + //handle result + //output.handle(model.context.dispatcher) { this.handle(it) } + + return output + } + + /** + * Build new TaskModel and apply specific model transformation for this + * task. By default model uses the meta node with the same node as the name of the task. + * + * @param workspace + * @param taskConfig + * @return + */ + override fun build(workspace: Workspace, taskConfig: Meta): TaskModel { + val taskMeta = taskConfig[name]?.node ?: taskConfig + val builder = TaskModelBuilder(name, taskMeta) + modelTransform.invoke(builder, taskMeta) + return builder.build() + } + //TODO add validation +} \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/SimpleWorkspace.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/SimpleWorkspace.kt new file mode 100644 index 00000000..c7a54f3d --- /dev/null +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/SimpleWorkspace.kt @@ -0,0 +1,29 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.context.Global +import hep.dataforge.data.DataNode +import hep.dataforge.meta.Meta +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import hep.dataforge.provider.top + + +/** + * A simple workspace without caching + */ +class SimpleWorkspace( + override val context: Context, + override val data: DataNode, + override val targets: Map, + tasks: Collection> +) : Workspace { + override val tasks: Map> by lazy { + context.top>(Task.TYPE) + tasks.associate { it.name.toName() to it } + } + + companion object { + fun build(parent: Context = Global, block: SimpleWorkspaceBuilder.() -> Unit): SimpleWorkspace = + SimpleWorkspaceBuilder(parent).apply(block).build() + } +} \ 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 index bb6d33f3..0b11a7d5 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Task.kt @@ -2,13 +2,14 @@ package hep.dataforge.workspace import hep.dataforge.context.Named import hep.dataforge.data.DataNode +import hep.dataforge.descriptors.Described 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 { +interface Task : Named, Described { /** * Terminal task is the one that could not build model lazily */ @@ -41,10 +42,11 @@ interface Task : Named { * Run given task model. Type check expected to be performed before actual * calculation. * - * @param model + * @param workspace - a workspace to run task model in + * @param model - a model to be executed * @return */ - fun run(model: TaskModel): DataNode + fun run(workspace: Workspace, model: TaskModel): DataNode companion object { const val TYPE = "task" diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt index 8d0de0c0..eaaac235 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/TaskModel.kt @@ -12,6 +12,7 @@ import hep.dataforge.meta.* import hep.dataforge.names.EmptyName import hep.dataforge.names.Name import hep.dataforge.names.toName +import hep.dataforge.workspace.TaskModel.Companion.MODEL_TARGET_KEY /** @@ -39,23 +40,31 @@ data class TaskModel( //TODO ensure all dependencies are listed } } + + companion object { + const val MODEL_TARGET_KEY = "@target" + } } /** * Build input for the task */ fun TaskModel.buildInput(workspace: Workspace): DataTree { - return DataTreeBuilder().apply { - dependencies.asSequence().flatMap { it.apply(workspace).dataSequence() }.forEach { (name, data) -> + return DataTreeBuilder(Any::class).apply { + dependencies.asSequence().flatMap { it.apply(workspace).data() }.forEach { (name, data) -> //TODO add concise error on replacement this[name] = data } }.build() } +@DslMarker +annotation class TaskBuildScope + /** * A builder for [TaskModel] */ +@TaskBuildScope class TaskModelBuilder(val name: String, meta: Meta = EmptyMeta) { /** * Meta for current task. By default uses the whole input meta @@ -63,13 +72,21 @@ class TaskModelBuilder(val name: String, meta: Meta = EmptyMeta) { var meta: MetaBuilder = meta.builder() val dependencies = HashSet() + var target: String by this.meta.string(key = MODEL_TARGET_KEY, default = "") + /** * Add dependency for */ - fun dependsOn(name: String, meta: Meta, placement: Name = EmptyName) { + fun dependsOn(name: String, meta: Meta = this.meta, placement: Name = EmptyName) { dependencies.add(TaskModelDependency(name, meta, placement)) } + fun dependsOn(task: Task<*>, meta: Meta = this.meta, placement: Name = EmptyName) = + dependsOn(task.name, meta, placement) + + fun dependsOn(task: Task<*>, placement: Name = EmptyName, metaBuilder: MetaBuilder.() -> Unit) = + dependsOn(task.name, buildMeta(metaBuilder), placement) + /** * Add custom data dependency */ @@ -89,9 +106,12 @@ class TaskModelBuilder(val name: String, meta: Meta = EmptyMeta) { /** * Add all data as root node */ - fun allData() { - dependencies.add(DataDependency.all) + fun allData(to: Name = EmptyName) { + dependencies.add(AllDataDependency(to)) } fun build(): TaskModel = TaskModel(name, meta.seal(), dependencies) } + + +val TaskModel.target get() = meta[MODEL_TARGET_KEY]?.string ?: "" \ No newline at end of file diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt index b0b1cf4d..1945c74e 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/Workspace.kt @@ -1,11 +1,11 @@ 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.meta.MetaBuilder +import hep.dataforge.meta.buildMeta import hep.dataforge.names.Name import hep.dataforge.names.toName import hep.dataforge.provider.Provider @@ -27,62 +27,68 @@ interface Workspace : ContextAware, Provider { /** * All tasks associated with the workspace */ - val tasks: Map> + 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()] + Task.TYPE -> tasks[name] Data.TYPE -> data[name] DataNode.TYPE -> data.getNode(name) else -> null } } - override fun listTop(target: String): Sequence { + override fun listNames(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 } + Task.TYPE -> tasks.keys.asSequence().map { it } + Data.TYPE -> data.data().map { it.first } + DataNode.TYPE -> data.nodes().map { it.first } else -> emptySequence() } } - operator fun Task.invoke(config: Meta): DataNode { + + /** + * Invoke a task in the workspace utilizing caching if possible + */ + fun run(task: Task, config: Meta): DataNode { context.activate(this) try { - val model = build(this@Workspace, config) - validate(model) - return run(model) + val model = task.build(this, config) + task.validate(model) + return task.run(this, 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) - } +// /** +// * 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}") +// context.logger.info { "Running ${this.name} on $target" } +// 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 } - } - +fun Workspace.run(task: Task<*>, target: String): DataNode { + val meta = targets[target] ?: error("A target with name $target not found in ${this}") + return run(task, meta) } + +fun Workspace.run(task: String, target: String) = + tasks[task.toName()]?.let { run(it, target) } ?: error("Task with name $task not found") + +fun Workspace.run(task: String, meta: Meta) = + tasks[task.toName()]?.let { run(it, meta) } ?: error("Task with name $task not found") + +fun Workspace.run(task: String, block: MetaBuilder.() -> Unit = {}) = + run(task, buildMeta(block)) diff --git a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt index 9aebdcd0..f62753a2 100644 --- a/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt +++ b/dataforge-workspace/src/commonMain/kotlin/hep/dataforge/workspace/WorkspaceBuilder.kt @@ -2,39 +2,91 @@ package hep.dataforge.workspace import hep.dataforge.context.Context import hep.dataforge.context.ContextBuilder +import hep.dataforge.data.Data +import hep.dataforge.data.DataNode import hep.dataforge.data.DataTreeBuilder -import hep.dataforge.meta.Meta -import hep.dataforge.meta.MetaBuilder -import hep.dataforge.meta.buildMeta +import hep.dataforge.meta.* +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.GlobalScope + +@TaskBuildScope +interface WorkspaceBuilder { + val parentContext: Context + var context: Context + var data: DataTreeBuilder + var tasks: MutableSet> + var targets: MutableMap + + fun build(): Workspace +} + /** - * A builder for a workspace + * Set the context for future workspcace */ -class WorkspaceBuilder(var context: Context) { - val data = DataTreeBuilder() - val targets = HashMap() - val tasks = HashSet>() +fun WorkspaceBuilder.context(name: String, block: ContextBuilder.() -> Unit = {}) { + context = ContextBuilder(name, parentContext).apply(block).build() +} - fun context(action: ContextBuilder.() -> Unit) { - this.context = ContextBuilder().apply(action).build() +fun WorkspaceBuilder.data(name: Name, data: Data) { + this.data[name] = data +} + +fun WorkspaceBuilder.data(name: String, data: Data) = data(name.toName(), data) + +fun WorkspaceBuilder.static(name: Name, data: Any, scope: CoroutineScope = GlobalScope, meta: Meta = EmptyMeta) = + data(name, Data.static(scope, data, meta)) + +fun WorkspaceBuilder.static(name: Name, data: Any, scope: CoroutineScope = GlobalScope, block: MetaBuilder.() -> Unit = {}) = + data(name, Data.static(scope, data, buildMeta(block))) + +fun WorkspaceBuilder.static(name: String, data: Any, scope: CoroutineScope = GlobalScope, block: MetaBuilder.() -> Unit = {}) = + data(name, Data.static(scope, data, buildMeta(block))) + +fun WorkspaceBuilder.data(name: Name, node: DataNode) { + this.data[name] = node +} + +fun WorkspaceBuilder.data(name: String, node: DataNode) = data(name.toName(), node) + +fun WorkspaceBuilder.data(name: Name, block: DataTreeBuilder.() -> Unit) { + this.data[name] = DataNode.build(Any::class, block) +} + +fun WorkspaceBuilder.data(name: String, block: DataTreeBuilder.() -> Unit) = data(name.toName(), block) + +fun WorkspaceBuilder.target(name: String, block: MetaBuilder.() -> Unit) { + targets[name] = buildMeta(block).seal() +} + +/** + * Use existing target as a base updating it with the block + */ +fun WorkspaceBuilder.target(name: String, base: String, block: MetaBuilder.() -> Unit) { + val parentTarget = targets[base] ?: error("Base target with name $base not found") + targets[name] = parentTarget.builder() + .apply { "@baseTarget" to base } + .apply(block) + .seal() +} + +fun WorkspaceBuilder.task(task: Task) { + this.tasks.add(task) +} + + +/** + * A builder for a simple workspace + */ +class SimpleWorkspaceBuilder(override val parentContext: Context) : WorkspaceBuilder { + override var context: Context = parentContext + override var data = DataTreeBuilder(Any::class) + override var tasks: MutableSet> = HashSet() + override var targets: MutableMap = HashMap() + + override fun build(): SimpleWorkspace { + return SimpleWorkspace(context, data.build(), targets, tasks) } - - 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/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt new file mode 100644 index 00000000..761dc8f3 --- /dev/null +++ b/dataforge-workspace/src/jvmMain/kotlin/hep/dataforge/workspace/TaskBuilder.kt @@ -0,0 +1,218 @@ +package hep.dataforge.workspace + +import hep.dataforge.context.Context +import hep.dataforge.data.* +import hep.dataforge.descriptors.NodeDescriptor +import hep.dataforge.meta.Meta +import hep.dataforge.meta.get +import hep.dataforge.meta.string +import hep.dataforge.names.Name +import hep.dataforge.names.toName +import kotlin.reflect.KClass + +@TaskBuildScope +class TaskBuilder(val name: String) { + private var modelTransform: TaskModelBuilder.(Meta) -> Unit = { data("*") } + var descriptor: NodeDescriptor? = null + + /** + * TODO will look better as extension class + */ + private class DataTransformation( + val from: String = "", + val to: String = "", + val transform: (Context, TaskModel, DataNode) -> DataNode + ) { + operator fun invoke(workspace: Workspace, model: TaskModel, node: DataNode): DataNode? { + val localData = if (from.isEmpty()) { + node + } else { + node.getNode(from.toName()) ?: return null + } + return transform(workspace.context, model, localData) + } + } + + private val dataTransforms: MutableList = ArrayList(); + + fun model(modelTransform: TaskModelBuilder.(Meta) -> Unit) { + this.modelTransform = modelTransform + } + + fun transform( + inputType: KClass, + from: String = "", + to: String = "", + block: TaskModel.(Context, DataNode) -> DataNode + ) { + dataTransforms += DataTransformation(from, to) { context, model, data -> + block(model, context, data.cast(inputType)) + } + } + + inline fun transform( + from: String = "", + to: String = "", + noinline block: TaskModel.(Context, DataNode) -> DataNode + ) { + transform(T::class, from, to, block) + } + + /** + * Perform given action on data elements in `from` node in input and put the result to `to` node + */ + inline fun action( + from: String = "", + to: String = "", + crossinline block: Context.() -> Action + ) { + transform(from, to) { context, data: DataNode -> + block(context).invoke(data, meta) + } + } + + class TaskEnv(val name: Name, val meta: Meta, val context: Context) + + /** + * A customized pipe action with ability to change meta and name + */ + inline fun customPipe( + from: String = "", + to: String = "", + crossinline block: PipeBuilder.(Context) -> Unit + ) { + action(from, to) { + val context = this + PipeAction( + inputType = T::class, + outputType = R::class + ) { block(context) } + } + } + + /** + * A simple pipe action without changing meta or name + */ + inline fun pipe( + from: String = "", + to: String = "", + crossinline block: suspend TaskEnv.(T) -> R + ) { + action(from, to) { + val context = this + PipeAction( + inputType = T::class, + outputType = R::class + ) { + //TODO automatically append task meta + result = { data -> + TaskEnv(name, meta, context).block(data) + } + } + } + } + + /** + * Join elements in gathered data by multiple groups + */ + inline fun joinByGroup( + from: String = "", + to: String = "", + crossinline block: JoinGroupBuilder.(Context) -> Unit + ) { + action(from, to) { + JoinAction( + inputType = T::class, + outputType = R::class + ) { block(this@action) } + } + } + + /** + * Join all elemlents in gathered data matching input type + */ + inline fun join( + from: String = "", + to: String = "", + crossinline block: suspend TaskEnv.(Map) -> R + ) { + action(from, to) { + val context = this + JoinAction( + inputType = T::class, + outputType = R::class, + action = { + result( + actionMeta[TaskModel.MODEL_TARGET_KEY]?.string ?: "@anonymous" + ) { data -> + TaskEnv(name, meta, context).block(data) + } + } + ) + } + } + + /** + * Split each element in gathered data into fixed number of fragments + */ + inline fun split( + from: String = "", + to: String = "", + crossinline block: SplitBuilder.(Context) -> Unit + ) { + action(from, to) { + SplitAction( + inputType = T::class, + outputType = R::class + ) { block(this@action) } + } + } + + /** + * Use DSL to create a descriptor for this task + */ + fun description(transform: NodeDescriptor.() -> Unit) { + this.descriptor = NodeDescriptor.build(transform) + } + + internal fun build(): GenericTask = + GenericTask( + name, + Any::class, + descriptor ?: NodeDescriptor.empty(), + modelTransform + ) { + val workspace = this + { data -> + val model = this + if (dataTransforms.isEmpty()) { + //return data node as is + logger.warn("No transformation present, returning input data") + data + } else { + val builder = DataTreeBuilder(Any::class) + dataTransforms.forEach { transformation -> + val res = transformation(workspace, model, data) + if (res != null) { + if (transformation.to.isEmpty()) { + builder.update(res) + } else { + builder[transformation.to.toName()] = res + } + } + } + builder.build() + } + } + } +} + +fun task(name: String, builder: TaskBuilder.() -> Unit): GenericTask { + return TaskBuilder(name).apply(builder).build() +} + +fun WorkspaceBuilder.task(name: String, builder: TaskBuilder.() -> Unit) { + task(TaskBuilder(name).apply(builder).build()) +} + +//TODO add delegates to build gradle-like tasks \ No newline at end of file diff --git a/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt new file mode 100644 index 00000000..52ba85b5 --- /dev/null +++ b/dataforge-workspace/src/jvmTest/kotlin/hep/dataforge/workspace/SimpleWorkspaceTest.kt @@ -0,0 +1,73 @@ +package hep.dataforge.workspace + +import hep.dataforge.data.first +import hep.dataforge.data.get +import org.junit.Test +import kotlin.test.assertEquals + + +class SimpleWorkspaceTest { + val workspace = SimpleWorkspace.build { + + repeat(100) { + static("myData[$it]", it) + } + + + task("square") { + model { + allData() + } + pipe { data -> + context.logger.info { "Starting square on $data" } + data * data + } + } + + task("sum") { + model { + dependsOn("square") + } + join { data -> + context.logger.info { "Starting sum" } + data.values.sum() + } + } + + task("average") { + model { + allData() + } + joinByGroup { context -> + group("even", filter = { name, data -> name.toString().toInt() % 2 == 0 }) { + result { data -> + context.logger.info { "Starting even" } + data.values.average() + } + } + group("odd", filter = { name, data -> name.toString().toInt() % 2 == 1 }) { + result { data -> + context.logger.info { "Starting odd" } + data.values.average() + } + } + } + } + + task("delta"){ + model{ + dependsOn("average") + } + join {data-> + data["even"]!! - data["odd"]!! + } + } + } + + @Test + fun testWorkspace() { + val node = workspace.run("sum") + val res = node.first() + assertEquals(328350, res.get()) + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index c7ab404e..03b9bff9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,13 +18,14 @@ pluginManagement { enableFeaturePreview("GRADLE_METADATA") -rootProject.name = "dataforge-core" +//rootProject.name = "dataforge-core" include( ":dataforge-meta", - ":dataforge-meta-io", + ":dataforge-io", ":dataforge-context", ":dataforge-data", - ":dataforge-io", + ":dataforge-output", + ":dataforge-output:dataforge-output-html", ":dataforge-workspace", ":dataforge-scripting" ) \ No newline at end of file