Skip to content

Commit

Permalink
Improves support for upcasting lenses (#925)
Browse files Browse the repository at this point in the history
Introduces a dedicated `Lens`-factory fpr upcasting lenses:
`lensForUpcasting`

Its inner code will catch `ClassCastException`s, which can
occur in nested `Mountpoints`, and throw a dedicated `CollectionLensGetException`.
This solution is similar to the handling of false index access or
access by keys for collections, we already have integrated.

This is necessary due to the fact, that fritz2 does not maintain
any sort of `Flow`-hierarchie from the outermost to the innermost
renderings. So the framework cannot control the sequence of collecting
the involved flows, in order to prevent outdated data or - for this case - *types*.

This is a practical compromise, that will work very well for UIs,
as the visual appearance is kind of *dictated* by the properties of the underlying type.

Co-authored-by: christian.hausknecht <christian.hausknecht@oeffentliche.de>
  • Loading branch information
Lysander and christian.hausknecht authored Dec 6, 2024
1 parent 94ea2ad commit c72db28
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 57 deletions.
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 @@ -178,6 +178,15 @@ fun <K, V> lensForElement(key: K): Lens<Map<K, V>, V> = object : Lens<Map<K, V>,
else throw CollectionLensSetException("no item found with key='$key'")
}

/**
* create a [Lens] for upcasting a base (sealed) class or interface to a specific subtype.
*/
inline fun <P, reified C : P> lensForUpcasting(): Lens<P, C> = object : Lens<P, C> {
override val id: String = ""
override fun get(parent: P): C = (parent as? C) ?: throw CollectionLensGetException()
override fun set(parent: P, value: C): P = value
}

