From 9ecb34385a56f193d794251ef01484bbf027871e Mon Sep 17 00:00:00 2001 From: Domenic Cassisi Date: Wed, 11 Dec 2024 23:13:41 +0100 Subject: [PATCH] EphemeralView implementation --- application-arrow/api/application-arrow.api | 4 + .../EphemeralViewArrowExtension.kt | 42 +++++++++ .../fmodel/application/EphemeralViewTest.kt | 86 +++++++++++++++++++ .../EvenNumberEphemeralViewRepository.kt | 44 ++++++++++ .../api/application-vanilla.api | 4 + .../application/EphemeralViewExtension.kt | 14 +++ .../fmodel/application/EphemeralViewTest.kt | 60 +++++++++++++ .../EvenNumberEphemeralViewRepository.kt | 43 ++++++++++ application/api/application.api | 28 ++++++ .../fmodel/application/EphemeralView.kt | 39 +++++++++ .../application/EphemeralViewRepository.kt | 23 +++++ .../fraktalio/fmodel/application/Result.kt | 1 + 12 files changed, 388 insertions(+) create mode 100644 application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewArrowExtension.kt create mode 100644 application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt create mode 100644 application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt create mode 100644 application-vanilla/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewExtension.kt create mode 100644 application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt create mode 100644 application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt create mode 100644 application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralView.kt create mode 100644 application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewRepository.kt diff --git a/application-arrow/api/application-arrow.api b/application-arrow/api/application-arrow.api index 81838c95..69adbde9 100644 --- a/application-arrow/api/application-arrow.api +++ b/application-arrow/api/application-arrow.api @@ -1,3 +1,7 @@ +public final class com/fraktalio/fmodel/application/EphemeralViewArrowExtensionKt { + public static final fun handleWithEffect (Lcom/fraktalio/fmodel/application/ViewStateComputation;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class com/fraktalio/fmodel/application/EventSourcingAggregateArrowExtensionKt { public static final fun handleOptimisticallyWithEffect (Lcom/fraktalio/fmodel/application/EventSourcingLockingAggregate;Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; public static final fun handleOptimisticallyWithEffect (Lcom/fraktalio/fmodel/application/EventSourcingLockingAggregate;Ljava/lang/Object;Ljava/util/Map;)Lkotlinx/coroutines/flow/Flow; diff --git a/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewArrowExtension.kt b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewArrowExtension.kt new file mode 100644 index 00000000..85f4a558 --- /dev/null +++ b/application-arrow/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewArrowExtension.kt @@ -0,0 +1,42 @@ +package com.fraktalio.fmodel.application + +import arrow.core.Either +import arrow.core.raise.catch +import arrow.core.raise.either +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.fold + +/** + * Extension function - Handles the query of type [Q] + * + * @param query Query of type [Q] to be handled + * @return [Either] (either [Error] or State of type [S]) + * + * @author Domenic Cassisi + */ +suspend fun EV.handleWithEffect(query: Q): Either + where EV : ViewStateComputation, EV : EphemeralViewRepository { + + fun Q.fetchEventsWithEffect(): Either> = + either { + catch({ + fetchEvents() + }) { + raise(Error.FetchingEventsFailed(query, it)) + } + } + + suspend fun Flow.computeStateWithEffect(): Either = + either { + catch({ + fold(initialState) { s, e -> evolve(s, e) } + }) { + raise(Error.CalculatingNewViewStateFailed(this@computeStateWithEffect, it)) + } + } + + return either { + query.fetchEventsWithEffect().bind() + .computeStateWithEffect().bind() + } +} diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt new file mode 100644 index 00000000..365f55f6 --- /dev/null +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt @@ -0,0 +1,86 @@ +package com.fraktalio.fmodel.application + +import arrow.core.Either +import com.fraktalio.fmodel.application.examples.numbers.even.query.EvenNumberEphemeralViewRepository +import com.fraktalio.fmodel.application.examples.numbers.even.query.evenNumberEphemeralViewRepository +import com.fraktalio.fmodel.domain.IView +import com.fraktalio.fmodel.domain.examples.numbers.api.Description +import com.fraktalio.fmodel.domain.examples.numbers.api.EvenNumberState +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue +import com.fraktalio.fmodel.domain.examples.numbers.even.query.evenNumberView +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe +import io.kotest.matchers.types.shouldBeInstanceOf + +/** + * DSL - Given + */ +private suspend fun IView.given(repository: EphemeralViewRepository, query: () -> Q): Either = + EphemeralView( + view = this, + ephemeralViewRepository = repository + ).handleWithEffect(query()) + +/** + * DSL - When + */ +private fun whenQuery(query: Q): Q = query + +/** + * DSL - Then + */ +private infix fun Either.thenState(expected: S) { + val state = when (this) { + is Either.Right -> value + is Either.Left -> throw AssertionError("Expected Either.Right, but found Either.Left with value $value") + } + state shouldBe expected +} + +private fun Either.thenError() { + val error = when (this) { + is Either.Right -> throw AssertionError("Expected Either.Left, but found Either.Right with value $value") + is Either.Left -> value + } + error.shouldBeInstanceOf() +} + +/** + * Ephemeral View Test + */ +class EphemeralViewTest : FunSpec({ + val evenView = evenNumberView() + val ephemeralViewRepository = evenNumberEphemeralViewRepository() as EvenNumberEphemeralViewRepository + + test("Ephemeral View - load number flow 1") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(1) + } thenState EvenNumberState(Description("Initial state, Number 2, Number 4"), NumberValue(6)) + } + } + + test("Ephemeral View - load number flow 2") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(2) + } thenState EvenNumberState(Description("Initial state, Number 4, Number 2"), NumberValue(2)) + } + } + + test("Ephemeral View - load number flow 3 - with error") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(3) + }.thenError() + } + } + + test("Ephemeral View - load non-existing number flow") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(4) + } thenState EvenNumberState(Description("Initial state"), NumberValue(0)) + } + } +}) diff --git a/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt new file mode 100644 index 00000000..8c81cc03 --- /dev/null +++ b/application-arrow/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt @@ -0,0 +1,44 @@ +package com.fraktalio.fmodel.application.examples.numbers.even.query + +import com.fraktalio.fmodel.application.EphemeralViewRepository +import com.fraktalio.fmodel.domain.examples.numbers.api.Description +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberEvent +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf + +/** + * Simple flows of events to represent previously stored events in the event store + */ +private var numberFlow1 = flowOf( + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 2"), NumberValue(2)), + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)) +) + +private var numberFlow2 = flowOf( + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)), + NumberEvent.EvenNumberEvent.EvenNumberSubtracted(Description("Number 2"), NumberValue(2)) +) + +/** + * Even number ephemeral view implementation + */ +class EvenNumberEphemeralViewRepository : EphemeralViewRepository { + + override fun Int.fetchEvents(): Flow { + return when (this) { + 1 -> numberFlow1 + 2 -> numberFlow2 + 3 -> throw RuntimeException("Some fake error while fetching events.") + else -> emptyFlow() + } + } + +} + +/** + * Helper function to create an [EvenNumberEphemeralViewRepository] + */ +fun evenNumberEphemeralViewRepository(): EphemeralViewRepository = + EvenNumberEphemeralViewRepository() diff --git a/application-vanilla/api/application-vanilla.api b/application-vanilla/api/application-vanilla.api index f326fe7f..b64dceac 100644 --- a/application-vanilla/api/application-vanilla.api +++ b/application-vanilla/api/application-vanilla.api @@ -1,3 +1,7 @@ +public final class com/fraktalio/fmodel/application/EphemeralViewExtensionKt { + public static final fun handle (Lcom/fraktalio/fmodel/application/ViewStateComputation;Ljava/lang/Object;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; +} + public final class com/fraktalio/fmodel/application/EventSourcingAggregateActorExtensionKt { public static final fun handleConcurrently (Lcom/fraktalio/fmodel/application/EventSourcingAggregate;Lkotlinx/coroutines/flow/Flow;IILkotlinx/coroutines/CoroutineStart;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function1;)Lkotlinx/coroutines/flow/Flow; public static final fun handleConcurrently (Lcom/fraktalio/fmodel/application/EventSourcingAggregate;Lkotlinx/coroutines/flow/Flow;IILkotlinx/coroutines/CoroutineStart;Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;)Lkotlinx/coroutines/flow/Flow; diff --git a/application-vanilla/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewExtension.kt b/application-vanilla/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewExtension.kt new file mode 100644 index 00000000..7b4b587e --- /dev/null +++ b/application-vanilla/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewExtension.kt @@ -0,0 +1,14 @@ +package com.fraktalio.fmodel.application + +import kotlinx.coroutines.flow.fold + +/** + * Extension function - Handles the query of type [Q] + * + * @param query Query of type [Q] to be handled + * @return State of type [S] + * + * @author Domenic Cassisi + */ +suspend fun EV.handle(query: Q): S where EV : ViewStateComputation, EV : EphemeralViewRepository = + query.fetchEvents().fold(initialState) { s, e -> evolve(s, e) } diff --git a/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt b/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt new file mode 100644 index 00000000..1380a1c1 --- /dev/null +++ b/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/EphemeralViewTest.kt @@ -0,0 +1,60 @@ +package com.fraktalio.fmodel.application + +import com.fraktalio.fmodel.application.examples.numbers.even.query.EvenNumberEphemeralViewRepository +import com.fraktalio.fmodel.application.examples.numbers.even.query.evenNumberEphemeralViewRepository +import com.fraktalio.fmodel.domain.IView +import com.fraktalio.fmodel.domain.examples.numbers.api.Description +import com.fraktalio.fmodel.domain.examples.numbers.api.EvenNumberState +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue +import com.fraktalio.fmodel.domain.examples.numbers.even.query.evenNumberView +import io.kotest.core.spec.style.FunSpec +import io.kotest.matchers.shouldBe + +/** + * DSL - Given + */ +private suspend fun IView.given(repository: EphemeralViewRepository, query: () -> Q): S = + EphemeralView( + view = this, + ephemeralViewRepository = repository + ).handle(query()) + +/** + * DSL - When + */ +private fun whenQuery(query: Q): Q = query + +/** + * DSL - Then + */ +private infix fun S.thenState(expected: S) = shouldBe(expected) + +class EphemeralViewTest : FunSpec({ + val evenView = evenNumberView() + val ephemeralViewRepository = evenNumberEphemeralViewRepository() as EvenNumberEphemeralViewRepository + + test("Ephemeral View - load number flow 1") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(1) + } thenState EvenNumberState(Description("Initial state, Number 2, Number 4"), NumberValue(6)) + } + } + + test("Ephemeral View - load number flow 2") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(2) + } thenState EvenNumberState(Description("Initial state, Number 4, Number 2"), NumberValue(2)) + } + } + + test("Ephemeral View - load non-existing number flow") { + with(evenView) { + given(ephemeralViewRepository) { + whenQuery(3) + } thenState EvenNumberState(Description("Initial state"), NumberValue(0)) + } + } + +}) diff --git a/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt b/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt new file mode 100644 index 00000000..48a94464 --- /dev/null +++ b/application-vanilla/src/commonTest/kotlin/com/fraktalio/fmodel/application/examples/numbers/even/query/EvenNumberEphemeralViewRepository.kt @@ -0,0 +1,43 @@ +package com.fraktalio.fmodel.application.examples.numbers.even.query + +import com.fraktalio.fmodel.application.EphemeralViewRepository +import com.fraktalio.fmodel.domain.examples.numbers.api.Description +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberEvent +import com.fraktalio.fmodel.domain.examples.numbers.api.NumberValue +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flowOf + +/** + * Simple flows of events to represent previously stored events in the event store + */ +private var numberFlow1 = flowOf( + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 2"), NumberValue(2)), + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)) +) + +private var numberFlow2 = flowOf( + NumberEvent.EvenNumberEvent.EvenNumberAdded(Description("Number 4"), NumberValue(4)), + NumberEvent.EvenNumberEvent.EvenNumberSubtracted(Description("Number 2"), NumberValue(2)) +) + +/** + * Even number ephemeral view implementation + */ +class EvenNumberEphemeralViewRepository : EphemeralViewRepository { + + override fun Int.fetchEvents(): Flow { + return when (this) { + 1 -> numberFlow1 + 2 -> numberFlow2 + else -> emptyFlow() + } + } + +} + +/** + * Helper function to create an [EvenNumberEphemeralViewRepository] + */ +fun evenNumberEphemeralViewRepository(): EphemeralViewRepository = + EvenNumberEphemeralViewRepository() diff --git a/application/api/application.api b/application/api/application.api index 2f26e3fa..186db33a 100644 --- a/application/api/application.api +++ b/application/api/application.api @@ -7,6 +7,21 @@ public final class com/fraktalio/fmodel/application/ActionPublisher$DefaultImpls public static fun publish (Lcom/fraktalio/fmodel/application/ActionPublisher;Lkotlinx/coroutines/flow/Flow;Ljava/util/Map;)Lkotlinx/coroutines/flow/Flow; } +public abstract interface class com/fraktalio/fmodel/application/EphemeralView : com/fraktalio/fmodel/application/EphemeralViewRepository, com/fraktalio/fmodel/application/ViewStateComputation { +} + +public final class com/fraktalio/fmodel/application/EphemeralView$DefaultImpls { + public static fun computeNewState (Lcom/fraktalio/fmodel/application/EphemeralView;Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; +} + +public final class com/fraktalio/fmodel/application/EphemeralViewKt { + public static final fun EphemeralView (Lcom/fraktalio/fmodel/domain/IView;Lcom/fraktalio/fmodel/application/EphemeralViewRepository;)Lcom/fraktalio/fmodel/application/EphemeralView; +} + +public abstract interface class com/fraktalio/fmodel/application/EphemeralViewRepository { + public abstract fun fetchEvents (Ljava/lang/Object;)Lkotlinx/coroutines/flow/Flow; +} + public abstract class com/fraktalio/fmodel/application/Error : com/fraktalio/fmodel/application/Result { public abstract fun getThrowable ()Ljava/lang/Throwable; } @@ -110,6 +125,19 @@ public final class com/fraktalio/fmodel/application/Error$EventPublishingFailed public fun toString ()Ljava/lang/String; } +public final class com/fraktalio/fmodel/application/Error$FetchingEventsFailed : com/fraktalio/fmodel/application/Error { + public fun (Ljava/lang/Object;Ljava/lang/Throwable;)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Ljava/lang/Throwable; + public final fun copy (Ljava/lang/Object;Ljava/lang/Throwable;)Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed; + public static synthetic fun copy$default (Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed;Ljava/lang/Object;Ljava/lang/Throwable;ILjava/lang/Object;)Lcom/fraktalio/fmodel/application/Error$FetchingEventsFailed; + public fun equals (Ljava/lang/Object;)Z + public final fun getId ()Ljava/lang/Object; + public fun getThrowable ()Ljava/lang/Throwable; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + public final class com/fraktalio/fmodel/application/Error$FetchingStateFailed : com/fraktalio/fmodel/application/Error { public fun (Ljava/lang/Object;Ljava/lang/Throwable;)V public synthetic fun (Ljava/lang/Object;Ljava/lang/Throwable;ILkotlin/jvm/internal/DefaultConstructorMarker;)V diff --git a/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralView.kt b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralView.kt new file mode 100644 index 00000000..357406e5 --- /dev/null +++ b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralView.kt @@ -0,0 +1,39 @@ +package com.fraktalio.fmodel.application + +import com.fraktalio.fmodel.domain.IView + +/** + * EphemeralView is using/delegating a `view` / [ViewStateComputation]<[S], [E]> to handle events of type [E] without maintaining a state of projection(s). + * + * [EphemeralView] extends [ViewStateComputation] and [EphemeralViewRepository] interfaces, + * clearly communicating that it is composed out of these two behaviours. + * + * @param S Ephemeral View state of type [S] + * @param E Events of type [E] that are handled by this Ephemeral View + * @param Q Query of type [Q] + * + * @author Domenic Cassisi + */ +interface EphemeralView : ViewStateComputation, EphemeralViewRepository + +/** + * Ephemeral View constructor-like function. + * + * The Delegation pattern has proven to be a good alternative to implementation inheritance, and Kotlin supports it natively requiring zero boilerplate code. + * + * @param S Ephemeral View state of type [S] + * @param E Events of type [E] that are used internally to build/fold new state + * @param Q Identifier of type [Q] + * @property view A view component of type [IView]<[S], [E]> + * @property ephemeralViewRepository Interface for fetching events for [Q] - dependencies by delegation + * @return An object/instance of type [EphemeralView]<[S], [E], [Q]> + * + * @author Domenic Cassisi + */ +fun EphemeralView( + view: IView, + ephemeralViewRepository: EphemeralViewRepository +): EphemeralView = + object : EphemeralView, + EphemeralViewRepository by ephemeralViewRepository, + IView by view {} diff --git a/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewRepository.kt b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewRepository.kt new file mode 100644 index 00000000..225410c6 --- /dev/null +++ b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/EphemeralViewRepository.kt @@ -0,0 +1,23 @@ +package com.fraktalio.fmodel.application + +import kotlinx.coroutines.flow.Flow + +/** + * Ephemeral view repository interface + * + * @param E Event + * @param Q Query + * + * @author Domenic Cassisi + */ +fun interface EphemeralViewRepository { + + /** + * Fetch events + * + * @receiver Query of type [Q] + * @return the Flow of events of type [E] + */ + fun Q.fetchEvents(): Flow + +} diff --git a/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/Result.kt b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/Result.kt index aa34ad34..5bf4697f 100644 --- a/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/Result.kt +++ b/application/src/commonMain/kotlin/com/fraktalio/fmodel/application/Result.kt @@ -55,6 +55,7 @@ sealed class Error : Result() { ) : Error() data class StoringStateFailed(val state: S, override val throwable: Throwable? = null) : Error() + data class FetchingEventsFailed(val id: I, override val throwable: Throwable?): Error() }