Skip to content

Commit

Permalink
Add new @NoLens annotation
Browse files Browse the repository at this point in the history
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"
}
```
  • Loading branch information
christian.hausknecht committed Sep 9, 2024
1 parent 029fa6a commit 828488d
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 1 deletion.
9 changes: 9 additions & 0 deletions core/src/commonMain/kotlin/dev/fritz2/core/lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<KSPropertyDeclaration>
Expand All @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand Down Expand Up @@ -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<Framework, String> = 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 <PARENT> Lens<PARENT, Framework>.foo(): Lens<PARENT, String> = this + Framework.foo()
|
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> = lensOf(
| "",
| { it as Fritz2 },
| { _, v -> v }
|)
|
|public fun Framework.Companion.spring(): Lens<Framework, Spring> = 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
)
)
}
}

0 comments on commit 828488d

Please sign in to comment.