diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1c2e3b02..52309d1f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ datastore = "1.0.0" constraintlayoutCompose = "1.0.1" datastorePreferences = "1.0.0" kotlin = "1.9.0" -kotlinpoet = "1.14.2" +kotlinpoet = "1.16.0" kotlinxSerializationJson = "1.6.0" runtimeLivedata = "1.5.3" symbolProcessingApi = "1.9.10-1.0.13" diff --git a/vss-core/src/main/java/org/eclipse/kuksa/vsscore/model/VssSpecification.kt b/vss-core/src/main/java/org/eclipse/kuksa/vsscore/model/VssSpecification.kt index 193e3946..48747baa 100644 --- a/vss-core/src/main/java/org/eclipse/kuksa/vsscore/model/VssSpecification.kt +++ b/vss-core/src/main/java/org/eclipse/kuksa/vsscore/model/VssSpecification.kt @@ -101,6 +101,16 @@ val VssSpecification.parentKey: String return keys[keys.size - 2] } +/** + * Similar to the [variableName] but for the parent and does not lowercase the [name] wherever necessary. + */ +val VssSpecification.parentClassName: String + get() { + if (parentKey.isEmpty()) return "" + + return (classNamePrefix + parentKey).toCamelCase.replaceFirstChar { it.uppercase() } + } + /** * Iterates through all nested children which also may have children and aggregates them into one big collection. */ diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/VssDefinitionProcessor.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/VssDefinitionProcessor.kt index 6b9d239d..73b11c81 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/VssDefinitionProcessor.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/VssDefinitionProcessor.kt @@ -39,6 +39,7 @@ import com.squareup.kotlinpoet.ksp.toClassName import com.squareup.kotlinpoet.ksp.writeTo import org.eclipse.kuksa.vsscore.annotation.VssDefinition import org.eclipse.kuksa.vsscore.model.VssNode +import org.eclipse.kuksa.vsscore.model.parentClassName import org.eclipse.kuksa.vssprocessor.parser.YamlDefinitionParser import org.eclipse.kuksa.vssprocessor.spec.VssPath import org.eclipse.kuksa.vssprocessor.spec.VssSpecificationSpecModel @@ -115,6 +116,7 @@ class VssDefinitionProcessor( logger.info("Ambiguous specifications - Generate nested classes: $duplicateSpecificationNames") + val generatedFilesVssPathToClassName = mutableMapOf() for ((vssPath, specModel) in vssPathToSpecification) { // Every duplicate is produced as a nested class - No separate file should be generated if (duplicateSpecificationNames.contains(vssPath.leaf)) { @@ -128,11 +130,53 @@ class VssDefinitionProcessor( duplicateSpecificationNames, ) - val file = FileSpec.builder(PACKAGE_NAME, classSpec.name!!) + val className = classSpec.name ?: throw NoSuchFieldException("Class spec $classSpec has no name field!") + val fileSpecBuilder = FileSpec.builder(PACKAGE_NAME, className) + + val parentImport = buildParentImport(specModel, generatedFilesVssPathToClassName) + if (parentImport.isNotEmpty()) { + fileSpecBuilder.addImport(PACKAGE_NAME, parentImport) + } + + val file = fileSpecBuilder .addType(classSpec) .build() file.writeTo(codeGenerator, false) + generatedFilesVssPathToClassName[vssPath.path] = className + } + } + + // Uses a map of vssPaths to ClassNames which are validated if it contains a parent of the given specModel. + // If the actual parent is a sub class (Driver) in another class file (e.g. Vehicle) then this method returns + // a sub import e.g. "Vehicle.Driver". Otherwise just "Vehicle" is returned. + private fun buildParentImport( + specModel: VssSpecificationSpecModel, + parentVssPathToClassName: Map, + ): String { + var availableParentVssPath = specModel.vssPath + var parentSpecClassName: String? = null + + // Iterate up from the parent until the actual file name = class name was found. This indicates + // that the actual parent is a sub class in this file. + while (availableParentVssPath.contains(".")) { + availableParentVssPath = availableParentVssPath.substringBeforeLast(".") + + parentSpecClassName = parentVssPathToClassName[availableParentVssPath] + if (parentSpecClassName != null) break + } + + if (parentSpecClassName == null) { + logger.info("Could not create import string for: ${specModel.vssPath} - No parent was found") + return "" + } + + val parentClassName = specModel.parentClassName + + return if (parentSpecClassName != parentClassName) { + "$parentSpecClassName.$parentClassName" // Sub class in another file + } else { + parentClassName // Main class = File name } } } diff --git a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModel.kt b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModel.kt index e1fdc1db..eba569b2 100644 --- a/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModel.kt +++ b/vss-processor/src/main/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModel.kt @@ -37,6 +37,7 @@ import org.eclipse.kuksa.vsscore.model.VssProperty import org.eclipse.kuksa.vsscore.model.VssSpecification import org.eclipse.kuksa.vsscore.model.className import org.eclipse.kuksa.vsscore.model.name +import org.eclipse.kuksa.vsscore.model.parentClassName import org.eclipse.kuksa.vsscore.model.parentKey import org.eclipse.kuksa.vsscore.model.variableName import kotlin.reflect.KClass @@ -152,6 +153,7 @@ internal class VssSpecificationSpecModel( relevantRelatedSpecifications, nestedClasses, ) + nestedChildSpecs.add(childSpec) } @@ -274,13 +276,14 @@ internal class VssSpecificationSpecModel( } 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", "$className::class") + .addStatement("return %L", parentClass) .build(), ) .build() diff --git a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModelTest.kt b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModelTest.kt index 6a120577..9f70a262 100644 --- a/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModelTest.kt +++ b/vss-processor/src/test/kotlin/org/eclipse/kuksa/vssprocessor/spec/VssSpecificationSpecModelTest.kt @@ -150,28 +150,45 @@ class VssSpecificationSpecModelTest : BehaviorSpec({ } } and("related specifications") { + val vehicleSpeedSpecModel = VssSpecificationSpecModel(datatype = "float", vssPath = "Vehicle.Speed") val relatedSpecifications = listOf( VssSpecificationSpecModel(vssPath = "Vehicle.SmartphoneProjection"), VssSpecificationSpecModel(datatype = "boolean", vssPath = "Vehicle.IsBrokenDown"), - VssSpecificationSpecModel(datatype = "float", vssPath = "Vehicle.Speed"), + vehicleSpeedSpecModel, VssSpecificationSpecModel(datatype = "string[]", vssPath = "Vehicle.SupportedMode"), VssSpecificationSpecModel(datatype = "boolean[]", vssPath = "Vehicle.AreSeatsHeated"), VssSpecificationSpecModel(datatype = "invalid", vssPath = "Vehicle.Invalid"), ) - `when`("creating a child class spec with children") { - val classSpec = specModel.createClassSpec("test", relatedSpecifications) + `when`("creating a class spec with children") { + val rootClassSpec = specModel.createClassSpec("test", relatedSpecifications) then("it should contain the child properties") { - val isBrokenDownPropertySpec = classSpec.propertySpecs.find { it.name == "isBrokenDown" } - val childrenPropertySpec = classSpec.propertySpecs.find { it.name == "children" } + val isBrokenDownPropertySpec = rootClassSpec.propertySpecs.find { it.name == "isBrokenDown" } + val childrenPropertySpec = rootClassSpec.propertySpecs.find { it.name == "children" } - classSpec.name shouldBe "VssVehicle" - classSpec.propertySpecs.size shouldBe 13 // 8 interface props + 5 children + rootClassSpec.name shouldBe "VssVehicle" + rootClassSpec.propertySpecs.size shouldBe 13 // 8 interface props + 5 children isBrokenDownPropertySpec shouldNotBe null childrenPropertySpec?.getter.toString() shouldContain "smartphoneProjection, isBrokenDown" } + + then("it should have no parent") { + val parentPropertySpec = rootClassSpec.propertySpecs.find { it.name == "parentClass" } + + parentPropertySpec?.getter.toString() shouldContain "null" + } + + and("a child class spec is created") { + val vehicleSpeedClassSpec = vehicleSpeedSpecModel.createClassSpec("test") + + then("it should have the root class as parent") { + val parentPropertySpec = vehicleSpeedClassSpec.propertySpecs.find { it.name == "parentClass" } + + parentPropertySpec?.getter.toString() shouldContain "VssVehicle" + } + } } and("nested specifications") { val nestedSpecifications = listOf("Speed")