Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce automatic Generation of Delegating Lenses: Improves Validation Support for Sealed Class Hierarchies #876

Merged
merged 2 commits into from
Sep 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions core/src/commonMain/kotlin/dev/fritz2/core/lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ package dev.fritz2.core
annotation class Lenses

/**
* Describes a focus point into a data structure, i.e. a property of a given complex entity
* Describes a focus point into a data structure, i.e. a property of a given complex entity for read and write
* access.
*
* @property id identifies the focus of this lens
*/
Expand Down Expand Up @@ -37,9 +38,9 @@ interface Lens<P, T> {
*/
suspend fun apply(parent: P, mapper: suspend (T) -> T): P = set(parent, mapper(get(parent)))


/**
* appends to [Lens]es so that the resulting [Lens] points from the parent of the [Lens] this is called on to the target of [other]
* appends to [Lens]es so that the resulting [Lens] points from the parent of the [Lens] this is called on to
* the target of [other]
*
* @param other [Lens] to append to this one
*/
Expand All @@ -56,9 +57,11 @@ interface Lens<P, T> {
*/
fun withNullParent(): Lens<P?, T> = object : Lens<P?, T> {
override val id: String = [email protected]

override fun get(parent: P?): T =
if (parent != null) [email protected](parent)
else throw NullPointerException("get called with null parent on not-nullable lens@$id")

override fun set(parent: P?, value: T): P? =
if (parent != null) [email protected](parent, value)
else throw NullPointerException("set called with null parent on not-nullable lens@$id")
Expand Down Expand Up @@ -176,5 +179,5 @@ fun <K, V> lensForElement(key: K): Lens<Map<K, V>, V> = object : Lens<Map<K, V>,
internal fun <T> defaultLens(id: String, default: T): Lens<T?, T> = object : Lens<T?, T> {
override val id: String = id
override fun get(parent: T?): T = parent ?: default
override fun set(parent: T?, value: T): T? = value.takeUnless { it == default }
override fun set(parent: T?, value: T): T? = value.takeUnless { it == default }
}
66 changes: 55 additions & 11 deletions core/src/commonTest/kotlin/dev/fritz2/core/inspector.kt
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,16 @@ class InspectorTests {
assertEquals(personList[0].name, p1NameInspector.data, "sub sub model data for element not correct")

val p1StreetInspector = p1Inspector.map(addressLens).map(streetLens)
assertEquals(".${personList[0].id}.address.street", p1StreetInspector.path, "sub sub sub model id for element not correct")
assertEquals(personList[0].address.street, p1StreetInspector.data, "sub sub sub model data for element not correct")
assertEquals(
".${personList[0].id}.address.street",
p1StreetInspector.path,
"sub sub sub model id for element not correct"
)
assertEquals(
personList[0].address.street,
p1StreetInspector.data,
"sub sub sub model data for element not correct"
)
}

@Test
Expand All @@ -92,12 +100,24 @@ class InspectorTests {
assertEquals(personList[i], it.data, "[$i] sub model data for element not correct")

val p1NameInspector = it.map(nameLens)
assertEquals(".${personList[i].id}.name", p1NameInspector.path, "[$i] sub sub model id for element not correct")
assertEquals(
".${personList[i].id}.name",
p1NameInspector.path,
"[$i] sub sub model id for element not correct"
)
assertEquals(personList[i].name, p1NameInspector.data, "[$i] sub sub model data for element not correct")

val p1StreetInspector = it.map(addressLens).map(streetLens)
assertEquals(".${personList[i].id}.address.street", p1StreetInspector.path, "[$i] sub sub sub model id for element not correct")
assertEquals(personList[i].address.street, p1StreetInspector.data, "[$i] sub sub sub model data for element not correct")
assertEquals(
".${personList[i].id}.address.street",
p1StreetInspector.path,
"[$i] sub sub sub model id for element not correct"
)
assertEquals(
personList[i].address.street,
p1StreetInspector.data,
"[$i] sub sub sub model data for element not correct"
)

i++
}
Expand All @@ -123,7 +143,11 @@ class InspectorTests {

val p1StreetInspector = p1Inspector.map(addressLens).map(streetLens)
assertEquals(".0.address.street", p1StreetInspector.path, "sub sub sub model id for element not correct")
assertEquals(personList[0].address.street, p1StreetInspector.data, "sub sub sub model data for element not correct")
assertEquals(
personList[0].address.street,
p1StreetInspector.data,
"sub sub sub model data for element not correct"
)
}

@Test
Expand All @@ -146,8 +170,16 @@ class InspectorTests {
assertEquals(personList[i].name, p1NameInspector.data, "[$i] sub sub model data for element not correct")

val p1StreetInspector = it.map(addressLens).map(streetLens)
assertEquals(".$i.address.street", p1StreetInspector.path, "[$i] sub sub sub model id for element not correct")
assertEquals(personList[i].address.street, p1StreetInspector.data, "[$i] sub sub sub model data for element not correct")
assertEquals(
".$i.address.street",
p1StreetInspector.path,
"[$i] sub sub sub model id for element not correct"
)
assertEquals(
personList[i].address.street,
p1StreetInspector.data,
"[$i] sub sub sub model data for element not correct"
)

i++
}
Expand All @@ -173,7 +205,11 @@ class InspectorTests {

val p1StreetInspector = p1Inspector.map(addressLens).map(streetLens)
assertEquals(".2.address.street", p1StreetInspector.path, "sub sub sub model id for element not correct")
assertEquals(personList[2]?.address?.street, p1StreetInspector.data, "sub sub sub model data for element not correct")
assertEquals(
personList[2]?.address?.street,
p1StreetInspector.data,
"sub sub sub model data for element not correct"
)
}

@Test
Expand All @@ -197,8 +233,16 @@ class InspectorTests {
assertEquals(personList[i]?.name, p1NameInspector.data, "[$i] sub sub model data for element not correct")

val p1StreetInspector = inspector.map(addressLens).map(streetLens)
assertEquals(".$i.address.street", p1StreetInspector.path, "[$i] sub sub sub model id for element not correct")
assertEquals(personList[i]?.address?.street, p1StreetInspector.data, "[$i] sub sub sub model data for element not correct")
assertEquals(
".$i.address.street",
p1StreetInspector.path,
"[$i] sub sub sub model id for element not correct"
)
assertEquals(
personList[i]?.address?.street,
p1StreetInspector.data,
"[$i] sub sub sub model data for element not correct"
)

i++
}
Expand Down
20 changes: 14 additions & 6 deletions core/src/commonTest/kotlin/dev/fritz2/core/lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ class LensesTests {
private val heightLens = lensOf(Size::height.name, Size::height) { p, v -> p.copy(height = v) }
private val ageLens = lensOf(Tree::age.name, Tree::age) { p, v -> p.copy(age = v) }
private val sizeLens = lensOf(Tree::size.name, Tree::size) { p, v -> p.copy(size = v) }
private val tagLens = lensOf(Tree::tags.name, Tree::tags) { p, v -> p.copy(tags = v) }

@Test
fun testFormatLens() {
Expand Down Expand Up @@ -89,10 +88,19 @@ class LensesTests {
val notNullLens: Lens<PostalAddress?, String> = streetLens.withNullParent()

assertEquals(someStreet, notNullLens.get(addressWithCo), "not null lens does get value on non null parent")
assertFailsWith(NullPointerException::class, "not null lens does not throw exception when get on null parent") { notNullLens.get(null) }
assertFailsWith(
NullPointerException::class,
"not null lens does not throw exception when get on null parent"
) { notNullLens.get(null) }

assertEquals(newValue, notNullLens.set(addressWithCo, newValue)?.street, "not null lens does set value on non null parent")
assertFailsWith(NullPointerException::class, "not null lens does not throw exception when set on null parent") { notNullLens.set(null, newValue)?.street }
assertEquals(
newValue,
notNullLens.set(addressWithCo, newValue)?.street,
"not null lens does set value on non null parent"
)
assertFailsWith(
NullPointerException::class,
"not null lens does not throw exception when set on null parent"
) { notNullLens.set(null, newValue)?.street }
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package dev.fritz2.lens

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

internal fun interface LenseablePropertiesDeterminer {
fun determine(classDeclaration: KSClassDeclaration): List<KSPropertyDeclaration>
}

internal val determineLensablePropertiesInConstructor = LenseablePropertiesDeterminer { classDeclaration ->
val allPublicCtorProps = classDeclaration.primaryConstructor!!.parameters.filter { it.isVal }.map { it.name }
classDeclaration.getDeclaredProperties()
.filter { it.isPublic() && allPublicCtorProps.contains(it.simpleName) }.toList()
}
internal val determineLensablePropertiesInBody = LenseablePropertiesDeterminer { classDeclaration ->
classDeclaration.getDeclaredProperties().filter { it.isPublic() }.toList()
}
Loading
Loading