Skip to content
This repository was archived by the owner on Dec 17, 2025. It is now read-only.

Commit c72db28

Browse files
Lysanderchristian.hausknecht
andauthored
Improves support for upcasting lenses (#925)
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>
1 parent 94ea2ad commit c72db28

File tree

5 files changed

+71
-57
lines changed

5 files changed

+71
-57
lines changed

core/src/commonMain/kotlin/dev/fritz2/core/Lens.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ fun <K, V> lensForElement(key: K): Lens<Map<K, V>, V> = object : Lens<Map<K, V>,
178178
else throw CollectionLensSetException("no item found with key='$key'")
179179
}
180180

181+
/**
182+
* create a [Lens] for upcasting a base (sealed) class or interface to a specific subtype.
183+
*/
184+
inline fun <P, reified C : P> lensForUpcasting(): Lens<P, C> = object : Lens<P, C> {
185+
override val id: String = ""
186+
override fun get(parent: P): C = (parent as? C) ?: throw CollectionLensGetException()
187+
override fun set(parent: P, value: C): P = value
188+
}
189+
181190
/**
182191
* Creates a lens from a nullable parent to a non-nullable value using a given default-value.
183192
* Use this method to apply a default value that will be used in the case that the real value is null.

core/src/commonTest/kotlin/dev/fritz2/core/Lens.kt

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,30 @@ class LensesTests {
103103
"not null lens does not throw exception when set on null parent"
104104
) { notNullLens.set(null, newValue)?.street }
105105
}
106+
107+
sealed interface ConsultationModel {
108+
val stockNumber: String
109+
110+
data class Agency(override val stockNumber: String, val branch: String) : ConsultationModel
111+
112+
data class Private(override val stockNumber: String) : ConsultationModel
113+
}
114+
115+
@Test
116+
fun lensForUpcasting_withSuitableSubtypeTarget_willGetSubtype() {
117+
val agency: ConsultationModel = ConsultationModel.Agency("123", "woodworking")
118+
val sut = lensForUpcasting<ConsultationModel, ConsultationModel.Agency>()
119+
120+
val result = sut.get(agency)
121+
122+
assertEquals(agency, result)
123+
}
124+
125+
@Test
126+
fun lensForUpcasting_withInvalidTarget_willThrow() {
127+
val agency: ConsultationModel = ConsultationModel.Agency("123", "woodworking")
128+
val sut = lensForUpcasting<ConsultationModel, ConsultationModel.Private>()
129+
130+
assertFailsWith<CollectionLensGetException> { sut.get(agency) }
131+
}
106132
}

lenses-annotation-processor/src/jvmMain/kotlin/dev/fritz2/lens/LensesProcessor.kt

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -265,14 +265,9 @@ private class LensesVisitor(
265265
.receiver(compObj.asType(emptyList()).toTypeName())
266266
.apply {
267267
addCode(
268-
"""
269-
|return %M(
270-
| "",
271-
| { it as %T },
272-
| { _, v -> v }
273-
|)
274-
""".trimMargin(),
275-
MemberName("dev.fritz2.core", "lensOf"),
268+
"return %M<%T,%T>()",
269+
MemberName("dev.fritz2.core", "lensForUpcasting"),
270+
classDeclaration.toClassName(),
276271
child.toClassName(),
277272
)
278273
}

lenses-annotation-processor/src/jvmTest/kotlin/dev/fritz2/lens/LensesProcessorTests.kt

Lines changed: 16 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ class LensesProcessorTests {
124124
|package dev.fritz2.lenstest
125125
|
126126
|import dev.fritz2.core.Lens
127+
|import dev.fritz2.core.lensForUpcasting
127128
|import dev.fritz2.core.lensOf
128129
|import kotlin.Int
129130
|
@@ -143,11 +144,7 @@ class LensesProcessorTests {
143144
|
144145
|public fun <PARENT> Lens<PARENT, Bar>.bar(): Lens<PARENT, Int> = this + Bar.bar()
145146
|
146-
|public fun Bar.Companion.barImpl(): Lens<Bar, BarImpl> = lensOf(
147-
| "",
148-
| { it as BarImpl },
149-
| { _, v -> v }
150-
|)
147+
|public fun Bar.Companion.barImpl(): Lens<Bar, BarImpl> = lensForUpcasting<Bar,BarImpl>()
151148
""".trimMargin()
152149
)
153150
}
@@ -581,6 +578,7 @@ class LensesProcessorTests {
581578
|package dev.fritz2.lenstest
582579
|
583580
|import dev.fritz2.core.Lens
581+
|import dev.fritz2.core.lensForUpcasting
584582
|import dev.fritz2.core.lensOf
585583
|import kotlin.Int
586584
|import kotlin.String
@@ -659,17 +657,11 @@ class LensesProcessorTests {
659657
|public fun <PARENT> Lens<PARENT, Framework>.baz(): Lens<PARENT, MyGenericType<Int>> = this +
660658
| Framework.baz()
661659
|
662-
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> = lensOf(
663-
| "",
664-
| { it as Fritz2 },
665-
| { _, v -> v }
666-
|)
660+
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> =
661+
| lensForUpcasting<Framework,Fritz2>()
667662
|
668-
|public fun Framework.Companion.spring(): Lens<Framework, Spring> = lensOf(
669-
| "",
670-
| { it as Spring },
671-
| { _, v -> v }
672-
|)
663+
|public fun Framework.Companion.spring(): Lens<Framework, Spring> =
664+
| lensForUpcasting<Framework,Spring>()
673665
""".trimMargin()
674666

675667
@JvmStatic
@@ -993,13 +985,9 @@ class LensesProcessorTests {
993985
|package dev.fritz2.lenstest
994986
|
995987
|import dev.fritz2.core.Lens
996-
|import dev.fritz2.core.lensOf
988+
|import dev.fritz2.core.lensForUpcasting
997989
|
998-
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensOf(
999-
| "",
1000-
| { it as FooImpl },
1001-
| { _, v -> v }
1002-
|)
990+
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensForUpcasting<Foo,FooImpl>()
1003991
""".trimMargin()
1004992
),
1005993
arguments(
@@ -1034,13 +1022,9 @@ class LensesProcessorTests {
10341022
|package dev.fritz2.lenstest
10351023
|
10361024
|import dev.fritz2.core.Lens
1037-
|import dev.fritz2.core.lensOf
1025+
|import dev.fritz2.core.lensForUpcasting
10381026
|
1039-
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensOf(
1040-
| "",
1041-
| { it as FooImpl },
1042-
| { _, v -> v }
1043-
|)
1027+
|public fun Foo.Companion.fooImpl(): Lens<Foo, FooImpl> = lensForUpcasting<Foo,FooImpl>()
10441028
""".trimMargin()
10451029
),
10461030
)
@@ -1355,6 +1339,7 @@ class LensesProcessorTests {
13551339
|package dev.fritz2.lenstest
13561340
|
13571341
|import dev.fritz2.core.Lens
1342+
|import dev.fritz2.core.lensForUpcasting
13581343
|import dev.fritz2.core.lensOf
13591344
|import kotlin.String
13601345
|
@@ -1376,17 +1361,11 @@ class LensesProcessorTests {
13761361
|
13771362
|public fun <PARENT> Lens<PARENT, Framework>.foo(): Lens<PARENT, String> = this + Framework.foo()
13781363
|
1379-
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> = lensOf(
1380-
| "",
1381-
| { it as Fritz2 },
1382-
| { _, v -> v }
1383-
|)
1364+
|public fun Framework.Companion.fritz2(): Lens<Framework, Fritz2> =
1365+
| lensForUpcasting<Framework,Fritz2>()
13841366
|
1385-
|public fun Framework.Companion.spring(): Lens<Framework, Spring> = lensOf(
1386-
| "",
1387-
| { it as Spring },
1388-
| { _, v -> v }
1389-
|)
1367+
|public fun Framework.Companion.spring(): Lens<Framework, Spring> =
1368+
| lensForUpcasting<Framework,Spring>()
13901369
""".trimMargin()
13911370

13921371

www/src/pages/docs/50_StoreMapping.md

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -412,13 +412,14 @@ Take a look at our complete [validation example](/examples/validation) to get an
412412

413413
### Summary Lens-Factories
414414

415-
| Factory | Use case |
416-
|---------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------|
417-
| `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. |
418-
| `lensOf(parse: (String) -> P, format: (P) -> String): Lens<P, String>` | Formatting lens: Use for mapping into `String`s. |
419-
| `lensForElement(element: T, idProvider: IdProvider<T, I>): Lens<List, T>` | Select one element from a list of entities, therefore a stable Id is needed. |
420-
| `lensForElement(index: Int): Lens<List, T>` | Select one element from a list by index. Useful for value objects. |
421-
| `lensForElement(key: K): Lens<Map<K, V>, V>` | Select one element from a map by key. |
415+
| Factory | Use case |
416+
|---------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
417+
| `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. |
418+
| `lensOf(parse: (String) -> P, format: (P) -> String): Lens<P, String>` | Formatting lens: Use for mapping into `String`s. |
419+
| `lensForElement(element: T, idProvider: IdProvider<T, I>): Lens<List, T>` | Select one element from a list of entities, therefore a stable Id is needed. |
420+
| `lensForElement(index: Int): Lens<List, T>` | Select one element from a list by index. Useful for value objects. |
421+
| `lensForElement(key: K): Lens<Map<K, V>, V>` | Select one element from a map by key. |
422+
| `lensForUpcasting(): Lens<Map<P, C>` | Casting lens: Interpret parent as specific subtype. Handles `ClassCastException` for render edge-cases |
422423

423424
## Advanced Topics
424425

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

643+
But beware that the above code behaves still a bit brittle, as we do not cope with casting problems!
644+
That's why fritz2 offers a dedicated lens factory for this use case: `lensForUpcasting`:
645+
```kotlin
646+
val computerLens: Lens<Wish, Computer> = lensForUpcasting<Wish, Computer>()
647+
```
648+
Our automatic lens generator relies on this factory too. So strive to use this, if you craft your up-casting lenses
649+
manually.
650+
642651
Armed with such an up-casting lenses, we can easily access or change values of our example `WishList`-object:
643652
```kotlin
644653
val wishlist = Wishlist(
@@ -649,11 +658,7 @@ val wishlist = Wishlist(
649658
)
650659
)
651660

652-
val upcastingLens: Lens<Wish, Computer> = lensOf(
653-
"",
654-
{ it as Computer },
655-
{ _, v -> v }
656-
)
661+
val upcastingLens: Lens<Wish, Computer> = lensForUpcasting<Wish, Computer>()
657662

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

0 commit comments

Comments
 (0)