Skip to content

Commit 32dacf2

Browse files
authored
Add mapNullable function to Stores and Inspectors (#928)
1 parent f89a5c1 commit 32dacf2

File tree

6 files changed

+130
-21
lines changed

6 files changed

+130
-21
lines changed

core/api/core.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ public final class dev/fritz2/core/InspectorKt {
3636
public static final fun mapByIndex (Ldev/fritz2/core/Inspector;I)Ldev/fritz2/core/Inspector;
3737
public static final fun mapByKey (Ldev/fritz2/core/Inspector;Ljava/lang/Object;)Ldev/fritz2/core/Inspector;
3838
public static final fun mapNull (Ldev/fritz2/core/Inspector;Ljava/lang/Object;)Ldev/fritz2/core/Inspector;
39+
public static final fun mapNullable (Ldev/fritz2/core/Inspector;Ljava/lang/Object;)Ldev/fritz2/core/Inspector;
3940
}
4041

4142
public abstract interface class dev/fritz2/core/Lens {

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,26 @@ class SubInspector<P, T>(
6161
* Creates a new [Inspector] from a _nullable_ parent inspector that either contains the original value or a given
6262
* [default] value if the original value was `null`.
6363
*
64-
* The resulting inspector behaves similarly to a `Store` created via `Store.mapNull`.
65-
* This means that the resulting [Inspector.path] will be the same as if `mapNull`
66-
* was called on an equivalent store of the same value.
64+
* When updating the value of the resulting [Inspector] to this [default] value,
65+
* null is used instead updating the parent. When this [Inspector]'s value would be null according to it's parent's
66+
* value, the [default] value will be used instead.
67+
*
68+
* @param default value to be used instead of `null`
6769
*/
6870
fun <D> Inspector<D?>.mapNull(default: D): Inspector<D> =
69-
SubInspector(this, defaultLens("", default))
71+
SubInspector(this, mapToNonNullLens(default))
72+
73+
/**
74+
* Creates a new [Inspector] from a _non-nullable_ parent inspector that either contains the original value or `null` if
75+
* its value matches the given [placeholder].
76+
*
77+
* When updating the value of the resulting [Store] to `null`, the [placeholder] is used instead.
78+
* When the resulting [Inspector]'s value would be the [placeholder], `null` will be used instead.
79+
*
80+
* @param placeholder value to be mapped to `null`
81+
*/
82+
fun <T> Inspector<T>.mapNullable(placeholder: T): Inspector<T?> =
83+
map(mapToNullableLens(placeholder))
7084

7185
/**
7286
* Creates a new [Inspector] containing the element for the given [element] and [idProvider]

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

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,33 @@ inline fun <P, reified C : P> lensForUpcasting(): Lens<P, C> = object : Lens<P,
188188
}
189189

190190
/**
191-
* Creates a lens from a nullable parent to a non-nullable value using a given default-value.
191+
* Creates a [Lens] from a nullable parent to a non-nullable value using the provided [default] value.
192+
*
192193
* Use this method to apply a default value that will be used in the case that the real value is null.
193194
* When setting that value to the default value it will accordingly translate to null.
194195
*
195-
* @param default value to be used instead of null
196+
* The inverse Lens can be created using the [mapToNullableLens] factory.
197+
*
198+
* @param default value to be used instead of `null`
196199
*/
197-
internal fun <T> defaultLens(id: String, default: T): Lens<T?, T> = object : Lens<T?, T> {
198-
override val id: String = id
200+
internal fun <T> mapToNonNullLens(default: T): Lens<T?, T> = object : Lens<T?, T> {
201+
override val id: String = ""
199202
override fun get(parent: T?): T = parent ?: default
200203
override fun set(parent: T?, value: T): T? = value.takeUnless { it == default }
204+
}
205+
206+
/**
207+
* Creates a [Lens] from a _non-nullable_ parent to a _nullable_ value, mapping the provided [placeholder] to `null`
208+
* and vice versa.
209+
*
210+
* Use this method in cases where a nullable Store is needed but the data model used is actually non-nullable.
211+
*
212+
* The inverse Lens can be created using the [mapToNonNullLens] factory.
213+
*
214+
* @param placeholder value to be mapped to `null`
215+
*/
216+
internal fun <T> mapToNullableLens(placeholder: T): Lens<T, T?> = object : Lens<T, T?> {
217+
override val id: String = ""
218+
override fun get(parent: T): T? = parent.takeUnless { parent == placeholder }
219+
override fun set(parent: T, value: T?): T = value ?: placeholder
201220
}

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

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package dev.fritz2.core
22

3+
import kotlin.js.JsName
34
import kotlin.test.Test
45
import kotlin.test.assertEquals
56
import kotlin.test.assertFailsWith
7+
import kotlin.test.assertNull
68

79
class LensesTests {
810

@@ -64,7 +66,7 @@ class LensesTests {
6466
fun testDefaultLens() {
6567
val defaultValue = "fritz2"
6668
val nonNullValue = "some value"
67-
val defaultLens = defaultLens("", defaultValue)
69+
val defaultLens = mapToNonNullLens(defaultValue)
6870

6971
assertEquals(defaultValue, defaultLens.get(null), "default value not applied on null")
7072
assertEquals(nonNullValue, defaultLens.get(nonNullValue), "wrong value on not-null")
@@ -75,7 +77,7 @@ class LensesTests {
7577
}
7678

7779
@Test
78-
fun testNotNullLens() {
80+
fun test_mapToNonNullLens() {
7981
data class PostalAddress(val street: String, val co: String?)
8082

8183
val streetLens = lensOf("street", PostalAddress::street) { p, v -> p.copy(street = v) }
@@ -104,6 +106,48 @@ class LensesTests {
104106
) { notNullLens.set(null, newValue)?.street }
105107
}
106108

109+
@Test
110+
fun mapToNullableLens_replaces_placeholder_with_null_when_getting() {
111+
val placeholder = "Unknown"
112+
val sut = mapToNullableLens(placeholder)
113+
114+
val result = sut.get(placeholder)
115+
116+
assertNull(result, "The placeholder should be returned as `null` when getting")
117+
}
118+
119+
@Test
120+
fun mapToNullableLens_returns_same_value_as_parent_when_parent_is_not_the_placeholder() {
121+
val placeholder = "Unknown"
122+
val sut = mapToNullableLens(placeholder)
123+
124+
val parent = "Some actual value"
125+
val result = sut.get("Some actual value")
126+
127+
assertEquals(expected = parent, actual = result)
128+
}
129+
130+
@Test
131+
fun mapToNullableLens_sets_parent_to_placeholder_when_given_null() {
132+
val placeholder = "Unknown"
133+
val sut = mapToNullableLens(placeholder)
134+
135+
val result = sut.set("", null)
136+
137+
assertEquals(expected = placeholder, actual = result)
138+
}
139+
140+
@Test
141+
fun mapToNullableLens_set_parent_to_given_value_when_other_than_null() {
142+
val placeholder = "Unknown"
143+
val sut = mapToNullableLens(placeholder)
144+
145+
val newValue = "New value"
146+
val result = sut.set("", newValue)
147+
148+
assertEquals(expected = newValue, actual = result)
149+
}
150+
107151
sealed interface ConsultationModel {
108152
val stockNumber: String
109153

core/src/jsMain/kotlin/dev/fritz2/core/SubStores.kt

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,19 @@ fun <P, T> Store<P?>.map(lens: Lens<P & Any, T>): Store<T> =
104104
* null is used instead updating the parent. When this [Store]'s value would be null according to it's parent's
105105
* value, the [default] value will be used instead.
106106
*
107-
* @param default value to translate null to and from
107+
* @param default value to be used instead of `null`
108108
*/
109109
fun <T> Store<T?>.mapNull(default: T): Store<T> =
110-
map(defaultLens("", default))
110+
map(mapToNonNullLens(default))
111+
112+
/**
113+
* Creates a new [Store] from a _non-nullable_ parent store that either contains the original value or `null` if its
114+
* value matches the given [placeholder].
115+
*
116+
* When updating the value of the resulting [Store] to `null`, the [placeholder] is used instead.
117+
* When the resulting [Store]'s value would be the [placeholder], `null` will be used instead.
118+
*
119+
* @param placeholder value to be mapped to `null`
120+
*/
121+
fun <T> Store<T>.mapNullable(placeholder: T): Store<T?> =
122+
map(mapToNullableLens(placeholder))

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

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,24 @@ val personStore = storeOf(Person(null), job = Job())
299299
val nameStore = personStore.map(Person.name()).mapNull("")
300300
```
301301

302+
#### The other way around
303+
304+
You may also encounter special cases where you would like to apply the above-mentioned mapping the other way around:
305+
e.g. when dealing with a non-nullable data model in combination with a nullable data-binding of a component such as a
306+
combobox.
307+
308+
For those cases, use the `mapNullable` mapper function:
309+
310+
```kotlin
311+
val nonNullableStore: Store<String> = storeOf("")
312+
313+
val nullableStore: Store<String?> =
314+
nonNullableStore.mapNullable(placeholder = "Unknown")
315+
// ^^^^^^^^^^^^^^^^^^^^^^^
316+
// When the parent has the specified placeholder value,
317+
// the mapped Store will have `null` as its value.
318+
```
319+
302320
### Combining Lenses
303321

304322
A `Lens` supports the `plus`-operator with another lens in order to create a new lens which combines the two.
@@ -400,15 +418,16 @@ Take a look at our complete [validation example](/examples/validation) to get an
400418

401419
### Summary of Store-Mapping-Factories
402420

403-
| Factory | Use case |
404-
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
405-
| `Store<P>.map(lens: Lens<P, T>): Store<T>` | Most generic map-function. Maps any `Store` given a `Lens`. Use for model destructuring with automatic generated lenses for example. |
406-
| `Store<P?>.map(lens: Lens<P & Any, T>): Store<T>` | Maps any nullable `Store` given a `Lens` to a `Store` of a definitely none nullable `T`. Use in `render*`-content expressions combined with a null check. |
407-
| `Store<List<T>>.mapByElement(element: T, idProvider): Store<T>` | Maps a `Store` of a `List<T>` to one element of that list. Works for entities, as a stable Id is needed. |
408-
| `Store<List<T>>.mapByIndex(index: Int): Store<T>` | Maps a `Store` of a `List<T>` to one element of that list using the index. |
409-
| `Store<Map<K, V>>.mapByKey(key: K): Store<V>` | Maps a `Store` of a `Map<T>` to one element of that map using the key. |
410-
| `Store<T?>.mapNull(default: T): Store<T>` | Maps a `Store` of a nullable `T` to a `Store` of a definitely none nullable `T` using a default value in case of `null` in source-store. |
411-
| `MapRouter.mapByKey(key: String): Store<String>` | Maps a `MapRouter` to a `Store`. See [chapter about routers](/docs/routing/#maprouter) for more information. |
421+
| Factory | Use case |
422+
|-----------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
423+
| `Store<P>.map(lens: Lens<P, T>): Store<T>` | Most generic map-function. Maps any `Store` given a `Lens`. Use for model destructuring with automatic generated lenses for example. |
424+
| `Store<P?>.map(lens: Lens<P & Any, T>): Store<T>` | Maps any nullable `Store` given a `Lens` to a `Store` of a definitely none nullable `T`. Use in `render*`-content expressions combined with a null check. |
425+
| `Store<List<T>>.mapByElement(element: T, idProvider): Store<T>` | Maps a `Store` of a `List<T>` to one element of that list. Works for entities, as a stable Id is needed. |
426+
| `Store<List<T>>.mapByIndex(index: Int): Store<T>` | Maps a `Store` of a `List<T>` to one element of that list using the index. |
427+
| `Store<Map<K, V>>.mapByKey(key: K): Store<V>` | Maps a `Store` of a `Map<T>` to one element of that map using the key. |
428+
| `Store<T?>.mapNull(default: T): Store<T>` | Maps a `Store` of a nullable `T` to a `Store` of a definitely none nullable `T` using a default value in case of `null` in source-store. |
429+
| `Store<T>.mapNullable(placeholder: T): Store<T?>` | Maps a `Store` of `T` to a `Store` of `T?`, replacing the given `placeholder` from the parent with `null` in the sub Store. This function is the reverse equivalent of `mapNull`. |
430+
| `MapRouter.mapByKey(key: String): Store<String>` | Maps a `MapRouter` to a `Store`. See [chapter about routers](/docs/routing/#maprouter) for more information. |
412431

413432
### Summary Lens-Factories
414433

0 commit comments

Comments
 (0)