From 828488dcaa16d5d5affdf95889023adbba31d0b6 Mon Sep 17 00:00:00 2001 From: "christian.hausknecht" Date: Mon, 9 Sep 2024 10:16:01 +0200 Subject: [PATCH] Add new `@NoLens` annotation The newly introduced support for lenses generation for sealed classes or interfaces (see #876) must allow to configure whether some property should be picked for lens generation or not. This heavily depends on the implementation of those. The default case is to pick a property, but sometimes a property will not be implemented as constructor property inside the child data class. For such cases it is now possible to mark such properties with the `@NoLens` annotation inside the sealed type. Such marked properties will get ignored by the lens generator, so no delegating lens will be created. Beware that this annotation is not evaluated inside the constructor of data classes! Imagine the following example to see `@NoLens` in action: ```kotlin @Lenses sealed class Framework { // Ignore this property for delegating lens generation. // The property is considered to be constant for all objects, // see data class below @NoLens val ignore: String abstract val foo: String companion object } data class Fritz2 ( override val foo: String, ) : Framework { // not part of the "data", so not possible to change at copy! // Because of that, we cannot define any valid lens in the sealed base, // so we must mark it to exclude it for lens creation! override val ignore: String = "Fritz2" } ``` --- .../commonMain/kotlin/dev/fritz2/core/lens.kt | 9 ++ .../lens/LenseablePropertiesDeterminer.kt | 6 +- .../dev/fritz2/lens/LensesProcessorTests.kt | 127 ++++++++++++++++++ 3 files changed, 141 insertions(+), 1 deletion(-) diff --git a/core/src/commonMain/kotlin/dev/fritz2/core/lens.kt b/core/src/commonMain/kotlin/dev/fritz2/core/lens.kt index bafbd216d..ca491f7f4 100644 --- a/core/src/commonMain/kotlin/dev/fritz2/core/lens.kt +++ b/core/src/commonMain/kotlin/dev/fritz2/core/lens.kt @@ -6,6 +6,15 @@ package dev.fritz2.core @Target(AnnotationTarget.CLASS) annotation class Lenses +/** + * Used by the fritz2 gradle-plugin to identify properties in sealed classes or interfaces, that should get ignored + * by the lens generation. + * + * Typical use case are const properties, that are override inside the data class body and not the ctor. + */ +@Target(AnnotationTarget.PROPERTY) +annotation class NoLens + /** * Describes a focus point into a data structure, i.e. a property of a given complex entity for read and write * access. diff --git a/lenses-annotation-processor/src/jvmMain/kotlin/dev/fritz2/lens/LenseablePropertiesDeterminer.kt b/lenses-annotation-processor/src/jvmMain/kotlin/dev/fritz2/lens/LenseablePropertiesDeterminer.kt index 11fc8169c..63652bdcf 100644 --- a/lenses-annotation-processor/src/jvmMain/kotlin/dev/fritz2/lens/LenseablePropertiesDeterminer.kt +++ b/lenses-annotation-processor/src/jvmMain/kotlin/dev/fritz2/lens/LenseablePropertiesDeterminer.kt @@ -4,6 +4,7 @@ import com.google.devtools.ksp.getDeclaredProperties import com.google.devtools.ksp.isPublic import com.google.devtools.ksp.symbol.KSClassDeclaration import com.google.devtools.ksp.symbol.KSPropertyDeclaration +import dev.fritz2.core.NoLens internal fun interface LenseablePropertiesDeterminer { fun determine(classDeclaration: KSClassDeclaration): List @@ -15,5 +16,8 @@ internal val determineLensablePropertiesInConstructor = LenseablePropertiesDeter .filter { it.isPublic() && allPublicCtorProps.contains(it.simpleName) }.toList() } internal val determineLensablePropertiesInBody = LenseablePropertiesDeterminer { classDeclaration -> - classDeclaration.getDeclaredProperties().filter { it.isPublic() }.toList() + classDeclaration.getDeclaredProperties() + .filter { it.isPublic() } + .filter { it.annotations.none { annotation -> annotation.shortName.asString() == NoLens::class.simpleName } } + .toList() } \ No newline at end of file diff --git a/lenses-annotation-processor/src/jvmTest/kotlin/dev/fritz2/lens/LensesProcessorTests.kt b/lenses-annotation-processor/src/jvmTest/kotlin/dev/fritz2/lens/LensesProcessorTests.kt index 1c7b0f052..e786c82a4 100644 --- a/lenses-annotation-processor/src/jvmTest/kotlin/dev/fritz2/lens/LensesProcessorTests.kt +++ b/lenses-annotation-processor/src/jvmTest/kotlin/dev/fritz2/lens/LensesProcessorTests.kt @@ -533,6 +533,24 @@ class LensesProcessorTests { ) } + @ExperimentalPathApi + @Suppress("UNUSED_PARAMETER") + @DisplayName("validate NoLens annotation in sealed base class or interface will not create lens for annotated properties") + @ParameterizedTest(name = "{index}: {0}") + @MethodSource("createNoLensAnnotatedClasses") + fun validateNoLensAnnotationWorks(description: String, kotlinSource: SourceFile, expectedCode: String) { + val compilationResult = compileSource(kotlinSource) + + assertAll( + { assertThat(compilationResult.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) }, + { + assertThat(compilationResult.kspGeneratedSources.find { it.name == "FrameworkLenses.kt" }) + .usingCharset(StandardCharsets.UTF_8) + .hasContent(expectedCode) + } + ) + } + companion object { private val validCodeForSealedHierarchies = """ |// GENERATED by fritz2 - NEVER CHANGE CONTENT MANUALLY! @@ -1222,5 +1240,114 @@ class LensesProcessorTests { ) ) ) + + private val validCodeForNoLensResult = """ + |// GENERATED by fritz2 - NEVER CHANGE CONTENT MANUALLY! + |package dev.fritz2.lenstest + | + |import dev.fritz2.core.Lens + |import dev.fritz2.core.lensOf + |import kotlin.String + | + |public fun Framework.Companion.foo(): Lens = lensOf( + | "foo", + | { parent -> + | when(parent) { + | is Fritz2 -> parent.foo + | is Spring -> parent.foo + | } + | }, + | { parent, value -> + | when(parent) { + | is Fritz2 -> parent.copy(foo = value) + | is Spring -> parent.copy(foo = value) + | } + | } + |) + | + |public fun Lens.foo(): Lens = this + Framework.foo() + | + |public fun Framework.Companion.fritz2(): Lens = lensOf( + | "", + | { it as Fritz2 }, + | { _, v -> v } + |) + | + |public fun Framework.Companion.spring(): Lens = lensOf( + | "", + | { it as Spring }, + | { _, v -> v } + |) + """.trimMargin() + + + @JvmStatic + fun createNoLensAnnotatedClasses() = listOf( + arguments( + "sealed class", + SourceFile.kotlin( + "sealedClassesForNoLensesTests.kt", + """ + package dev.fritz2.lenstest + + import dev.fritz2.core.Lenses + + @Lenses + sealed class Framework { + @NoLens + val ignore: String + abstract val foo: String + companion object + } + + data class Fritz2 ( + override val foo: String, + ) : Framework { + override val ignore: String = "Fritz2" + } + + data class Spring ( + override val foo: String, + ) : Framework { + override val ignore: String = "Spring" + } + """ + ), + validCodeForNoLensResult + ), + arguments( + "sealed interface", + SourceFile.kotlin( + "sealedInterfacesForNoLensesTests.kt", + """ + package dev.fritz2.lenstest + + import dev.fritz2.core.Lenses + + @Lenses + sealed interface Framework { + @NoLens + val ignore: String + val foo: String + + companion object + } + + data class Fritz2 ( + override val foo: String, + ) : Framework { + override val ignore: String = "Fritz2" + } + + data class Spring ( + override val foo: String, + ) : Framework { + override val ignore: String = "Spring" + } + """ + ), + validCodeForNoLensResult + ) + ) } } \ No newline at end of file