Merge pull request #3 from altavir/dev

0.1.1-dev
This commit is contained in:
Alexander Nozik 2019-03-16 07:28:25 +01:00 committed by GitHub
commit df8a5509d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 4439 additions and 793 deletions

6
.gitignore vendored
View File

@ -1,10 +1,10 @@
.idea/
*.iws
out/
*/out/**
.gradle
/build/
*/build/**
!gradle-wrapper.jar
gradle.properties

0
README.md Normal file
View File

View File

@ -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
View 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())
}
}
}
}
}

View 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")
}
}
}
}

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}

View File

@ -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

View File

@ -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"
}
}

View File

@ -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)
}
}

View File

@ -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 }

View File

@ -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)
}
}
}
}

View File

@ -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))

View File

@ -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") }
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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 }
// }
//}
//

View File

@ -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()
}

View File

@ -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])

View 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")
}
}
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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))

View File

@ -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.}

View 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() })
}

View File

@ -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() }

View 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"))
}
}
}
}

View File

@ -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)
}

View File

@ -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

View File

@ -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')
}
}

View File

@ -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

View File

@ -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

View 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 {
// }
}
}

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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()))
}

View File

@ -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)
}
}

View File

@ -0,0 +1,2 @@
hep.dataforge.meta.io.BinaryMetaFormatFactory
hep.dataforge.meta.io.JsonMetaFormatFactory

View File

@ -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"
}

View File

@ -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"

View 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"
// }
//}

View File

@ -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())
}
}
}

View File

@ -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) }

View File

@ -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) }

View File

@ -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)

View File

@ -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

View 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()

View File

@ -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)

View File

@ -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)

View File

@ -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) }

View File

@ -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
}
}

View File

@ -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))
fun NameToken.toName() = Name(listOf(this))
val EmptyName = Name(emptyList())
fun Name.isEmpty(): Boolean = this.length == 0

View File

@ -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
*/
@ -178,4 +221,17 @@ fun String.parseValue(): Value {
//Give up and return a StringValue
return StringValue(this)
}
class LazyParsedValue(override val string: String) : Value {
private val parsedValue by lazy { string.parseValue() }
override val value: Any?
get() = parsedValue.value
override val type: ValueType
get() = parsedValue.type
override val number: Number
get() = parsedValue.number
override fun toString(): String = value.toString()
}

View File

@ -1,15 +1,16 @@
package hep.dataforge.meta
import hep.dataforge.values.asValue
import kotlin.test.Test
import kotlin.test.assertEquals
class MetaBuilderTest{
class MetaBuilderTest {
@Test
fun testBuilder(){
fun testBuilder() {
val meta = buildMeta {
"a" to 22
"b" to listOf(1,2,3)
"b" to listOf(1, 2, 3)
this["c"] = "myValue".asValue()
"node" to {
"e" to 12.2

View File

@ -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

View File

@ -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)
}
}

View File

@ -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)
}
}

View 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")
}
}
}
}

View File

@ -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())
}

View File

@ -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)
}
}

View 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")
}
}
}
}

View 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"))
}
}
}
}

View File

@ -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
}
}

View File

@ -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"
}
}

View File

@ -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)
}

View File

@ -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 }
}
}

View File

@ -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
View 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
View 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

Binary file not shown.

View 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
View 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
View 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

View File

@ -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
View 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"
)

View File

@ -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) }

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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
}
}
}
}
}

View File

@ -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) }

View File

@ -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
}
}

View File

@ -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())
}
}