From 7cde308114f2a367f7701973287f01225fd316ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D0=BE=D0=BB=D0=BF=D0=B0=D0=BA=D0=BE=D0=B2=20=D0=9C?= =?UTF-8?q?=D0=B0=D0=BA=D1=81=D0=B8=D0=BC?= Date: Thu, 19 Dec 2024 03:31:22 +0300 Subject: [PATCH] New hierarchical structure based on CompositeDeviceSpec and ConfigurableCompositeDevice. Also added new builders for PropertyDescriptor and ActionDescriptor --- .../constructor/ConstructorElement.kt | 14 +- .../kscience/controls/api/descriptors.kt | 103 ++- .../spec/CompositeControlComponents.kt | 809 ++++++++++++++++++ .../CompositeControlComponentsDelegates.kt | 384 +++++++++ .../kscience/controls/spec/DeviceSpec.kt | 81 +- .../space/kscience/controls/spec/fromSpec.kt | 8 +- .../controls/spec/propertySpecDelegates.kt | 35 +- .../kscience/controls/spec/fromSpec.js.kt | 8 +- .../kscience/controls/spec/fromSpec.jvm.kt | 8 +- .../kscience/controls/spec/fromSpec.native.kt | 8 +- .../src/wasmJsMain/kotlin/fromSpec.wasm.kt | 8 +- .../kscience/controls/demo/DemoDevice.kt | 1 - 12 files changed, 1365 insertions(+), 102 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt diff --git a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt index 8073689..4d2f476 100644 --- a/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt +++ b/controls-constructor/src/commonMain/kotlin/space/kscience/controls/constructor/ConstructorElement.kt @@ -32,7 +32,7 @@ public class StateConstructorElement( public val state: DeviceState, ) : ConstructorElement -public class ConnectionConstrucorElement( +public class ConnectionConstructorElement( public val reads: Collection>, public val writes: Collection>, ) : ConstructorElement @@ -58,7 +58,7 @@ public interface StateContainer : ContextAware, CoroutineScope { reads: Collection> = emptySet(), onChange: suspend (T) -> Unit, ): Job = valueFlow.onEach(onChange).launchIn(this@StateContainer).also { - registerElement(ConnectionConstrucorElement(reads + this, writes)) + registerElement(ConnectionConstructorElement(reads + this, writes)) } public fun DeviceState.onChange( @@ -72,7 +72,7 @@ public interface StateContainer : ContextAware, CoroutineScope { onChange(pair.first, pair.second) } }.launchIn(this@StateContainer).also { - registerElement(ConnectionConstrucorElement(reads + this, writes)) + registerElement(ConnectionConstructorElement(reads + this, writes)) } } @@ -164,7 +164,7 @@ public fun StateContainer.combineState( * On resulting [Job] cancel the binding is unregistered */ public fun StateContainer.bind(sourceState: DeviceState, targetState: MutableDeviceState): Job { - val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) + val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState)) registerElement(descriptor) return sourceState.valueFlow.onEach { targetState.value = it @@ -186,7 +186,7 @@ public fun StateContainer.transformTo( targetState: MutableDeviceState, transformation: suspend (T) -> R, ): Job { - val descriptor = ConnectionConstrucorElement(setOf(sourceState), setOf(targetState)) + val descriptor = ConnectionConstructorElement(setOf(sourceState), setOf(targetState)) registerElement(descriptor) return sourceState.valueFlow.onEach { targetState.value = transformation(it) @@ -208,7 +208,7 @@ public fun StateContainer.combineTo( targetState: MutableDeviceState, transformation: suspend (T1, T2) -> R, ): Job { - val descriptor = ConnectionConstrucorElement(setOf(sourceState1, sourceState2), setOf(targetState)) + val descriptor = ConnectionConstructorElement(setOf(sourceState1, sourceState2), setOf(targetState)) registerElement(descriptor) return kotlinx.coroutines.flow.combine(sourceState1.valueFlow, sourceState2.valueFlow, transformation).onEach { targetState.value = it @@ -229,7 +229,7 @@ public inline fun StateContainer.combineTo( targetState: MutableDeviceState, noinline transformation: suspend (Array) -> R, ): Job { - val descriptor = ConnectionConstrucorElement(sourceStates, setOf(targetState)) + val descriptor = ConnectionConstructorElement(sourceStates, setOf(targetState)) registerElement(descriptor) return kotlinx.coroutines.flow.combine(sourceStates.map { it.valueFlow }, transformation).onEach { targetState.value = it diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt index f3ffa93..bda25bb 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/descriptors.kt @@ -4,34 +4,109 @@ import kotlinx.serialization.Serializable import space.kscience.dataforge.meta.descriptors.MetaDescriptor import space.kscience.dataforge.meta.descriptors.MetaDescriptorBuilder -//TODO add proper builders +//TODO check proper builders /** - * A descriptor for property + * A descriptor for a property */ @Serializable public class PropertyDescriptor( public val name: String, - public var description: String? = null, - public var metaDescriptor: MetaDescriptor = MetaDescriptor(), - public var readable: Boolean = true, - public var mutable: Boolean = false, + public val description: String? = null, + public val metaDescriptor: MetaDescriptor = MetaDescriptor(), + public val readable: Boolean = true, + public val mutable: Boolean = false, ) -public fun PropertyDescriptor.metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) { - metaDescriptor = MetaDescriptor { - from(metaDescriptor) - block() +/** + * A builder for PropertyDescriptor + */ +public class PropertyDescriptorBuilder(public val name: String) { + public var description: String? = null + public var metaDescriptor: MetaDescriptor = MetaDescriptor() + public var readable: Boolean = true + public var mutable: Boolean = false + + /** + * Configure the metaDescriptor using a block + */ + public fun metaDescriptor(block: MetaDescriptorBuilder.() -> Unit) { + metaDescriptor = MetaDescriptor { + from(metaDescriptor) + block() + } } + + /** + * Build the PropertyDescriptor + */ + public fun build(): PropertyDescriptor = PropertyDescriptor( + name = name, + description = description, + metaDescriptor = metaDescriptor, + readable = readable, + mutable = mutable + ) } /** - * A descriptor for property + * Create a PropertyDescriptor using a builder + */ +public fun propertyDescriptor(name: String, builder: PropertyDescriptorBuilder.() -> Unit = {}): PropertyDescriptor = + PropertyDescriptorBuilder(name).apply(builder).build() + +/** + * A descriptor for an action */ @Serializable public class ActionDescriptor( public val name: String, - public var description: String? = null, - public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor(), - public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor() + public val description: String? = null, + public val inputMetaDescriptor: MetaDescriptor = MetaDescriptor(), + public val outputMetaDescriptor: MetaDescriptor = MetaDescriptor(), ) + +/** + * A builder for ActionDescriptor + */ +public class ActionDescriptorBuilder(public val name: String) { + public var description: String? = null + public var inputMetaDescriptor: MetaDescriptor = MetaDescriptor() + public var outputMetaDescriptor: MetaDescriptor = MetaDescriptor() + + /** + * Configure the inputMetaDescriptor using a block + */ + public fun inputMeta(block: MetaDescriptorBuilder.() -> Unit) { + inputMetaDescriptor = MetaDescriptor { + from(inputMetaDescriptor) + block() + } + } + + /** + * Configure the outputMetaDescriptor using a block + */ + public fun outputMeta(block: MetaDescriptorBuilder.() -> Unit) { + outputMetaDescriptor = MetaDescriptor { + from(outputMetaDescriptor) + block() + } + } + + /** + * Build the ActionDescriptor + */ + public fun build(): ActionDescriptor = ActionDescriptor( + name = name, + description = description, + inputMetaDescriptor = inputMetaDescriptor, + outputMetaDescriptor = outputMetaDescriptor + ) +} + +/** + * Create an ActionDescriptor using a builder + */ +public fun actionDescriptor(name: String, builder: ActionDescriptorBuilder.() -> Unit = {}): ActionDescriptor = + ActionDescriptorBuilder(name).apply(builder).build() diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt new file mode 100644 index 0000000..259238a --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponents.kt @@ -0,0 +1,809 @@ +package space.kscience.controls.spec + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancelAndJoin +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.coroutines.withTimeoutOrNull +import space.kscience.controls.api.* +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.error +import space.kscience.dataforge.context.logger +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.MutableMeta +import space.kscience.dataforge.names.Name +import space.kscience.dataforge.names.asName +import space.kscience.dataforge.names.parseAsName +import space.kscience.dataforge.names.plus +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty +import kotlin.time.Duration +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.launchIn +import space.kscience.dataforge.context.AbstractPlugin +import space.kscience.dataforge.context.ContextAware +import space.kscience.dataforge.context.ContextBuilder +import space.kscience.dataforge.context.PluginFactory +import space.kscience.dataforge.context.PluginTag + +/** + * Defines how child device lifecycle should be managed. + */ +public enum class LifecycleMode { + /** + * Device starts and stops with parent + */ + LINKED, + + /** + * Device is started and stopped independently + */ + INDEPENDENT, + + /** + * Device is created but starts only when explicitly requested + */ + LAZY +} + +public sealed class DeviceChangeEvent { + public abstract val deviceName: Name + + public data class Added(override val deviceName: Name, val device: Device) : DeviceChangeEvent() + public data class Removed(override val deviceName: Name) : DeviceChangeEvent() +} + +public interface ComponentRegistry : ContextAware { + public fun > getSpec(name: Name): CompositeControlComponentSpec? +} + +public class ComponentRegistryManager : AbstractPlugin(), ComponentRegistry { + private val specs = mutableMapOf>() + + override val tag: PluginTag = Companion.tag + + override fun > getSpec(name: Name): CompositeControlComponentSpec? { + try { + return specs[name] as? CompositeControlComponentSpec + } catch (e: ClassCastException) { + logger.error(e) { "Failed to get spec $name" } + } + return null + } + + public fun registerSpec(spec: CompositeControlComponentSpec<*>) { + specs[(spec as DevicePropertySpec<*, *>).name.asName()] = spec + } + + public companion object : PluginFactory { + override val tag: PluginTag = PluginTag("controls.spechub", group = PluginTag.DATAFORGE_GROUP) + + override fun build(context: Context, meta: Meta): ComponentRegistryManager = ComponentRegistryManager() + + } +} +public val Context.componentRegistry: ComponentRegistry? get() = plugins[ComponentRegistryManager] + +public fun ContextBuilder.withSpecHub() { + plugin(ComponentRegistryManager) +} + +/** + * Builder for [DeviceLifecycleConfig]. + */ +public class DeviceLifecycleConfigBuilder { + public var lifecycleMode: LifecycleMode = LifecycleMode.LINKED + public var messageBuffer: Int = 1000 + public var startDelay: Duration = Duration.ZERO + public var startTimeout: Duration? = null + public var stopTimeout: Duration? = null + public var coroutineScope: CoroutineScope? = null + public var dispatcher: CoroutineDispatcher? = null + public var onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART + + public fun build(): DeviceLifecycleConfig = DeviceLifecycleConfig( + lifecycleMode, messageBuffer, startDelay, startTimeout, stopTimeout, coroutineScope, dispatcher, onError + ) +} + +public fun DeviceLifecycleConfigBuilder.linked() { + lifecycleMode = LifecycleMode.LINKED +} + +public fun DeviceLifecycleConfigBuilder.independent() { + lifecycleMode = LifecycleMode.INDEPENDENT +} + +public fun DeviceLifecycleConfigBuilder.lazy() { + lifecycleMode = LifecycleMode.LAZY +} + +public fun DeviceLifecycleConfigBuilder.restartOnError() { + onError = ChildDeviceErrorHandler.RESTART +} + +public fun DeviceLifecycleConfigBuilder.propagateError() { + onError = ChildDeviceErrorHandler.PROPAGATE +} + +public fun DeviceLifecycleConfigBuilder.withCustomTimeout(timeout: Duration) { + startTimeout = timeout + stopTimeout = timeout +} + +@OptIn(InternalDeviceAPI::class) +public abstract class CompositeControlComponentSpec() : CompositeDeviceSpec { + + private val _properties = hashMapOf>( + DeviceMetaPropertySpec.name to DeviceMetaPropertySpec + ) + + override val properties: Map> get() = _properties + + private val _actions = hashMapOf>() + + override val actions: Map> get() = _actions + + private val _childSpecs = mutableMapOf>() + + override val childSpecs: Map> get() = _childSpecs + + public fun , CD: ConfigurableCompositeControlComponent> childSpec( + deviceName: String? = null, + specName: Name? = null, + metaBuilder: (MutableMeta.() -> Unit)? = null, + configBuilder: DeviceLifecycleConfigBuilder.() -> Unit = {}, + ): PropertyDelegateProvider, ReadOnlyProperty, CompositeControlComponentSpec>> = + PropertyDelegateProvider { thisRef, property -> + ReadOnlyProperty { _, _ -> + val childSpecName = specName ?: property.name.asName() + val nameForDevice = deviceName?.asName() ?: property.name.asName() + val config = DeviceLifecycleConfigBuilder().apply(configBuilder).build() + val meta = metaBuilder?.let { Meta(it) } + val spec = (thisRef as ConfigurableCompositeControlComponent<*>).context.componentRegistry?.getSpec(childSpecName) ?: error("Spec with name '$specName' is not found") + val childComponentConfig = object : ChildComponentConfig{ + override val spec: CompositeControlComponentSpec = spec + override val config: DeviceLifecycleConfig = config + override val meta: Meta? = meta + override val name: Name = nameForDevice + } + _childSpecs[property.name] = childComponentConfig + childComponentConfig.spec + } + } + + override fun validate(device: D) { + properties.map { it.value.descriptor }.forEach { specProperty -> + check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" } + } + actions.map { it.value.descriptor }.forEach { specAction -> + check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" } + } + } + + override fun > registerProperty(deviceProperty: P): P { + _properties[deviceProperty.name] = deviceProperty + return deviceProperty + } + + override fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec { + _actions[deviceAction.name] = deviceAction + return deviceAction + } + + override fun createPropertyDescriptorInternal( + propertyName: String, + converter: MetaConverter<*>, + mutable: Boolean, + property: KProperty<*>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + ): PropertyDescriptor { + return propertyDescriptor(propertyName) { + this.mutable = mutable + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) + } + } + fromSpec(property) + descriptorBuilder() + } + } + + override fun createActionDescriptor( + actionName: String, + inputConverter: MetaConverter<*>, + outputConverter: MetaConverter<*>, + property: KProperty<*>, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit, + ): ActionDescriptor { + return actionDescriptor(actionName) { + inputConverter.descriptor?.let { converterDescriptor -> + inputMeta { + from(converterDescriptor) + } + } + outputConverter.descriptor?.let { converterDescriptor -> + outputMeta { + from(converterDescriptor) + } + } + fromSpec(property) + descriptorBuilder() + } + } + + override fun property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + name: String?, + read: suspend D.(propertyName: String) -> T?, + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val propertyName = name ?: property.name + val descriptor = createPropertyDescriptorInternal( + propertyName = propertyName, + converter = converter, + mutable = false, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceProperty = registerProperty(object : DevicePropertySpec { + override val descriptor = descriptor + override val converter = converter + + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } + }) + ReadOnlyProperty { _, _ -> + deviceProperty + } + } + + + override fun mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit, + name: String?, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val propertyName = name ?: property.name + val descriptor = createPropertyDescriptorInternal( + propertyName = propertyName, + converter = converter, + mutable = true, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceProperty = registerProperty(object : MutableDevicePropertySpec { + override val descriptor = descriptor + override val converter = converter + override suspend fun read(device: D): T? = + withContext(device.coroutineContext) { device.read(propertyName) } + + override suspend fun write(device: D, value: T): Unit = withContext(device.coroutineContext) { + device.write(propertyName, value) + } + }) + ReadOnlyProperty { _, _ -> + deviceProperty + } + } + + + override fun action( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit, + name: String?, + execute: suspend D.(I) -> O, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + PropertyDelegateProvider { _: CompositeControlComponentSpec, property -> + val actionName = name ?: property.name + val descriptor = createActionDescriptor( + actionName = actionName, + inputConverter = inputConverter, + outputConverter = outputConverter, + property = property, + descriptorBuilder = descriptorBuilder + ) + val deviceAction = registerAction(object : DeviceActionSpec { + override val descriptor = descriptor + override val inputConverter = inputConverter + override val outputConverter = outputConverter + override suspend fun execute(device: D, input: I): O = withContext(device.coroutineContext) { + device.execute(input) + } + }) + + ReadOnlyProperty { _, _ -> + deviceAction + } + } + + override suspend fun D.onOpen() {} + override suspend fun D.onClose() {} + +} + +public fun CompositeControlComponentSpec.unitAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.unit, + MetaConverter.unit, + descriptorBuilder, + name + ) { + execute() + } + + +public fun CompositeControlComponentSpec.metaAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.meta, + MetaConverter.meta, + descriptorBuilder, + name + ) { + execute(it) + } + +/** + * Basic interface for device description + */ +public interface CompositeDeviceSpec { + public val properties: Map> + + public val actions: Map> + + public val childSpecs: Map> + + /** + * Called on `start()` + */ + public suspend fun D.onOpen() + + /** + * Called on `stop()` + */ + public suspend fun D.onClose() + + /** + * Registers a property in the spec. + */ + public fun > registerProperty(deviceProperty: P): P + + /** + * Registers an action in the spec. + */ + public fun registerAction(deviceAction: DeviceActionSpec): DeviceActionSpec + + public fun validate(device: D) + + public fun createPropertyDescriptorInternal( + propertyName: String, + converter: MetaConverter<*>, + mutable: Boolean, + property: KProperty<*>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit + ): PropertyDescriptor + + public fun createActionDescriptor( + actionName: String, + inputConverter: MetaConverter<*>, + outputConverter: MetaConverter<*>, + property: KProperty<*>, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit + ): ActionDescriptor + + public fun property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> + + public fun mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit, + ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> + + public fun action( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> O, + ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> +} + +public data class DeviceLifecycleConfig( + val lifecycleMode: LifecycleMode = LifecycleMode.LINKED, + val messageBuffer: Int = 1000, + val startDelay: Duration = Duration.ZERO, + val startTimeout: Duration? = null, + val stopTimeout: Duration? = null, + val coroutineScope: CoroutineScope? = null, + val dispatcher: CoroutineDispatcher? = null, + val onError: ChildDeviceErrorHandler = ChildDeviceErrorHandler.RESTART +) { + init { + require(messageBuffer > 0) { "Message buffer size must be positive." } + startTimeout?.let { require(it.isPositive()) { "Start timeout must be positive." } } + stopTimeout?.let { require(it.isPositive()) { "Stop timeout must be positive." } } + } +} + +public enum class ChildDeviceErrorHandler { + IGNORE, + RESTART, + STOP_PARENT, + PROPAGATE +} + +/** + * Base class for managing child devices. Manages lifecycle and message flow. + */ +public abstract class AbstractDeviceHubManager( + public val context: Context, + private val dispatcher: CoroutineDispatcher = Dispatchers.Default, +) { + internal val childrenJobs: MutableMap = mutableMapOf() + public val devices: Map get() = childrenJobs.mapValues { it.value.device } + + internal data class ChildJob( + val device: Device, + val job: Job, + val lifecycleMode: LifecycleMode, + val messageBus: MutableSharedFlow, + val meta: Meta? = null + ) + + /** + * A centralized bus for messages + */ + public abstract val messageBus: MutableSharedFlow + + /** + * A centralized bus for device change events + */ + public abstract val deviceChanges: MutableSharedFlow + + + /** + * Launches a child device with a specific lifecycle mode and error handling. + */ + @OptIn(ExperimentalCoroutinesApi::class) + internal fun launchChild(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null): ChildJob { + val childMessageBus = MutableSharedFlow( + replay = config.messageBuffer, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val childScope = config.coroutineScope ?: context + val job = childScope.launch(dispatcher + CoroutineName("Child device $name")) { + try { + if (config.lifecycleMode != LifecycleMode.INDEPENDENT) { + if (config.lifecycleMode == LifecycleMode.LINKED || device.lifecycleState == LifecycleState.STARTING){ + delay(config.startDelay) + withTimeoutOrNull(config.startTimeout ?: Duration.INFINITE) { + device.start() + } ?: error("Timeout on start for $name") + } + } + + device.messageFlow.collect { message -> + childMessageBus.emit(message.changeSource { name.plus(it) }) + messageBus.emit(message.changeSource { name.plus(it) }) + } + } catch (ex: Exception) { + val errorMessage = DeviceMessage.error(ex, name) + messageBus.emit(errorMessage) + + when (config.onError) { + ChildDeviceErrorHandler.IGNORE -> context.logger.error(ex) { "Error in child device $name ignored" } + ChildDeviceErrorHandler.RESTART -> { + context.logger.error(ex) { "Error in child device $name, restarting" } + removeDevice(name) + childrenJobs[name] = launchChild(name, device, config, meta) + } + ChildDeviceErrorHandler.STOP_PARENT -> { + context.logger.error(ex) { "Error in child device $name, stopping parent" } + coroutineContext[Job]?.cancelAndJoin() + } + ChildDeviceErrorHandler.PROPAGATE -> { + context.logger.error(ex) { "Error in child device $name propagated to parent" } + throw ex + } + } + } finally { + childrenJobs.remove(name) + clearReplayCache(childMessageBus) + deviceChanges.emit(DeviceChangeEvent.Removed(name)) + messageBus.emit(DeviceLogMessage("Device $name stopped", sourceDevice = name)) + if (device is ConfigurableCompositeControlComponent<*>) { + device.onChildStop() + } + } + } + return ChildJob(device, job, config.lifecycleMode, childMessageBus, meta) + } + + @OptIn(ExperimentalCoroutinesApi::class) + private fun clearReplayCache(mutableSharedFlow: MutableSharedFlow){ + val cached = mutableSharedFlow.replayCache + mutableSharedFlow.resetReplayCache() + cached.forEach { mutableSharedFlow.tryEmit(it) } + } + + /** + * Add a device to the hub and manage its lifecycle according to its spec + */ + public fun addDevice(name: Name, device: Device, config: DeviceLifecycleConfig, meta: Meta? = null) { + val existingDevice = devices[name] + if (existingDevice != null) { + if(existingDevice == device) { + error("Device with name $name is already installed") + } + context.launch(device.coroutineContext) { existingDevice.stopWithTimeout(config.stopTimeout ?: Duration.INFINITE) } + } + childrenJobs[name] = launchChild(name, device, config, meta) + + context.launch { + deviceChanges.emit(DeviceChangeEvent.Added(name, device)) + messageBus.emit(DeviceLogMessage("Device $name added", sourceDevice = name)) + } + } + + public fun removeDevice(name: Name) { + childrenJobs[name]?.let { childJob -> + context.launch(childJob.device.coroutineContext) { + val timeout = when (childJob.lifecycleMode) { + LifecycleMode.INDEPENDENT -> childJob.device.meta["stopTimeout".asName()]?.value?.let { + Duration.parse(it.toString()) + } ?: Duration.INFINITE + + else -> Duration.INFINITE + } + withTimeoutOrNull(timeout) { + childJob.job.cancelAndJoin() + childJob.device.stop() + } ?: error("Timeout on stop for $name") + } + childrenJobs.remove(name) + context.launch { + messageBus.emit(DeviceLogMessage("Device $name removed", sourceDevice = name)) + deviceChanges.emit(DeviceChangeEvent.Removed(name)) + } + } + } + + /** + * Change lifecycle mode of a child device + */ + public fun changeLifecycleMode(name: Name, mode: LifecycleMode) { + val job = childrenJobs[name] ?: error("Device with name '$name' is not found") + val config = DeviceLifecycleConfig(lifecycleMode = mode, messageBuffer = job.messageBus.replayCache.size) + context.launch { + job.job.cancelAndJoin() + childrenJobs[name] = launchChild(name, job.device, config, job.meta) + } + } + + /** + * Get local message bus for a child device + */ + public fun getChildMessageBus(name: Name) : MutableSharedFlow? = childrenJobs[name]?.messageBus + + /** + * Method for explicit call when child device is stopped. + */ + internal open fun onChildStop(){ + + } +} + + +private class DeviceHubManagerImpl(context: Context, dispatcher: CoroutineDispatcher = Dispatchers.Default) : AbstractDeviceHubManager(context, dispatcher){ + override val messageBus: MutableSharedFlow = MutableSharedFlow( + replay = 1000, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + override val deviceChanges: MutableSharedFlow = MutableSharedFlow(replay = 1) +} + +/** + * An interface for a device that contains other devices as children. + */ +public interface CompositeControlComponent : Device { + /** + * A centralized flow of all device messages from this node and all its children + */ + public val messageBus: SharedFlow + + /** + * A map of child devices + */ + public val devices: Map +} + +/** + * A base class for devices created from specification, using AbstractDeviceHubManager for children management + */ +public open class ConfigurableCompositeControlComponent( + public open val spec: CompositeControlComponentSpec, + context: Context, + meta: Meta = Meta.EMPTY, + config: DeviceLifecycleConfig = DeviceLifecycleConfig(), + private val hubManager: AbstractDeviceHubManager = DeviceHubManagerImpl(context, config.dispatcher ?: Dispatchers.Default), +) : DeviceBase(context, meta), CompositeControlComponent { + + override val properties: Map> + get() = spec.properties + + override val actions: Map> + get() = spec.actions + + final override val messageBus: MutableSharedFlow + get() = hubManager.messageBus + + public val deviceChanges: MutableSharedFlow + get() = hubManager.deviceChanges + + public val aggregatedMessageFlow: SharedFlow + get() = hubManager.messageBus + + init { + spec.childSpecs.forEach{ (name, childSpec) -> + val childDevice = ConfigurableCompositeControlComponent(childSpec.spec, context, childSpec.meta ?: Meta.EMPTY, childSpec.config) + addDevice(childSpec.name, childDevice, childSpec.config, childSpec.meta) + } + + spec.actions.values.forEach { actionSpec -> + launch { + val actionName = actionSpec.name + messageFlow.filterIsInstance().filter { it.action == actionName }.onEach { + val result = execute(actionName, it.argument) + messageBus.emit( + ActionResultMessage( + action = actionName, + result = result, + requestId = it.requestId, + sourceDevice = id.asName() + ) + ) + }.launchIn(this) + } + } + } + + override suspend fun onStart() { + with(spec) { + self.onOpen() + validate(self) + } + hubManager.devices.values.filter { + it.lifecycleState != LifecycleState.STARTED && it.lifecycleState != LifecycleState.STARTING + }.forEach { + if (hubManager.childrenJobs[it.id.parseAsName()]?.lifecycleMode != LifecycleMode.LAZY) { + it.start() + } + } + } + + private suspend fun getTimeout(device: Device): Duration { + return (hubManager.childrenJobs[device.id.parseAsName()]?.lifecycleMode ?: LifecycleMode.LINKED).let{ + if(it == LifecycleMode.INDEPENDENT) + it.name.let{ meta["stopTimeout".parseAsName()]?.let { durationMeta -> + Duration.parse(durationMeta.value.toString()) + } ?: Duration.INFINITE } + else Duration.INFINITE + } + } + + override suspend fun onStop() { + hubManager.devices.values.forEach { + launch(it.coroutineContext){ + withTimeoutOrNull(getTimeout(it)){ + it.stop() + } + } + } + with(spec) { + self.onClose() + } + } + + override fun toString(): String = "Device(spec=$spec)" + + internal open fun onChildStop() { + } + + /** + * Add existing device to this hub + */ + private fun addDevice(name: Name = device.id.asName(), device: Device, config: DeviceLifecycleConfig = DeviceLifecycleConfig(), meta: Meta? = null) { + hubManager.addDevice(name, device, config, meta) + } + + /** + * Remove a child device from the hub by name. + */ + private fun removeDevice(name: Name) { + hubManager.removeDevice(name) + } + + /** + * Get list of all children devices + */ + public override val devices: Map + get() = hubManager.devices + + /** + * Get child device from this hub by name + */ + public fun > getChildDevice(name: Name): ConfigurableCompositeControlComponent = + hubManager.devices[name] as? ConfigurableCompositeControlComponent? ?: error("Device $name not found") + + public fun > childDevice(name: Name? = null): + PropertyDelegateProvider, ReadOnlyProperty, CD>> + = PropertyDelegateProvider{ thisRef, property -> + ReadOnlyProperty{ _, _ -> + val deviceName = name ?: property.name.asName() + thisRef.devices[deviceName] as? CD ?: error("Device $deviceName not found") + } + } + + /** + * Get child device message bus by name + */ + public fun getChildMessageBus(name: Name): SharedFlow? = hubManager.getChildMessageBus(name) + + /** + * Get device, using delegate method + */ + public inline operator fun get(name: Name): D? = devices[name] as? D + + /** + * Get device, using delegate method + */ + public inline operator fun get(name: String): D? = devices[name.asName()] as? D + +} + +public suspend fun WithLifeCycle.stopWithTimeout(timeout: Duration = Duration.INFINITE) { + withTimeoutOrNull(timeout) { + stop() + } +} + +public interface ChildComponentConfig{ + public val spec: CompositeControlComponentSpec + public val config: DeviceLifecycleConfig + public val meta: Meta? + public val name: Name +} diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt new file mode 100644 index 0000000..b042163 --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/CompositeControlComponentsDelegates.kt @@ -0,0 +1,384 @@ +package space.kscience.controls.spec + +import kotlinx.coroutines.Deferred +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.Device +import space.kscience.controls.api.PropertyDescriptorBuilder +import space.kscience.controls.api.id +import space.kscience.dataforge.meta.Meta +import space.kscience.dataforge.meta.MetaConverter +import space.kscience.dataforge.meta.ValueType +import space.kscience.dataforge.meta.descriptors.MetaDescriptor +import space.kscience.dataforge.meta.string +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KMutableProperty1 +import kotlin.reflect.KProperty1 + +/** + * Create a [MetaConverter] for enum values + */ +public fun > createEnumConverter(enumValues: Array): MetaConverter = object : MetaConverter { + override val descriptor: MetaDescriptor = MetaDescriptor { + valueType(ValueType.STRING) + allowedValues(enumValues.map { it.name }) + } + + override fun readOrNull(source: Meta): E? { + val value = source.value ?: return null + return enumValues.firstOrNull { it.name == value.string } + } + + override fun convert(obj: E): Meta = Meta(obj.name) +} + +/** + * A read-only device property that delegates reading to a device [KProperty1] + */ +public fun CompositeControlComponentSpec.property( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> { + return property(converter, descriptorBuilder, name, read) +} + +/** + * Mutable property that delegates reading and writing to a device [KMutableProperty1] + */ +public fun CompositeControlComponentSpec.mutableProperty( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> T?, + write: suspend D.(propertyName: String, value: T) -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> { + return mutableProperty(converter, descriptorBuilder, name, read, write) +} + +/** + * Register a mutable logical property (without a corresponding physical state) for a device + */ +public fun > CompositeControlComponentSpec.logical( + converter: MetaConverter, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty( + converter, + descriptorBuilder, + name, + read = { propertyName -> getProperty(propertyName)?.let(converter::readOrNull) }, + write = { propertyName, value -> writeProperty(propertyName, converter.convert(value)) } + ) + +/** + * Creates a boolean property for a device. + */ +public fun CompositeControlComponentSpec.boolean( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Boolean?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.boolean, descriptorBuilder, name, read) + +/** + * Creates a mutable boolean property for a device. + */ +public fun CompositeControlComponentSpec.booleanMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Boolean?, + write: suspend D.(propertyName: String, value: Boolean) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.boolean, descriptorBuilder, name, read, write) + +/** + * Creates a read-only number property for a device. + */ +public fun CompositeControlComponentSpec.number( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Number?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.number, descriptorBuilder, name, read) + +/** + * Creates a mutable number property for a device. + */ +public fun CompositeControlComponentSpec.numberMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Number?, + write: suspend D.(propertyName: String, value: Number) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.number, descriptorBuilder, name, read, write) + + +/** + * Creates a read-only double property for a device. + */ +public fun CompositeControlComponentSpec.double( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Double?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.double, descriptorBuilder, name, read) + +/** + * Creates a mutable double property for a device. + */ +public fun CompositeControlComponentSpec.doubleMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Double?, + write: suspend D.(propertyName: String, value: Double) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.double, descriptorBuilder, name, read, write) + +/** + * Creates a read-only string property for a device. + */ +public fun CompositeControlComponentSpec.string( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> String? +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.string, descriptorBuilder, name, read) + +/** + * Creates a mutable string property for a device. + */ +public fun CompositeControlComponentSpec.stringMutableProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> String?, + write: suspend D.(propertyName: String, value: String) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) + +/** + * Creates a read-only meta property for a device. + */ +public fun CompositeControlComponentSpec.meta( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.meta, descriptorBuilder, name, read) + +/** + * Creates a mutable meta property for a device. + */ +public fun CompositeControlComponentSpec.metaMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, + write: suspend D.(propertyName: String, value: Meta) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) + +/** + * Creates a read-only enum property for a device. + */ +public fun , D : Device> CompositeControlComponentSpec.enum( + enumValues: Array, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> E?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> { + val converter = createEnumConverter(enumValues) + return property(converter, descriptorBuilder, name, read) +} + +/** + * Creates a mutable enum property for a device. + */ +public fun , D : Device> CompositeControlComponentSpec.enumMutable( + enumValues: Array, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> E?, + write: suspend D.(propertyName: String, value: E) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> { + val converter = createEnumConverter(enumValues) + return mutableProperty(converter, descriptorBuilder, name, read, write) +} + +/** + * Creates a read-only float property for a device. + */ +public fun CompositeControlComponentSpec.float( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Float?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.float, descriptorBuilder, name, read) + +/** + * Creates a mutable float property for a device. + */ +public fun CompositeControlComponentSpec.floatMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Float?, + write: suspend D.(propertyName: String, value: Float) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.float, descriptorBuilder, name, read, write) + +/** + * Creates a read-only long property for a device. + */ +public fun CompositeControlComponentSpec.long( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Long?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.long, descriptorBuilder, name, read) + +/** + * Creates a mutable long property for a device. + */ +public fun CompositeControlComponentSpec.longMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Long?, + write: suspend D.(propertyName: String, value: Long) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.long, descriptorBuilder, name, read, write) + +/** + * Creates a read-only int property for a device. + */ +public fun CompositeControlComponentSpec.int( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Int?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.int, descriptorBuilder, name, read) + +/** + * Creates a mutable int property for a device. + */ +public fun CompositeControlComponentSpec.intMutable( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Int?, + write: suspend D.(propertyName: String, value: Int) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.int, descriptorBuilder, name, read, write) + +/** + * Creates a read-only list property for a device. + */ +public fun CompositeControlComponentSpec.list( + converter: MetaConverter>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> List?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>>> = + property(converter, descriptorBuilder, name, read) + +/** + * Creates a mutable list property for a device. + */ +public fun CompositeControlComponentSpec.listMutable( + converter: MetaConverter>, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> List?, + write: suspend D.(propertyName: String, value: List) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>>> = + mutableProperty(converter, descriptorBuilder, name, read, write) + + +public fun CompositeControlComponentSpec.asyncActionProperty( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> Deferred, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action(inputConverter, outputConverter, descriptorBuilder, name) { input -> + execute(input).await() + } + +public fun CompositeControlComponentSpec.metaProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, +): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = + property(MetaConverter.meta, descriptorBuilder, name, read) + + +public fun CompositeControlComponentSpec.mutableMetaProperty( + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, + name: String? = null, + read: suspend D.(propertyName: String) -> Meta?, + write: suspend D.(propertyName: String, value: Meta) -> Unit +): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = + mutableProperty(MetaConverter.meta, descriptorBuilder, name, read, write) + +/** + * An action that takes no parameters and returns no values + */ +public fun CompositeControlComponentSpec.unitAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.() -> Unit, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.unit, + MetaConverter.unit, + descriptorBuilder, + name + ) { + execute() + } + +public fun CompositeControlComponentSpec.asyncAction( + inputConverter: MetaConverter, + outputConverter: MetaConverter, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(I) -> Deferred, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + inputConverter, + outputConverter, + descriptorBuilder, + name + ) { + execute(it).await() + } + +/** + * An action that takes [Meta] and returns [Meta]. No conversions are done + */ +public fun CompositeControlComponentSpec.metaAction( + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, + name: String? = null, + execute: suspend D.(Meta) -> Meta, +): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = + action( + MetaConverter.meta, + MetaConverter.meta, + descriptorBuilder, + name + ) { + execute(it) + } + +/** + * Throw an exception if device does not have all properties and actions defined by this specification + */ +public fun CompositeControlComponentSpec<*>.validate(device: Device) { + properties.map { it.value.descriptor }.forEach { specProperty -> + check(specProperty in device.propertyDescriptors) { "Property ${specProperty.name} not registered in ${device.id}" } + } + + actions.map { it.value.descriptor }.forEach { specAction -> + check(specAction in device.actionDescriptors) { "Action ${specAction.name} not registered in ${device.id}" } + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt index c6f8b0a..945fe38 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceSpec.kt @@ -44,24 +44,24 @@ public abstract class DeviceSpec { public fun property( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> T?, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property -> val propertyName = name ?: property.name - val deviceProperty = object : DevicePropertySpec { - - override val descriptor: PropertyDescriptor = PropertyDescriptor(propertyName).apply { - converter.descriptor?.let { converterDescriptor -> - metaDescriptor { - from(converterDescriptor) - } + val descriptor = propertyDescriptor(propertyName) { + mutable = false + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) } - fromSpec(property) - descriptorBuilder() } - + fromSpec(property) + descriptorBuilder() + } + val deviceProperty = object : DevicePropertySpec { + override val descriptor = descriptor override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -75,26 +75,25 @@ public abstract class DeviceSpec { public fun mutableProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> T?, write: suspend D.(propertyName: String, value: T) -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val propertyName = name ?: property.name - val deviceProperty = object : MutableDevicePropertySpec { - override val descriptor: PropertyDescriptor = PropertyDescriptor( - propertyName, - mutable = true - ).apply { - converter.descriptor?.let { converterDescriptor -> - metaDescriptor { - from(converterDescriptor) - } + val descriptor = propertyDescriptor(propertyName) { + this.mutable = true + converter.descriptor?.let { converterDescriptor -> + metaDescriptor { + from(converterDescriptor) } - fromSpec(property) - descriptorBuilder() } + fromSpec(property) + descriptorBuilder() + } + val deviceProperty = object : MutableDevicePropertySpec { + override val descriptor = descriptor override val converter: MetaConverter = converter override suspend fun read(device: D): T? = @@ -119,30 +118,28 @@ public abstract class DeviceSpec { public fun action( inputConverter: MetaConverter, outputConverter: MetaConverter, - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.(I) -> O, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = PropertyDelegateProvider { _: DeviceSpec, property: KProperty<*> -> val actionName = name ?: property.name - val deviceAction = object : DeviceActionSpec { - override val descriptor: ActionDescriptor = ActionDescriptor(actionName).apply { - inputConverter.descriptor?.let { converterDescriptor -> - inputMetaDescriptor = MetaDescriptor { - from(converterDescriptor) - from(inputMetaDescriptor) - } + val descriptor = actionDescriptor(actionName) { + inputConverter.descriptor?.let { converterDescriptor -> + inputMeta { + from(converterDescriptor) } - outputConverter.descriptor?.let { converterDescriptor -> - outputMetaDescriptor = MetaDescriptor { - from(converterDescriptor) - from(outputMetaDescriptor) - } - } - - fromSpec(property) - descriptorBuilder() } + outputConverter.descriptor?.let { converterDescriptor -> + outputMeta { + from(converterDescriptor) + } + } + fromSpec(property) + descriptorBuilder() + } + val deviceAction = object : DeviceActionSpec { + override val descriptor: ActionDescriptor = descriptor override val inputConverter: MetaConverter = inputConverter override val outputConverter: MetaConverter = outputConverter @@ -162,7 +159,7 @@ public abstract class DeviceSpec { * An action that takes no parameters and returns no values */ public fun DeviceSpec.unitAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.() -> Unit, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = @@ -179,7 +176,7 @@ public fun DeviceSpec.unitAction( * An action that takes [Meta] and returns [Meta]. No conversions are done */ public fun DeviceSpec.metaAction( - descriptorBuilder: ActionDescriptor.() -> Unit = {}, + descriptorBuilder: ActionDescriptorBuilder.() -> Unit = {}, name: String? = null, execute: suspend D.(Meta) -> Meta, ): PropertyDelegateProvider, ReadOnlyProperty, DeviceActionSpec>> = diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt index ad5862b..57a9b4a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/fromSpec.kt @@ -1,10 +1,10 @@ package space.kscience.controls.spec -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.PropertyDescriptorBuilder import kotlin.reflect.KProperty -internal expect fun PropertyDescriptor.fromSpec(property: KProperty<*>) +internal expect fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) -internal expect fun ActionDescriptor.fromSpec(property: KProperty<*>) \ No newline at end of file +internal expect fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt index efb66a7..34440fc 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/propertySpecDelegates.kt @@ -1,8 +1,7 @@ package space.kscience.controls.spec import space.kscience.controls.api.Device -import space.kscience.controls.api.PropertyDescriptor -import space.kscience.controls.api.metaDescriptor +import space.kscience.controls.api.PropertyDescriptorBuilder import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.MetaConverter import space.kscience.dataforge.meta.ValueType @@ -17,7 +16,7 @@ import kotlin.reflect.KProperty1 public fun DeviceSpec.property( converter: MetaConverter, readOnlyProperty: KProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( converter, descriptorBuilder, @@ -31,7 +30,7 @@ public fun DeviceSpec.property( public fun DeviceSpec.mutableProperty( converter: MetaConverter, readWriteProperty: KMutableProperty1, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( converter, @@ -49,7 +48,7 @@ public fun DeviceSpec.mutableProperty( */ public fun > DeviceSpec.property( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -60,7 +59,7 @@ public fun > DeviceSpec.property( ) public fun DeviceSpec.booleanProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Boolean? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -76,8 +75,8 @@ public fun DeviceSpec.booleanProperty( ) private inline fun numberDescriptor( - crossinline descriptorBuilder: PropertyDescriptor.() -> Unit = {} -): PropertyDescriptor.() -> Unit = { + crossinline descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {} +): PropertyDescriptorBuilder.() -> Unit = { metaDescriptor { valueType(ValueType.NUMBER) } @@ -85,7 +84,7 @@ private inline fun numberDescriptor( } public fun DeviceSpec.numberProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Number? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -96,7 +95,7 @@ public fun DeviceSpec.numberProperty( ) public fun DeviceSpec.doubleProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Double? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -107,7 +106,7 @@ public fun DeviceSpec.doubleProperty( ) public fun DeviceSpec.stringProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> String? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -123,7 +122,7 @@ public fun DeviceSpec.stringProperty( ) public fun DeviceSpec.metaProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Meta? ): PropertyDelegateProvider, ReadOnlyProperty, DevicePropertySpec>> = property( @@ -147,7 +146,7 @@ public fun DeviceSpec.metaProperty( */ public fun > DeviceSpec.mutableProperty( converter: MetaConverter, - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, ): PropertyDelegateProvider, ReadOnlyProperty, MutableDevicePropertySpec>> = mutableProperty( @@ -159,7 +158,7 @@ public fun > DeviceSpec.mutableProperty( ) public fun DeviceSpec.mutableBooleanProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Boolean?, write: suspend D.(propertyName: String, value: Boolean) -> Unit @@ -179,7 +178,7 @@ public fun DeviceSpec.mutableBooleanProperty( public fun DeviceSpec.mutableNumberProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Number, write: suspend D.(propertyName: String, value: Number) -> Unit @@ -187,7 +186,7 @@ public fun DeviceSpec.mutableNumberProperty( mutableProperty(MetaConverter.number, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.mutableDoubleProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Double, write: suspend D.(propertyName: String, value: Double) -> Unit @@ -195,7 +194,7 @@ public fun DeviceSpec.mutableDoubleProperty( mutableProperty(MetaConverter.double, numberDescriptor(descriptorBuilder), name, read, write) public fun DeviceSpec.mutableStringProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> String, write: suspend D.(propertyName: String, value: String) -> Unit @@ -203,7 +202,7 @@ public fun DeviceSpec.mutableStringProperty( mutableProperty(MetaConverter.string, descriptorBuilder, name, read, write) public fun DeviceSpec.mutableMetaProperty( - descriptorBuilder: PropertyDescriptor.() -> Unit = {}, + descriptorBuilder: PropertyDescriptorBuilder.() -> Unit = {}, name: String? = null, read: suspend D.(propertyName: String) -> Meta, write: suspend D.(propertyName: String, value: Meta) -> Unit diff --git a/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt index cd77248..bf44a4e 100644 --- a/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt +++ b/controls-core/src/jsMain/kotlin/space/kscience/controls/spec/fromSpec.js.kt @@ -1,9 +1,9 @@ package space.kscience.controls.spec -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.PropertyDescriptorBuilder import kotlin.reflect.KProperty -internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){} +internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){} -internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt index cb64fc3..91406f2 100644 --- a/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt +++ b/controls-core/src/jvmMain/kotlin/space/kscience/controls/spec/fromSpec.jvm.kt @@ -1,18 +1,18 @@ package space.kscience.controls.spec -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.PropertyDescriptorBuilder import space.kscience.dataforge.descriptors.Description import kotlin.reflect.KProperty import kotlin.reflect.full.findAnnotation -internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) { +internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) { property.findAnnotation()?.let { description = it.value } } -internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){ +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){ property.findAnnotation()?.let { description = it.value } diff --git a/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt index 1d1ccc4..eeec224 100644 --- a/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt +++ b/controls-core/src/nativeMain/kotlin/space/kscience/controls/spec/fromSpec.native.kt @@ -1,9 +1,9 @@ package space.kscience.controls.spec -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.PropertyDescriptorBuilder import kotlin.reflect.KProperty -internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>) {} +internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>) {} -internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt index cd77248..bf44a4e 100644 --- a/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt +++ b/controls-core/src/wasmJsMain/kotlin/fromSpec.wasm.kt @@ -1,9 +1,9 @@ package space.kscience.controls.spec -import space.kscience.controls.api.ActionDescriptor -import space.kscience.controls.api.PropertyDescriptor +import space.kscience.controls.api.ActionDescriptorBuilder +import space.kscience.controls.api.PropertyDescriptorBuilder import kotlin.reflect.KProperty -internal actual fun PropertyDescriptor.fromSpec(property: KProperty<*>){} +internal actual fun PropertyDescriptorBuilder.fromSpec(property: KProperty<*>){} -internal actual fun ActionDescriptor.fromSpec(property: KProperty<*>){} \ No newline at end of file +internal actual fun ActionDescriptorBuilder.fromSpec(property: KProperty<*>){} \ No newline at end of file diff --git a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt index 5cf1b28..1514eac 100644 --- a/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt +++ b/demo/all-things/src/main/kotlin/space/kscience/controls/demo/DemoDevice.kt @@ -2,7 +2,6 @@ package space.kscience.controls.demo import kotlinx.coroutines.launch import space.kscience.controls.api.Device -import space.kscience.controls.api.metaDescriptor import space.kscience.controls.spec.* import space.kscience.dataforge.context.Context import space.kscience.dataforge.context.Factory