Skip to content

Commit

Permalink
Chaining nullable lenses
Browse files Browse the repository at this point in the history
  • Loading branch information
dmcg committed Dec 30, 2023
1 parent 6f525cd commit 66bd0b4
Show file tree
Hide file tree
Showing 3 changed files with 51 additions and 7 deletions.
10 changes: 5 additions & 5 deletions src/main/java/com/gildedrose/foundation/PropertySet.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@ inline fun <reified T> PropertySet.valueOf(key: String): T =

object PropertySets {
@JvmName("lensPropertySet")
fun lens(propertyName: String) = lens<PropertySet>(propertyName)
fun lens(propertyName: String): Lens<PropertySet, PropertySet> = lens<PropertySet>(propertyName)

@JvmName("aslensPropertySet")
fun String.asLens() = lens(this)
fun String.asLens(): Lens<PropertySet, PropertySet> = lens(this)

inline fun <reified R> String.asLens() = lens<R>(this)
inline fun <reified R> String.asLens(): Lens<PropertySet, R> = lens<R>(this)

inline fun <reified R> lens(propertyName: String) =
LensObject<PropertySet, R>(
inline fun <reified R> lens(propertyName: String): Lens<PropertySet, R> =
LensObject(
getter = { it.valueOf<R>(propertyName) },
injector = { subject, value ->
subject.toMutableMap().apply {
Expand Down
16 changes: 14 additions & 2 deletions src/main/java/com/gildedrose/foundation/lenses.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ interface Lens<T, R> : (T) -> R {
operator fun invoke(subject: T, value: R): T = inject(subject, value)
}

infix fun <T1, T2, R> LensObject<T1, T2>.andThen(second: LensObject<T2, R>) = LensObject<T1, R>(
infix fun <T1, T2, R> Lens<T1, T2>.andThen(second: Lens<T2, R>): Lens<T1, R> = LensObject(
{ second.get(get(it)) },
{ subject, value ->
inject(
Expand All @@ -22,6 +22,18 @@ infix fun <T1, T2, R> LensObject<T1, T2>.andThen(second: LensObject<T2, R>) = Le
}
)

@JvmName("andThenMaybe")
infix fun <T1, T2, R> Lens<T1, T2?>.andThen(second: Lens<T2, R>): Lens<T1, R?> = LensObject(
getter = { get(it)?.let { outer -> second.get(outer) } },
injector = { subject, value ->
val outer = get(subject) ?: error("No parent found to inject into")
inject(
subject,
second.inject(outer, value ?: error("Cannot remove the parent to inject null"))
)
}
)

operator fun <T: Any, R> T.get(extractor: (T) -> R) = extractor(this)
fun <T: Any, R> T.with(lens: Lens<T, R>, of: R) = lens.inject(this, of)
fun <T: Any, R> T.updatedWith(lens: Lens<T, R>, f: (R) -> R) = lens.update(this, f)
Expand All @@ -34,7 +46,7 @@ data class LensObject<T, R>(
override fun inject(subject: T, value: R) = injector(subject, value)
}

inline fun <reified T: Any, R> KProperty1<T, R>.asLens(): LensObject<T, R> = LensObject(
inline fun <reified T: Any, R> KProperty1<T, R>.asLens(): Lens<T, R> = LensObject(
::get,
reflectiveCopy(name)
)
Expand Down
32 changes: 32 additions & 0 deletions src/test/java/com/gildedrose/competition/CompetitionTests.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.readValue
import com.gildedrose.competition.FetchData.Companion.dataFile
import com.gildedrose.foundation.*
import com.gildedrose.foundation.PropertySets.asLens
import com.gildedrose.foundation.PropertySets.lens
import org.http4k.testing.ApprovalTest
import org.http4k.testing.Approver
import org.http4k.testing.assertApproved
Expand All @@ -14,6 +15,7 @@ import strikt.api.expectThat
import strikt.api.expectThrows
import strikt.assertions.isEqualTo
import strikt.assertions.isNull
import strikt.assertions.message

@ExtendWith(ApprovalTest::class)
class CompetitionTests {
Expand Down Expand Up @@ -67,6 +69,34 @@ class CompetitionTests {
expectThat(reverted).isEqualTo(mapOf("propertyName" to null))
}

@Test
fun `chaining nullable lenses`() {
val lens: Lens<PropertySet, String?> = lens<PropertySet?>("outer") andThen "inner".asLens<String>()

val populatedData = mapOf("outer" to mapOf("inner" to "value"))
expectThat(populatedData[lens]).isEqualTo("value")
expectThat(populatedData.with(lens, "new value")).isEqualTo(
mapOf("outer" to mapOf("inner" to "new value"))
)
expectThrows<IllegalStateException> { populatedData.with(lens, null) }
.message.isEqualTo("Cannot remove the parent to inject null")

val emptyData = mapOf<String, Any?>()
expectThat(emptyData[lens]).isNull()
expectThrows<IllegalStateException> { emptyData.with(lens, "value") }
.message.isEqualTo("No parent found to inject into")
expectThrows<IllegalStateException> { emptyData.with(lens, null) }
.message.isEqualTo("No parent found to inject into")

val outerButNoInner = mapOf("outer" to emptyData)
expectThrows<NoSuchElementException> { outerButNoInner[lens] }
expectThat(outerButNoInner.with(lens, "value")).isEqualTo(
mapOf("outer" to mapOf("inner" to "value"))
)
expectThrows<IllegalStateException> { outerButNoInner.with(lens, null) }
.message.isEqualTo("Cannot remove the parent to inject null")
}

data class Place(val properties: PropertySet) : PropertySet by properties {
private val displayNameTextLens = "displayName".asLens() andThen "text".asLens<String>()
private val addressComponents get() = valueOf<List<PropertySet>>("addressComponents").map(::AddressComponent)
Expand All @@ -83,3 +113,5 @@ class CompetitionTests {
val types = valueOf<List<String>>("types")
}
}


0 comments on commit 66bd0b4

Please sign in to comment.