Split ItemDescriptor to builder and read-only descriptor

This commit is contained in:
Alexander Nozik 2021-04-11 11:15:25 +03:00
parent 1a983665f8
commit 2352f1cff1
12 changed files with 396 additions and 241 deletions

View File

@ -5,6 +5,7 @@
- LogManager plugin
- dataforge-context API dependency on SLF4j
- Context `withEnv` and `fetch` methods to manipulate plugins without changing plugins after creation.
- Split `ItemDescriptor` into builder and read-only part
### Changed
- Kotlin-logging moved from common to JVM and JS. Replaced by console for native.

View File

@ -4,7 +4,7 @@ plugins {
allprojects {
group = "space.kscience"
version = "0.4.0-dev-7"
version = "0.4.0-dev-8"
}
subprojects {
@ -22,5 +22,8 @@ ksciencePublish {
}
apiValidation {
if(project.version.toString().contains("dev")) {
validationDisabled = true
}
nonPublicMarkers.add("space.kscience.dataforge.misc.DFExperimental")
}

View File

@ -8,6 +8,7 @@ import space.kscience.dataforge.meta.transformations.nullableItemToObject
import space.kscience.dataforge.meta.transformations.nullableObjectToMetaItem
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.startsWith
@DFExperimental
public class ConfigProperty<T : Any>(
@ -24,7 +25,7 @@ public class ConfigProperty<T : Any>(
override fun onChange(owner: Any?, callback: (T?) -> Unit) {
config.onChange(owner) { name, oldItem, newItem ->
if (name == this.name && oldItem != newItem) callback(converter.nullableItemToObject(newItem))
if (name.startsWith(this.name) && oldItem != newItem) callback(converter.nullableItemToObject(newItem))
}
}

View File

@ -2,6 +2,7 @@ package space.kscience.dataforge.properties
import space.kscience.dataforge.meta.ItemPropertyProvider
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.startsWith
import space.kscience.dataforge.names.toName
import kotlin.reflect.KMutableProperty1
@ -16,7 +17,7 @@ public fun <P : ItemPropertyProvider, T : Any> P.property(property: KMutableProp
override fun onChange(owner: Any?, callback: (T?) -> Unit) {
this@property.onChange(this) { name, oldItem, newItem ->
if (name == property.name.toName() && oldItem != newItem) {
if (name.startsWith(property.name.toName()) && oldItem != newItem) {
callback(property.get(this@property))
}
}

View File

@ -98,6 +98,12 @@ public fun Meta.toConfig(): Config = Config().also { builder ->
}
}
/**
* Create a copy of this config, optionally applying the given [block].
* The listeners of the original Config are not retained.
*/
public inline fun Config.copy(block: Config.() -> Unit = {}): Config = toConfig().apply(block)
/**
* Return this [Meta] as [Config] if it is [Config] and create a new copy otherwise
*/

View File

@ -2,6 +2,7 @@ package space.kscience.dataforge.meta
import space.kscience.dataforge.misc.DFExperimental
import space.kscience.dataforge.names.Name
import space.kscience.dataforge.names.startsWith
import space.kscience.dataforge.names.toName
import kotlin.reflect.KProperty1
@ -34,7 +35,7 @@ public fun <O : ObservableItemProvider, T> O.useProperty(
//Pass initial value.
callBack(property.get(this))
onChange(owner) { name, oldItem, newItem ->
if (name == property.name.toName() && oldItem != newItem) {
if (name.startsWith(property.name.toName()) && oldItem != newItem) {
callBack(property.get(this))
}
}

View File

@ -3,28 +3,26 @@ package space.kscience.dataforge.meta.descriptors
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFBuilder
import space.kscience.dataforge.names.*
import space.kscience.dataforge.values.*
/**
* A common parent for [ValueDescriptor] and [NodeDescriptor]. Describes a single [TypedMetaItem] or a group of same-name-siblings.
*/
@DFBuilder
public sealed class ItemDescriptor(final override val config: Config) : Configurable {
public sealed interface ItemDescriptor: MetaRepr {
/**
* True if same name siblings with this name are allowed
*/
public var multiple: Boolean by config.boolean(false)
public val multiple: Boolean
/**
* The item description text
*/
public var info: String? by config.string()
public val info: String?
/**
* True if the item is required
*/
public abstract var required: Boolean
public val required: Boolean
/**
@ -32,14 +30,56 @@ public sealed class ItemDescriptor(final override val config: Config) : Configur
*
* @return
*/
public var attributes: Config? by config.node()
public val attributes: Meta?
/**
* An index field by which this node is identified in case of same name siblings construct
*/
public var indexKey: String by config.string(DEFAULT_INDEX_KEY)
public val indexKey: String
public abstract fun copy(): ItemDescriptor
public companion object {
public const val DEFAULT_INDEX_KEY: String = "@index"
}
}
/**
* The builder for [ItemDescriptor]
*/
@DFBuilder
public sealed class ItemDescriptorBuilder(final override val config: Config) : Configurable, ItemDescriptor {
/**
* True if same name siblings with this name are allowed
*/
override var multiple: Boolean by config.boolean(false)
/**
* The item description text
*/
override var info: String? by config.string()
/**
* True if the item is required
*/
abstract override var required: Boolean
/**
* Additional attributes of an item. For example validation and widget parameters
*
* @return
*/
override var attributes: Config? by config.node()
/**
* An index field by which this node is identified in case of same name siblings construct
*/
override var indexKey: String by config.string(DEFAULT_INDEX_KEY)
public abstract fun build(): ItemDescriptor
override fun toMeta(): Meta = config
public companion object {
public const val DEFAULT_INDEX_KEY: String = "@index"
@ -49,7 +89,7 @@ public sealed class ItemDescriptor(final override val config: Config) : Configur
/**
* Configure attributes of the descriptor, creating an attributes node if needed.
*/
public inline fun ItemDescriptor.attributes(block: Config.() -> Unit) {
public inline fun ItemDescriptorBuilder.attributes(block: Config.() -> Unit) {
(attributes ?: Config().also { this.attributes = it }).apply(block)
}
@ -66,140 +106,6 @@ public fun ItemDescriptor.validateItem(item: MetaItem?): Boolean {
}
}
/**
* Descriptor for meta node. Could contain additional information for viewing
* and editing.
*
* @author Alexander Nozik
*/
@DFBuilder
public class NodeDescriptor(config: Config = Config()) : ItemDescriptor(config) {
init {
config[IS_NODE_KEY] = true
}
/**
* True if the node is required
*
* @return
*/
override var required: Boolean by config.boolean { default == null }
/**
* The default for this node. Null if there is no default.
*
* @return
*/
public var default: Config? by config.node()
/**
* The map of children item descriptors (both nodes and values)
*/
public val items: Map<String, ItemDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.associate { (name, item) ->
if (name == null) error("Child item index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
if (node[IS_NODE_KEY].boolean == true) {
name to NodeDescriptor(node as Config)
} else {
name to ValueDescriptor(node as Config)
}
}
/**
* The map of children node descriptors
*/
@Suppress("UNCHECKED_CAST")
public val nodes: Map<String, NodeDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.filter {
it.value.node[IS_NODE_KEY].boolean == true
}.associate { (name, item) ->
if (name == null) error("Child node index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
name to NodeDescriptor(node as Config)
}
/**
* The list of children value descriptors
*/
public val values: Map<String, ValueDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.filter {
it.value.node[IS_NODE_KEY].boolean != true
}.associate { (name, item) ->
if (name == null) error("Child value index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
name to ValueDescriptor(node as Config)
}
private fun buildNode(name: Name): NodeDescriptor {
return when (name.length) {
0 -> this
1 -> {
val token = NameToken(ITEM_KEY.toString(), name.toString())
val config: Config = config[token].node ?: Config().also {
it[IS_NODE_KEY] = true
config[token] = it
}
NodeDescriptor(config)
}
else -> buildNode(name.firstOrNull()?.asName()!!).buildNode(name.cutFirst())
}
}
/**
* Define a child item descriptor for this node
*/
private fun newItem(key: String, descriptor: ItemDescriptor) {
if (items.keys.contains(key)) error("The key $key already exists in descriptor")
val token = ITEM_KEY.withIndex(key)
config[token] = descriptor.config
}
public fun item(name: Name, descriptor: ItemDescriptor) {
buildNode(name.cutLast()).newItem(name.lastOrNull().toString(), descriptor)
}
public fun item(name: String, descriptor: ItemDescriptor) {
item(name.toName(), descriptor)
}
/**
* Create and configure a child node descriptor
*/
public fun node(name: Name, block: NodeDescriptor.() -> Unit) {
item(name, NodeDescriptor().apply(block))
}
public fun node(name: String, block: NodeDescriptor.() -> Unit) {
node(name.toName(), block)
}
/**
* Create and configure child value descriptor
*/
public fun value(name: Name, block: ValueDescriptor.() -> Unit) {
require(name.length >= 1) { "Name length for value descriptor must be non-empty" }
item(name, ValueDescriptor().apply(block))
}
public fun value(name: String, block: ValueDescriptor.() -> Unit) {
value(name.toName(), block)
}
override fun copy(): NodeDescriptor = NodeDescriptor(config.toConfig())
public companion object {
internal val ITEM_KEY: Name = "item".asName()
internal val IS_NODE_KEY: Name = "@isNode".asName()
//TODO infer descriptor from spec
}
}
public inline fun NodeDescriptor(block: NodeDescriptor.() -> Unit): NodeDescriptor =
NodeDescriptor().apply(block)
/**
* Get a descriptor item associated with given name or null if item for given name not provided
*/
@ -213,93 +119,3 @@ public operator fun ItemDescriptor.get(name: Name): ItemDescriptor? {
public operator fun ItemDescriptor.get(name: String): ItemDescriptor? = get(name.toName())
/**
* A descriptor for meta value
*
* Descriptor can have non-atomic path. It is resolved when descriptor is added to the node
*
* @author Alexander Nozik
*/
@DFBuilder
public class ValueDescriptor(config: Config = Config()) : ItemDescriptor(config) {
/**
* True if the value is required
*
* @return
*/
override var required: Boolean by config.boolean { default == null }
/**
* The default for this value. Null if there is no default.
*
* @return
*/
public var default: Value? by config.value()
public fun default(v: Any) {
this.default = Value.of(v)
}
/**
* A list of allowed ValueTypes. Empty if any value type allowed
*
* @return
*/
public var type: List<ValueType>? by config.listValue { ValueType.valueOf(it.string) }
public fun type(vararg t: ValueType) {
this.type = listOf(*t)
}
/**
* Check if given value is allowed for here. The type should be allowed and
* if it is value should be within allowed values
*
* @param value
* @return
*/
public fun isAllowedValue(value: Value): Boolean {
return (type?.let { it.contains(ValueType.STRING) || it.contains(value.type) } ?: true)
&& (allowedValues.isEmpty() || allowedValues.contains(value))
}
/**
* A list of allowed values with descriptions. If empty than any value is
* allowed.
*
* @return
*/
public var allowedValues: List<Value> by config.item().convert(
reader = {
val value = it.value
when {
value?.list != null -> value.list
type?.let { type -> type.size == 1 && type[0] === ValueType.BOOLEAN } ?: false -> listOf(True, False)
else -> emptyList()
}
},
writer = {
MetaItemValue(it.asValue())
}
)
/**
* Allow given list of value and forbid others
*/
public fun allow(vararg v: Any) {
this.allowedValues = v.map { Value.of(it) }
}
override fun copy(): ValueDescriptor = ValueDescriptor(config.toConfig())
}
/**
* Merge two node descriptors into one using first one as primary
*/
public operator fun NodeDescriptor.plus(other: NodeDescriptor): NodeDescriptor {
return NodeDescriptor().apply {
config.update(other.config)
config.update(this@plus.config)
}
}

View File

@ -0,0 +1,191 @@
package space.kscience.dataforge.meta.descriptors
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFBuilder
import space.kscience.dataforge.names.*
/**
* Descriptor for meta node. Could contain additional information for viewing
* and editing.
*
* @author Alexander Nozik
*/
@DFBuilder
public sealed interface NodeDescriptor: ItemDescriptor {
/**
* True if the node is required
*
* @return
*/
override val required: Boolean
/**
* The default for this node. Null if there is no default.
*
* @return
*/
public val default: Config?
/**
* The map of children item descriptors (both nodes and values)
*/
public val items: Map<String, ItemDescriptor>
/**
* The map of children node descriptors
*/
public val nodes: Map<String, NodeDescriptor>
/**
* The list of children value descriptors
*/
public val values: Map<String, ValueDescriptor>
public companion object {
internal val ITEM_KEY: Name = "item".asName()
internal val IS_NODE_KEY: Name = "@isNode".asName()
//TODO infer descriptor from spec
}
}
@DFBuilder
public class NodeDescriptorBuilder(config: Config = Config()) : ItemDescriptorBuilder(config), NodeDescriptor {
init {
config[IS_NODE_KEY] = true
}
/**
* True if the node is required
*
* @return
*/
override var required: Boolean by config.boolean { default == null }
/**
* The default for this node. Null if there is no default.
*
* @return
*/
override var default: Config? by config.node()
/**
* The map of children item descriptors (both nodes and values)
*/
override val items: Map<String, ItemDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.associate { (name, item) ->
if (name == null) error("Child item index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
if (node[IS_NODE_KEY].boolean == true) {
name to NodeDescriptorBuilder(node as Config)
} else {
name to ValueDescriptorBuilder(node as Config)
}
}
/**
* The map of children node descriptors
*/
@Suppress("UNCHECKED_CAST")
override val nodes: Map<String, NodeDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.filter {
it.value.node[IS_NODE_KEY].boolean == true
}.associate { (name, item) ->
if (name == null) error("Child node index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
name to NodeDescriptorBuilder(node as Config)
}
/**
* The list of children value descriptors
*/
override val values: Map<String, ValueDescriptor>
get() = config.getIndexed(ITEM_KEY).entries.filter {
it.value.node[IS_NODE_KEY].boolean != true
}.associate { (name, item) ->
if (name == null) error("Child value index should not be null")
val node = item.node ?: error("Node descriptor must be a node")
name to ValueDescriptorBuilder(node as Config)
}
private fun buildNode(name: Name): NodeDescriptorBuilder {
return when (name.length) {
0 -> this
1 -> {
val token = NameToken(ITEM_KEY.toString(), name.toString())
val config: Config = config[token].node ?: Config().also {
it[IS_NODE_KEY] = true
config[token] = it
}
NodeDescriptorBuilder(config)
}
else -> buildNode(name.firstOrNull()?.asName()!!).buildNode(name.cutFirst())
}
}
/**
* Define a child item descriptor for this node
*/
private fun newItem(key: String, descriptor: ItemDescriptor) {
if (items.keys.contains(key)) error("The key $key already exists in descriptor")
val token = ITEM_KEY.withIndex(key)
config[token] = descriptor.toMeta()
}
public fun item(name: Name, descriptor: ItemDescriptor) {
buildNode(name.cutLast()).newItem(name.lastOrNull().toString(), descriptor)
}
public fun item(name: String, descriptor: ItemDescriptor) {
item(name.toName(), descriptor)
}
/**
* Create and configure a child node descriptor
*/
public fun node(name: Name, block: NodeDescriptorBuilder.() -> Unit) {
item(name, NodeDescriptorBuilder().apply(block))
}
public fun node(name: String, block: NodeDescriptorBuilder.() -> Unit) {
node(name.toName(), block)
}
/**
* Create and configure child value descriptor
*/
public fun value(name: Name, block: ValueDescriptorBuilder.() -> Unit) {
require(name.length >= 1) { "Name length for value descriptor must be non-empty" }
item(name, ValueDescriptorBuilder().apply(block))
}
public fun value(name: String, block: ValueDescriptorBuilder.() -> Unit) {
value(name.toName(), block)
}
override fun build(): NodeDescriptor = NodeDescriptorBuilder(config.copy())
public companion object {
internal val ITEM_KEY: Name = "item".asName()
internal val IS_NODE_KEY: Name = "@isNode".asName()
//TODO infer descriptor from spec
}
}
public inline fun NodeDescriptor(block: NodeDescriptorBuilder.() -> Unit): NodeDescriptor =
NodeDescriptorBuilder().apply(block)
/**
* Merge two node descriptors into one using first one as primary
*/
public operator fun NodeDescriptor.plus(other: NodeDescriptor): NodeDescriptor {
return NodeDescriptorBuilder().apply {
config.update(other.toMeta())
config.update(this@plus.toMeta())
}
}

View File

@ -0,0 +1,135 @@
package space.kscience.dataforge.meta.descriptors
import space.kscience.dataforge.meta.*
import space.kscience.dataforge.misc.DFBuilder
import space.kscience.dataforge.values.*
/**
* A descriptor for meta value
*
* Descriptor can have non-atomic path. It is resolved when descriptor is added to the node
*
* @author Alexander Nozik
*/
@DFBuilder
public sealed interface ValueDescriptor: ItemDescriptor{
/**
* True if the value is required
*
* @return
*/
override val required: Boolean
/**
* The default for this value. Null if there is no default.
*
* @return
*/
public val default: Value?
/**
* A list of allowed ValueTypes. Empty if any value type allowed
*
* @return
*/
public val type: List<ValueType>?
/**
* Check if given value is allowed for here. The type should be allowed and
* if it is value should be within allowed values
*
* @param value
* @return
*/
public fun isAllowedValue(value: Value): Boolean =
(type?.let { it.contains(ValueType.STRING) || it.contains(value.type) } ?: true)
&& (allowedValues.isEmpty() || allowedValues.contains(value))
/**
* A list of allowed values with descriptions. If empty than any value is
* allowed.
*
* @return
*/
public val allowedValues: List<Value>
}
/**
* A builder fir [ValueDescriptor]
*/
@DFBuilder
public class ValueDescriptorBuilder(config: Config = Config()) : ItemDescriptorBuilder(config), ValueDescriptor {
/**
* True if the value is required
*
* @return
*/
override var required: Boolean by config.boolean { default == null }
/**
* The default for this value. Null if there is no default.
*
* @return
*/
override var default: Value? by config.value()
public fun default(v: Any) {
this.default = Value.of(v)
}
/**
* A list of allowed ValueTypes. Empty if any value type allowed
*
* @return
*/
override var type: List<ValueType>? by config.listValue { ValueType.valueOf(it.string) }
public fun type(vararg t: ValueType) {
this.type = listOf(*t)
}
/**
* Check if given value is allowed for here. The type should be allowed and
* if it is value should be within allowed values
*
* @param value
* @return
*/
override fun isAllowedValue(value: Value): Boolean {
return (type?.let { it.contains(ValueType.STRING) || it.contains(value.type) } ?: true)
&& (allowedValues.isEmpty() || allowedValues.contains(value))
}
/**
* A list of allowed values with descriptions. If empty than any value is
* allowed.
*
* @return
*/
override var allowedValues: List<Value> by config.item().convert(
reader = {
val value = it.value
when {
value?.list != null -> value.list
type?.let { type -> type.size == 1 && type[0] === ValueType.BOOLEAN } ?: false -> listOf(True, False)
else -> emptyList()
}
},
writer = {
MetaItemValue(it.asValue())
}
)
/**
* Allow given list of value and forbid others
*/
public fun allow(vararg v: Any) {
this.allowedValues = v.map { Value.of(it) }
}
override fun build(): ValueDescriptor = ValueDescriptorBuilder(config.copy())
}

View File

@ -4,7 +4,7 @@ import space.kscience.dataforge.names.Name
import space.kscience.dataforge.values.ValueType
import space.kscience.dataforge.values.asValue
public inline fun <reified E : Enum<E>> NodeDescriptor.enum(
public inline fun <reified E : Enum<E>> NodeDescriptorBuilder.enum(
key: Name,
default: E?,
crossinline modifier: ValueDescriptor.() -> Unit = {},

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.8.3-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -7,7 +7,7 @@ pluginManagement {
maven("https://dl.bintray.com/kotlin/kotlin-eap")
}
val toolsVersion = "0.9.4"
val toolsVersion = "0.9.5-dev"
val kotlinVersion = "1.5.0-M2"
plugins {