From 07e9f0482e15b402936482116a8d71a25ee536bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Thu, 14 Mar 2024 16:32:59 +0100 Subject: [PATCH 1/9] fix: Fix UInt not supported for VssNodes UInt could not be used before this commit because @JVMOverloads was used for all generated VSS data classes. Since UInt is a Kotlin specific data type it was incompatible with the annotation. As long as we are only using one constructor param for VssSignals we can remove the annotation because the default constructor will still be available. Note: The java byte code will still show an Int for the Kotlin UInt model. This is fine as long as the SDK is speaking Kotlin while interacting with these models. close #93 --- .../eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt | 6 +++--- .../eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt index a128d35e..3b668bf5 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt @@ -92,8 +92,8 @@ fun VssSignal.copy(datapoint: Datapoint): VssSignal { BOOL -> bool INT32 -> int32 INT64 -> int64 - UINT32 -> uint32 - UINT64 -> uint64 + UINT32 -> uint32.toUInt() + UINT64 -> uint64.toULong() FLOAT -> float DOUBLE -> double STRING_ARRAY -> stringArray.valuesList @@ -179,7 +179,7 @@ fun T.copy( // region Operators /** - * Convenience operator for [deepCopy] with a [VssNode]. It will return the [VssNode] with the updated + * Convenience operator for [deepCopy] with a [VssNode]. It will return the parent [VssNode] with the updated child * [VssNode]. * * @throws [IllegalArgumentException] if the copied types do not match. diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index 29eebf43..bc99bfec 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -65,16 +65,17 @@ internal class VssNodeSpecModel( return when (datatype) { "string" -> String::class.asTypeName() "boolean" -> Boolean::class.asTypeName() - // Do not use UInt because it is incompatible with @JvmOverloads annotation - "uint8", "uint16", "uint32" -> Int::class.asTypeName() + "uint8", "uint16", "uint32" -> UInt::class.asTypeName() + "uint64" -> ULong::class.asTypeName() "int8", "int16", "int32" -> Int::class.asTypeName() - "int64", "uint64" -> Long::class.asTypeName() + "int64" -> Long::class.asTypeName() "float" -> Float::class.asTypeName() "double" -> Double::class.asTypeName() "string[]" -> Array::class.parameterizedBy(String::class) "boolean[]" -> BooleanArray::class.asTypeName() "uint8[]", "uint16[]", "uint32[]", "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() "int64[]", "uint64[]" -> LongArray::class.asTypeName() + "float[]" -> FloatArray::class.asTypeName() else -> Any::class.asTypeName() } } @@ -112,7 +113,6 @@ internal class VssNodeSpecModel( val nestedChildSpecs = mutableListOf() val constructorBuilder = FunSpec.constructorBuilder() - .addAnnotation(JvmOverloads::class) val propertySpecs = mutableListOf() val superInterfaces = mutableSetOf(VssBranch::class.asTypeName()) From 9bd7e1a488427375b5a3f57c24e1b19ee5699a8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Thu, 14 Mar 2024 16:33:27 +0100 Subject: [PATCH 2/9] test: Add test for incompatible data type --- .../databroker/DataBrokerConnectionTest.kt | 7 ++++--- .../org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt | 15 +++++++++++++++ .../eclipse/kuksa/vsscore/model/VssVehicle.kt | 6 ++++++ .../vssprocessor/spec/VssNodeSpecModelTest.kt | 16 ++++++++++++++-- 4 files changed, 39 insertions(+), 5 deletions(-) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt index 3a992aa6..2bdac1fe 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt @@ -37,6 +37,7 @@ import org.eclipse.kuksa.connectivity.databroker.request.SubscribeRequest import org.eclipse.kuksa.connectivity.databroker.request.UpdateRequest import org.eclipse.kuksa.connectivity.databroker.request.VssNodeFetchRequest import org.eclipse.kuksa.connectivity.databroker.request.VssNodeSubscribeRequest +import org.eclipse.kuksa.connectivity.databroker.request.VssNodeUpdateRequest import org.eclipse.kuksa.mocking.FriendlyVssNodeListener import org.eclipse.kuksa.proto.v1.KuksaValV1 import org.eclipse.kuksa.proto.v1.Types @@ -202,10 +203,10 @@ class DataBrokerConnectionTest : BehaviorSpec({ } } - and("Any subscribed node was changed") { + and("Any subscribed uInt node was changed") { val newHeartRateValue = 50 - val datapoint = Types.Datapoint.newBuilder().setUint32(newHeartRateValue).build() - val updateRequest = UpdateRequest(vssDriver.heartRate.vssPath, datapoint) + val newVssHeartRate = VssDriver.VssHeartRate(newHeartRateValue) + val updateRequest = VssNodeUpdateRequest(newVssHeartRate) dataBrokerConnection.update(updateRequest) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt index 3e711dd6..8fdc51ee 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt @@ -77,6 +77,21 @@ class VssNodeCopyTest : BehaviorSpec({ } } + and("a changed invalid DataPoint") { + val newValue = 50 + val datapoint = Types.Datapoint.newBuilder().setUint32(newValue).build() + + `when`("a copy is done") { + val exception = shouldThrow { + driverHeartRate.copy(datapoint) + } + + then("it should throw an IllegalArgumentException") { + exception.message shouldStartWith "argument type mismatch" + } + } + } + and("a changed DataPoint") { val newValue = 50 val datapoint = Types.Datapoint.newBuilder().setInt32(newValue).build() diff --git a/vss-core/src/test/kotlin/org/eclipse/kuksa/vsscore/model/VssVehicle.kt b/vss-core/src/test/kotlin/org/eclipse/kuksa/vsscore/model/VssVehicle.kt index eb237186..c632853f 100644 --- a/vss-core/src/test/kotlin/org/eclipse/kuksa/vsscore/model/VssVehicle.kt +++ b/vss-core/src/test/kotlin/org/eclipse/kuksa/vsscore/model/VssVehicle.kt @@ -67,6 +67,9 @@ data class VssDriver( override val comment: String = "", override val value: Int = 100, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val parentClass: KClass<*> get() = VssDriver::class } @@ -93,6 +96,9 @@ data class VssPassenger( override val comment: String = "", override val value: Int = 80, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val parentClass: KClass<*> get() = VssPassenger::class } diff --git a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt index a8c0751d..ca129e7a 100644 --- a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt +++ b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt @@ -53,17 +53,23 @@ class VssNodeSpecModelTest : BehaviorSpec({ } } - given("uint32 spec model") { + given("uint32 spec model (inline class)") { val specModel = VssNodeSpecModel(datatype = "uint32", vssPath = "Vehicle.IgnitionType") `when`("creating a class spec") { val classSpec = specModel.createClassSpec("test") - then("it should have a value with the correct datatype") { + then("it should have a value with the correct data type") { val propertySpec = classSpec.primaryConstructor?.parameters?.find { it.name == "value" } propertySpec.toString() shouldContain "kotlin.Int = 0" } + + then("it should the correct inline class data type") { + val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } + + propertySpec?.getter.toString() shouldContain "kotlin.UInt::class" + } } } @@ -92,6 +98,12 @@ class VssNodeSpecModelTest : BehaviorSpec({ propertySpec.toString() shouldContain "kotlin.LongArray = LongArray(0)" } + + then("it should the correct inline class data type") { + val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } + + propertySpec?.getter.toString() shouldContain "kotlin.LongArray::class" + } } } From 461eda0f7d4ee066e4bcf52be9551e522d25aff3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Fri, 15 Mar 2024 17:18:26 +0100 Subject: [PATCH 3/9] feature(VssProcessor): Add dataType to VssSignal VssNodeSpecModel: - Adds now an experimental annotation for some data types so warnings are properly ignored. - The generation code of the VssSignal now iterates through the member properties of the interface and generates the correct implementation. The VssHeartRate for integration tests is currently the only VssSignal which has a different dataType (UInt) than the value type (Int). --- .../databroker/JavaDataBrokerEngine.java | 2 +- .../kuksa/extension/AnyCopyExtension.kt | 16 +- .../kuksa/extension/DataPointExtension.kt | 8 +- .../extension/vss/VssNodeCopyExtension.kt | 15 +- .../org/eclipse/kuksa/vssNode/VssDriver.kt | 3 + .../eclipse/kuksa/vssNode/VssNodeCopyTest.kt | 6 +- .../org/eclipse/kuksa/vssNode/VssVehicle.kt | 3 + .../java/com/example/sample/JavaActivity.java | 2 +- .../eclipse/kuksa/vsscore/model/VssNode.kt | 24 ++- .../vssprocessor/spec/VssNodeSpecModel.kt | 198 ++++++++++++------ 10 files changed, 189 insertions(+), 88 deletions(-) diff --git a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java index 920d0098..0c17f17b 100644 --- a/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java +++ b/app/src/main/java/org/eclipse/kuksa/testapp/databroker/JavaDataBrokerEngine.java @@ -26,8 +26,8 @@ import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnection; import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnector; import org.eclipse.kuksa.connectivity.databroker.listener.DisconnectListener; -import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.listener.VssNodeListener; +import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.request.FetchRequest; import org.eclipse.kuksa.connectivity.databroker.request.SubscribeRequest; import org.eclipse.kuksa.connectivity.databroker.request.UpdateRequest; diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt index 8fb824a7..b6ea635f 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/AnyCopyExtension.kt @@ -19,9 +19,9 @@ package org.eclipse.kuksa.extension -import kotlin.reflect.KParameter import kotlin.reflect.full.instanceParameter import kotlin.reflect.full.memberFunctions +import kotlin.reflect.full.valueParameters /** * Uses reflection to create a copy with any constructor parameter which matches the given [paramToValue] map. @@ -37,16 +37,20 @@ internal fun T.copy(paramToValue: Map = emptyMap()): T { val copyFunction = instanceClass::memberFunctions.get().first { it.name == "copy" } val instanceParameter = copyFunction.instanceParameter ?: return this - val valueArgs = copyFunction.parameters - .filter { parameter -> - parameter.kind == KParameter.Kind.VALUE - }.mapNotNull { parameter -> + val valueArgs = copyFunction.valueParameters + .mapNotNull { parameter -> paramToValue[parameter.name]?.let { value -> parameter to value } } val parameterToInstance = mapOf(instanceParameter to this) val parameterToValue = parameterToInstance + valueArgs - val copy = copyFunction.callBy(parameterToValue) ?: this + + val copy: Any + try { + copy = copyFunction.callBy(parameterToValue) ?: this + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("${this::class.simpleName} copy parameters do not match: $paramToValue", e) + } return copy as T } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt index 8254d28d..7bea7313 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt @@ -35,14 +35,16 @@ val Types.Metadata.valueType: ValueCase get() = dataType.dataPointValueCase /** - * Converts the [VssSignal.value] into a [Datapoint] object. + * Converts the [VssSignal.value] into a [Datapoint] object. The [VssSignal.dataType] is used to derive the correct + * [ValueCase]. * * @throws IllegalArgumentException if the [VssSignal] could not be converted to a [Datapoint]. */ +@OptIn(ExperimentalUnsignedTypes::class) val VssSignal.datapoint: Datapoint get() { val stringValue = value.toString() - return when (value::class) { + return when (dataType) { String::class -> ValueCase.STRING.createDatapoint(stringValue) Boolean::class -> ValueCase.BOOL.createDatapoint(stringValue) Float::class -> ValueCase.FLOAT.createDatapoint(stringValue) @@ -54,6 +56,8 @@ val VssSignal.datapoint: Datapoint IntArray::class -> ValueCase.INT32_ARRAY.createDatapoint(stringValue) BooleanArray::class -> ValueCase.BOOL_ARRAY.createDatapoint(stringValue) LongArray::class -> ValueCase.INT64_ARRAY.createDatapoint(stringValue) + FloatArray::class -> ValueCase.FLOAT_ARRAY.createDatapoint(stringValue) + UIntArray::class -> ValueCase.UINT32_ARRAY.createDatapoint(stringValue) else -> throw IllegalArgumentException("Could not create datapoint for the value class: ${value::class}!") } diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt index 3b668bf5..f3700021 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt @@ -27,7 +27,7 @@ import org.eclipse.kuksa.vsscore.model.VssSignal import org.eclipse.kuksa.vsscore.model.findHeritageLine import org.eclipse.kuksa.vsscore.model.heritage import org.eclipse.kuksa.vsscore.model.variableName -import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.declaredMemberProperties /** * Creates a copy of the [VssNode] where the whole [VssNode.findHeritageLine] is replaced @@ -92,8 +92,8 @@ fun VssSignal.copy(datapoint: Datapoint): VssSignal { BOOL -> bool INT32 -> int32 INT64 -> int64 - UINT32 -> uint32.toUInt() - UINT64 -> uint64.toULong() + UINT32 -> uint32 + UINT64 -> uint64 FLOAT -> float DOUBLE -> double STRING_ARRAY -> stringArray.valuesList @@ -136,11 +136,12 @@ fun VssSignal.copy(datapoint: Datapoint): VssSignal { * Calls the generated copy method of the data class for the [VssSignal] and returns a new copy with the new [value]. * * @throws [IllegalArgumentException] if the copied types do not match. - * @throws [NoSuchElementException] if no copy method was found for the class. + * @throws [NoSuchElementException] if no copy method nor [valuePropertyName] was found for the class. */ -fun VssSignal.copy(value: T): VssSignal { - val memberProperties = VssSignal::class.memberProperties - val firstPropertyName = memberProperties.first().name +@JvmOverloads +fun VssSignal.copy(value: T, valuePropertyName: String = "value"): VssSignal { + val memberProperties = VssSignal::class.declaredMemberProperties + val firstPropertyName = memberProperties.first { it.name == valuePropertyName }.name val valueMap = mapOf(firstPropertyName to value) return this@copy.copy(valueMap) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt index d81fc36f..86e45109 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssDriver.kt @@ -64,6 +64,9 @@ data class VssDriver @JvmOverloads constructor( data class VssHeartRate @JvmOverloads constructor( override val `value`: Int = 0, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val comment: String get() = "" diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt index 8fdc51ee..325cc442 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssNodeCopyTest.kt @@ -78,8 +78,7 @@ class VssNodeCopyTest : BehaviorSpec({ } and("a changed invalid DataPoint") { - val newValue = 50 - val datapoint = Types.Datapoint.newBuilder().setUint32(newValue).build() + val datapoint = Types.Datapoint.newBuilder().setBool(false).build() `when`("a copy is done") { val exception = shouldThrow { @@ -87,7 +86,8 @@ class VssNodeCopyTest : BehaviorSpec({ } then("it should throw an IllegalArgumentException") { - exception.message shouldStartWith "argument type mismatch" + val signalName = driverHeartRate::class.simpleName + exception.message shouldStartWith "$signalName copy parameters do not match" } } } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt index ff1cad8a..a9faf7a3 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/vssNode/VssVehicle.kt @@ -69,6 +69,9 @@ data class VssPassenger( override val comment: String = "", override val value: Int = 80, ) : VssSignal { + override val dataType: KClass<*> + get() = UInt::class + override val parentClass: KClass<*> get() = VssPassenger::class } diff --git a/samples/src/main/java/com/example/sample/JavaActivity.java b/samples/src/main/java/com/example/sample/JavaActivity.java index 1271330f..87b54bd4 100644 --- a/samples/src/main/java/com/example/sample/JavaActivity.java +++ b/samples/src/main/java/com/example/sample/JavaActivity.java @@ -26,8 +26,8 @@ import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnection; import org.eclipse.kuksa.connectivity.databroker.DataBrokerConnector; import org.eclipse.kuksa.connectivity.databroker.listener.DisconnectListener; -import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.listener.VssNodeListener; +import org.eclipse.kuksa.connectivity.databroker.listener.VssPathListener; import org.eclipse.kuksa.connectivity.databroker.request.FetchRequest; import org.eclipse.kuksa.connectivity.databroker.request.SubscribeRequest; import org.eclipse.kuksa.connectivity.databroker.request.UpdateRequest; diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt index b613f616..e4959bad 100644 --- a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt @@ -82,11 +82,33 @@ interface VssBranch : VssNode { * Some [VssNode] may have an additional [value] property. These are children [VssSignal] which do not have other * children. */ -interface VssSignal : VssNode { +interface VssSignal : VssNode { /** * A primitive type value. */ val value: T + + /** + * The VSS data type which is compatible with the data broker. This may differ from the [value] type because + * Java compatibility needs to be ensured and inline classes like [UInt] (Kotlin) are not known to Java. + * + * ### Example + * Vehicle.Driver.HeartRate: + * datatype: uint16 + * + * generates --> + * + * public data class VssHeartRate ( + * override val `value`: Int = 0, + * ) : VssSignal { + * override val dataType: KClass<*> + * get() = UInt:class + * } + * + * To ensure java compatibility [UInt] is not used here for Kotlin (inline class). + */ + val dataType: KClass<*> + get() = value::class } /** diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index bc99bfec..555fb3a4 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -20,10 +20,12 @@ package org.eclipse.kuksa.vssprocessor.spec import com.google.devtools.ksp.processing.KSPLogger +import com.squareup.kotlinpoet.AnnotationSpec import com.squareup.kotlinpoet.ClassName import com.squareup.kotlinpoet.FunSpec import com.squareup.kotlinpoet.KModifier import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter import com.squareup.kotlinpoet.PropertySpec @@ -58,9 +60,11 @@ internal class VssNodeSpecModel( private val stringTypeName = String::class.asTypeName() private val vssNodeSetTypeName = Set::class.parameterizedBy(VssNode::class) - private val genericClassTypeName = KClass::class.asClassName().parameterizedBy(STAR).copy(nullable = true) + private val genericClassTypeName = KClass::class.asClassName().parameterizedBy(STAR) + private val genericClassTypeNameNullable = KClass::class.asClassName().parameterizedBy(STAR).copy(nullable = true) - private val datatypeProperty: TypeName + @OptIn(ExperimentalUnsignedTypes::class) + private val datatypeTypeName: TypeName get() { return when (datatype) { "string" -> String::class.asTypeName() @@ -73,19 +77,33 @@ internal class VssNodeSpecModel( "double" -> Double::class.asTypeName() "string[]" -> Array::class.parameterizedBy(String::class) "boolean[]" -> BooleanArray::class.asTypeName() - "uint8[]", "uint16[]", "uint32[]", "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() + "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() + "uint8[]", "uint16[]", "uint32[]" -> UIntArray::class.asTypeName() "int64[]", "uint64[]" -> LongArray::class.asTypeName() "float[]" -> FloatArray::class.asTypeName() else -> Any::class.asTypeName() } } + @OptIn(ExperimentalUnsignedTypes::class) + private val valueTypeName: TypeName + get() { + return when (datatypeTypeName) { + // Convert the following Kotlin types because they are incompatible with the @JvmOverloads annotation + UInt::class.asTypeName() -> Int::class.asTypeName() + ULong::class.asTypeName() -> Long::class.asTypeName() + UIntArray::class.asTypeName() -> IntArray::class.asTypeName() + else -> datatypeTypeName + } + } + /** * Returns valid default values as string literals. */ + @OptIn(ExperimentalUnsignedTypes::class) private val defaultValue: String get() { - return when (datatypeProperty) { + return when (valueTypeName) { String::class.asTypeName() -> "\"\"" Boolean::class.asTypeName() -> "false" Float::class.asTypeName() -> "0f" @@ -97,8 +115,10 @@ internal class VssNodeSpecModel( IntArray::class.asTypeName() -> "IntArray(0)" BooleanArray::class.asTypeName() -> "BooleanArray(0)" LongArray::class.asTypeName() -> "LongArray(0)" + UIntArray::class.asTypeName() -> "UIntArray(0)" + FloatArray::class.asTypeName() -> "FloatArray(0)" - else -> throw IllegalArgumentException("No default value found for $datatypeProperty!") + else -> throw IllegalArgumentException("No default value found for $valueTypeName!") } } @@ -113,22 +133,21 @@ internal class VssNodeSpecModel( val nestedChildSpecs = mutableListOf() val constructorBuilder = FunSpec.constructorBuilder() + .addAnnotation(JvmOverloads::class) val propertySpecs = mutableListOf() val superInterfaces = mutableSetOf(VssBranch::class.asTypeName()) // The last element in the chain should have a value like "isLocked". - if (childNodes.isEmpty()) { - val (valuePropertySpec, parameterSpec) = createValueSpec() - - constructorBuilder.addParameter(parameterSpec) - propertySpecs.add(valuePropertySpec) + val isVssSignal = childNodes.isEmpty() + if (isVssSignal) { + val (vssSignalTypeName, vssSignalPropertySpecs, vssSignalParameterSpec) = createVssSignalSpec() // Final leafs should ONLY implement the VssSignal interface superInterfaces.clear() - val vssSignalInterface = VssSignal::class - .asTypeName() - .plusParameter(datatypeProperty) - superInterfaces.add(vssSignalInterface) + superInterfaces.add(vssSignalTypeName) + + propertySpecs.addAll(vssSignalPropertySpecs) + vssSignalParameterSpec?.let { constructorBuilder.addParameter(it) } } val propertySpec = createVssNodeSpecs(className, packageName = packageName) @@ -162,11 +181,10 @@ internal class VssNodeSpecModel( } val defaultClassName = childNode.className - val defaultParameter = createDefaultParameterSpec( - mainClassPropertySpec.name, - defaultClassName, - uniquePackageName, - ) + val defaultParameter = ParameterSpec + .builder(mainClassPropertySpec.name, ClassName(uniquePackageName, defaultClassName)) + .defaultValue("%L()", defaultClassName) + .build() constructorBuilder.addParameter(defaultParameter) } @@ -186,31 +204,62 @@ internal class VssNodeSpecModel( .build() } - private fun createValueSpec(): Pair { - val valuePropertySpec = PropertySpec - .builder(PROPERTY_VALUE_NAME, datatypeProperty) - .initializer(PROPERTY_VALUE_NAME) + private fun createVssSignalSpec(): Triple, ParameterSpec?> { + val propertySpecs = mutableListOf() + var parameterSpec: ParameterSpec? = null + + val vssSignalMembers = VssSignal::class.declaredMemberProperties + vssSignalMembers.forEach { member -> + val memberName = member.name + when (val memberType = member.returnType.asTypeName()) { + genericClassTypeName -> { + val genericClassSpec = createGenericClassSpec( + memberName, + memberType, + datatypeTypeName.toString(), + ) + propertySpecs.add(genericClassSpec) + } + + else -> { + val (classPropertySpec, classParameterSpec) = createClassParamSpec( + memberName, + valueTypeName, + defaultValue, + ) + parameterSpec = classParameterSpec + propertySpecs.add(classPropertySpec) + } + } + } + + val typeName = VssSignal::class + .asTypeName() + .plusParameter(valueTypeName) + + return Triple(typeName, propertySpecs, parameterSpec) + } + + private fun createClassParamSpec( + memberName: String, + typeName: TypeName, + defaultValue: String, + ): Pair { + val propertySpec = PropertySpec + .builder(memberName, typeName) + .initializer(memberName) .addModifiers(KModifier.OVERRIDE) .build() // Adds a default value (mainly 0 or an empty string) val parameterSpec = ParameterSpec.builder( - valuePropertySpec.name, - valuePropertySpec.type, + propertySpec.name, + propertySpec.type, ).defaultValue("%L", defaultValue).build() - return Pair(valuePropertySpec, parameterSpec) + return Pair(propertySpec, parameterSpec) } - private fun createDefaultParameterSpec( - parameterName: String, - defaultClassName: String, - packageName: String, - ) = ParameterSpec - .builder(parameterName, ClassName(packageName, defaultClassName)) - .defaultValue("%L()", defaultClassName) - .build() - private fun createVssNodeSpecs( className: String, packageName: String, @@ -268,43 +317,14 @@ internal class VssNodeSpecModel( } private fun createVssNodeTreeSpecs(childNodes: List): List { - fun createSetSpec(memberName: String, memberType: TypeName): PropertySpec { - val vssNodeNamesJoined = childNodes.joinToString(", ") { it.variableName } - - return PropertySpec - .builder(memberName, memberType) - .mutable(false) - .addModifiers(KModifier.OVERRIDE) - .getter( - FunSpec.getterBuilder() - .addStatement("return setOf(%L)", vssNodeNamesJoined) - .build(), - ) - .build() - } - - fun createParentSpec(memberName: String, memberType: TypeName): PropertySpec { - val parentClass = if (parentClassName.isNotEmpty()) "$parentClassName::class" else "null" - return PropertySpec - .builder(memberName, memberType) - .mutable(false) - .addModifiers(KModifier.OVERRIDE) - .getter( - FunSpec.getterBuilder() - .addStatement("return %L", parentClass) - .build(), - ) - .build() - } - val propertySpecs = mutableListOf() val members = VssNode::class.declaredMemberProperties members.forEach { member -> val memberName = member.name when (val memberType = member.returnType.asTypeName()) { - vssNodeSetTypeName -> createSetSpec(memberName, memberType) - genericClassTypeName -> createParentSpec(memberName, memberType) + vssNodeSetTypeName -> createSetSpec(memberName, memberType, childNodes) + genericClassTypeNameNullable -> createGenericClassSpec(memberName, memberType, parentClassName) else -> null }?.let { propertySpec -> propertySpecs.add(propertySpec) @@ -314,6 +334,50 @@ internal class VssNodeSpecModel( return propertySpecs } + private fun createGenericClassSpec(memberName: String, memberType: TypeName, className: String): PropertySpec { + val parentClass = if (className.isNotEmpty()) "$className::class" else "null" + + val propertySpecBuilder = PropertySpec + .builder(memberName, memberType) + + // Removed the warning about ExperimentalUnsignedTypes + if (experimentalUnsignedTypes.contains(className)) { + val optInClassName = ClassName("kotlin", "OptIn") + val optInAnnotationSpec = AnnotationSpec.builder(optInClassName) + .addMember("ExperimentalUnsignedTypes::class") + .build() + + propertySpecBuilder.addAnnotation(optInAnnotationSpec) + } + + return propertySpecBuilder + .addModifiers(KModifier.OVERRIDE) + .getter( + FunSpec.getterBuilder() + .addStatement("return %L", parentClass) + .build(), + ) + .build() + } + + private fun createSetSpec( + memberName: String, + memberType: TypeName, + members: Collection, + ): PropertySpec { + val vssNodeNamesJoined = members.joinToString(", ") { it.variableName } + + return PropertySpec + .builder(memberName, memberType) + .addModifiers(KModifier.OVERRIDE) + .getter( + FunSpec.getterBuilder() + .addStatement("return setOf(%L)", vssNodeNamesJoined) + .build(), + ) + .build() + } + override fun equals(other: Any?): Boolean { if (other !is VssNodeSpecModel) return false @@ -329,7 +393,7 @@ internal class VssNodeSpecModel( } companion object { - private const val PROPERTY_VALUE_NAME = "value" + private val experimentalUnsignedTypes = setOf("kotlin.UIntArray") } } From b3d887ca3661f82f437bf304faec89eaa6bc9c96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Mon, 18 Mar 2024 11:34:46 +0100 Subject: [PATCH 4/9] test: Add test for updating an invalid VssModel value --- .../databroker/DataBrokerConnectionTest.kt | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt index 2bdac1fe..6d4e5054 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt @@ -153,22 +153,36 @@ class DataBrokerConnectionTest : BehaviorSpec({ and("A VssNode") { val vssDriver = VssDriver() - `when`("Fetching the node") { + and("A default HeartRate") { + val newHeartRateValue = 60 + val datapoint = Types.Datapoint.newBuilder().setUint32(newHeartRateValue).build() + val defaultUpdateRequest = UpdateRequest(vssDriver.heartRate.vssPath, datapoint) - and("The initial value is different from the default for a child") { - val newHeartRateValue = 60 - val datapoint = Types.Datapoint.newBuilder().setUint32(newHeartRateValue).build() - val updateRequest = UpdateRequest(vssDriver.heartRate.vssPath, datapoint) + dataBrokerConnection.update(defaultUpdateRequest) - dataBrokerConnection.update(updateRequest) + `when`("Fetching the node") { - val fetchRequest = VssNodeFetchRequest(vssDriver) - val updatedDriver = dataBrokerConnection.fetch(fetchRequest) + and("The initial value is different from the default for a child") { + val fetchRequest = VssNodeFetchRequest(vssDriver) + val updatedDriver = dataBrokerConnection.fetch(fetchRequest) - then("Every child node has been updated with the correct value") { - val heartRate = updatedDriver.heartRate + then("Every child node has been updated with the correct value") { + val heartRate = updatedDriver.heartRate - heartRate.value shouldBe newHeartRateValue + heartRate.value shouldBe newHeartRateValue + } + } + } + + `when`("Updating the node with an invalid value") { + val invalidHeartRate = VssDriver.VssHeartRate(-5) // UInt on DataBroker side + val vssNodeUpdateRequest = VssNodeUpdateRequest(invalidHeartRate) + dataBrokerConnection.update(vssNodeUpdateRequest) + + val fetchRequest = VssNodeFetchRequest(invalidHeartRate) + val fetchedVssHeartRate = dataBrokerConnection.fetch(fetchRequest) + then("the updated value should be ignored") { + fetchedVssHeartRate.value shouldBe 60 } } } From 16bb945c35bc1539a8c8e3b929890b91b1380512 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Mon, 18 Mar 2024 15:32:30 +0100 Subject: [PATCH 5/9] chore: Add ULongArray to supported data types --- .../databroker/DataBrokerConnectionTest.kt | 12 ++++++------ .../org/eclipse/kuksa/vsscore/model/VssNode.kt | 2 +- .../kuksa/vssprocessor/spec/VssNodeSpecModel.kt | 7 +++++-- .../kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt index 6d4e5054..b4644d32 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt @@ -23,6 +23,7 @@ import io.grpc.ManagedChannel import io.kotest.assertions.nondeterministic.eventually import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.kotest.matchers.string.shouldContain import io.mockk.clearMocks import io.mockk.every @@ -106,7 +107,7 @@ class DataBrokerConnectionTest : BehaviorSpec({ val response = dataBrokerConnection.update(updateRequest) then("No error should appear") { - Assertions.assertFalse(response.hasError()) + response.hasError() shouldBe false } and("When fetching it afterwards") { @@ -177,12 +178,11 @@ class DataBrokerConnectionTest : BehaviorSpec({ `when`("Updating the node with an invalid value") { val invalidHeartRate = VssDriver.VssHeartRate(-5) // UInt on DataBroker side val vssNodeUpdateRequest = VssNodeUpdateRequest(invalidHeartRate) - dataBrokerConnection.update(vssNodeUpdateRequest) + val response = dataBrokerConnection.update(vssNodeUpdateRequest) - val fetchRequest = VssNodeFetchRequest(invalidHeartRate) - val fetchedVssHeartRate = dataBrokerConnection.fetch(fetchRequest) - then("the updated value should be ignored") { - fetchedVssHeartRate.value shouldBe 60 + then("the updated show") { + val errorResponse = response.firstOrNull { it.errorsCount >= 1 } + errorResponse shouldNotBe null } } } diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt index e4959bad..b9acc481 100644 --- a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt @@ -89,7 +89,7 @@ interface VssSignal : VssNode { val value: T /** - * The VSS data type which is compatible with the data broker. This may differ from the [value] type because + * The VSS data type which is compatible with the Databroker. This may differ from the [value] type because * Java compatibility needs to be ensured and inline classes like [UInt] (Kotlin) are not known to Java. * * ### Example diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index 555fb3a4..12f5cd48 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -79,7 +79,8 @@ internal class VssNodeSpecModel( "boolean[]" -> BooleanArray::class.asTypeName() "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() "uint8[]", "uint16[]", "uint32[]" -> UIntArray::class.asTypeName() - "int64[]", "uint64[]" -> LongArray::class.asTypeName() + "int64[]" -> LongArray::class.asTypeName() + "uint64[]" -> ULongArray::class.asTypeName() "float[]" -> FloatArray::class.asTypeName() else -> Any::class.asTypeName() } @@ -93,6 +94,7 @@ internal class VssNodeSpecModel( UInt::class.asTypeName() -> Int::class.asTypeName() ULong::class.asTypeName() -> Long::class.asTypeName() UIntArray::class.asTypeName() -> IntArray::class.asTypeName() + ULongArray::class.asTypeName() -> LongArray::class.asTypeName() else -> datatypeTypeName } } @@ -114,9 +116,10 @@ internal class VssNodeSpecModel( Array::class.parameterizedBy(String::class) -> "emptyArray()" IntArray::class.asTypeName() -> "IntArray(0)" BooleanArray::class.asTypeName() -> "BooleanArray(0)" + FloatArray::class.asTypeName() -> "FloatArray(0)" LongArray::class.asTypeName() -> "LongArray(0)" + ULongArray::class.asTypeName() -> "ULongArray(0)" UIntArray::class.asTypeName() -> "UIntArray(0)" - FloatArray::class.asTypeName() -> "FloatArray(0)" else -> throw IllegalArgumentException("No default value found for $valueTypeName!") } diff --git a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt index ca129e7a..c2303ed7 100644 --- a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt +++ b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt @@ -65,7 +65,7 @@ class VssNodeSpecModelTest : BehaviorSpec({ propertySpec.toString() shouldContain "kotlin.Int = 0" } - then("it should the correct inline class data type") { + then("it should have the correct inline class data type") { val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } propertySpec?.getter.toString() shouldContain "kotlin.UInt::class" @@ -99,7 +99,7 @@ class VssNodeSpecModelTest : BehaviorSpec({ propertySpec.toString() shouldContain "kotlin.LongArray = LongArray(0)" } - then("it should the correct inline class data type") { + then("it should have the correct inline class data type") { val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } propertySpec?.getter.toString() shouldContain "kotlin.LongArray::class" From f3c5147f9fb319b7aea1d97b54d176ea9fa253c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Mon, 18 Mar 2024 16:44:11 +0100 Subject: [PATCH 6/9] test: Adapt test for ULongArray data type --- .../org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt index c2303ed7..2a9ca264 100644 --- a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt +++ b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt @@ -102,7 +102,7 @@ class VssNodeSpecModelTest : BehaviorSpec({ then("it should have the correct inline class data type") { val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } - propertySpec?.getter.toString() shouldContain "kotlin.LongArray::class" + propertySpec?.getter.toString() shouldContain "kotlin.ULongArray::class" } } } From 610f9ca643653e30baa0e05500dcea2efda01099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Tue, 19 Mar 2024 15:02:55 +0100 Subject: [PATCH 7/9] chore: Add missing types for Vss <-> Kotlin <-> Kuksa --- .../kuksa/extension/DataPointExtension.kt | 40 +++++++++++-------- .../extension/DataTypeConversionExtension.kt | 17 +++++--- .../databroker/DataBrokerConnectionTest.kt | 2 +- .../vssprocessor/spec/VssNodeSpecModel.kt | 3 ++ 4 files changed, 38 insertions(+), 24 deletions(-) diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt index 7bea7313..45c003b3 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataPointExtension.kt @@ -43,24 +43,30 @@ val Types.Metadata.valueType: ValueCase @OptIn(ExperimentalUnsignedTypes::class) val VssSignal.datapoint: Datapoint get() { - val stringValue = value.toString() - return when (dataType) { - String::class -> ValueCase.STRING.createDatapoint(stringValue) - Boolean::class -> ValueCase.BOOL.createDatapoint(stringValue) - Float::class -> ValueCase.FLOAT.createDatapoint(stringValue) - Double::class -> ValueCase.DOUBLE.createDatapoint(stringValue) - Int::class -> ValueCase.INT32.createDatapoint(stringValue) - Long::class -> ValueCase.INT64.createDatapoint(stringValue) - UInt::class -> ValueCase.UINT32.createDatapoint(stringValue) - Array::class -> ValueCase.DOUBLE.createDatapoint(stringValue) - IntArray::class -> ValueCase.INT32_ARRAY.createDatapoint(stringValue) - BooleanArray::class -> ValueCase.BOOL_ARRAY.createDatapoint(stringValue) - LongArray::class -> ValueCase.INT64_ARRAY.createDatapoint(stringValue) - FloatArray::class -> ValueCase.FLOAT_ARRAY.createDatapoint(stringValue) - UIntArray::class -> ValueCase.UINT32_ARRAY.createDatapoint(stringValue) - - else -> throw IllegalArgumentException("Could not create datapoint for the value class: ${value::class}!") + val valueCase = when (dataType) { + String::class -> ValueCase.STRING + Boolean::class -> ValueCase.BOOL + Int::class -> ValueCase.INT32 + Float::class -> ValueCase.FLOAT + Double::class -> ValueCase.DOUBLE + Long::class -> ValueCase.INT64 + UInt::class -> ValueCase.UINT32 + ULong::class -> ValueCase.UINT64 + Array::class -> ValueCase.DOUBLE + BooleanArray::class -> ValueCase.BOOL_ARRAY + IntArray::class -> ValueCase.INT32_ARRAY + FloatArray::class -> ValueCase.FLOAT_ARRAY + DoubleArray::class -> ValueCase.DOUBLE_ARRAY + LongArray::class -> ValueCase.INT64_ARRAY + UIntArray::class -> ValueCase.UINT32_ARRAY + ULongArray::class -> ValueCase.UINT64_ARRAY + + else -> throw IllegalArgumentException("Could not create datapoint for value class: ${dataType::class}!") } + + val stringValue = value.toString() + + return valueCase.createDatapoint(stringValue) } /** diff --git a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataTypeConversionExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataTypeConversionExtension.kt index a3a49786..4d0b7d87 100644 --- a/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataTypeConversionExtension.kt +++ b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/DataTypeConversionExtension.kt @@ -41,30 +41,35 @@ val Types.DataType.dataPointValueCase: ValueCase -> ValueCase.INT32 Types.DataType.DATA_TYPE_INT64 -> ValueCase.INT64 + Types.DataType.DATA_TYPE_UINT8, Types.DataType.DATA_TYPE_UINT16, Types.DataType.DATA_TYPE_UINT32, - Types.DataType.DATA_TYPE_UINT64, - -> ValueCase.UINT64 + -> ValueCase.UINT32 + + Types.DataType.DATA_TYPE_UINT64 -> ValueCase.UINT64 - Types.DataType.DATA_TYPE_BOOLEAN_ARRAY -> ValueCase.BOOL_ARRAY Types.DataType.DATA_TYPE_INT8_ARRAY, Types.DataType.DATA_TYPE_INT16_ARRAY, Types.DataType.DATA_TYPE_INT32_ARRAY, -> ValueCase.INT32_ARRAY Types.DataType.DATA_TYPE_INT64_ARRAY -> ValueCase.INT64_ARRAY + Types.DataType.DATA_TYPE_UINT8_ARRAY, Types.DataType.DATA_TYPE_UINT16_ARRAY, Types.DataType.DATA_TYPE_UINT32_ARRAY, -> ValueCase.UINT32_ARRAY Types.DataType.DATA_TYPE_UINT64_ARRAY -> ValueCase.UINT64_ARRAY - Types.DataType.DATA_TYPE_FLOAT_ARRAY -> ValueCase.FLOAT_ARRAY - Types.DataType.DATA_TYPE_DOUBLE_ARRAY -> ValueCase.DOUBLE_ARRAY - Types.DataType.DATA_TYPE_STRING_ARRAY -> ValueCase.STRING_ARRAY + Types.DataType.DATA_TYPE_STRING -> ValueCase.STRING Types.DataType.DATA_TYPE_FLOAT -> ValueCase.FLOAT Types.DataType.DATA_TYPE_DOUBLE -> ValueCase.DOUBLE + + Types.DataType.DATA_TYPE_BOOLEAN_ARRAY -> ValueCase.BOOL_ARRAY + Types.DataType.DATA_TYPE_FLOAT_ARRAY -> ValueCase.FLOAT_ARRAY + Types.DataType.DATA_TYPE_DOUBLE_ARRAY -> ValueCase.DOUBLE_ARRAY + Types.DataType.DATA_TYPE_STRING_ARRAY -> ValueCase.STRING_ARRAY } } diff --git a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt index 78e5f4ff..4551e05a 100644 --- a/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt +++ b/kuksa-sdk/src/test/kotlin/org/eclipse/kuksa/connectivity/databroker/DataBrokerConnectionTest.kt @@ -193,7 +193,7 @@ class DataBrokerConnectionTest : BehaviorSpec({ val vssNodeUpdateRequest = VssNodeUpdateRequest(invalidHeartRate) val response = dataBrokerConnection.update(vssNodeUpdateRequest) - then("the updated show") { + then("the update response should contain an error") { val errorResponse = response.firstOrNull { it.errorsCount >= 1 } errorResponse shouldNotBe null } diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index 12f5cd48..ea4cd052 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -82,6 +82,7 @@ internal class VssNodeSpecModel( "int64[]" -> LongArray::class.asTypeName() "uint64[]" -> ULongArray::class.asTypeName() "float[]" -> FloatArray::class.asTypeName() + "double[]" -> DoubleArray::class.asTypeName() else -> Any::class.asTypeName() } } @@ -113,11 +114,13 @@ internal class VssNodeSpecModel( Int::class.asTypeName() -> "0" Long::class.asTypeName() -> "0L" UInt::class.asTypeName() -> "0u" + ULong::class.asTypeName() -> "0u" Array::class.parameterizedBy(String::class) -> "emptyArray()" IntArray::class.asTypeName() -> "IntArray(0)" BooleanArray::class.asTypeName() -> "BooleanArray(0)" FloatArray::class.asTypeName() -> "FloatArray(0)" LongArray::class.asTypeName() -> "LongArray(0)" + DoubleArray::class.asTypeName() -> "DoubleArray(0)" ULongArray::class.asTypeName() -> "ULongArray(0)" UIntArray::class.asTypeName() -> "UIntArray(0)" From aecb9e4e3733a434b3f5b2f73bb4e17a8d3c71e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Tue, 19 Mar 2024 15:03:29 +0100 Subject: [PATCH 8/9] chore: Extract VssSignal + VssBranch interface --- .../eclipse/kuksa/vsscore/model/VssBranch.kt | 28 ++++++++ .../eclipse/kuksa/vsscore/model/VssNode.kt | 60 ---------------- .../eclipse/kuksa/vsscore/model/VssSignal.kt | 72 +++++++++++++++++++ 3 files changed, 100 insertions(+), 60 deletions(-) create mode 100644 vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssBranch.kt create mode 100644 vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssSignal.kt diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssBranch.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssBranch.kt new file mode 100644 index 00000000..91520d31 --- /dev/null +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssBranch.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.kuksa.vsscore.model + +/** + * Defines a [VssNode] which is not a [VssSignal] and only acts as a branch with one or more children. The [type] is + * always "branch". + */ +interface VssBranch : VssNode { + override val type: String + get() = "branch" +} diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt index b9acc481..746ffaad 100644 --- a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssNode.kt @@ -69,48 +69,6 @@ interface VssNode { get() = null } -/** - * Defines a [VssNode] which is not a [VssSignal] and only acts as a branch with one or more children. The [type] is - * always "branch". - */ -interface VssBranch : VssNode { - override val type: String - get() = "branch" -} - -/** - * Some [VssNode] may have an additional [value] property. These are children [VssSignal] which do not have other - * children. - */ -interface VssSignal : VssNode { - /** - * A primitive type value. - */ - val value: T - - /** - * The VSS data type which is compatible with the Databroker. This may differ from the [value] type because - * Java compatibility needs to be ensured and inline classes like [UInt] (Kotlin) are not known to Java. - * - * ### Example - * Vehicle.Driver.HeartRate: - * datatype: uint16 - * - * generates --> - * - * public data class VssHeartRate ( - * override val `value`: Int = 0, - * ) : VssSignal { - * override val dataType: KClass<*> - * get() = UInt:class - * } - * - * To ensure java compatibility [UInt] is not used here for Kotlin (inline class). - */ - val dataType: KClass<*> - get() = value::class -} - /** * Splits the [VssNode.vssPath] into its parts. */ @@ -241,21 +199,3 @@ fun VssNode.findHeritageLine( return heritageLine } - -/** - * Finds the given [signal] inside the current [VssNode]. - */ -inline fun , V : Any> VssNode.findSignal(signal: T): VssNode { - return heritage - .first { it.uuid == signal.uuid } -} - -/** - * Finds all [VssSignal] which matches the given [KClass.simpleName]. This is useful when multiple nested objects - * with the same Name exists but are pretty much the same besides the [VssNode.vssPath] etc. - */ -inline fun , V : Any> VssNode.findSignal(type: KClass): Map { - return heritage - .filter { it::class.simpleName == type.simpleName } - .associateBy { it.vssPath } -} diff --git a/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssSignal.kt b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssSignal.kt new file mode 100644 index 00000000..6bc531fa --- /dev/null +++ b/vss-core/src/main/kotlin/org/eclipse/kuksa/vsscore/model/VssSignal.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.eclipse.kuksa.vsscore.model + +import kotlin.reflect.KClass + +/** + * Some [VssNode] may have an additional [value] property. These are children [VssSignal] which do not have other + * children. + */ +interface VssSignal : VssNode { + /** + * A primitive type value. + */ + val value: T + + /** + * The VSS data type which is compatible with the Databroker. This may differ from the [value] type because + * Java compatibility needs to be ensured and inline classes like [UInt] (Kotlin) are not known to Java. + * + * ### Example + * Vehicle.Driver.HeartRate: + * datatype: uint16 + * + * generates --> + * + * public data class VssHeartRate ( + * override val `value`: Int = 0, + * ) : VssSignal { + * override val dataType: KClass<*> + * get() = UInt:class + * } + * + * To ensure java compatibility [UInt] is not used here for Kotlin (inline class). + */ + val dataType: KClass<*> + get() = value::class +} + +/** + * Finds the given [signal] inside the current [VssNode]. + */ +inline fun , V : Any> VssNode.findSignal(signal: T): VssNode { + return heritage + .first { it.uuid == signal.uuid } +} + +/** + * Finds all [VssSignal] which matches the given [KClass.simpleName]. This is useful when multiple nested objects + * with the same Name exists but are pretty much the same besides the [VssNode.vssPath] etc. + */ +inline fun , V : Any> VssNode.findSignal(type: KClass): Map { + return heritage + .filter { it::class.simpleName == type.simpleName } + .associateBy { it.vssPath } +} From 767a785f911f21ccae58576c2d91448e838195e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mark=20Hu=CC=88sers?= Date: Tue, 19 Mar 2024 15:05:55 +0100 Subject: [PATCH 9/9] chore: Add VssDataType enum for mapping VSS types to Kotlin/Java types --- .../kuksa/vssprocessor/spec/VssDataType.kt | 73 +++++++++++++++++++ .../vssprocessor/spec/VssNodeSpecModel.kt | 62 +++------------- .../vssprocessor/spec/VssNodeSpecModelTest.kt | 21 +++--- 3 files changed, 92 insertions(+), 64 deletions(-) create mode 100644 vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssDataType.kt diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssDataType.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssDataType.kt new file mode 100644 index 00000000..485f4dc9 --- /dev/null +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssDataType.kt @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2023 Contributors to the Eclipse Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +@file:OptIn(ExperimentalUnsignedTypes::class) + +package org.eclipse.kuksa.vssprocessor.spec + +import kotlin.reflect.KClass + +/** + * The [dataType] is the compatible Kotlin representation of the VSS type. The [stringRepresentation] is the string + * literal which was used by the VSS standard. The [defaultValue] returns a valid default values as a string literal. + * Use the [valueDataType] if Java compatibility needs to be ensured because some [dataType]s are using Kotlin inline + * types which are not supported by Java e.g. [UInt]. + */ +enum class VssDataType( + val dataType: KClass<*>, + val stringRepresentation: String, + val defaultValue: String, + val valueDataType: KClass<*> = dataType, +) { + UNKNOWN(Any::class, "Any", "null"), + STRING(String::class, "string", "\"\""), + BOOL(Boolean::class, "boolean", "false"), + INT8(Int::class, "int8", "0"), + INT16(Int::class, "int16", "0"), + INT32(Int::class, "int32", "0"), + INT64(Long::class, "int64", "0L"), + UINT8(Int::class, "uint8", "0", Int::class), + UINT16(UInt::class, "uint16", "0", Int::class), + UINT32(UInt::class, "uint32", "0", Int::class), + UINT64(ULong::class, "uint64", "0L", Long::class), + FLOAT(Float::class, "float", "0f"), + DOUBLE(Double::class, "double", "0.0"), + STRING_ARRAY(Array::class, "string[]", "emptyArray()"), + BOOL_ARRAY(BooleanArray::class, "boolean[]", "BooleanArray(0)"), + INT8_ARRAY(IntArray::class, "int8[]", "IntArray(0)"), + INT16_ARRAY(IntArray::class, "int16[]", "IntArray(0)"), + INT32_ARRAY(IntArray::class, "int32[]", "IntArray(0)"), + INT64_ARRAY(LongArray::class, "int64[]", "LongArray(0)"), + UINT8_ARRAY(UIntArray::class, "uint8[]", "IntArray(0)", IntArray::class), + UINT16_ARRAY(UIntArray::class, "uint16[]", "IntArray(0)", IntArray::class), + UINT32_ARRAY(UIntArray::class, "uint32[]", "IntArray(0)", IntArray::class), + UINT64_ARRAY(ULongArray::class, "uint64[]", "LongArray(0)", LongArray::class), + FLOAT_ARRAY(FloatArray::class, "float[]", "FloatArray(0)"), + DOUBLE_ARRAY(DoubleArray::class, "double[]", "DoubleArray(0)"), + ; + + companion object { + /** + * Find the correct [VssDataType] by the given [stringRepresentation]. Returns [UNKNOWN] for undefined + * [stringRepresentation]s. + */ + fun find(stringRepresentation: String): VssDataType { + return entries.find { it.stringRepresentation == stringRepresentation } ?: UNKNOWN + } + } +} diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt index ea4cd052..1783980d 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModel.kt @@ -63,70 +63,26 @@ internal class VssNodeSpecModel( private val genericClassTypeName = KClass::class.asClassName().parameterizedBy(STAR) private val genericClassTypeNameNullable = KClass::class.asClassName().parameterizedBy(STAR).copy(nullable = true) - @OptIn(ExperimentalUnsignedTypes::class) + private val vssDataType by lazy { VssDataType.find(datatype) } + private val datatypeTypeName: TypeName get() { - return when (datatype) { - "string" -> String::class.asTypeName() - "boolean" -> Boolean::class.asTypeName() - "uint8", "uint16", "uint32" -> UInt::class.asTypeName() - "uint64" -> ULong::class.asTypeName() - "int8", "int16", "int32" -> Int::class.asTypeName() - "int64" -> Long::class.asTypeName() - "float" -> Float::class.asTypeName() - "double" -> Double::class.asTypeName() - "string[]" -> Array::class.parameterizedBy(String::class) - "boolean[]" -> BooleanArray::class.asTypeName() - "int8[]", "int16[]", "int32[]" -> IntArray::class.asTypeName() - "uint8[]", "uint16[]", "uint32[]" -> UIntArray::class.asTypeName() - "int64[]" -> LongArray::class.asTypeName() - "uint64[]" -> ULongArray::class.asTypeName() - "float[]" -> FloatArray::class.asTypeName() - "double[]" -> DoubleArray::class.asTypeName() - else -> Any::class.asTypeName() + return when (vssDataType) { + VssDataType.STRING_ARRAY -> vssDataType.dataType.parameterizedBy(String::class) + else -> vssDataType.dataType.asTypeName() } } - @OptIn(ExperimentalUnsignedTypes::class) private val valueTypeName: TypeName get() { - return when (datatypeTypeName) { - // Convert the following Kotlin types because they are incompatible with the @JvmOverloads annotation - UInt::class.asTypeName() -> Int::class.asTypeName() - ULong::class.asTypeName() -> Long::class.asTypeName() - UIntArray::class.asTypeName() -> IntArray::class.asTypeName() - ULongArray::class.asTypeName() -> LongArray::class.asTypeName() - else -> datatypeTypeName + return when (vssDataType) { + VssDataType.STRING_ARRAY -> vssDataType.valueDataType.parameterizedBy(String::class) + else -> vssDataType.valueDataType.asTypeName() } } - /** - * Returns valid default values as string literals. - */ - @OptIn(ExperimentalUnsignedTypes::class) private val defaultValue: String - get() { - return when (valueTypeName) { - String::class.asTypeName() -> "\"\"" - Boolean::class.asTypeName() -> "false" - Float::class.asTypeName() -> "0f" - Double::class.asTypeName() -> "0.0" - Int::class.asTypeName() -> "0" - Long::class.asTypeName() -> "0L" - UInt::class.asTypeName() -> "0u" - ULong::class.asTypeName() -> "0u" - Array::class.parameterizedBy(String::class) -> "emptyArray()" - IntArray::class.asTypeName() -> "IntArray(0)" - BooleanArray::class.asTypeName() -> "BooleanArray(0)" - FloatArray::class.asTypeName() -> "FloatArray(0)" - LongArray::class.asTypeName() -> "LongArray(0)" - DoubleArray::class.asTypeName() -> "DoubleArray(0)" - ULongArray::class.asTypeName() -> "ULongArray(0)" - UIntArray::class.asTypeName() -> "UIntArray(0)" - - else -> throw IllegalArgumentException("No default value found for $valueTypeName!") - } - } + get() = vssDataType.defaultValue override fun createClassSpec( packageName: String, diff --git a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt index 2a9ca264..e384a724 100644 --- a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt +++ b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssNodeSpecModelTest.kt @@ -18,7 +18,6 @@ package org.eclipse.kuksa.vssprocessor.spec -import io.kotest.assertions.throwables.shouldThrow import io.kotest.core.spec.style.BehaviorSpec import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe @@ -139,12 +138,12 @@ class VssNodeSpecModelTest : BehaviorSpec({ val specModel = VssNodeSpecModel(datatype = "any", vssPath = "Vehicle.IgnitionType") `when`("creating a class spec") { - val exception = shouldThrow { - specModel.createClassSpec("test") - } + val classSpec = specModel.createClassSpec("test") + + then("it should have a value with an UNKNOWN (Any) datatype") { + val propertySpec = classSpec.primaryConstructor?.parameters?.find { it.name == "value" } - then("it should throw an exception") { - exception shouldNotBe null + propertySpec.toString() shouldContain "kotlin.Any = null" } } } @@ -153,12 +152,12 @@ class VssNodeSpecModelTest : BehaviorSpec({ val specModel = VssNodeSpecModel(vssPath = "Vehicle") `when`("creating a class spec without children and nested classes") { - val exception = shouldThrow { - specModel.createClassSpec("test") - } + val classSpec = specModel.createClassSpec("test") + + then("it should have a value with an UNKNOWN (Any) datatype") { + val propertySpec = classSpec.primaryConstructor?.parameters?.find { it.name == "value" } - then("it should throw an exception because it is missing a value") { - exception shouldNotBe null + propertySpec.toString() shouldContain "kotlin.Any = null" } } and("related nodes") {