/**
* Creates a lens from a nullable parent to a non-nullable value using a given default-value.
* Use this method to apply a default value that will be used in the case that the real value is null.
Expand Down
26 changes: 26 additions & 0 deletions core/src/commonTest/kotlin/dev/fritz2/core/Lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,4 +103,30 @@ class LensesTests {
"not null lens does not throw exception when set on null parent"
) { notNullLens.set(null, newValue)?.street }
}

sealed interface ConsultationModel {
val stockNumber: String

data class Agency(override val stockNumber: String, val branch: String) : ConsultationModel

data class Private(override val stockNumber: String) : ConsultationModel
}

@Test
fun lensForUpcasting_withSuitableSubtypeTarget_willGetSubtype() {
val agency: ConsultationModel = ConsultationModel.Agency("123", "woodworking")
val sut = lensForUpcasting<ConsultationModel, ConsultationModel.Agency>()

val result = sut.get(agency)

assertEquals(agency, result)
}

@Test
fun lensForUpcasting_withInvalidTarget_willThrow() {
val agency: ConsultationModel = ConsultationModel.Agency("123", "woodworking")
val sut = lensForUpcasting<ConsultationModel, ConsultationModel.Private>()

assertFailsWith<CollectionLensGetException> { sut.get(agency) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -265,14 +265,9 @@ private class LensesVisitor(
.receiver(compObj.asType(emptyList()).toTypeName())
.apply {
addCode(
"""
|return %M(
| "",
| { it as %T },
| { _, v -> v }
|)
""".trimMargin(),
MemberName("dev.fritz2.core", "lensOf"),
"return %M<%T,%T>()",
MemberName("dev.fritz2.core", "lensForUpcasting"),
classDeclaration.toClassName(),
child.toClassName(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ class LensesProcessorTests {
|package dev.fritz2.lenstest
|
|import dev.fritz2.core.Lens
|import dev.fritz2.core.lensForUpcasting
|import dev.fritz2.core.lensOf
|import kotlin.Int
|
Expand All @@ -143,11 +144,7 @@ class LensesProcessorTests {
|
|public fun <PARENT> Lens<PARENT, Bar>.bar(): Lens<PARENT, Int> = this + Bar.bar()
|
|public fun Bar.Companion.barImpl(): Lens<Bar, BarImpl> = lensOf(
| "",
| { it as BarImpl },
| { _, v -> v }
|)
|public fun Bar.Companion.barImpl(): Lens<Bar, BarImpl> = lensForUpcasting<Bar,BarImpl>()
""".trimMargin()
)
}
Expand Down Expand Up @@ -581,6 +578,7 @@ class LensesProcessorTests {
|package dev.fritz2.lenstest
|
|import dev.fritz2.core.Lens
|import dev.fritz2.core.lensForUpcasting
|import dev.fritz2.core.lensOf
|import kotlin.Int
|import kotlin.String
Expand Down Expand Up @@ -659,17 +657,11 @@ class LensesProcessorTests {
|public fun <PARENT> Lens<PARENT, Framework>.baz(): Lens<PARENT, MyGenericType<Int>> = this +
| Framework.baz()
|
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> = lensOf(
| "",
| { it as Fritz2 },
| { _, v -> v }
|)
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> =
| lensForUpcasting<Framework,Fritz2>()
|
|public fun Framework.Companion.spring(): Lens<Framework, Spring> = lensOf(
| "",
| { it as Spring },
| { _, v -> v }
|)
|public fun Framework.Companion.spring(): Lens<Framework, Spring> =
| lensForUpcasting<Framework,Spring>()
""".trimMargin()

@JvmStatic
Expand Down Expand Up @@ -993,13 +985,9 @@ class LensesProcessorTests {
|package dev.fritz2.lenstest
|
|import dev.fritz2.core.Lens
|import dev.fritz2.core.lensOf
|import dev.fritz2.core.lensForUpcasting
|
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensOf(
| "",
| { it as FooImpl },
| { _, v -> v }
|)
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensForUpcasting<Foo,FooImpl>()
""".trimMargin()
),
arguments(
Expand Down Expand Up @@ -1034,13 +1022,9 @@ class LensesProcessorTests {
|package dev.fritz2.lenstest
|
|import dev.fritz2.core.Lens
|import dev.fritz2.core.lensOf
|import dev.fritz2.core.lensForUpcasting
|
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensOf(
| "",
| { it as FooImpl },
| { _, v -> v }
|)
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensForUpcasting<Foo,FooImpl>()
""".trimMargin()
),
)
Expand Down Expand Up @@ -1355,6 +1339,7 @@ class LensesProcessorTests {
|package dev.fritz2.lenstest
|
|import dev.fritz2.core.Lens
|import dev.fritz2.core.lensForUpcasting
|import dev.fritz2.core.lensOf
|import kotlin.String
|
Expand All @@ -1376,17 +1361,11 @@ class LensesProcessorTests {
|
|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.fritz2(): Lens<Framework, Fritz2> =
| lensForUpcasting<Framework,Fritz2>()
|
|public fun Framework.Companion.spring(): Lens<Framework, Spring> = lensOf(
| "",
| { it as Spring },
| { _, v -> v }
|)
|public fun Framework.Companion.spring(): Lens<Framework, Spring> =
| lensForUpcasting<Framework,Spring>()
""".trimMargin()


Expand Down
29 changes: 17 additions & 12 deletions www/src/pages/docs/50_StoreMapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -412,13 +412,14 @@ Take a look at our complete [validation example](/examples/validation) to get an

### Summary Lens-Factories

| Factory | Use case |
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
| `lensOf(id: String, getter: (P) -> T, setter: (P, T) -> P): Lens<P, T>` | Most generic lens (used by `lenses-annotation-processor`. Fits for complex model destructuring. |
| `lensOf(parse: (String) -> P, format: (P) -> String): Lens<P, String>` | Formatting lens: Use for mapping into `String`s. |
| `lensForElement(element: T, idProvider: IdProvider<T, I>): Lens<List, T>` | Select one element from a list of entities, therefore a stable Id is needed. |
| `lensForElement(index: Int): Lens<List, T>` | Select one element from a list by index. Useful for value objects. |
| `lensForElement(key: K): Lens<Map<K, V>, V>` | Select one element from a map by key. |
| Factory | Use case |
|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| `lensOf(id: String, getter: (P) -> T, setter: (P, T) -> P): Lens<P, T>` | Most generic lens (used by `lenses-annotation-processor`. Fits for complex model destructuring. |
| `lensOf(parse: (String) -> P, format: (P) -> String): Lens<P, String>` | Formatting lens: Use for mapping into `String`s. |
| `lensForElement(element: T, idProvider: IdProvider<T, I>): Lens<List, T>` | Select one element from a list of entities, therefore a stable Id is needed. |
| `lensForElement(index: Int): Lens<List, T>` | Select one element from a list by index. Useful for value objects. |
| `lensForElement(key: K): Lens<Map<K, V>, V>` | Select one element from a map by key. |
| `lensForUpcasting(): Lens<Map<P, C>` | Casting lens: Interpret parent as specific subtype. Handles `ClassCastException` for render edge-cases |

## Advanced Topics

Expand Down Expand Up @@ -639,6 +640,14 @@ Casting from the base type to a more specific type is called up-casting.
Since we apply this to a lens, we call this kind of lens *up-casting* lens.
:::

But beware that the above code behaves still a bit brittle, as we do not cope with casting problems!
That's why fritz2 offers a dedicated lens factory for this use case: `lensForUpcasting`:
```kotlin
val computerLens: Lens<Wish, Computer> = lensForUpcasting<Wish, Computer>()
```
Our automatic lens generator relies on this factory too. So strive to use this, if you craft your up-casting lenses
manually.

Armed with such an up-casting lenses, we can easily access or change values of our example `WishList`-object:
```kotlin
val wishlist = Wishlist(
Expand All @@ -649,11 +658,7 @@ val wishlist = Wishlist(
)
)

val upcastingLens: Lens<Wish, Computer> = lensOf(
"",
{ it as Computer },
{ _, v -> v }
)
val upcastingLens: Lens<Wish, Computer> = lensForUpcasting<Wish, Computer>()

// craft a lens to access the `Computer.raminKb`-property from a `WishList` by combining the
// intermediate lenses by `+`-operator:
Expand Down

0 comments on commit c72db28

Please sign in to comment.