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