Clean up property access syntax

This commit is contained in:
Alexander Nozik 2023-05-07 16:39:58 +03:00
parent 691b2ae67a
commit b66e23cca6
6 changed files with 102 additions and 90 deletions

View File

@ -39,7 +39,7 @@ public interface Device : AutoCloseable, ContextAware, CoroutineScope {
public val actionDescriptors: Collection<ActionDescriptor>
/**
* 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

View File

@ -13,10 +13,29 @@ import space.kscience.dataforge.meta.Meta
import kotlin.coroutines.CoroutineContext
@OptIn(InternalDeviceAPI::class)
private suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.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 <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
read(device)?.let(converter::objectToMeta)
private suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.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<D : Device>(
override val context: Context = Global,
override val meta: Meta = Meta.EMPTY,
@ -83,10 +102,17 @@ public abstract class DeviceBase<D : Device>(
* 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<D : Device>(
}
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<D, out Any?>)?.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<D : Device>(
public val spec: DeviceSpec<in D>,
context: Context = Global,
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun open(): Unit = with(spec) {
super.open()
self.onOpen()
}
override fun close(): Unit = with(spec) {
self.onClose()
super.close()
}
}

View File

@ -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<D : Device>(
public val spec: DeviceSpec<in D>,
context: Context = Global,
meta: Meta = Meta.EMPTY,
) : DeviceBase<D>(context, meta) {
override val properties: Map<String, DevicePropertySpec<D, *>> get() = spec.properties
override val actions: Map<String, DeviceActionSpec<D, *, *>> get() = spec.actions
override suspend fun open(): Unit = with(spec) {
super.open()
self.onOpen()
}
override fun close(): Unit = with(spec) {
self.onClose()
super.close()
}
}

View File

@ -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<in D : Device, T> {
*/
@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 <D : Device, T> DevicePropertySpec<D, T>.readMeta(device: D): Meta? =
read(device)?.let(converter::objectToMeta)
public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/**
@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
*/
@InternalDeviceAPI
public suspend fun write(device: D, value: T)
}
@OptIn(InternalDeviceAPI::class)
public suspend fun <D : Device, T> WritableDevicePropertySpec<D, T>.writeMeta(device: D, item: Meta) {
write(device, converter.metaToObject(item) ?: error("Meta $item could not be read with $converter"))
}
public interface DeviceActionSpec<in D : Device, I, O> {
@ -82,29 +74,16 @@ public interface DeviceActionSpec<in D : Device, I, O> {
*/
public val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.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 <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): 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 <T, D : Device> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? {
val res = propertySpec.read(this) ?: return null
@Suppress("UNCHECKED_CAST")
(this as? DeviceBase<D>)?.updateLogical(propertySpec, res)
return res
}
public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
readOrNull(propertySpec) ?: error("Failed to read property ${propertySpec.name} state")
public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject)
@ -112,34 +91,17 @@ public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>
/**
* Write typed property state and invalidate logical state
*/
@OptIn(InternalDeviceAPI::class)
public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, 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 <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Unit {
launch {
write(propertySpec, value)
}
public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
write(propertySpec, value)
}
/**
* Reset the logical state of a property
*/
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
invalidate(propertySpec.name)
}
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
actionSpec.execute(this, input)
/**
* A type safe property change listener
@ -152,4 +114,14 @@ public fun <D : Device, T> Device.onPropertyChange(
.filter { it.property == spec.name }
.onEach { change ->
change.callback(spec.converter.metaToObject(change.value))
}.launchIn(this)
}.launchIn(this)
/**
* Reset the logical state of a property
*/
public suspend fun <D : Device> D.invalidate(propertySpec: DevicePropertySpec<D, *>) {
invalidate(propertySpec.name)
}
public suspend fun <I, O, D : Device> D.execute(actionSpec: DeviceActionSpec<D, I, O>, input: I? = null): O? =
actionSpec.execute(this, input)

View File

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

View File

@ -28,8 +28,6 @@ class OpcUaClientTest {
return DemoOpcUaDevice(config)
}
inline fun <R> 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))
}
}