From b66e23cca6d5ce97e91d896b19d6657871cdb78f Mon Sep 17 00:00:00 2001 From: Alexander Nozik Date: Sun, 7 May 2023 16:39:58 +0300 Subject: [PATCH] Clean up property access syntax --- .../space/kscience/controls/api/Device.kt | 4 +- .../kscience/controls/spec/DeviceBase.kt | 85 +++++++++++-------- .../kscience/controls/spec/DeviceBySpec.kt | 29 +++++++ .../controls/spec/DevicePropertySpec.kt | 68 +++++---------- .../controls/spec/deviceExtensions.kt | 2 +- .../controls/opcua/client/OpcUaClientTest.kt | 4 +- 6 files changed, 102 insertions(+), 90 deletions(-) create mode 100644 controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt index a9dab80..5056123 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/api/Device.kt @@ -39,7 +39,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { public val actionDescriptors: Collection /** - * Read physical state of property and update/push notifications if needed. + * Read the physical state of property and update/push notifications if needed. */ public suspend fun readProperty(propertyName: String): Meta @@ -71,7 +71,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope { * Send an action request and suspend caller while request is being processed. * Could return null if request does not return a meaningful answer. */ - public suspend fun execute(action: String, argument: Meta? = null): Meta? + public suspend fun execute(actionName: String, argument: Meta? = null): Meta? /** * Initialize the device. This function suspends until the device is finished initialization diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt index e74ca2d..bafe688 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBase.kt @@ -13,10 +13,29 @@ import space.kscience.dataforge.meta.Meta import kotlin.coroutines.CoroutineContext +@OptIn(InternalDeviceAPI::class) +private suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { + write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) +} + +@OptIn(InternalDeviceAPI::class) +private suspend fun DevicePropertySpec.readMeta(device: D): Meta? = + read(device)?.let(converter::objectToMeta) + + +private suspend fun DeviceActionSpec.executeWithMeta( + device: D, + item: Meta?, +): Meta? { + val arg = item?.let { inputConverter.metaToObject(item) } + val res = execute(device, arg) + return res?.let { outputConverter.objectToMeta(res) } +} + + /** * A base abstractions for [Device], introducing specifications for properties */ -@OptIn(InternalDeviceAPI::class) public abstract class DeviceBase( override val context: Context = Global, override val meta: Meta = Meta.EMPTY, @@ -83,10 +102,17 @@ public abstract class DeviceBase( * The logical state is updated after read */ override suspend fun readProperty(propertyName: String): Meta { - val newValue = properties[propertyName]?.readMeta(self) - ?: error("A property with name $propertyName is not registered in $this") - updateLogical(propertyName, newValue) - return newValue + val spec = properties[propertyName] ?: error("Property with name $propertyName not found") + val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName") + updateLogical(propertyName, meta) + return meta + } + + public suspend fun readPropertyOrNull(propertyName: String): Meta? { + val spec = properties[propertyName] ?: return null + val meta = spec.readMeta(self) ?: return null + updateLogical(propertyName, meta) + return meta } override fun getProperty(propertyName: String): Meta? = logicalState[propertyName] @@ -98,40 +124,27 @@ public abstract class DeviceBase( } override suspend fun writeProperty(propertyName: String, value: Meta): Unit { - //If there is a physical property with a given name, invalidate logical property and write physical one - (properties[propertyName] as? WritableDevicePropertySpec)?.let { - invalidate(propertyName) - it.writeMeta(self, value) - } ?: run { - updateLogical(propertyName, value) + when (val property = properties[propertyName]) { + null -> { + //If there is a physical property with a given name, invalidate logical property and write physical one + updateLogical(propertyName, value) + } + + is WritableDevicePropertySpec -> { + invalidate(propertyName) + property.writeMeta(self, value) + } + + else -> { + error("Property $property is not writeable") + } } } - override suspend fun execute(action: String, argument: Meta?): Meta? = - actions[action]?.executeWithMeta(self, argument) + override suspend fun execute(actionName: String, argument: Meta?): Meta? { + val spec = actions[actionName] ?: error("Action with name $actionName not found") + return spec.executeWithMeta(self, argument) + } } -/** - * A device generated from specification - * @param D recursive self-type for properties and actions - */ -public open class DeviceBySpec( - public val spec: DeviceSpec, - context: Context = Global, - meta: Meta = Meta.EMPTY, -) : DeviceBase(context, meta) { - override val properties: Map> get() = spec.properties - override val actions: Map> get() = spec.actions - - override suspend fun open(): Unit = with(spec) { - super.open() - self.onOpen() - } - - override fun close(): Unit = with(spec) { - self.onClose() - super.close() - } -} - diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt new file mode 100644 index 0000000..e8a071c --- /dev/null +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DeviceBySpec.kt @@ -0,0 +1,29 @@ +package space.kscience.controls.spec + +import space.kscience.controls.api.Device +import space.kscience.dataforge.context.Context +import space.kscience.dataforge.context.Global +import space.kscience.dataforge.meta.Meta + +/** + * A device generated from specification + * @param D recursive self-type for properties and actions + */ +public open class DeviceBySpec( + public val spec: DeviceSpec, + context: Context = Global, + meta: Meta = Meta.EMPTY, +) : DeviceBase(context, meta) { + override val properties: Map> get() = spec.properties + override val actions: Map> get() = spec.actions + + override suspend fun open(): Unit = with(spec) { + super.open() + self.onOpen() + } + + override fun close(): Unit = with(spec) { + self.onClose() + super.close() + } +} \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt index 53b3953..ede312a 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/DevicePropertySpec.kt @@ -10,7 +10,6 @@ import space.kscience.controls.api.ActionDescriptor import space.kscience.controls.api.Device import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.api.PropertyDescriptor -import space.kscience.dataforge.meta.Meta import space.kscience.dataforge.meta.transformations.MetaConverter @@ -36,17 +35,14 @@ public interface DevicePropertySpec { */ @InternalDeviceAPI public suspend fun read(device: D): T? + } /** - * Property name, should be unique in device + * Property name should be unique in a device */ public val DevicePropertySpec<*, *>.name: String get() = descriptor.name -@OptIn(InternalDeviceAPI::class) -public suspend fun DevicePropertySpec.readMeta(device: D): Meta? = - read(device)?.let(converter::objectToMeta) - public interface WritableDevicePropertySpec : DevicePropertySpec { /** @@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec : DevicePropertySp */ @InternalDeviceAPI public suspend fun write(device: D, value: T) -} -@OptIn(InternalDeviceAPI::class) -public suspend fun WritableDevicePropertySpec.writeMeta(device: D, item: Meta) { - write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter")) } public interface DeviceActionSpec { @@ -82,29 +74,16 @@ public interface DeviceActionSpec { */ public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name -public suspend fun DeviceActionSpec.executeWithMeta( - device: D, - item: Meta?, -): Meta? { - val arg = item?.let { inputConverter.metaToObject(item) } - val res = execute(device, arg) - return res?.let { outputConverter.objectToMeta(res) } -} +public suspend fun D.read(propertySpec: DevicePropertySpec): T = + propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta") /** * Read typed value and update/push event if needed. * Return null if property read is not successful or property is undefined. */ -@OptIn(InternalDeviceAPI::class) -public suspend fun D.readOrNull(propertySpec: DevicePropertySpec): T? { - val res = propertySpec.read(this) ?: return null - @Suppress("UNCHECKED_CAST") - (this as? DeviceBase)?.updateLogical(propertySpec, res) - return res -} +public suspend fun > D.readOrNull(propertySpec: DevicePropertySpec): T? = + readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject) -public suspend fun D.read(propertySpec: DevicePropertySpec): T = - readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state") public operator fun D.get(propertySpec: DevicePropertySpec): T? = getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) @@ -112,34 +91,17 @@ public operator fun D.get(propertySpec: DevicePropertySpec /** * Write typed property state and invalidate logical state */ -@OptIn(InternalDeviceAPI::class) public suspend fun D.write(propertySpec: WritableDevicePropertySpec, value: T) { - invalidate(propertySpec.name) - propertySpec.write(this, value) - //perform asynchronous read and update after write - launch { - read(propertySpec) - } + writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value)) } /** * Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope */ -public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Unit { - launch { - write(propertySpec, value) - } +public operator fun D.set(propertySpec: WritableDevicePropertySpec, value: T): Job = launch { + write(propertySpec, value) } -/** - * Reset the logical state of a property - */ -public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { - invalidate(propertySpec.name) -} - -public suspend fun D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = - actionSpec.execute(this, input) /** * A type safe property change listener @@ -152,4 +114,14 @@ public fun Device.onPropertyChange( .filter { it.property == spec.name } .onEach { change -> change.callback(spec.converter.metaToObject(change.value)) - }.launchIn(this) \ No newline at end of file + }.launchIn(this) + +/** + * Reset the logical state of a property + */ +public suspend fun D.invalidate(propertySpec: DevicePropertySpec) { + invalidate(propertySpec.name) +} + +public suspend fun D.execute(actionSpec: DeviceActionSpec, input: I? = null): O? = + actionSpec.execute(this, input) \ No newline at end of file diff --git a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt index ed48d39..55fc216 100644 --- a/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt +++ b/controls-core/src/commonMain/kotlin/space/kscience/controls/spec/deviceExtensions.kt @@ -9,7 +9,7 @@ import kotlin.time.Duration /** * Perform a recurring asynchronous read action and return a flow of results. - * The flow is lazy so action is not performed unless flow is consumed. + * The flow is lazy, so action is not performed unless flow is consumed. * The flow uses called context. In order to call it on device context, use `flowOn(coroutineContext)`. * * The flow is canceled when the device scope is canceled diff --git a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt index d16cb1e..e9b4c69 100644 --- a/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt +++ b/controls-opcua/src/test/kotlin/space/kscience/controls/opcua/client/OpcUaClientTest.kt @@ -28,8 +28,6 @@ class OpcUaClientTest { return DemoOpcUaDevice(config) } - inline fun use(block: (DemoOpcUaDevice) -> R): R = build().use(block) - val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble) } @@ -40,7 +38,7 @@ class OpcUaClientTest { @OptIn(ExperimentalCoroutinesApi::class) @Test fun testReadDouble() = runTest { - DemoOpcUaDevice.use{ + DemoOpcUaDevice.build().use{ println(it.read(DemoOpcUaDevice.randomDouble)) } }