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> 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 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. * Send an action request and suspend caller while request is being processed.
* Could return null if request does not return a meaningful answer. * 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 * 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 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 * A base abstractions for [Device], introducing specifications for properties
*/ */
@OptIn(InternalDeviceAPI::class)
public abstract class DeviceBase<D : Device>( public abstract class DeviceBase<D : Device>(
override val context: Context = Global, override val context: Context = Global,
override val meta: Meta = Meta.EMPTY, override val meta: Meta = Meta.EMPTY,
@ -83,10 +102,17 @@ public abstract class DeviceBase<D : Device>(
* The logical state is updated after read * The logical state is updated after read
*/ */
override suspend fun readProperty(propertyName: String): Meta { override suspend fun readProperty(propertyName: String): Meta {
val newValue = properties[propertyName]?.readMeta(self) val spec = properties[propertyName] ?: error("Property with name $propertyName not found")
?: error("A property with name $propertyName is not registered in $this") val meta = spec.readMeta(self) ?: error("Failed to read property $propertyName")
updateLogical(propertyName, newValue) updateLogical(propertyName, meta)
return newValue 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] 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 { override suspend fun writeProperty(propertyName: String, value: Meta): Unit {
when (val property = properties[propertyName]) {
null -> {
//If there is a physical property with a given name, invalidate logical property and write physical one //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) updateLogical(propertyName, value)
} }
is WritableDevicePropertySpec -> {
invalidate(propertyName)
property.writeMeta(self, value)
} }
override suspend fun execute(action: String, argument: Meta?): Meta? = else -> {
actions[action]?.executeWithMeta(self, argument) error("Property $property is not writeable")
} }
/**
* 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()
} }
} }
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)
}
}

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.Device
import space.kscience.controls.api.PropertyChangedMessage import space.kscience.controls.api.PropertyChangedMessage
import space.kscience.controls.api.PropertyDescriptor import space.kscience.controls.api.PropertyDescriptor
import space.kscience.dataforge.meta.Meta
import space.kscience.dataforge.meta.transformations.MetaConverter import space.kscience.dataforge.meta.transformations.MetaConverter
@ -36,17 +35,14 @@ public interface DevicePropertySpec<in D : Device, T> {
*/ */
@InternalDeviceAPI @InternalDeviceAPI
public suspend fun read(device: D): T? 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 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> { public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySpec<D, T> {
/** /**
@ -54,11 +50,7 @@ public interface WritableDevicePropertySpec<in D : Device, T> : DevicePropertySp
*/ */
@InternalDeviceAPI @InternalDeviceAPI
public suspend fun write(device: D, value: T) 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> { 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 val DeviceActionSpec<*, *, *>.name: String get() = descriptor.name
public suspend fun <D : Device, I, O> DeviceActionSpec<D, I, O>.executeWithMeta( public suspend fun <T, D : Device> D.read(propertySpec: DevicePropertySpec<D, T>): T =
device: D, propertySpec.converter.metaToObject(readProperty(propertySpec.name)) ?: error("Can't convert result from meta")
item: Meta?,
): Meta? {
val arg = item?.let { inputConverter.metaToObject(item) }
val res = execute(device, arg)
return res?.let { outputConverter.objectToMeta(res) }
}
/** /**
* Read typed value and update/push event if needed. * Read typed value and update/push event if needed.
* Return null if property read is not successful or property is undefined. * Return null if property read is not successful or property is undefined.
*/ */
@OptIn(InternalDeviceAPI::class) public suspend fun <T, D : DeviceBase<D>> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? =
public suspend fun <T, D : Device> D.readOrNull(propertySpec: DevicePropertySpec<D, T>): T? { readPropertyOrNull(propertySpec.name)?.let(propertySpec.converter::metaToObject)
val res = propertySpec.read(this) ?: return null
@Suppress("UNCHECKED_CAST")
(this as? DeviceBase<D>)?.updateLogical(propertySpec, res)
return res
}
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? = public operator fun <T, D : Device> D.get(propertySpec: DevicePropertySpec<D, T>): T? =
getProperty(propertySpec.name)?.let(propertySpec.converter::metaToObject) 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 * 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) { public suspend fun <T, D : Device> D.write(propertySpec: WritableDevicePropertySpec<D, T>, value: T) {
invalidate(propertySpec.name) writeProperty(propertySpec.name, propertySpec.converter.objectToMeta(value))
propertySpec.write(this, value)
//perform asynchronous read and update after write
launch {
read(propertySpec)
}
} }
/** /**
* Fire and forget variant of property writing. Actual write is performed asynchronously on a [Device] scope * 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 { public operator fun <T, D : Device> D.set(propertySpec: WritableDevicePropertySpec<D, T>, value: T): Job = launch {
launch {
write(propertySpec, value) 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 * A type safe property change listener
@ -153,3 +115,13 @@ public fun <D : Device, T> Device.onPropertyChange(
.onEach { change -> .onEach { change ->
change.callback(spec.converter.metaToObject(change.value)) 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. * 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 uses called context. In order to call it on device context, use `flowOn(coroutineContext)`.
* *
* The flow is canceled when the device scope is canceled * The flow is canceled when the device scope is canceled

View File

@ -28,8 +28,6 @@ class OpcUaClientTest {
return DemoOpcUaDevice(config) return DemoOpcUaDevice(config)
} }
inline fun <R> use(block: (DemoOpcUaDevice) -> R): R = build().use(block)
val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble) val randomDouble by doubleProperty(read = DemoOpcUaDevice::readRandomDouble)
} }
@ -40,7 +38,7 @@ class OpcUaClientTest {
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@Test @Test
fun testReadDouble() = runTest { fun testReadDouble() = runTest {
DemoOpcUaDevice.use{ DemoOpcUaDevice.build().use{
println(it.read(DemoOpcUaDevice.randomDouble)) println(it.read(DemoOpcUaDevice.randomDouble))
} }
} }