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..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 @@ -35,28 +35,38 @@ 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) { - 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) - - 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/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt b/kuksa-sdk/src/main/kotlin/org/eclipse/kuksa/extension/vss/VssNodeCopyExtension.kt index a128d35e..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 @@ -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) @@ -179,7 +180,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/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 a0b85868..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 @@ -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.mockk.every import io.mockk.mockk import io.mockk.verify @@ -34,6 +35,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.extensions.updateRandomFloatValue import org.eclipse.kuksa.mocking.FriendlyVssNodeListener import org.eclipse.kuksa.mocking.FriendlyVssPathListener @@ -118,7 +120,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") { @@ -165,22 +167,35 @@ 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) + val response = dataBrokerConnection.update(vssNodeUpdateRequest) + + then("the update response should contain an error") { + val errorResponse = response.firstOrNull { it.errorsCount >= 1 } + errorResponse shouldNotBe null } } } @@ -213,10 +228,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/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 3e711dd6..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 @@ -77,6 +77,21 @@ class VssNodeCopyTest : BehaviorSpec({ } } + and("a changed invalid DataPoint") { + val datapoint = Types.Datapoint.newBuilder().setBool(false).build() + + `when`("a copy is done") { + val exception = shouldThrow { + driverHeartRate.copy(datapoint) + } + + then("it should throw an IllegalArgumentException") { + val signalName = driverHeartRate::class.simpleName + exception.message shouldStartWith "$signalName copy parameters do not match" + } + } + } + and("a changed DataPoint") { val newValue = 50 val datapoint = Types.Datapoint.newBuilder().setInt32(newValue).build() 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/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 b613f616..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,26 +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 -} - /** * Splits the [VssNode.vssPath] into its parts. */ @@ -219,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 } +} 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/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 29eebf43..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 @@ -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,49 +60,30 @@ 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 + private val vssDataType by lazy { VssDataType.find(datatype) } + + private val datatypeTypeName: TypeName get() { - 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() - "int8", "int16", "int32" -> Int::class.asTypeName() - "int64", "uint64" -> 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() - else -> Any::class.asTypeName() + return when (vssDataType) { + VssDataType.STRING_ARRAY -> vssDataType.dataType.parameterizedBy(String::class) + else -> vssDataType.dataType.asTypeName() } } - /** - * Returns valid default values as string literals. - */ - private val defaultValue: String + private val valueTypeName: TypeName get() { - return when (datatypeProperty) { - 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" - Array::class.parameterizedBy(String::class) -> "emptyArray()" - IntArray::class.asTypeName() -> "IntArray(0)" - BooleanArray::class.asTypeName() -> "BooleanArray(0)" - LongArray::class.asTypeName() -> "LongArray(0)" - - else -> throw IllegalArgumentException("No default value found for $datatypeProperty!") + return when (vssDataType) { + VssDataType.STRING_ARRAY -> vssDataType.valueDataType.parameterizedBy(String::class) + else -> vssDataType.valueDataType.asTypeName() } } + private val defaultValue: String + get() = vssDataType.defaultValue + override fun createClassSpec( packageName: String, relatedNodes: Collection, @@ -117,18 +100,16 @@ internal class VssNodeSpecModel( 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 +143,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 +166,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 +279,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 +296,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 +355,7 @@ internal class VssNodeSpecModel( } companion object { - private const val PROPERTY_VALUE_NAME = "value" + private val experimentalUnsignedTypes = setOf("kotlin.UIntArray") } } 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..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 @@ -53,17 +52,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 have the correct inline class data type") { + val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } + + propertySpec?.getter.toString() shouldContain "kotlin.UInt::class" + } } } @@ -92,6 +97,12 @@ class VssNodeSpecModelTest : BehaviorSpec({ propertySpec.toString() shouldContain "kotlin.LongArray = LongArray(0)" } + + then("it should have the correct inline class data type") { + val propertySpec = classSpec.propertySpecs.find { it.name == "dataType" } + + propertySpec?.getter.toString() shouldContain "kotlin.ULongArray::class" + } } } @@ -127,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" } } } @@ -141,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") {